无需Windows系统验证Windows二进制文件:深入解析Authenticode签名

本文介绍了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实现一样,容易受到以下问题的影响:

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

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

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

对于2000年代的微软来说,一个 somewhat unusual 的举动是,Authenticode格式的大部分实际上已记录并可下载。一些部分 conspicuously under-defined 或标记为“超出范围”;我们将在下面介绍其中一些。

Authenticode的核心有两个组件:

  • 证书表,包含一个或多个条目,每个条目可能是一个SignedData。
  • SignedData对象,主要是普通的PKCS#7容器(根据RFC 2315标记为SignedData内容类型)。

证书表

证书表是将Authenticode签名嵌入PE文件的机制。

它有一些有趣的属性:

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

一旦定位,实际解析证书表很简单:它是一个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签名中的哈希,用于最终比较:

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

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

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

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

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

其他零碎

我们尚未讨论剩下的两个主要Authenticode功能:页面哈希和时间戳计数器签名。

页面哈希

如上所述,页面哈希 conspicuously 未在Authenticode规范中记录,并被描述为存储在“[…]二进制结构[该结构]超出本文范围。”

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

  • 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中计算。该函数中的代码相当 dense,但它似乎生成如下结构表:

…其中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实现

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


¹ 没有此解析错误添加额外证书仍然相对 trivial,但要求攻击者修改PE的更多部分,而不仅仅是附加到它。↩︎

² 对于某些 helpful 的定义。↩︎

³ Authenticode的OID树显示了许多其他有趣的OID,其中大多数未公开记录。↩︎

⁴ 我已提交它们,等待存储库管理员的批准。↩︎

如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

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