Windows驱动中校验数字签名(使用 ci.dll)

1.背景

  对于常规应用程序来说,校验数字签名在在应用层可以使用 WinVerifyTrust, 在驱动层使用常规的 API无法使用,自己分析数据又太麻烦。

  但在内核中 ci.dll 包装了数据签名验证相关的功能,我们可以使用该 dll 来实现我们的数字签名验证。

  详细的分析见《内核中的代码完整性:深入分析ci.dll》。下面直接上相关代码。

 

2.相关代码

  源代码地址为 https://github.com/Ido-Moshe-Github/CiDllDemo。这里作了稍微的修改以及添加一些打印信息。

2.1 ci.h

#pragma once

#include <wdm.h>
#include <minwindef.h>

#if DBG
#define KDPRINT(projectName, format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,\
																						  projectName "::【" __FUNCTION__  "】" ##format, \
																						  ##__VA_ARGS__ ) 
#else
#define KDPRINT(format, ...)
#endif

/**
*  This struct was copied from <wintrust.h> and encapsulates a signature used in verifying executable files.
*/
typedef struct _WIN_CERTIFICATE {
    DWORD dwLength;                         // Specifies the length, in bytes, of the signature
    WORD  wRevision;                        // Specifies the certificate revision
    WORD  wCertificateType;                 // Specifies the type of certificate
    BYTE  bCertificate[ANYSIZE_ARRAY];      // An array of certificates
} WIN_CERTIFICATE, * LPWIN_CERTIFICATE;


/**
*  Describes the location (address) and size of a ASN.1 blob within a buffer.
*
*  @note  The data itself is not contained in the struct.
*/
typedef struct _Asn1BlobPtr
{
    int size;               // size of the ASN.1 blob
    PVOID ptrToData;        // where the ASN.1 blob starts
} Asn1BlobPtr, * pAsn1BlobPtr;


/**
*  Describes the location (address) and size of a certificate subject/issuer name, within a buffer.
*
*  @note  The data itself (name) is not contained in the struct.
*
*  @note  the reason for separating these fields into their own struct was to match the padding we
*         observed in CertChainMember struct after the second 'short' field - once you enclose it 
*         into a struct, on x64 bit machines there will be a padding of 4 bytes at the end of the struct,
*         because the largest member of the struct is of size 8 and it dictates the alignment of the struct.
*/
typedef struct _CertificatePartyName
{
    PVOID pointerToName;
    short nameLen;
    short unknown;
} CertificatePartyName, * pCertificatePartyName;


/**
*  Contains various data about a specific certificate in the chain and also points to the actual certificate.
*
*  @note  the digest described in this struct is the digest that was used to create the certificate - not for
*         signing the file.
*
*  @note  The size reserved for digest is 64 byte regardless of the digest type, in order to accomodate SHA2/3's
*         max size of 512bit. The memory is not zeroed, so we must take the actual digestSize into account when
*         reading it.
*/
typedef struct _CertChainMember
{
    int digestIdetifier;                // e.g. 0x800c for SHA256
    int digestSize;                     // e.g. 0x20 for SHA256
    BYTE digestBuffer[64];              // contains the digest itself, where the digest size is dictated by digestSize

    CertificatePartyName subjectName;   // pointer to the subject name
    CertificatePartyName issuerName;    // pointer to the issuer name

    Asn1BlobPtr certificate;            // ptr to actual cert in ASN.1 - including the public key
} CertChainMember, * pCertChainMember;


/**
*  Describes the format of certChainInfo buffer member of PolicyInfo struct. This header maps the types,
*  locations, and quantities of the data which is contained in the buffer.
*
*  @note  when using this struct make sure to check its size first (bufferSize) because it's not guaranteed
*         that all the fields below will exist.
*/
typedef struct _CertChainInfoHeader
{
    // The size of the dynamically allocated buffer
    int bufferSize;

    // points to the start of a series of Asn1Blobs which contain the public keys of the certificates in the chain
    pAsn1BlobPtr ptrToPublicKeys;
    int numberOfPublicKeys;
    
    // points to the start of a series of Asn1Blobs which contain the EKUs
    pAsn1BlobPtr ptrToEkus;
    int numberOfEkus;

    // points to the start of a series of CertChainMembers
    pCertChainMember ptrToCertChainMembers;
    int numberOfCertChainMembers;

    int unknown;

    // ASN.1 blob of authenticated attributes - spcSpOpusInfo, contentType, etc.
    Asn1BlobPtr variousAuthenticodeAttributes;
} CertChainInfoHeader, * pCertChainInfoHeader;


/**
*  Contains information regarding the certificates that were used for signing/timestamping
*
*  @note  you must check structSize before accessing the other members, since some members were added later.
*
*  @note  all structs members, including the length, are populated by ci functions - no need
*         to fill them in adavnce.
*/
typedef struct _PolicyInfo
{
    int structSize;
    NTSTATUS verificationStatus;
    int flags;
    pCertChainInfoHeader certChainInfo; // if not null - contains info about certificate chain
    FILETIME revocationTime;            // when was the certificate revoked (if applicable)
    FILETIME notBeforeTime;             // the certificate is not valid before this time
    FILETIME notAfterTime;              // the certificate is not valid before this time
} PolicyInfo, *pPolicyInfo;


/**
*  Given a file digest and signature of a file, verify the signature and provide information regarding
*  the certificates that was used for signing (the entire certificate chain)
*
*  @note  the function allocates a buffer from the paged pool --> can be used only where IRQL < DISPATCH_LEVEL
*
*  @param  digestBuffer - buffer containing the digest
*
*  @param  digestSize - size of the digest, e.g. 0x20 for SHA256, 0x14 for SHA1
*
*  @param  digestIdentifier - digest algorithm identifier, e.g. 0x800c for SHA256, 0x8004 for SHA1
*
*  @param  winCert - pointer to the start of the security directory
*
*  @param  sizeOfSecurityDirectory - size the security directory
*
*  @param  policyInfoForSigner[out] - PolicyInfo containing information about the signer certificate chain
*
*  @param  signingTime[out] - when the file was signed (FILETIME format)
*
*  @param  policyInfoForTimestampingAuthority[out] - PolicyInfo containing information about the timestamping 
*          authority (TSA) certificate chain
*
*  @return  0 if the file digest in the signature matches the given digest and the signer cetificate is verified.
*           Various error values otherwise, for example:
*           STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
*           STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
*           STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/
extern "C" __declspec(dllimport) NTSTATUS _stdcall CiCheckSignedFile(
    const PVOID digestBuffer,
    int digestSize,
    int digestIdentifier,
    const LPWIN_CERTIFICATE winCert,
    int sizeOfSecurityDirectory,
    PolicyInfo* policyInfoForSigner,
    LARGE_INTEGER* signingTime,
    PolicyInfo* policyInfoForTimestampingAuthority);


/**
*  Resets a PolicyInfo struct - frees the dynamically allocated buffer in PolicyInfo (certChainInfo) if not null.
*  Zeros the entire PolicyInfo struct.
*
*  @param  policyInfo - the struct to reset.
*
*  @return  the struct which was reset.
*/
extern "C" __declspec(dllimport) PVOID _stdcall CiFreePolicyInfo(PolicyInfo* policyInfo);


/**
*  Given a file object, verify the signature and provide information regarding
*  the certificates that was used for signing (the entire certificate chain)
*
*  @note  the function allocates memory from the paged pool --> can be used only where IRQL < DISPATCH_LEVEL
*
*  @param  fileObject[in] - fileObject of the PE in question
*
*  @param  a2[in] - unknown, needs to be reversed. 0 is a valid value.
*
*  @param  a3[in] - unknown, needs to be reversed. 0 is a valid value.
*
*  @param  policyInfoForSigner[out] - PolicyInfo containing information about the signer certificate chain
*
*  @param  signingTime[out] - when the file was signed
*
*  @param  policyInfoForTimestampingAuthority[out] - PolicyInfo containing information about the timestamping
*          authority (TSA) certificate chain
*
*  @param  digestBuffer[out] - buffer to be filled with the digest, must be at least 64 bytes
*
*  @param  digestSize[inout] - size of the digest. Must be at leat 64 and will be changed by the function to 
*                              reflect the actual digest length.
*
*  @param  digestIdentifier[out] - digest algorithm identifier, e.g. 0x800c for SHA256, 0x8004 for SHA1
*
*  @return  0 if the file digest in the signature matches the given digest and the signer cetificate is verified.
*           Various error values otherwise, for example:
*           STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
*           STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
*           STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/
extern "C" __declspec(dllimport) NTSTATUS _stdcall CiValidateFileObject(
    struct _FILE_OBJECT* fileObject,
    int a2,
    int a3,
    PolicyInfo* policyInfoForSigner,
    PolicyInfo* policyInfoForTimestampingAuthority,
    LARGE_INTEGER* signingTime,
    BYTE* digestBuffer,
    int* digestSize,
    int* digestIdentifier
);

2.2 RAIIUtils.h

#pragma once

#include <ntddk.h>
#include <wdm.h>
#include "ci.h"


/**
 *  create a file handle for read.
 *  release handle when exiting the current context.
 */
class FileReadHandleGuard
{
public:
        FileReadHandleGuard(PCUNICODE_STRING imageFileName) : _handle(nullptr), _isValid(false)
        {
                IO_STATUS_BLOCK ioStatusBlock = { 0 };
                OBJECT_ATTRIBUTES  objAttr = { 0 };
                InitializeObjectAttributes(
                        &objAttr,
                        const_cast<PUNICODE_STRING>(imageFileName),
                        OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
                        nullptr,
                        nullptr);

                const NTSTATUS openFileRet = ZwOpenFile(
                        &_handle,
                        SYNCHRONIZE | FILE_READ_DATA, // ACCESS_MASK, we use SYNCHRONIZE because we might need to wait on the handle in order to wait for the file to be read
                        &objAttr,
                        &ioStatusBlock,
                        FILE_SHARE_READ,
                        FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT // FILE_SYNCHRONOUS_IO_NONALERT so that zwReadfile will pend for us until reading is done
                );

                if (!NT_SUCCESS(openFileRet))
                {
                        KDPRINT("【CiDemoDriver】", "failed to open file - openFileRet = %d\n", openFileRet);
                        return;
                }

                if (ioStatusBlock.Status != STATUS_SUCCESS || _handle == nullptr)
                {
                        KDPRINT("【CiDemoDriver】", "ioStatusBlock.Status != STATUS_SUCCESS, or _handle is null\n");
                        return;
                }

                _isValid = true;
        }

        ~FileReadHandleGuard()
        {
                if (_handle != nullptr)
                {
                        ZwClose(_handle);
                }
        }

        HANDLE& get() { return _handle; }
        bool isValid() const { return _isValid; }

private:
        HANDLE _handle;
        bool _isValid;
};


/**
 *  create a section handle.
 *  release handle when exiting the current context.
 */
class SectionHandleGuard
{
public:
        SectionHandleGuard(HANDLE& fileHandle) : _handle(nullptr), _isValid(false)
        {
                OBJECT_ATTRIBUTES objectAttributes = { 0 };
                InitializeObjectAttributes(
                        &objectAttributes,
                        nullptr,
                        OBJ_KERNEL_HANDLE, // to make sure user mode cannot access this handle
                        nullptr,
                        nullptr);

                const NTSTATUS createSectionRet = ZwCreateSection(
                        &_handle,
                        SECTION_MAP_READ,
                        &objectAttributes,
                        nullptr, // maximum size - use the file size, in order to map the entire file
                        PAGE_READONLY,
                        SEC_COMMIT, // map as commit and not as SEC_IMAGE, because SEC_IMAGE will not map things which are not needed for the PE - such as resources and certificates
                        fileHandle
                );

                if (!NT_SUCCESS(createSectionRet))
                {
                        KDPRINT("【CiDemoDriver】", "failed to create section - ZwCreateSection returned %x\n", createSectionRet);
                        return;
                }

                _isValid = true;
        }

        ~SectionHandleGuard()
        {
                if (_handle != nullptr)
                {
                        ZwClose(_handle);
                }
        }

        HANDLE& get() { return _handle; }
        bool isValid() const { return _isValid; }

private:
        HANDLE _handle;
        bool _isValid;
};


/**
 *  retrieve a section object from a section handle.
 *  release object reference when exiting the current context.
 */
class SectionObjectGuard
{
public:
        SectionObjectGuard(HANDLE& sectionHandle) : _object(nullptr), _isValid(false)
        {
                const NTSTATUS ret = ObReferenceObjectByHandle(
                        sectionHandle,
                        SECTION_MAP_READ,
                        nullptr,
                        KernelMode,
                        &_object,
                        nullptr
                );

                if (!NT_SUCCESS(ret))
                {
                        KDPRINT("【CiDemoDriver】", "ObReferenceObjectByHandle failed -  returned %x\n", ret);
                        return;
                }

                _isValid = true;
        }

        ~SectionObjectGuard()
        {
                if (_object != nullptr)
                {
                        ObfDereferenceObject(_object);
                }
        }

        PVOID& get() { return _object; }
        bool isValid() const { return _isValid; }

private:
        PVOID _object;
        bool _isValid;
};


/**
 *  create a view of file.
 *  unmap the view when exiting the current context.
 */
class SectionViewGuard
{
public:
        SectionViewGuard(PVOID sectionObject) : _baseAddrOfView(nullptr), _viewSize(0), _isValid(false)
        {
                const NTSTATUS ret = MmMapViewInSystemSpace(
                        sectionObject,
                        &_baseAddrOfView,
                        &_viewSize
                );

                if (!NT_SUCCESS(ret))
                {
                        KDPRINT("【CiDemoDriver】", "MmMapViewInSystemSpace failed -  returned %x\n", ret);
                        return;
                }

                _isValid = true;
        }

        ~SectionViewGuard()
        {
                if (_baseAddrOfView != nullptr)
                {
                        MmUnmapViewInSystemSpace(_baseAddrOfView);
                }
        }

        PVOID getViewBaseAddress() const { return _baseAddrOfView; }
        SIZE_T getViewSize() const { return _viewSize; }
        bool isValid() const { return _isValid; }

private:
        PVOID _baseAddrOfView;
        SIZE_T _viewSize;
        bool _isValid;
};


/**
 *  create a PoicyInfo struct.
 *  Release the memory used by the struct when exiting the current context.
 */
class PolicyInfoGuard
{
public:
        PolicyInfoGuard() : _policyInfo{} {}

        ~PolicyInfoGuard()
        {
                // CiFreePolicyInfo checks internally if there's memory to free
                CiFreePolicyInfo(&_policyInfo);
        }

        PolicyInfo& get() { return _policyInfo; }

private:
        PolicyInfo _policyInfo;
};

2.3 SignatureCheck.cpp

#include "RAIIUtils.h"
#include "SignatureCheck.h"
#include "ci.h"

#define SHA1_IDENTIFIER 0x8004
#define SHA256_IDENTIFIER 0x800C
#define IMAGE_DIRECTORY_ENTRY_SECURITY  4


extern "C" PVOID RtlImageDirectoryEntryToData(PVOID BaseAddress, BOOLEAN MappedAsImage, USHORT Directory, PULONG Size);
bool inRange(const BYTE* rangeStartAddr, const BYTE* rangeEndAddr, const BYTE* addrToCheck);
void parsePolicyInfo(const pPolicyInfo policyInfo);
bool ciCheckSignedFileWrapper(const LPWIN_CERTIFICATE win_cert, ULONG sizeOfSecurityDirectory);


void validateFileUsingCiCheckSignedFile(PCUNICODE_STRING imageFileName)
{
        KDPRINT("【CiDemoDriver】", "Validating file using CiCheckSignedFile...\n");

        FileReadHandleGuard fileHandleGuard(imageFileName);
        if (!fileHandleGuard.isValid()) return;

        // create section for the file
        SectionHandleGuard sectionHandleGuard(fileHandleGuard.get());
        if (!sectionHandleGuard.isValid()) return;

        // get section object from section handle
        SectionObjectGuard sectionObjectGuard(sectionHandleGuard.get());
        if (!sectionObjectGuard.isValid()) return;

        // map a view of the section
        SectionViewGuard viewGuard(sectionObjectGuard.get());
        if (!viewGuard.isValid()) return;

        // fetch the security directory
        PVOID securityDirectoryEntry = nullptr;
        ULONG securityDirectoryEntrySize = 0;
        securityDirectoryEntry = RtlImageDirectoryEntryToData(
                viewGuard.getViewBaseAddress(),
                TRUE, // we tell RtlImageDirectoryEntryToData it's mapped as image because then it will treat the RVA as offset from the beginning of the view, which is what we want. See https://doxygen.reactos.org/dc/d30/dll_2win32_2dbghelp_2compat_8c_source.html#l00102
                IMAGE_DIRECTORY_ENTRY_SECURITY,
                &securityDirectoryEntrySize
        );

        if (securityDirectoryEntry == nullptr)
        {
                KDPRINT("【CiDemoDriver】", "no security directory\n");
                return;
        }

        KDPRINT("【CiDemoDriver】", "securityDirectoryEntry found at: %p, size: %x\n",
                securityDirectoryEntry, securityDirectoryEntrySize);

        // Make sure the security directory is contained in the file view
        const BYTE* endOfFileAddr = static_cast<BYTE*>(viewGuard.getViewBaseAddress()) + viewGuard.getViewSize();
        const BYTE* endOfSecurityDir = static_cast<BYTE*>(securityDirectoryEntry) + securityDirectoryEntrySize;
        if (endOfSecurityDir > endOfFileAddr || securityDirectoryEntry < viewGuard.getViewBaseAddress())
        {
                KDPRINT("【CiDemoDriver】", "security directory is not contained in file view!\n");
                return;
        }

        // technically, there can be several WIN_CERTIFICATE in a file. This not common, and, for simplicity,
        // we'll assume there's only one
        LPWIN_CERTIFICATE winCert = static_cast<LPWIN_CERTIFICATE>(securityDirectoryEntry);
        KDPRINT("【CiDemoDriver】", "WIN_CERTIFICATE at: %p, revision = %x, type = %x, length = %xd, bCertificate = %p\n",
                securityDirectoryEntry, winCert->wRevision, winCert->wCertificateType, winCert->dwLength, static_cast<PVOID>(winCert->bCertificate));

        ciCheckSignedFileWrapper(winCert, securityDirectoryEntrySize);
}


bool ciCheckSignedFileWrapper(const LPWIN_CERTIFICATE win_cert, ULONG sizeOfSecurityDirectory)
{
        // prepare the parameters required for calling CiCheckSignedFile
        PolicyInfoGuard signerPolicyInfo;
        PolicyInfoGuard timestampingAuthorityPolicyInfo;
        LARGE_INTEGER signingTime = {};
        //const int digestSize = 20; // sha1 len, 0x14
        const  int digestSize = 32; // sha256 len, 0x20
        //const int digestIdentifier = 0x8004; // sha1
        const int digestIdentifier = 0x800C; // sha256
        const BYTE digestBuffer[] = // 

        { 0x5f, 0x6d, 0xa4, 0x95, 0x78, 0xa4, 0x39, 0x4b, 0xb4, 0x0f, 
          0xf6, 0x9b, 0xaa, 0x2a, 0xd7, 0x02, 0xda, 0x7d, 0x3d, 0xbe,
          0xb8, 0x12, 0xb8, 0xc7, 0x24, 0xcd, 0xe3, 0x68, 0x89, 0x65,
          0x86, 0x00 };

        // CiCheckSignedFile() allocates memory from the paged pool, so make sure we're at IRQL < 2,
        // where access to paged memory is allowed
        NT_ASSERT(KeGetCurrentIrql() < DISPATCH_LEVEL);

        const NTSTATUS status = CiCheckSignedFile(
                (PVOID)digestBuffer,
                digestSize,
                digestIdentifier,
                win_cert,
                (int)sizeOfSecurityDirectory,
                &signerPolicyInfo.get(),
                &signingTime,
                &timestampingAuthorityPolicyInfo.get());
        KDPRINT("【CiDemoDriver】", "CiCheckSignedFile returned 0x%08X\n", status);

        if (NT_SUCCESS(status))
        {
                parsePolicyInfo(&signerPolicyInfo.get());
                return true;
        }

        return false;
}

UCHAR HexToChar(UCHAR temp)
{
        UCHAR dst;
        if (temp == ' ')
        {
                // do nothing 
                dst = temp;
        }
        else if (temp < 10) {
                dst = temp + '0';
        }
        else {
                dst = temp - 10 + 'A';
        }
        return dst;
}

void validateFileUsingCiValidateFileObject(PFILE_OBJECT fileObject)
{
        KDPRINT("【CiDemoDriver】", "Validating file using CiValidateFileObject...\n");
        NT_ASSERT(KeGetCurrentIrql() < DISPATCH_LEVEL);

        PolicyInfoGuard signerPolicyInfo;
        PolicyInfoGuard timestampingAuthorityPolicyInfo;
        LARGE_INTEGER signingTime = {};
        int digestSize = 64; //大小必须为64,否则返回缓冲区大小太小
        int digestIdentifier = 0;
        BYTE digestBuffer[64] = {}; //大小必须为64,否则返回缓冲区大小太小

        const NTSTATUS status = CiValidateFileObject(
                fileObject,
                0,
                0,
                &signerPolicyInfo.get(),
                &timestampingAuthorityPolicyInfo.get(),
                &signingTime,
                digestBuffer,
                &digestSize,
                &digestIdentifier
        );

        KDPRINT("【CiDemoDriver】", "CiValidateFileObject returned 0x%08X\n", status);
        if (NT_SUCCESS(status))
        {
                CHAR digestTempBuffer[98] = { 0 };
                for (int i = 0; i <= 31; i++)
                {
                        digestTempBuffer[3 * i] = digestBuffer[i] >> 4;
                        digestTempBuffer[3 * i + 1] = digestBuffer[i] & 0xf;
                        digestTempBuffer[3 * i + 2] = ' ';
                }
                for (int i = 0; i < 96; i++)
                {
                        digestTempBuffer[i] = HexToChar(digestTempBuffer[i]);
                }
                KDPRINT("【CiDemoDriver】", "Signer certificate:\n  digest algorithm - 0x%x\n digest size - %d\r\n digest - %s\n",
                        digestIdentifier, digestSize, digestTempBuffer);
                parsePolicyInfo(&signerPolicyInfo.get());
                return;
        }
}


void parsePolicyInfo(const pPolicyInfo policyInfo)
{
        if (policyInfo == nullptr)
        {
                KDPRINT("【CiDemoDriver】", "parsePolicyInfo - paramter is null\n");
                return;
        }

        if (policyInfo->structSize == 0)
        {
                KDPRINT("【CiDemoDriver】", "policy info is empty\n");
                return;
        }

        if (policyInfo->certChainInfo == nullptr)
        {
                KDPRINT("【CiDemoDriver】", "certChainInfo is null\n");
                return;
        }

        const pCertChainInfoHeader chainInfoHeader = policyInfo->certChainInfo;

        const BYTE* startOfCertChainInfo = (BYTE*)(chainInfoHeader);
        const BYTE* endOfCertChainInfo = (BYTE*)(policyInfo->certChainInfo) + chainInfoHeader->bufferSize;
        DWORD dwChainCount = policyInfo->certChainInfo->numberOfCertChainMembers;
        for (DWORD dwChainIndex = 0; dwChainIndex < dwChainCount; dwChainIndex++)
        {
                if (!inRange(startOfCertChainInfo, endOfCertChainInfo, (BYTE*)(chainInfoHeader->ptrToCertChainMembers + dwChainIndex)))
                {
                        KDPRINT("【CiDemoDriver】", "chain members out of range\n");
                        continue;
                }

                // need to make sure we have enough room to accomodate the chain member struct
                if (!inRange(startOfCertChainInfo, endOfCertChainInfo, (BYTE*)(chainInfoHeader->ptrToCertChainMembers + dwChainIndex) + sizeof(CertChainMember)))
                {
                        KDPRINT("【CiDemoDriver】", "chain member out of range\n");
                        continue;
                }

                // we are interested in the first certificate in the chain - the signer itself
                pCertChainMember signerChainMember = chainInfoHeader->ptrToCertChainMembers + dwChainIndex;
                UTF8_STRING utf8SubjectName = { 0 };
                utf8SubjectName.MaximumLength = utf8SubjectName.Length = signerChainMember->subjectName.nameLen;
                utf8SubjectName.Buffer = static_cast<char*>(signerChainMember->subjectName.pointerToName);
                UNICODE_STRING usSubjectName = { 0 };
                RtlUTF8StringToUnicodeString(&usSubjectName, &utf8SubjectName, true);


                UTF8_STRING utf8IssuerName = { 0 };
                utf8IssuerName.MaximumLength = utf8IssuerName.Length = signerChainMember->issuerName.nameLen;
                utf8IssuerName.Buffer = static_cast<char*>(signerChainMember->issuerName.pointerToName);
                UNICODE_STRING usIssuerName = { 0 };
                RtlUTF8StringToUnicodeString(&usIssuerName, &utf8IssuerName, true);

                //KDPRINT("【CiDemoDriver】", "Signer certificate:\n  digest algorithm - 0x%x\n  size - %d\n  subject - %.*s\n  issuer - %.*s\n", 
                KDPRINT("【CiDemoDriver】", "Signer certificate[%d]:\n  digest algorithm - 0x%x\n  size - %d\n  subject - %wZ\n  issuer - %wZ\n",
                        dwChainIndex + 1,
                        signerChainMember->digestIdetifier, \
                        signerChainMember->certificate.size, \
                        /*  signerChainMember->subjectName.nameLen,
                          static_cast<char*>(signerChainMember->subjectName.pointerToName),*/
                        & usSubjectName,
                        /*signerChainMember->issuerName.nameLen,
                        static_cast<char*>(signerChainMember->issuerName.pointerToName)*/
                        &usIssuerName);

                CHAR digestTempBuffer[98] = { 0 };
                for (int i = 0; i <= 31; i++)
                {
                        digestTempBuffer[3 * i] = signerChainMember->digestBuffer[i] >> 4;
                        digestTempBuffer[3 * i + 1] = signerChainMember->digestBuffer[i] & 0xf;
                        digestTempBuffer[3 * i + 2] = ' ';
                }
                for (int i = 0; i < 96; i++)
                {
                        digestTempBuffer[i] = HexToChar(digestTempBuffer[i]);
                }
                KDPRINT("【CiDemoDriver】", "Signer certificate:\n  digest algorithm - 0x%x\n digest size - %d\r\n digest - %s\n",
                        signerChainMember->digestIdetifier, signerChainMember->digestSize, digestTempBuffer);


                RtlFreeUnicodeString(&usSubjectName);
                RtlFreeUnicodeString(&usIssuerName);
        }

}

bool inRange(const BYTE* rangeStartAddr, const BYTE* rangeEndAddr, const BYTE* addrToCheck)
{
        if (addrToCheck > rangeEndAddr || addrToCheck < rangeStartAddr)
        {
                return false;
        }

        return true;
}

2.4 main.cpp

#include <ntddk.h> // PsSetCreateProcessNotifyRoutineEx
#include <wdm.h>
#include "SignatureCheck.h"
#include "ci.h"

DRIVER_UNLOAD MyDriverUnload;
void registerProcessCallback();
void unregisterProcessCallback();
void ProcessCreateProcessNotifyRoutineEx(PEPROCESS Process, HANDLE ProcessId, PPS_CREATE_NOTIFY_INFO CreateInfo);

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
        UNREFERENCED_PARAMETER(DriverObject);
        UNREFERENCED_PARAMETER(RegistryPath);
        DriverObject->DriverUnload = MyDriverUnload;

        KDPRINT("【CiDemoDriver】", "CiDemoDriver load\n");

        registerProcessCallback();

        return STATUS_SUCCESS;
}

VOID MyDriverUnload(_In_ struct _DRIVER_OBJECT* DriverObject)
{
        UNREFERENCED_PARAMETER(DriverObject);
        KDPRINT("【CiDemoDriver】", "CiDemoDriver unload\n");
        unregisterProcessCallback();
}

void registerProcessCallback()
{
        const NTSTATUS registerCallbackStatus = PsSetCreateProcessNotifyRoutineEx(ProcessCreateProcessNotifyRoutineEx, FALSE);
        if (!NT_SUCCESS(registerCallbackStatus))
        {
                KDPRINT("【CiDemoDriver】", "failed to register callback with status %d\n", registerCallbackStatus);
        }
        else
        {
                KDPRINT("【CiDemoDriver】", "successfully registered callback\n");
        }
}

void unregisterProcessCallback()
{
        const NTSTATUS registerCallbackStatus = PsSetCreateProcessNotifyRoutineEx(ProcessCreateProcessNotifyRoutineEx, TRUE);
        if (!NT_SUCCESS(registerCallbackStatus))
        {
                KDPRINT("【CiDemoDriver】", "failed to unregister callback\n");
        }
        else
        {
                KDPRINT("【CiDemoDriver】", "successfully unregistered callback\n");
        }
}

void ProcessCreateProcessNotifyRoutineEx(
        PEPROCESS Process,
        HANDLE ProcessId,
        PPS_CREATE_NOTIFY_INFO CreateInfo
)
{
        UNREFERENCED_PARAMETER(Process);
        UNREFERENCED_PARAMETER(ProcessId);

        if (CreateInfo == nullptr) return; //process died

        if (CreateInfo->FileObject == nullptr) return;
        if (nullptr == CreateInfo->ImageFileName) return;

        KDPRINT("【CiDemoDriver】", "New process - image name: %wZ\n", CreateInfo->ImageFileName);

        validateFileUsingCiValidateFileObject(CreateInfo->FileObject);
        validateFileUsingCiCheckSignedFile(CreateInfo->ImageFileName);
}

 

3. 相关逻辑分析及注意事项

 

  • main.cpp 中使用 PsSetCreateProcessNotifyRoutineEx 添加一个创建进程回调,回调用使用进程对象的 FileObject 和文件路径  ImageFileName来进行文件数字签名的检验。
  • SignatureCheck.cpp 的 ciCheckSignedFileWrapper 函数第 79 至 82 行为验证签名的参数 ,基中 digestSize = 20 时表示签名算法为 sha1 时的长度,为32时表示算法为 sha256 时的长度; 其中 digestIdentifier  =0x8004 表示算法为 sha1的标识符,0x800c 为 sha256的标识符。
  • SignatureCheck.cpp 的 ciCheckSignedFileWrapper 函数第 83 至 88 行为签名文件的数字摘要,其内容在 validateFileUsingCiValidateFileObject 第 158 至 170 中获取并打印。由于文件内容不同,签名得到的数字摘要也会不同。
  • SignatureCheck.cpp 的 parsePolicyInfo 函数打印数字证书的证书链上每个证书的详细情况及摘要。其中要注意的是此摘要是数字证书的摘要,即使文件内容不同,只要签名的数字证书为同一个,其摘要的内容是一样的,这个在以后可以加以利用。
  • SignatureCheck.cpp 的 validateFileUsingCiValidateFileObject 第139 行和 第 141 行的大小必须大于等于64,否则 CiValidateFileObject 将返回“缓冲区大小太小”。
  • SignatureCheck.cpp 的 validateFileUsingCiValidateFileObject 中使用 CiValidateFileObject 获取签名的摘要时要注意只能获取第一个签名的摘要,比如同时双签名了 sha1 以及 sha256 证书,获取到的证书摘要只有 sha1的,而无法获取到 sha256的,因此如果要再获取 sha356 的摘要,需要再使用只有 sha256 签名证书的文件进行分析。
  • SignatureCheck.cpp 中使用的 CiValidateFileObject 以及 CiCheckSignedFile 是在 ci.dll 中导出,链接时需要一些方法,详见 <<5.链接 Ci.dll>>。

 

4. CiValidateFileObject 和 CiCheckSignedFile 函数说明

4.1 CiValidateFileObject 

  用来验证文件的数字签名是否正确,它验证了整个证书链,如果文件在签名后进行了修改,此验证不通过。

  它验证完的同时会返回一个证书链的相关信息,我们可以判断证书链中的一些数字证书的摘要来确定是否为相应的签名。

4.2 CiCheckSignedFile 

  用来检查相应的摘要签名,通过传入PE文件的 IMAGE_DIRECTORY_ENTRY_SECURITY 表,以及给定的摘要数据来进行判断签名是否有效。此摘要是整个文件的数字签名摘要,可以通过 CiValidateFileObject 来获取,并不是证书的摘要。

 

5.链接 Ci.dll

  《内核中的代码完整性:深入分析ci.dll》文中也指出了相关方法,即使用 lib 工具来创建一个.lib文件。

  先创建一个.def文件,内容如下:

LIBRARY ci.dll
EXPORTS
CiCheckSignedFile
CiFreePolicyInfo
CiValidateFileObject

  然后使用 lib 工具执行命令:

lib /def:ci.def /machine:x64 /out:ci.lib

 

6.测试

  加载驱动,然后运行一个有数字签名的程序。该程序添加了数字签名。

  

  加载驱动后的打印消息如下:

  

   去掉数字签名后如下:

  

  再加载驱动调试信息如下:

  

  两种签名验证的方法都不通过。

posted @ 2023-12-03 11:28  禁锢在时空之中的灵魂  阅读(334)  评论(0编辑  收藏  举报