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

本文介绍了Trail of Bits开源的μthenticode库,用于在非Windows环境下验证Windows PE二进制文件的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加载规则),并且许多应用程序支持某种形式的代码执行作为功能(脚本、插件、sick WinAMP皮肤等)。Authenticode无法验证在初始签名二进制文件之外执行的代码的完整性或意图。

同样,Authenticode像所有PKI实现一样,容易受到某些问题的影响:

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

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

说到这里,让我们看看Authenticode的工作原理。

解析Authenticode签名:spicy PKCS#7

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

其核心是,Authenticode有两个组件:

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

证书表

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

它有一些有趣的属性:

  • 访问证书表涉及读取数据目录表中的证书表目录。与数据目录表中的所有其他条目不同,证书目录的RVA字段不是虚拟地址——它是直接文件偏移。这反映了Windows加载程序的行为,它实际上不将证书加载到程序的地址空间中。
  • 尽管如此,现实世界的工具似乎对证书表的放置和后续解析不灵活。微软的工具 consistently 将证书表放在PE的末尾(所有节之后);许多第三方工具天真地寻找到证书表偏移并解析直到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 and completely undocumented)ASN.1宏来解析微软的自定义结构:

实际检查签名

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

…然后验证它们:

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

计算和检查Authenticode哈希

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

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

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

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

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

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

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

其他零碎

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

页面哈希

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

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

  • 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, both in official and third-party sources。

正如Authenticode SignedData mostly a normal PKCS#7 SignedData,Authenticode的时间戳格式 mostly a normal PKCS#9 countersignature。一些值得注意的位包括:

  • 当向时间戳机构(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的更多部分,而不仅仅是附加到它。↩︎ ² 对于某些有用的定义。↩︎ ³ Authenticode的OID树显示许多其他有趣的OID,大多数未公开记录。↩︎ ⁴ 我已提交它们,等待存储库管理员的批准。↩︎

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

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