Windows平台上PE文件的数字签名有两个作用:确保文件来自指定的发布者和文件被签名后没有被修改过。因此有些软件用数字签名来验证文件是否来自家厂商以及文件的完整性,安全软件也经常通过验证文件是否有数字签名来防误报。但是因为windows对于常用的数字签名验证API- WinVerifyTrust实现的问题,以及一些并不恰当的示例代码,很多地方在验证数字签名时存在卡慢或者安全性不高的问题。本文将介绍数字签名的常见使用方法、常见的验证代码、验证原理、以及在使用过程中碰到的问题,并且提出一种在某些场景下安全性更高,速度更快的验证方法,以及其他的一点小东西。有些地方并未完全搞明白,估计有些错误,如有发现的话,欢迎指出,但是喷的时候轻点啊。
目录
1 如何使用工具给文件加签名验签名
1.1 生成证书
平时我们对PE文件加签名的时候使用的是从CA签发的证书,但是我们自己测试的时候也可以用微软的证书生成工具makecert可以生成测试用证书。下面的命令第一行成了一个自签名的根证书,然后用根证书签发一个子证书。
D:\sign_test>makecert -n "cn=root" -r -sv test_root.pvk test_root.cer Succeeded D:\sign_test>makecert -n "cn=child" -iv test_root.pvk -ic test_root.cer -sv test_child.pvk test_child.cer Succeeded |
当然这种证书是不被系统所信任的,需要手动把生成的证书导入到系统。
1.2 嵌入式签名
使用微软的signtool工具可以对PE文件进行嵌入式签名,执行signtool signwizard即可打开GUI界面进行向导式签名。
签名后的效果图:
1.3 编录签名
编录签名是将签名数据放到一个后缀为.cat的编录文件中,并不嵌入到PE文件中,所以右键查看文件属性是看不到数字签名这个标签的。这种签名方法可以对任意格式的文件签名,并不局限于PE文件。微软的系统文件基本都是用这种方式签名的,以至于有人会将这个称为微软编录签名,其实这种方式不只微软可以使用。
①,创建一个cdf文件,以签名7zFM.exe为例:
[CatalogHeader]Name=softsigntest.cat[CatalogFiles]<hash>7zFM=7zFM.exe |
②,从上一步创建的cdf文件生成cat文件
C:\signtest>makecat -v softsigntest.cdf opened: softsigntest.cdf processing: 7zFM Succeeded |
③,执行signtool signwizard对softsigntest.cat进行签名。
④,现在验证PE文件的签名的话,还不能通过验证,需要把cat文件导入到系统中。
C:\signtest>signtool verify -v -pa -a 7zFM.exe Verifying: 7zFM.exe File is signed in catalog: C:\WINDOWS\system32\CatRoot\{F750E6C3-38EE-11D1-85E5- 00C04FC295EE}\softsigntest.cat Signing Certificate Chain: Issued to: root Issued by: root Expires: 2040-1-1 7:59:59 SHA1 hash: B858A2990D04DED1C72334C9764CB9B0F15DCCC8 Issued to: child Issued by: root Expires: 2040-1-1 7:59:59 SHA1 hash: 325ADFF8533B319DD3EF98DB1896FB46456DCD18 File is not timestamped. Successfully verified: 7zFM.exe Number of files successfully Verified: 1 Number of warnings: 0 Number of errors: 0 |
2 常见的验证签名代码
BOOL CheckFileTrust( LPCWSTR lpFileName ) { BOOL bRet = FALSE; WINTRUST_DATA wd = { 0 }; WINTRUST_FILE_INFO wfi = { 0 }; WINTRUST_CATALOG_INFO wci = { 0 }; CATALOG_INFO ci = { 0 }; HCATADMIN hCatAdmin = NULL; if ( !CryptCATAdminAcquireContext( &hCatAdmin, NULL, 0 ) ) { return FALSE; } HANDLE hFile = CreateFileW( lpFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL ); if ( INVALID_HANDLE_VALUE == hFile ) { CryptCATAdminReleaseContext( hCatAdmin, 0 ); return FALSE; } DWORD dwCnt = 100; BYTE byHash[100]; CryptCATAdminCalcHashFromFileHandle( hFile, &dwCnt, byHash, 0 ); CloseHandle( hFile ); LPWSTR pszMemberTag = new WCHAR[dwCnt * 2 + 1]; for ( DWORD dw = 0; dw < dwCnt; ++dw ) { wsprintfW( &pszMemberTag[dw * 2], L"%02X", byHash[dw] ); } HCATINFO hCatInfo = CryptCATAdminEnumCatalogFromHash( hCatAdmin, byHash, dwCnt, 0, NULL ); if ( NULL == hCatInfo ) // 编录中没有则验证是否有嵌入式签名 { wfi.cbStruct = sizeof( WINTRUST_FILE_INFO ); wfi.pcwszFilePath = lpFileName; wfi.hFile = NULL; wfi.pgKnownSubject = NULL; wd.cbStruct = sizeof( WINTRUST_DATA ); wd.dwUnionChoice = WTD_CHOICE_FILE; wd.pFile = &wfi; wd.dwUIChoice = WTD_UI_NONE; wd.fdwRevocationChecks = WTD_REVOKE_NONE; wd.dwStateAction = WTD_STATEACTION_IGNORE; wd.dwProvFlags = WTD_SAFER_FLAG; wd.hWVTStateData = NULL; wd.pwszURLReference = NULL; } else // 编录中有,验证编录文件的签名是否有效 { CryptCATCatalogInfoFromContext( hCatInfo, &ci, 0 ); wci.cbStruct = sizeof( WINTRUST_CATALOG_INFO ); wci.pcwszCatalogFilePath = ci.wszCatalogFile; wci.pcwszMemberFilePath = lpFileName; wci.pcwszMemberTag = pszMemberTag; wd.cbStruct = sizeof( WINTRUST_DATA ); wd.dwUnionChoice = WTD_CHOICE_CATALOG; wd.pCatalog = &wci; wd.dwUIChoice = WTD_UI_NONE; wd.fdwRevocationChecks = WTD_STATEACTION_VERIFY; wd.dwProvFlags = 0; wd.hWVTStateData = NULL; wd.pwszURLReference = NULL; } GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2; HRESULT hr = WinVerifyTrust( NULL, &action, &wd ); bRet = SUCCEEDED( hr ); if ( NULL != hCatInfo ) { CryptCATAdminReleaseCatalogContext( hCatAdmin, hCatInfo, 0 ); } CryptCATAdminReleaseContext( hCatAdmin, 0 ); delete[] pszMemberTag; return bRet; } |
这段网上的代码基本流程是没有问题的,很多同学自用验签名代码或许也是从这个抄来的。但是这里也有几个小毛病:
①,如果只是要验证嵌入式签名的话,对于没有签名或者签名非嵌入式的文件这里会白白进行了整个文件的读取和HASH计算。这段网上的代码基本流程是没有问题的,很多同学自用验签名代码或许也是从这个抄来的。但是这里也有几个小毛病:
②,有个特殊情况,文件被进行嵌入式签名以后还是可以进行编录签名。如果编录签名失效而嵌入式签名有效的话,这里会返回验证失败。呃,当然,估计一般人没毛病也不会在一个文件上做两种方式的签名。
③,这里似乎有一处笔误。WINTRUST_DATA::fdwRevocationChecks的赋值在编录签名和嵌入式签名的分支不一样,而且WTD_STATEACTION_VERIFY是给dwStateAction填的枚举值,虽然值是一样的,但这么还是不妥。看到这里的同学可以看看自己的验证代码是不是从这里抄来的,是不是错的一样。
3 验证原理
3.1 嵌入式签名
数字签名的验证在取出签名后大概分为这几个步骤,首先从PE文件中取出数字签名,然后校验文件本身的签名,然后是校验证书链一直到根证书,最后对比文件摘要是否与数字签名中携带的一致。下面的描述仅针对PE文件的数字签名验证。先上一张微软文档里面的图,大概说明了数字签名在PE文件中所处的位置,以及自身的格式:
①,取出数字签名
PE头的Data Directories中Certificate Table里面指明了WIN_CERTIFICATE的存放位置和大小,WIN_CERTIFICATE的bCertificate就是是SignedData格式的签名。
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; // PE文件的偏移 DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; typedef struct _WIN_CERTIFICATE { DWORD dwLength; // WIN_CERTIFICATE 的长度(含bCertificate的大小) WORD wRevision; WORD wCertificateType; BYTE bCertificate[ANYSIZE_ARRAY]; // signedData开始的位置 } WIN_CERTIFICATE, *LPWIN_CERTIFICATE; |
②,校验文件本身的签名
文件签名本身是遵循PKCS7标准中的SignedData格式,用ASN1表述的格式如下:
SignedData ::= SEQUENCE { version Version, digestAlgorithms DigestAlgorithmIdentifiers, contentInfo ContentInfo, -- 这个里面包含了PE文件的Hash certificates --证书的数组(不包括根证书) [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL, Crls [1] IMPLICIT CertificateRevocationLists OPTIONAL, signerInfos SignerInfos } -- 签名者的信息 SignerInfos ::= SET OF SignerInfo |
PKCS7是加密消息的语法标准,后面会提的X509是证书的格式。ASN1是一种描述对象结构的语法,在一行的定义中可以简单的认为前面的是变量名后面的是类型。ASN1并未定义编码方法,后面会提到的DER是一种常见的编码方法。
SignerInfos的结构如下:
SignerInfo ::= SEQUENCE { version Version, issuerAndSerialNumber IssuerAndSerialNumber, digestAlgorithm DigestAlgorithmIdentifier, authenticatedAttributes -- 内含SignedData中contentInfo的摘要 [0] IMPLICIT Attributes OPTIONAL, digestEncryptionAlgorithm DigestEncryptionAlgorithmIdentifier, encryptedDigest EncryptedDigest, -- 加密后的摘要 unauthenticatedAttributes [1] IMPLICIT Attributes OPTIONAL } IssuerAndSerialNumber ::= SEQUENCE { issuer Name, serialNumber CertificateSerialNumber } EncryptedDigest ::= OCTET STRING |
authenticatedAttributes包含了contentType和messageDigest,messageDigest内就是对SignedData的ContentInfo做的摘要。对authenticatedAttributes做摘要得到一个DigestInfo结构的数据,DigestInfo的结构如下:
DigestInfo ::= SEQUENCE { digestAlgorithm DigestAlgorithmIdentifier, digest Digest } Digest ::= OCTET STRING |
用IssuerAndSerialNumber找到签名者的证书,使用里面的公钥解密EncryptedDigest得到一个DigestInfo结构(一般是RSA算法),将这个结构与authenticatedAttributes做摘要得到的结构对比,一致的话才进行下一步。
③,验证证书链
相关结构如下:
-- X509的证书格式 Certificate ::= SEQUENCE { tbsCertificate TBSCertificate, -- 证书主体 signatureAlgorithm AlgorithmIdentifier, -- 签名用的算法,一般为sha1RSA signatureValue BIT STRING } -- 证书的签名 TBSCertificate ::= SEQUENCE { version [0] EXPLICIT Version DEFAULT v1, -- PE文件数字签名用的版本为3 serialNumber CertificateSerialNumber, signature AlgorithmIdentifier, issuer Name, validity Validity, -- 有效期 subject Name, subjectPublicKeyInfo SubjectPublicKeyInfo, -- 含有这个证书的公钥 issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, -- If present, version MUST be v2 or v3 subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, -- If present, version MUST be v2 or v3 extensions [3] EXPLICIT Extensions OPTIONAL -- 扩展 -- If present, version MUST be v3 } -- 含有公钥的信息 SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING } |
首先构建证书链,从终端签发数字签名的证书一直到自签名的根证书。这时要了解到证书最后一个成员为扩展,扩展是一列其他的数据,其中两项比较重要的是AuthorityKeyIdentifier和SubjectKeyIdentifier,结构分别如下
AuthorityKeyIdentifier ::= SEQUENCE { keyIdentifier [0] KeyIdentifier OPTIONAL, authorityCertIssuer [1] GeneralNames OPTIONAL, authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL } KeyIdentifier ::= OCTET STRING SubjectKeyIdentifier ::= KeyIdentifier |
组建证书链时,将把A证书的中的AuthorityKeyIdentifier(简称AKID) 的keyIdentifier、authorityCertIssuer、authorityCertSerialNumber与B证书的SubjectKeyIdentifier(简称SKID)、issuer 、serialNumber分别匹配,如果匹配上则B证书为A证书的签发者。如果A证书的上面三项与自己对应数据匹配上,则A证书为自签名的证书,证书链构建完毕。
然后,校验证书链中每个证书的签名、有效期和用法(是否可以用于代码签名)。签名验证的算法为证书中的signatureAlgorithm,签名是signatureValue,被签名的数据为tbsCertificate,公钥从父证书的subjectPublicKeyInfo里面拿。
④,计算PE文件的Hash,并与签名数据中的Hash对比。
签名数据中的Hash算法和Hash在SignedData的contentInfo中,contentinfo的结构为:
ContentInfo ::= SEQUENCE { contentType ContentType, content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL } |
contentType 是SPC_INDIRECT_DATA_OBJID (1.3.6.1.4.1.311.2.1.4),表明了content的类型。content是是一个SpcIndirectDataContent结构的数据。
SpcIndirectDataContent ::= SEQUENCE { data SpcAttributeTypeAndOptionalValue, messageDigest DigestInfo } --#public— DigestInfo ::= SEQUENCE { digestAlgorithm AlgorithmIdentifier, digest OCTETSTRING } |
digestAlgorithm就是Hash算法,一般为sha1。digest就是文件的Hash。
Hash的计算原则为排除且仅排除掉签名过程中可能会改动的数据以及数字签名本身。大概计算过程如下:
除去PE头中checksum和Certificate Table计算PE头的HASH(含 Section Table),按每个节偏移的顺序依次对每个节的数据算HASH,对PE附加数据算HASH。附加数据的起始偏移为PE头大小+每个节的大小,附加数据大小=文件大小-(PE头+每个节)-签名的大小,签名的大小是Optional Header Data Directories[Certificate Table].Size。
另外,由这个Hash算法可以看出,PE文件的签名数据都是放到PE文件最尾部的,因为只有附加数据最末尾一段为签名数据大小的数据是没有计算在PE的Hash内的。
3.2 编录签名
编录文件的签名微软并未公开格式文档,所以大概就是靠微软一些边角的资料去猜。
编录文件的签名数据格式应该是跟嵌入式签名一样的。这里说下不一样的地方,微软编录文件的签名和PE文件是分开的,cat文件中存放着文件的Hash,而且可以存放多个文件的Hash,对cat文件中的多个Hash进行了一个签名,怀疑微软就是为了这个能节约体积做出来编录签名这个东西。编录签名导入系统后存放在%windir%\system32\catroot。但是这个在验证上就带来了问题,验证某个文件时,那么多cat文件都要打开看下有没有这个文件的Hash?效率太低了吧,微软似乎有个很聪明的作法,做个服务Cryptographic Services都给加载起来嘛,验证的时候跨进程通信来找我。呃,这些都是我猜的,谁知道的话,请告诉我。
4 使用时遇到的问题
4.1 WinVerifyTrust API的卡慢
WinVerifyTrust,这玩意里面的问题很多。目前已知有在以下几个地方可能有问题。
①,验证CRL(销证书列表)时使用的WinINet系列API性能不好。这个系列的API是出了名的不稳定,而且据说可能会导致堆破坏,实际使用中我们也发现过因为这里导致卡死的情况,但是找不到DUMP了,└(T_T;)┘。
②,枚举证书时的卡死,这是我们拿到的一个DUMP,此处卡了近10分钟。
③,如果验证编录签名的话,我们曾经发现过一个跨进程通信的卡死,在调用栈中可以看到验证签名的线程的栈顶上有几个线程有RPC的字样。
④,枚举签名时的死循环。据说在XP系统上WinVerfiyTrust在校验PE文件嵌入式签名时,循环枚举签名时没有判断签名的长度,如果PE文件被破坏,签名长度为0的话,WinVerfiyTrust会陷入死循环。
4.2 安全性
4.2.1 恶意软件可能会导入证书到系统中
恶意软件是在运行后可以通过CertAddCertificateContextToStore自己导入根证书,虽然windows系统会弹出警告框,但是可以模拟点击鼠标来点击警告框的确认。
下面是一个效果图:
4.2.2 滥发的证书
一般的CA都是有基本的安全概念的,对于证书的颁发都会比较谨慎。但是某些有途径向用户电脑插入根证书的厂商对于证书的颁发并不谨慎,比如工行网银的U盾中的证书并未限制用途,可以给PE文件签名。见下图
支付宝以前也爆过这个漏洞,现在已经被补了,下面这个是乌云上的截图。
这种证书签名的文件在对应装了工行或者支付宝根证书的机器上都是可以通过数字签名验证的,但是由于其证书是颁发给个人的,所以文件并不能认为是安全的。
5 一种更加快速安全的验证方法和一段验证代码
上面说了了数字签名验证在实际使用中的不少问题,那么,如果想快速验证签名而且保证安全的话,那么要怎么办呢?提出的问题主要是在于两点,一个是WinVerfiyTrust的卡慢问题,一个是证书被滥用或者用户环境被污染的问题。那么,要搞的话,可以写验证签名的代码,并且带一个可信的根证书列表下去,验证通过后,将整个证书链所有证书的信息都通过自己的CS协议发送到自己的后台来验证证书是否可信,这样我们还可以通过后台来干掉吊销列表中的证书。
只是对于PE文件的的签名校验,却有不少代码要写,包括签名数据ASN1编码格式数据的解析,签名的验证和证书链的验证,但是这种通用的代码我们可以从开源库里去找比如OpenSSL,下面是一段东拼西凑抄来的代码,可以验证PE文件的数字签名,但是这段代码很粗糙,有些地方并未完全搞明白,而且为了缩短篇幅去掉了大部分错误处理,轻喷,只是为了试下这条路能不能走通。
do { LPWIN_CERTIFICATE pCert; // .. 省略代码,从PE文件获取pCert 的地址 const unsigned char* pCertificate = pCert->bCertificate; DWORD dwPKCS7Len = pCert->dwLength - offsetof(WIN_CERTIFICATE, bCertificate); PKCS7* pPkcs7 = d2i_PKCS7(NULL, &pCertificate, dwPKCS7Len); // DER编码转换为openssl的内部格式 // 获取PE文件的摘要,这个可以直接调用CryptCATAdminCalcHashFromFileHandle,也可以按照文档的描述来自己计算 CAutoVectorPtr pSignSha1; DWORD dwSignSha1Len = 0; GetFileHashForSign(lpszPEPath, pSignSha1, dwSignSha1Len); // 对比文件的HASH与签名中的hash,抄来的代码 BOOL bMathc = IsHashMatch(pPkcs7, pSignSha1, dwSignSha1Len); if (!bTmp) { cout << endl << "PE Image Hash dismatch" << endl; break; } cout << "PE Image Hash match" << endl; // 把根证书放到X509_STORE中 X509_STORE *pX509Store = X509_STORE_new(); AddRootCerToStore(pX509Store, lpszCertPath); X509_STORE_set_purpose(pX509Store, X509_PURPOSE_ANY); // PKCS7_verify 验证的时候是把内容当做V_ASN1_OCTET_STRING 来验证的, // 但PE文件的数字签名里这里是一个V_ASN1_SEQUENCE // 所以需要手动把V_ASN1_SEQUENCE 的内容取出来放到BIO 里面给PKCS7_verify 来验证 // 毕竟openssl不是为了PE文件数字签名验证写的,用起来还是有点别扭的 cout << "Verifying PKCS #7..."; int seqhdrlen = asn1_simple_hdr_len(pPkcs7->d.sign->contents->d.other->value.sequence->data, pPkcs7->d.sign->contents->d.other->value.sequence->length); BIO* pContentBio = BIO_new_mem_buf(pPkcs7->d.sign->contents->d.other->value.sequence->data + seqhdrlen, pPkcs7->d.sign->contents->d.other->value.sequence->length - seqhdrlen); // 校验数字签名 int nOk = PKCS7_verify(pPkcs7, pPkcs7->d.sign->cert, pX509Store, pContentBio, NULL, PKCS7_NOCRL); if (1 != nOk) { cout << "failed" << endl; } else { cout << "ok" << endl; } } while (0); |
另外,我还简单的写了一个验证数字签名的小DEMO,拼代码的过程好辛苦的,DEMO效果如图:
6 其他
6.1 签名后真的完全不可以修改了?
从签名验证原理的文件HASH计算过程中,可以看到,PE文件几乎整个的被算了HASH,但是某些地方因为要写入签名相关的数据而被从HASH过程中排除。比如Data Directories中Certificate Table中指定了存放数字签名的位置和大小,这个就没有被计算在HASH过程中,我们可以在文件尾部存放一些数据后,调整Certificate Table中记录的大小。当然,这样的做法并不能对别人签发的PE文件做什么东西,但是我们可以对自己的文件做点事,比如如果文件有在签名后向尾部添加或者修改附加数据的需求,就可以这么做。据说chrome的安装包就在签名后修改PE文件来写入一些安装信息,http://blog.didierstevens.com/2013/08/13/a-bit-more-than-a-signature/。
参考资料:
Windows Authenticode Portable Executable Signature Format:http://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/Authenticode_PE.docx
验证微软数字签名:http://blog.titilima.com/show-184-1.html
Event ID 256 — System Catalog Database Integrity:https://technet.microsoft.com/en-us/library/cc734083(v=ws.10).aspx
PKCS #7: Cryptographic Message Syntax:http://www.ietf.org/rfc/rfc2315.txt
Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile:http://www.rfc-editor.org/rfc/rfc5280.txt
你好,最近在分析pe文件的签名。你的文章给我了很大帮助。能将你的demo源码给我来一份吗?
Pingback: 对Windows 平台下PE文件数字签名的一些研究 – 小飞侠