攻击者利用 Windows CryptoAPI 中的严重欺骗漏洞
编辑和内容补充:Tricia Howard
执行摘要
Akamai 安全情报组最近分析了美国国家安全局 (NSA) 和英国国家网络安全中心 (NCSC) 向 Microsoft 披露的 Windows CryptoAPI 中的一个严重漏洞。
该漏洞编号为 CVE-2022-34689,其 CVSS 评分为 7.5。该漏洞已于 2022 年 8 月得到了修补,但在 2022 年 10 月的 Patch Tuesday 中才公开宣布。
Microsoft 表示,该漏洞让攻击者可以伪装成合法实体。
出现此漏洞的根本原因是假设基于 MD5 的证书缓存索引密钥无冲突。众所周知,自 2009 年以来,MD5 的抗冲突性 已遭到破坏。
攻击流分为两个阶段。第一阶段需要获取合法证书,对其进行修改,然后将修改后的版本提供给受害者。第二阶段会创建一个新证书,其 MD5 与修改后的合法证书相冲突,并使用新证书来掩饰原始证书主体的身份。
在现实环境中,我们已经搜寻了使用 CryptoAPI 且容易被这种欺骗攻击利用的应用程序。到目前为止,我们发现旧版 Chrome(v48 及更早版本)和基于 Chromium 的应用程序可能会被此类攻击利用。我们相信现实环境中有更多易受攻击的目标,并且正在研究当中。
我们发现数据中心中只有不到 1% 的可见设备进行了修补,而其余设备无法免受此漏洞利用。
在这篇博文中,我们详细解释了潜在的攻击流和后果,以及演示完整攻击的 概念验证 (PoC) 。我们还提供了一个 OSQuery,用于检测 CryptoAPI 库的易受攻击版本。
背景
三个月前,在我们的 2022 年 10 月 Patch Tuesday 分析中,我们分享了对 Windows CryptoAPI 中一个严重欺骗漏洞 (CVE-2022-34689) 的基本介绍。Microsoft 表示,该漏洞使得攻击者可以“掩饰其身份并执行身份验证或将代码签名为目标证书等操作”。
CryptoAPI 是 Windows 中的事实 API,用于处理与加密相关的任何操作。特别是处理证书,从读取和解析证书到参照经过验证的证书颁发机构 (CA) 验证证书。浏览器还会使用 CryptoAPI 进行 TLS 证书验证,此过程会生成人人都要检查的锁定图标。
然而,证书验证并非浏览器独有操作,其他 TLS 客户端也执行此操作,例如 PowerShell Web 身份验证、curl、wget、FTP 管理器、EDR 和许多其他应用程序。此外,代码签名证书基于可执行文件和库进行验证,驱动程序签名证书在加载驱动程序时进行验证。可以想象,证书验证过程中的漏洞对于攻击者来说非常有利可图,因为该漏洞让攻击者可以掩饰自己的身份并绕过关键的安全防御措施。
这并非美国国家安全局第一次披露 CryptoAPI 中的漏洞。2020 年,他们发现并披露了 CurveBall (CVE-2020-0601)。利用 CurveBall 或 CVE-2022-34689 会导致身份欺骗,然而,尽管 CurveBall 影响了许多应用程序,但 CVE-2022-34689 的先决条件更多,因此易受攻击的目标范围更有限。
漏洞详情
为了分析该漏洞,我们首先尝试找到经过修补的代码。我们使用 BinDiff(一种常用的二进制文件对比工具)来观察 CryptoAPI 的各种代码变化。在 crypt32.dll 中,只有一个函数发生了变化:CreateChainContextFromPathGraph。在此函数中,比较了两个证书:一个作为输入接收,另一个驻留在接收应用程序的证书缓存中(稍后将详细介绍此缓存)。
检查变化后发现,memcmp 检查已添加到函数的两个位置(图 1)。
在修补之前,该函数仅根据其 MD5 指纹来确定接收到的证书是否已位于缓存中(并因此得到验证)。在修补之后, memcmp 添加要求两个证书的实际内容完全匹配。
据此,我们推测,如果攻击者可以提供一个恶意证书,其 MD5 与受害者证书缓存中已有的证书相冲突,攻击者将能够绕过易受攻击性检查并使其恶意证书受到信任(图 2)。
CryptoAPI 证书缓存
CryptoAPI 可以对接收到的最终证书使用缓存,从而提高性能和效率。默认情况下,此机制处于禁用状态。要启用此机制,应用程序开发人员需要将某些参数传递给 CertGetCertificateChain,这是最终生成易受攻击代码的 Windows API 函数(图 3)。
CertGetCertificateChain 接收了几个值得关注的参数:
hChainEngine ——用于控制证书验证方式的可配置对象
pCertContext ——输入证书的上下文,由 WinAPI 函数 CertCreateCertificateContext 使用输入证书构建的数据结构
dwFlags ——用于指定进一步配置的标志
ppChainContext ——包含(其他字段中)信任状态的输出对象;即链的验证判定
若要启用最终证书的缓存机制,开发人员需要在 dwFlags中设置 CERT_CHAIN_CACHE_END_CERT 标志,或者创建自定义链引擎,然后在其 dwFlags 字段中设置 CERT_CHAIN_CACHE_END_CERT 标志。
为了解缓存的实施和使用方式,我们来看看从缓存中提取证书的 FindIssuerObject 函数。大体上,该函数的行为如下:
根据其 MD5 指纹的四个最低有效字节计算缓存中输入证书的存储桶索引。
如果位于缓存中,该函数将比较缓存证书和输入证书的整个 MD5 指纹。
如果指纹匹配(缓存命中),则输入证书将受到信任并返回。从现在开始, 应用程序使用输入证书属性(如公钥、颁发机构等)而非缓存证书。
如果指纹不匹配(缓存未命中),将转到存储桶中的下一个证书,比较其 MD5 指纹,然后如此反复。
Microsoft 本质上信任缓存证书的有效性,并且在缓存中找到最终证书后不会执行任何额外的有效性检查。这本身就是一个合理的可行假设。然而,代码进一步做出假设,如果两个证书的 MD5 指纹匹配,则两个证书相同。这是一个可以利用的错误假设,也是修补程序的源头。
为了支持我们的假设,我们编写了一个小型应用程序,并在其中使用了 CertGetCertificateChain ,还调试了 crypt32.dll 中的证书验证流。我们使用 WinDbg 模拟了一个场景,在这个场景中,我们自己的(自签名)证书的 MD5 指纹与缓存中已有的合法证书相匹配。如图 4 所示,我们制作的证书可信。
仅绕过一项检查,我们就能让 Windows 相信我们的恶意证书是合法证书。
攻击者如何利用漏洞
使用与给定 MD5 值完全匹配的 MD5 指纹构造证书称为原像攻击,即使在今天,这在计算上也不可行。但是,这种方式可以有效地生成两个具有两个选定前缀的证书,最终将具有相同的 MD5 指纹,这种类型的攻击称为选择前缀冲突。
选择此路径,我们需要以某种方式向受害者应用程序提供两个证书。一个证书将经过正确签名、验证和缓存(我们称之为“修改后的目标证书”)。该证书将以一种有利于选择前缀冲突攻击的方式生成。第二个证书(我们称之为“恶意证书”)将包含欺骗性身份。该证书将与第一个证书的 MD5 指纹相冲突(图 5)。
通过 MD5 冲突进行证书欺骗
MD5 冲突可以追溯到大约 14 年前,那时,碧昂丝发行了单曲《单身女人》,奥巴马首次当选美国总统, MD5 冲突首次用于模拟 SSL 证书。第一次攻击与我们如今处理的场景有一个主要区别:之前的场景攻击的是 MD5 签名,但在当前的漏洞中,我们处理的是 MD5 指纹。让我们来看看到底有何区别。
根据 RFC 5280第 4.1 条,证书是一个包含两个部分的 ASN.1 序列(图 6):
tbsCertificate (或“待签名”证书)——此部分包含所有与身份相关的详细信息(主题、公钥、序列号、EKU 等)。这是进行签名的部分。
signatureAlgorithm 和 signatureValue ——这些字段包含 TBS 的签名。
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING }
图 6:定义证书的 ASN.1 序列
因此,证书 签名 是嵌入在证书中的一种结构,只对证书的 TBS 部分进行签名。另一方面,证书 指纹 是 整个 证书(包括签名)的哈希值。
因此,如果我们可以在不使证书失效的情况下修改 TBS 部分之外的任何证书部分, 那么我们将在不更改签名的情况下修改指纹。如果解析器正确解析签名且 TBS 未更改,则证书仍将被视为有效且已签名,即使完整的证书结构已更改(图 7)。
MD5 选择前缀冲突——简要概述
假设您有两个长度相同的任意字符串 A 和 B。然后,可以高效地计算两个字符串 C 和 D,则
MD5(A || C) = MD5(B || D) |
其中 || 表示字符串连接。
此外,不仅最终的 MD5 结果相同,而且附加 C 或 D 后的 MD5 内部状态也相同。因此,如果您采用任何后缀 E,那么将得到
MD5(A || C || E) = MD5(B || D || E) |
(前提是两边加上相同的后缀 E)。
为冲突块腾出空间
作为攻击者,我们需要生成一个看起来有效但还留有冲突块(上面解释中的字符串 C 和 D)空间的证书。这将支持我们创建我们接下来将提供的恶意证书(具有相同的 MD5 指纹)。
根据 RFC 5280第 4.1.1.2 条, signatureAlgorithm 的结构如下:
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
RSA 算法的参数字段(基于 RFC 3279)“应为 ASN.1 类型 NULL”。换句话说:RSA 不使用签名参数,而是取 NULL 作为值。CryptoAPI 是否有可能忽略 RSA 签名的此字段?
为了向该字段插入占位符字节(作为冲突块准备),我们尝试将其 ASN.1 类型从 NULL 更改为 BIT STRING。针对 CryptoAPI 和 OpenSSL 进行测试, 可正常运行 ,表明证书仍被视为有效。由于我们没有修改 TBS,因此签名未更改且未损坏。(当然,MD5 指纹确实发生了更改。)
证书 MD5 指纹冲突
现在,我们可以把所有环节组合起来,提供一种方法来操纵已经签名的现有证书,使之与恶意证书的 MD5 指纹相冲突。
获取合法的 RSA 签名最终证书,例如网站的 TLS 证书(我们的“目标证书”)。
修改证书 TBS 部分中的任何相关字段(主题、扩展名、EKU、公钥等)以创建恶意证书。注意:我们未触及签名,所以恶意证书被错误地签名了。在这里,修改公钥很重要,这允许攻击者签名为恶意证书。
修改两个证书的 signatureAlgorithm 字段的 parameters 字段,以便有足够的空间让 MD5 冲突块(上面解释中的 C 和 D)从两个证书的相同偏移量处开始。
在要放置 MD5 冲突块的位置截断两个证书。
执行 MD5 选择前缀冲突计算并将结果复制到证书中。
将合法证书的签名值(上面解释中的后缀 E)连接到两个不完整的证书。
真实示例
通过我们对 MD5 冲突的理解,我们现在可以尝试使用真实目标来利用此 CVE。在我们检查的众多应用程序中,我们可以找到一个易受攻击的目标:Chrome v48。(此应用程序很容易受到攻击,因为它将 CERT_CHAIN_CACHE_END_CERT 标志传递给 CertGetCertificateChain。)从那时起,其他基于 Chromium 的应用程序也容易受到此 CVE 的攻击。
为了利用此漏洞,我们首先需要创建两个具有相同 MD5 指纹的证书,我们使用 HashClash 做到了这一点(图 8)。
然后,我们必须找到一种方法将修改后的目标证书注入 Chrome 的缓存。这很难做到,因为在不知晓私钥的情况下无法提供证书。
在 TLS 1.2 中,有两个相关的验证阶段:
(TLS 1.3 与此不同,我们没有关注。)
请记住,在攻击的第一阶段,我们希望将修改后的证书注入 Chrome 的最终证书缓存中。
使用 Python 脚本作为代理,我们执行中间机 (MITM) 攻击:
我们的恶意 MITM 服务器与真实服务器通信,并将 TLS 握手的第一条消息反映给受害者。
在服务器证书消息中,我们的恶意 MITM 服务器修改了真实服务器的消息,并用修改后的证书替换了真实的目标证书。
服务器密钥交换消息无需任何更改就能反映出来。
我们的恶意服务器不能简单地转发服务器握手完成消息,因为握手确实遭到了篡改。因此,我们终止连接。
为了验证服务器密钥交换消息,Chrome 必须使用 CryptoAPI 加载修改后的证书,因此,该证书将被注入到缓存中。Chrome 不会将断开的连接视为 TLS 安全问题,而可能只是一个随机的网络问题。Chrome 尝试重新连接,这一次,恶意服务器不再反映来自真实网站的消息,而是为使用恶意证书的网站提供服务。Chrome 将跳过完整的验证过程,因为它认为证书已经在缓存中。结果将是无缝访问看似合法的 Microsoft 网站(图 9 和 10)。完整的漏洞利用流请观看 我们的视频。
检测
我们提供了一个 OSQuery 来检测易受攻击的库 crypt32.dll 的易受攻击版本(图 11)。 Akamai Guardicore Segmentation 客户可以将 Insight 功能与此查询结合使用来搜索易受攻击的资产。
请记住,如果资产具有未修补版本的 crypt32.dll 并运行易受攻击的应用程序,则会容易受到攻击。(到目前为止,我们只发现 Chrome v48 存在漏洞。)
WITH product_version AS (
WITH os_minor AS (
WITH os_major AS (
SELECT substr(product_version, 0, instr(product_version, ".")) as os_major, substr(product_version, instr(product_version, ".")+1) as no_os_major_substr
FROM file
WHERE path = "c:\windows\system32\crypt32.dll"
)
SELECT substr(no_os_major_substr, instr(no_os_major_substr, ".")+1) as no_os_minor_substr, substr(no_os_major_substr, 0, instr(no_os_major_substr, ".")) as os_minor, os_major
FROM os_major
)
SELECT
CAST(substr(no_os_minor_substr, instr(no_os_minor_substr, ".")+1) AS INTEGER) AS product_minor,
CAST(substr(no_os_minor_substr, 0, instr(no_os_minor_substr, ".")) AS INTEGER) AS product_major,
CAST(os_minor AS INTEGER) AS os_minor,
CAST(os_major AS INTEGER) AS os_major
FROM os_minor
)
SELECT
CASE
WHEN os_major = 6 AND os_minor = 3 THEN "not supported"
WHEN (
(product_major = 20348 AND product_minor >= 887)
OR
(product_major = 17763 AND product_minor >= 3287)
OR
(product_major = 14393 AND product_minor >= 5291)
OR
(product_major >= 19041 AND product_minor >= 1889)
)
THEN
"patched"
ELSE
"not patched"
END is_patched
FROM product_version
结论
证书对在线身份验证起着重要作用,使得此漏洞对攻击者来说有利可图。不过,尽管该漏洞被标记为严重级别,但 CVSS 评分仅为 7.5。我们认为这是由于满足漏洞先决条件的易受攻击应用程序和 Windows 组件的范围有限。
话虽如此,但仍然有很多代码使用此 API 并可能暴露在此漏洞威胁之下,因此即使对于已停用的 Windows 版本(如 Windows 7)也需要进行修补。
我们建议您使用 Microsoft 发布的最新安全修补程序来修补您的 Windows 服务器和端点。对于开发人员来说,缓解此漏洞的另一种选择是使用其他 WinAPI 在使用证书之前仔细检查证书的有效性,例如 CertVerifyCertificateChainPolicy。请记住,不使用最终证书缓存的应用程序不容易受到攻击。
请在以下位置查找我们的 PoC 代码: GitHub 存储库。 您还可以关注我们的公众号,了解所有 Akamai 安全情报组出版物的最新动态。