Windows驱动中数字签名认证(使用 ci.dll)

发布时间 2023-12-03 11:29:41作者: 禁锢在时空之中的灵魂

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 中导出,链接时需要一些方法,详见 <<4.链接 Ci.dll>>。

 

4.链接 Ci.dll

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

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

LIBRARY ci.dll
EXPORTS
CiCheckSignedFile
CiFreePolicyInfo
CiValidateFileObject

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

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

 

5.测试

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

  

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

  

   去掉数字签名后如下:

  

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

  

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