无需Windows系统验证Windows二进制文件:深入解析μthenticode库

本文详细介绍了Trail of Bits开源的μthenticode库,该库支持在非Windows环境下验证Windows PE二进制文件的Authenticode签名。内容涵盖Authenticode技术原理、PKCS#7解析、证书表结构、哈希计算及时间戳反签名机制,为开发者提供完整的跨平台签名验证解决方案。

验证Windows二进制文件,无需Windows系统 - Trail of Bits博客

TL;DR:我们开源了新库μthenticode,用于在非Windows机器上验证Windows PE二进制文件的Authenticode签名。该库已集成到最新版Winchecksec中,您现在即可使用它来验证Windows可执行文件的签名!

作为库,μthenticode旨在轻松集成:采用跨平台现代C++编写,避免了所替代的CryptoAPI接口(即WinVerifyTrust和CertVerifyCertificateChainPolicy)的复杂性。您现在可将其用作SignTool多项功能的替代品,更多功能正在开发中。

Authenticode快速入门

Authenticode是微软的代码签名技术,其理念(非实现)与苹果的Gatekeeper类似。核心上,Authenticode为签名程序提供(或可提供,作为可选功能)以下属性:

  • 真实性:具有有效Authenticode签名的程序包含足以验证该签名的证书链。该链最终根植于用户"受信任发布者"存储中的证书,防止未经用户明确选择的自签名证书。
  • 完整性:每个Authenticode签名包含签名二进制文件的加密哈希。该哈希在加载时与二进制文件的内存表示进行比较,防止恶意修改。
  • Authenticode还可嵌入每页内存的加密哈希,用于强制完整性签名(Windows内核驱动程序必需),需要特殊的微软交叉签名"软件发布者证书",而非自签名或独立信任的证书颁发机构(CA)证书。
  • 时效性:Authenticode支持时间戳机构(TSA)的反签名嵌入,允许二进制文件上的签名可能超出其签名证书的有效期。此类反签名还可防止有效签名的回溯日期,使攻击者更难重复使用过期的签名证书。

与所有代码签名技术一样,Authenticode无法对程序做出以下保证:

  • 无漏洞:任何人都可使用自签名证书或从经微软交叉签名的CA购买证书来签署有漏洞的软件。
  • 仅运行自身代码:Windows执行合约 notoriously lax(例如,桌面应用程序的DLL加载规则),许多应用程序支持某种形式的代码执行功能(脚本、插件、炫酷的WinAMP皮肤等)。Authenticode无法验证初始签名二进制文件外部执行代码的完整性或意图。

类似地,Authenticode像所有PKI实现一样易受以下问题影响:

  • 错误信任:CA希望销售尽可能多的证书,因此检查购买实体合法性的动机有限。任何人都可以几百美元创建美国LLC。
  • 证书被盗:代码签名和HTTPS证书是盗窃的主要目标;许多现实活动利用被盗证书欺骗用户信任恶意代码。公司定期将秘密材料检入源代码控制系统,代码签名证书也不例外。
  • 欺诈证书:Flame著名地利用MD5上的新颖选择前缀攻击来冒充意外被信任用于代码签名的微软证书。对SHA-1的类似攻击现在对于民族国家和有组织犯罪来说价格合理。

总之,Authenticode(及所有其他形式的代码签名)为二进制文件添加了有用的真实性和完整性检查,前提是您信任签名者及其存储密钥材料的能力。

解析Authenticode签名:辛辣的PKCS#7

对于2000年代微软来说,Authenticode格式的大部分实际上已记录并可下载,这是一个 somewhat unusual move。少数部分明显未定义或标记为"超出范围";我们将在下面介绍其中一些。

核心上,Authenticode有两个组件:

  • 证书表,包含一个或多个条目,每个条目可能是SignedData。
  • SignedData对象, mostly normal PKCS#7容器(按照RFC 2315标记为SignedData内容类型)。

证书表

证书表是将Authenticode签名嵌入PE文件的机制。它具有一些有趣属性:

  • 访问证书表涉及读取数据目录表中的证书表目录。与数据目录表中的所有其他条目不同,证书目录的RVA字段不是虚拟地址——它是直接文件偏移。这反映了Windows加载程序的行为,它实际上不将证书加载到程序的地址空间。
  • 尽管如此,现实工具似乎对证书表的位置和后续解析不灵活。微软的工具 consistently 将证书表放在PE末尾(所有节之后);许多第三方工具天真地寻址到证书表偏移并解析直到EOF,允许攻击者轻松附加额外证书¹。

一旦定位,实际解析证书表很简单:它是8字节对齐的WIN_CERTIFICATE结构blob:

  • wRevision:WIN_CERTIFICATE的"修订版"。MSDN最近修复了此字段的文档:WIN_CERT_REVISION_2_0=0x0200是Authenticode签名的当前版本;WIN_CERT_REVISION_1_0=0x0100用于"传统"签名。我未能在野外找到后者。
  • wCertificateType:封装证书数据的种类。MSDN记录了wCertificateType的四个可能值,但我们只关心一个:WIN_CERT_TYPE_PKCS_SIGNED_DATA。
  • bCertificate:实际证书数据。对于WIN_CERT_TYPE_PKCS_SIGNED_DATA,这是上述(mostly)PKCS#7 SignedData。

如您所推测,证书表的结构允许多个独立Authenticode签名。这对于跨多个Windows版本部署程序非常有用,特别是可能在其受信任发布者存储中具有传统证书或因任何原因不信任特定CA的版本。

Authenticode的SignedData

微软 helpfully² 提供了其SignedData结构的可视化:

这几乎是正常的PKCS#7 SignedData,但有一些关键偏差:

  • 代替RFC 2315内容类型之一,Authenticode SignedData的contentInfo具有SPC_INDIRECT_DATA_OBJID类型,微软定义为1.3.6.1.4.1.311.2.1.4。
  • 与此对象标识符(OID)对应的结构记录为SpcIndirectDataContent。微软方便地提供了其ASN.1定义: (注意自定义AlgorithmIdentifier实际上是X.509的AlgorithmIdentifier——参见RFC 3279及其更新)。

⚠ 下面的代码没有错误处理或内存管理;阅读μthenticode源代码获取完整版本。⚠

给定上面的ASN.1定义,我们可以使用OpenSSL的(hellish且完全未记录的)ASN.1宏来解析微软的自定义结构:

实际检查签名

有了我们的结构,我们可以使用OpenSSL的(mostly)未记录的PKCS#7 API来解析我们的SignedData和间接数据内容:

…然后验证它们:

Voilà:Authenticode的基础。注意我们传递PKCS7_NOVERIFY,因为我们不一定访问整个证书链——只有具有相关证书在其受信任发布者存储中的Windows用户才有。

计算和检查Authenticode哈希

现在我们有了真实性(模根证书),让我们做完整性。

首先,让我们获取嵌入在Authenticode签名中的哈希,用于最终比较:

接下来,我们需要计算二进制文件的实际哈希。这有点复杂,由于几个不同字段:

  • 每个PE有一个32位CheckSum字段,用于基本完整性目的(即意外损坏)。计算哈希时需要跳过此字段,因为它是针对整个文件计算的,并且会随着证书的添加而改变。
  • 证书数据目录条目本身需要跳过,因为重新定位和/或修改证书表的大小不应要求对预先存在的签名进行任何更改。
  • 证书表(及组成签名)本身自然不能是哈希输入的一部分。

为确保一致哈希,Authenticode规定节按每个节头的PointerToRawData值升序哈希,而非节头本身的顺序。这并不特别麻烦,但需要一些额外的簿记。

μthenticode的Authenticode哈希过程实现有点长,无法在下面复制,但伪代码如下:

  • 从空缓冲区开始。
  • 将所有PE头(DOS、COFF、Optional、节)插入缓冲区。
  • 从缓冲区中擦除证书表目录条目和CheckSum字段,按此顺序(以避免重新缩放前者的偏移)。
  • 使用pe-parse的IterSec API构建节缓冲区列表。截至#129,IterSec按文件偏移顺序生成节。
  • 跳过证书表并将尾部数据添加到缓冲区(如果存在)。
  • 使用从签名检索的NID创建并初始化新的OpenSSL消息摘要上下文。
  • 将缓冲区放入EVP_DigestUpdate并使用EVP_DigestFinal完成。
  • 将结果与Authenticode提供的哈希进行比较。

其他零碎

我们尚未讨论剩余两个主要Authenticode功能:页面哈希和时间戳反签名。

页面哈希

如上所述,页面哈希在Authenticode规范中 conspicuously not documented,并描述为存储在"[…]二进制结构[中,该结构超出本文范围。"。

关于所述结构的在线信息仅限于少数资源:

  • VirtualBox源代码引用了两个不同版本页面哈希结构的OID:
    • SPC_PE_IMAGE_PAGE_HASHES_V1_OBJID: 1.3.6.1.4.1.311.2.3.1
    • SPC_PE_IMAGE_PAGE_HASHES_V2_OBJID: 1.3.6.1.4.1.311.2.3.2 这些OID未在微软的OID参考或OID存储库⁴中列出,尽管它们确实出现在Wintrust.h中。
  • osslsigncode的至少一个分支支持生成和验证页面哈希,并给予我们进一步洞察:
    • V1 OID代表SHA-1页面哈希;V2代表SHA2-256。
    • 每个SpcSerializedObject的serializedData是ASN.1 SET,每个成员是ASN.1 SEQUENCE,效果如下: (上面的定义是我从get_page_hash_link主体重建的;osslsigncode confusingly 重用SpcAttributeTypeAndOptionalValue类型用于Impl_SpcPageHash并手动构造SpcSerializedObject的其余内容。)

据我所知,osslsigncode仅为整个PE插入一个Impl_SpcPageHash,它在pe_calc_page_hash中计算。该函数中的代码相当密集,但它似乎生成如下结构表:

…其中IMPL_PAGE_HASH_SIZE由使用的哈希算法确定(即由Impl_SpcPageHash.type),表中的第一个条目是空填充的"页面哈希",仅用于PE头,page_offset=0。此表未给出ASN.1定义——它直接插入到Impl_SpcPageHash.pageHashes中。

时间戳反签名

与页面哈希不同,Authenticode的时间戳反签名格式相对 well documented,在官方和第三方来源中。

正如Authenticode SignedData mostly 是正常的PKCS#7 SignedData,Authenticode的时间戳格式 mostly 是正常的PKCS#9反签名。一些值得注意的位包括:

  • 当向时间戳机构(TSA)发出时间戳请求(TSR)时,请求采用HTTP 1.1 POST形式,包含DER编码、然后base64编码的ASN.1消息: …其中countersignatureType是自定义微软OID 1.3.6.1.4.1.311.3.2.1(即SPC_TIME_STAMP_REQUEST_OBJID),content是原始Authenticode PKCS#7 ContentInfo。
  • TSA响应是PKCS#7 SignedData,从中提取SignerInfo并嵌入到主Authenticode SignedData中。TSA响应的证书类似地作为未验证属性嵌入到证书列表中。

总结

我们上面涵盖了Authenticode的所有四个主要组件:验证签名、根据验证的哈希检查文件完整性、计算页面哈希以及验证任何时间戳反签名。

μthenticode本身仍在进行中,目前仅支持签名和主Authenticode哈希。您可以通过贡献页面哈希解析和验证以及时间戳签名验证支持来帮助我们!

μthenticode的API完全记录并托管,大多数可立即与peparse::parsed_pe *一起使用:

查看svcli命令行工具获取应用示例,包括检索嵌入的Authenticode哈希。

先前工作和参考文献

μthenticode完全从头编写,并使用微软提供的官方Authenticode文档作为其主要参考。当发现不足时,以下资源派上用场:

  • ClamAV的Authenticode文档
  • Peter Gutmann的Authenticode笔记
  • 原始osslsigncode和此分支

以下资源未引用,但在研究此帖子时发现:

  • jsign:Authenticode的Java实现

想要了解我们的开源项目和其他安全创新?联系我们或注册我们的新闻通讯!


¹ 没有此解析错误添加额外证书仍然相对简单,但要求攻击者修改PE的更多部分,而不仅仅是附加到它。↩︎ ² 对于 helpful 的一些定义。↩︎ ³ Authenticode的OID树显示许多其他有趣的OID,大多数未公开记录。↩︎ ⁴ 我已提交它们,等待存储库管理员的批准。↩︎

如果您喜欢此帖子,分享它: Twitter LinkedIn GitHub Mastodon Hacker News

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计