[2021]R0驱动内对Windows数字签名验证的大概流程 huoji 驱动,数字签名,windows,M$ 2021-09-23 872 次浏览 0 次点赞 因为某些原因我不得不抛弃wintrust机制而自己在内核实现驱动数字签名验证,因此有了这篇文章: 首先PE文件有两种信息,一个是catalog一个是pe自带签名,目前文章只介绍PE自带签名,catalog签名较为简单,不做论述 数字签名验证分为几步: 1. 解析pe拿到security data 2. 通过asn1解析"signed security data"拿到证书信息(最为复杂) 3. 交叉验证证书链上的证书,拿到root证书,这个过程中还需要对证书是否被窜改进行确认,这个过程中确认时间戳 4. 检查root证书是否可信 5. 拿到证书hash算法,并且计算PE的hash,再拿PE的hash跟证书hash进行匹配,这一步确认PE是否有效 由于某些问题,不能直接开源代码,但是能稍微的介绍一下这个过程: ### 首先PE文件的数字签名在这个datadirctory里面: ```cpp pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY] ``` 他跟其他的datadirctory,有两个选项,一个是VirtualAddress一个是Size,但是,他跟普通的datadirctory不一样的是,他的"virtualaddress"是偷懒的结果,实际上是叫做absolutelyAddress,是的,你不需要对其做rva操作,直接就可以读得到 ```cpp WinCertData = (WIN_CERTIFICATE*)((char*)FileBaseAddress + SigData.dwVirtulAddress); ``` 注意,datadirctory不仅是一个,他有多个签名记录,比如sha1、sha256 会放在一起,因此你需要遍历他,**而且这里有个历史遗留问题是证书大小他不会直接给你,而是你自己通过dwLength-offset(bCertificate)计算** WIN_CERTIFICATE的结构是 ```cpp typedef struct _WIN_CERTIFICATE { DWORD dwLength; WORD wRevision; WORD wCertificateType; BYTE bCertificate[ANYSIZE_ARRAY]; } WIN_CERTIFICATE, * LPWIN_CERTIFICATE; ``` 此外目录是8字节对齐,使用round函数进行对齐操作 ```cpp WinCertData = (WIN_CERTIFICATE*)((char*)FileBaseAddress + SigData.dwVirtulAddress); //复制证书 result = STATUS_SUCCESS; while (WinCertOffset < SigData.dwSize) { if (IterNum > MAX_CERT_NUM) break; if (WinTrustVerifySignedFile(FileBaseAddress, pPESize, (WIN_CERTIFICATE*)((char*)WinCertData + WinCertOffset)) == FALSE) { goto _TAG_END; } unsigned int length = *(unsigned int*)((char*)WinCertData + WinCertOffset); WinCertOffset += sizeof(int) + sizeof(short) + sizeof(short); /* unsigned short revision = *(unsigned short*)((char*)WinCertData + WinCertOffset); WinCertOffset += sizeof(revision); unsigned short type = *(unsigned short*)((char*)WinCertData + WinCertOffset); WinCertOffset += sizeof(type); */ //八字节对齐 WinCertOffset += Round(length, 8); IterNum++; break; } ``` ### ASN1解码 拿到了PE的证书后,我们需要进行ASN1的解码,微软只支持一种签发oid,叫做"1.2.840.113549.1.7.2" ![](https://key08.com/usr/uploads/2021/09/4260327851.png) 并且需要确认有没有签名者 ![](https://key08.com/usr/uploads/2021/09/2748085331.png) 有了后就需要解码asn1,解码asn1在win7下名字叫做MinAsn1ParseSignedDataCertificatesEx,在ci.dll的特征是 ```cpp \x4C\x8B\xDC\x49\x89\xCC\xCC\x49\x89\xCC\xCC\x55\x56\x57\x41\x54\x41\x55\x41\x56\x41\x57\x48\xCC\xCC\xCC\x8B\x02\x4C\x8B\xCC\xCC\x33\xDB\x4D\x8B\xF9\x49\x8B\xF0\x8B\xEB\x89\x44\xCC\xCC\x39\x19\x0F\xCC\xCC\xCC\xCC\xCC\x8B\x11\x4D\x8D\xCC\xCC\x4D\x8D\xCC\xCC\x49\x8B\xCC\xE8\xCC\xCC\xCC\xCC\x3B\xC3\x7F\xCC\x83\xCC\xCC\xE9\xCC\xCC\xCC\xCC ``` ### MinAsn1ParseSignedDataCertificatesEx 解码分为两步,内存展开数据,解析数据 内存展开数据操作如下 ![](https://key08.com/usr/uploads/2021/09/2003383442.png) 长度计算 ![](https://key08.com/usr/uploads/2021/09/1821859991.png) 核心展开 ```cpp for (i=cbLength, pb=pbLength+1; i>0; i--, pb++) *pcbContent = (*pcbContent << 8) + (const DWORD)*pb; ``` 不要吐槽微软的代码,实际上当你发现M$的代码99%都是错误异常处理和判断,1%是核心功能的时候,你已经麻木了 解析数据操作如下: 根据解出的操作数,决定解出的数据 ![](https://key08.com/usr/uploads/2021/09/935200933.png) ### MinTrustCryptDecodeHashAlgorithmIdentifier 解密出来后,这一步是解出证书是否是支持的hash,并且拿到证书的hash类型,这一段微软写的非常复杂,个人总结如下: 1. 前面的函数解析出来了拿到rgAlgIdBlob,因此rgAlgIdBlob[MINASN1_ALGID_OID_IDX].cbData就是"cbEncodedOID"(加密类型的oid) ![](https://key08.com/usr/uploads/2021/09/2613232661.png) 2. 拿这个oid进行比对,检查是否是支持的算法 核心代码如下: ```cpp cbEncodedOID = rgAlgIdBlob[MINASN1_ALGID_OID_IDX].cbData; pbEncodedOID = rgAlgIdBlob[MINASN1_ALGID_OID_IDX].pbData; for (i = 0; i < HASH_ALG_CNT; i++) { if (cbEncodedOID == HashAlgTable[i].cbEncodedOID && 0 == memcmp(pbEncodedOID, HashAlgTable[i].pbEncodedOID, cbEncodedOID)) { HashAlgId = HashAlgTable[i].AlgId; break; } } ``` 至此拿到了证书的支持的加密类型(目前有这几个,一个是sha1,sha256,sha512),XP下比较奇怪,支持MD5和md2 ### MinCryptHashMemory 解出证书类型后,使用这个函数对证书数据进行hash win7下的ci.dll特征如下: ```cpp \x40\x53\x55\x56\x57\x41\x54\x48\xCC\xCC\xCC\xCC\xCC\xCC\x48\xCC\xCC\xCC\xCC\xCC\xCC\x48\x33\xC4\x48\x89\xCC\xCC\xCC\xCC\xCC\xCC\x48\x8B\xCC\xCC\xCC\xCC\xCC\xCC\x8B\xF9\x49\x8B\xF0\x8B\xEA\x48\x8D\xCC\xCC\xCC\x41\xCC\xCC\xCC\xCC\xCC\x33\xD2\x4D\x8B\xE1\xE8\xCC\xCC\xCC\xCC\x48\x8D\xCC\xCC\xCC\x48\x8B\xD3\x89\x7C\xCC\xCC\xE8\xCC\xCC\xCC\xCC\x85\xED\x74\xCC\x48\x8B\xDD\x44\x8B\x06\x45\x85\xC0\x74\xCC\x48\x8B\xCC\xCC\x48\x8D\xCC\xCC\xCC\xE8\xCC\xCC\xCC\xCC ``` 这个函数说白了,就是把一段内存做hash ![](https://key08.com/usr/uploads/2021/09/1785654234.png) ### WinTrustCryptVerifyCertificate 我们需要首先对之前解压的asn1的内存用MinCryptHashMemory进行hash,hash完毕后,对其与证书链上的证书进行校验 如果证书被篡改,那么证书链上的证书也就会校验不过,防止篡改 ![](https://key08.com/usr/uploads/2021/09/1794208038.png) 如果校验没问题,通过证书公钥去寻找根证书 ![](https://key08.com/usr/uploads/2021/09/3625851624.png) 有意思的是,XP和win7的内核里面,根证书是在内存里面的 ![](https://key08.com/usr/uploads/2021/09/255454695.png) 只有是在记录的根证书,证书校验才通过,否则证书就不可信 ### MinTrustCryptVerifySignedHash 拿到根证书后,还需要对签名者和这个证书进行hash比较,一样的套路,一样的配方,不过这次是MINASN1_SIGNED_DATA_SIGNER_INFO_ENCYRPT_DIGEST_IDX而不是前面的MINASN1_SIGNED_DATA_CERTS_IDX 简单来说,还是跟前面差不多 ![](https://key08.com/usr/uploads/2021/09/1011810534.png) 拿到签名者的RSA公钥,**并且转换成Bsafe的公钥** https://key08.com/index.php/2021/09/20/1328.html 这个bsafe非常有意思,即便是XP源码他也是没有开源的,并且为什么会费尽周折的转Bsafe的公钥而不走常规rsa加密解密呢?也许是历史遗留问题 拿到bsafe加密后的信息后,与前面的hash进行比较 ![](https://key08.com/usr/uploads/2021/09/2809467112.png) 比较函数核心代码如下 ![](https://key08.com/usr/uploads/2021/09/3350162946.png) 只要当签名者hash=证书hash(防篡改),证书链的hash校验通过(防篡改),证书的root是受信root的时候(控制是否受信) 才能允许这个证书进行接下来的PE校验 ### PE的hash计算 未完待续... 本文由 huoji 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。 点赞 0
还不快抢沙发