深入分析CVE-2020-0601 CurveBall漏洞

概述

在2020年1月,Microsoft发布的月度安全补丁中包含针对CVE-2020-0601的修复程序,该漏洞是由美国国家安全局(NSA)发现,漏洞位于Windows CryptoAPI系统中的一个核心加密库组件,影响加密证书的验证过程。该漏洞被称为CurveBall或“Chain of Fools”,攻击者可能会利用这一漏洞创建他们自己的加密证书,这些证书会成为Windows默认情况下完全信任的合法证书。

在漏洞披露的两天之内,概念深入分析CVE-2020-0601 CurveBall漏洞。

证明(PoC)开始在网络上浮出水面。随之而来的是关于该漏洞所设计的椭圆曲线密码学(ECC)概念的几种解释。

本文将主要分析漏洞存在的代码,重点关注应用程序可能如何使用CryptoAPI处理证书的上下文,特别是通过传输层安全性(TLS)进行通信的应用程序上下文,以找到漏洞的根本原因。

证书分析

X.509是国际电信联盟(ITU)标准,使用ASN.1表示方法规定了公钥证书的结构。基本上,证书是一个包含三个“第一层”项目的序列,包括证书、签名算法标识符和对证书进行验证的签名。下面的ASN.1代码片段展示了这种结构:

证书本身是包含多个嵌套项目的几个组件的序列:

其中,特别重要的是SubjectPublicKeyInfo项,这个序列中包含有关公钥所使用的算法的信息,后面跟着实际的公钥:

AlgorithmIdentifier结构用于存储公钥和签名算法的信息和参数,它由对象标识符(OID)和可选参数组成,具体取决于由OID标识的特定算法:

在公钥的上下文中,算法字段可能是众多OID中的一个,例如rsaEncryption,其OID为1.2.840.113549.1.1.1,dsa的OID为1.2.840.10040.4.1,ecPublicKey的OID为1.2.840.10045.2.1。当OID与ecPublicKey对应时,表示公钥基于椭圆曲线密码学。在这种情况下,会将参数字段设置为与RFC 3279中定义的EcpkParameters对应的选项之一:

换而言之,可以通过提供与众所周知的“命名曲线”之一(隐式指定曲线的参数)相对应的OID,或通过在ecParameters中显式定义曲线参数的方式,来定义椭圆曲线。当攻击者提供带有ecParameters,而不是命名曲线的特定证书时,就会产生这个漏洞。但是,为什么会发生这样的情况呢?首先我们迅速介绍一下椭圆曲线。

关于椭圆曲线

椭圆曲线是由以下方程式定义:

y^2 = x^3 + ax + b

在椭圆曲线密码学中,该方程的解是在有限域的范围内计算的。有限域(也称为Galois域)是具有有限数量元素的集合,通常是通过使用质数或质数的幂(可以表示为GF(pn))对域进行模运算来创建的。针对质数集合(也就是幂的n为1),指定椭圆曲线中的点是由{0,1,2,…,p-1}范围内的x和y坐标组成。集合中元素的数量称为域的阶,所以椭圆曲线的阶是由曲线上的所有点组成。

用于ECC的椭圆曲线定义了基点(Generator Point),这是曲线上的一个特定点,可以用于“生成”曲线上的任何其他点。这一过程是通过将点乘以有限域的阶范围内的某个整数来实现的。对于命名曲线来说,系数a和b、域标识符(通常是质数p)和基点都是预先确定的,并且在每条曲线的官方标准中进行了记录。

但是,如果证书中定义了显示的ecParameters,则曲线的所有参数都会被明确选择并显示在证书中,如ECParameters的ASN.1结构所示:

在漏洞的上下文中,可以以允许攻击者使用其创建的私钥生成证书的方式操纵这些参数。公用密钥与现有证书的公用密钥相同。通常,需要修改基点,并使所有其他曲线参数与原始证书上使用的命名曲线的预定参数相同。

随后,攻击者可以使用这个新制作的“受信任”证书和私钥来对其他证书进行签名,然后将制作的证书和称为“最终”证书的其他证书同时提供给目标。然后,目标将尝试使用攻击者提供的证书以及Windows证书存储区中包含的受信任证书的组合,对证书链进行验证。而这一验证过程中存在漏洞,因此,我们首先研究安全补丁中产生的改动,以深入了解Crypto API的工作原理。

分析补丁修改内容

在Windows CryptoAPI中包括多个不同的库,而其中的crypt32.dll中存在漏洞。我们对比最新未修复版本(10.0.18362.476)的DLL和已修复版本(10.0.18362.592)的DLL之间的二进制差异,发现了这两个文件之间的细微变化:

在已修复的版本中,增加了5个值得关注的新函数,并且对5个现有函数进行了更改。我们判断,很可能是由于增加了新的函数,所以需要对现有函数进行更新以利用这些新的函数。值得庆幸的是,Microsoft为绝大多数Windows组件提供了调试符号,因此我们仅通过检查函数的名称就可以获得很多信息。

首先,我们来看看变更函数的名称,crypt32.dll中已修改函数的名称如下:

我们可以看到,“CCertObject”类的构造函数和析构函数都已经进行了略微更改,但最大幅度的修改位于ChainGetSubjectStatus()和CCertObjectCache::FindKnownStoreFlags()之中。

接下来,我们查看新增函数的名称,crypt32.dll中新增函数的名称如下:

其中的几个新函数值得我们进行关注,我们找到了其中一个适合起步的函数,我们就从它开始入手。ChainLogMSRC54294Error()函数是一个新的日志记录功能,可以帮助Windows事件日志记录潜在的漏洞利用尝试。可以通过跟踪以下块来确定该函数的作用:

在这里,将一个包含CVE编号的字符串传递到名为CveEventWrite的外部库函数,这是一个相对较新的事件跟踪API函数,将基于CVE的事件写入到Windows事件日志中。

借助这个信息,我们可以检查对这一新函数的交叉引用,以发现有关记录事件的上下文的更多详细信息。在这里,对于ChainLogMSRC54294Error()的唯一直接引用是在ChainGetSubjectStatus()函数中,而这个函数存在较大的修改。

对函数ChainGetSubjectStatus()进行分析,以查看对ChainLogMSRC54294Error()的引用:

我们对新日志记录函数调用的周边环境进行查看,发现调用该函数是根据CryptVerifyCertificateSignatureEx()和ChainComparePublicKeyParametersAndBytes()的调用中特定结果而进行调用的,其中的ChainComparePublicKeyParametersAndBytes()是补丁中添加的新函数之一。因此,通过分析该函数的改动以及对其进行调用的上下文,发现ChainGetSubjectStatus()是一个不错的目标。

CryptoAPI中证书验证的内部工作原理分析

为了了解ChainGetSubjectStatus()是如何工作的,我们必须首先研究通常情况下程序是如何使用CryptoAPI来处理证书。

实际上,我们的分析工作是在PowerShell的Invoke-Webrequest cmdlet上执行的,但是其他TLS客户端可能会以类似的方式来运行。首次加载PowerShell时,将通过调用CertOpenStore来获取包含显式受信任证书的系统证书存储句柄,以便在必要时可以使用这些存储。随后,这些证书存储会被添加到一个“集合”中,该集合实际上相当于一个大型的证书存储集中地。

当使用像Invoke-Webrequest这样的命令,通过TLS向服务器发送HTTP请求时,服务器将发出TLS证书握手消息,其中包含最终证书以及可能用于验证证书链的其他证书。在收到这些证书后,将创建一个额外的“内存中”存储,并额外调用CertOpenStore()。随后,通过函数CertAddEncodedCertificateToStore()将接收到的证书添加到这个新的存储中,该函数将创建一个CERT_CONTEXT结构,该结构包含证书存储的句柄、指向原始编码证书的指针以及指向一个CERT_INFO结构(该结构基本对应于证书的ASN.1结构)的指针。

举例来说,下面这些是在最终证书调用CertAddEncodedCertificateToStore()后创建的结构。

通过调用CertAddEncodedCertificateToStore()创建的结构:

其中有一些地方非常关键。颁发者应该与该证书的签名方所属的证书标题完全匹配,而SubjectPublicKeyInfo结构中包含相同名称的ASN.1结构,其中包含相同名称的ASN.1结构中包含的精确信息。例如,由于算法标识符OID为1.2.840.10045.2.1,我们可以看到最终证书公钥的椭圆曲线:

我们还可以通过查看参数的第一个字节,来迅速确定是命名曲线还是显式曲线参数:

在这里,0x6是对象标识符的DER编码标签,表示已经指定了命名曲线。如果提供了显式的ecParameter,则该标签的值将为0x30,与序列相对应。

一旦将所有接收到的证书添加到内存存储中后,PowerShell就会使用CERT_CONTEXT结构调用CertGetCertificateChain()函数作为最终证书,以构造证书链的上下文,其中也包括所有收到的中间证书,如果可能的话会返回到受信任的根证书。

具体而言,CertGetCertificateChain()将返回CERT_CHAIN_CONTEXT结构,该结构是证书链的数组,以及包含有关链的有效性的数据可信状态结构。这一过程主要在CCertChainEngine::GetChainContext()函数中进行处理,该函数最终将返回一个CERT_CHAIN_CONTEXT结构,其中包含证书链的验证状态。

CCertChainEngine::GetChainContext()调用CCertChainEngine::CreateChainContextFromPathGraph(),该方法首先创建一个新的集合。然后,从集合中找到最终证书(所有证书都存储于这个集合中),并使用最终证书的CERT_INFO调用ChainCreateCertObject()。ChainCreateCertObject()尝试在创建的缓存中找到现有的CCertObject,如果无法找到,则会实例化一个新的CCertObject。

CcertObject的构造函数执行以下操作:初始化一些字段,从CERT_INFO结构中复制各种属性(包括签名哈希值、密钥标识符等),查找证书扩展中是否设置了任何策略,并检查CryptoAPI是否不支持任何标记为“关键”的扩展。

然后,构造函数调用ChainGetIssuerMatchInfo()来检索CERT_AUTHORITY_KEY_ID_INFO结构,该结构标识符用于签署证书的密钥。然后,它检查证书是否是自签名的,以及公钥是否使用长度较弱的RSA算法。

在创建最终证书的CCertObject后,将创建一个CChainPathObject。在执行了CChainPathObject的主要初始化之后,构造函数将调用CChainPathObject::FindAndAddIssuers(,这最终将导致对CChainPathObject::FindAndAddIssuersFromStoreByMatchType()的调用,以查找与最终证书的发行者相匹配的证书,这一过程通常根据最终证书颁发者的哈希值进行比较。如果在存储区中找到了另一个证书(即,该证书与最终证书一同发送),则会再次调用ChainCreateCertObject(),这次使用最终证书颁发者的证书的CERT_INFO结构。

这一过程将循环重复执行,直到找到自签名证书(也就是根证书)为止,这会导致对ChainGetSelfSignedStatus()的调用。首先,这一过程会检查主题和颁发者是否匹配,如果匹配,将会调用CryptVerifyCertificateSignatureEx()以验证证书上的签名,从证书的CERT_INFO结构中获得CERT_PUBLIC_KEY_INFO结构中提供的公钥和密钥算法信息。

有很多验证过程是在函数I_CryptCNGVerifyEncodedSignature()中执行的。如果证书提供与指定曲线相反的显式椭圆曲线参数,则调用函数I_ConvertPublicKeyInfoToCNGECCKeyBlobFull()来填充包含曲线参数的BCRYPT_ECCFULLKEY_BLOB结构,这个结构没有出现在正式的文档中,但可以在Windows SDK的bcrypt.h标头中找到。随后,在调用CNGECCVerifyEncodedSignature()时会使用这些参数,这里会调用bcrypt库函数BCryptVerifySignature()来执行给定参数和签名的实际验证。

如果验证通过,则会调用CCertIssuerList::AddIssuer()函数,该函数最终将创建新的CChainPathObject。然后,在对CCertIssuerList::CreateElement()的调用中使用最终证书和自签名证书的CChainPathObjects,这个调用过程首先会进行一些初始化,然后使用两个CChainPathObjects调用ChainGetSubjectStatus()。

ChainGetSubjectStatus()首先使用与每个CChainPathObjects相关联的CCertObjects来调用ChainGetMatchInfoStatus(),以验证两个标题是否相同。然后,检查CCertObject中与最终证书关联的标志是否存在。

调用ChainGetMatchInfoStatus()并检查与最终证书关联的CCertObjects中的标志:

在创建CCertObject时,这个标志初始化为0。这将导致对CryptVerifyCertificateSignatureEx()的调用,以验证自签名证书是否是最终证书的有效颁发者。

调用CryptVerifyCertificateSignatureEx()进行证书验证:

如果认为签名有效,则将CERT_ISSUER_PUBLIC_KEY_MD5_HASH属性添加到最终证书,并将上述标志设置为3。

将CERT_ISSUER_PUBLIC_KEY_MD5_HASH属性添加到最终证书:

一旦CCertIssuerList::AddIssuer()函数返回,并且所有其他函数返回到原来的CChainPathObject::FindAndAddIssuers()调用,就会检查最终证书中的上述标志,如果设置了该标志,则会再次调用CChainPathObject::FindAndAddIssuersFromStoreByMatchType()和CChainPathObject::FindAndAddIssuersFromStoreByMatchType()。

这次,将会搜索包含系统证书信任列表的集合存储,并为FindElementInCollectionStore()提供一个包含搜索参数的CERT_STORE_PROV_FIND_INFO结构,该结构将遍历集合中的存储,在每个存储中搜索是否存在相匹配的自签名证书公钥MD5哈希值。

如果找到相应的公钥哈希值,则再次调用ChainCreateCertObject(),这次使用从系统存储中检索到的证书的CERT_CONTEXT结构,创建新的CCertObject,由于证书来源于“已知存储”,所以会在这一过程进行标记。然后,将该对象作为颁发者对象添加到CCertObjectCache,并添加到颁发者列表,再次创建新的CChainPathObject并使用CChainPathObjects调用CCertIssuerList::CreateElement()以获得自签名证书和从存储中检索到的受信任证书。

在调用ChainGetSubjectStatus()时,现在会设置上述标志,从而导致会将此前在自签名证书上设置的CERT_ISSUER_PUBLIC_KEY_MD5_HASH与来自受信任存储区证书中公钥的MD5哈希值进行比对。

如果找到匹配项,则不会再对从受信任存储中检索到的证书进行自签名证书的进一步验证。这表明,此时已经认为所提供的证书是可信的,因为它已经与证书存储中检索到的公钥哈希值相匹配。并且,提供的自签名证书已经成功验证最终证书的签名。如果攻击者向证书提供与受信任根证书相同的公钥,并使用显式定义的椭圆曲线参数制作,那么最终证书的签名将会受到信任,就如同合法的根证书签名一样。

一旦所有函数返回到CCertChainEngine::CreateChainContextFromPathGraph(),就会产生这种信任。认证过程中的每个对象(即:最终证书和制作的根证书)都执行额外的检查,例如通过对比当前时间与证书吊销状态来确认证书的有效性。然后,通过调用CChainPathObject::CreateChainContextFromPath()来创建CERT_CHAIN_CONTEXT结构,并设置其中包含的CERT_TRUST_STATUS结构,以反映证书链的有效性。

一旦CertGetCertificateChain()函数返回,主应用程序就可以检查CERT_CHAIN_CONTEXT结构中包含的证书链验证状态,该结构在我们所描述的攻击场景中将显示为“有效”。

验证CERT_CHAIN_CONTEXT结构中包含的证书链的状态:

总结

总而言之,根据我们对CVE-2020-0601的分析,得出了以下几点结论:

1、最终证书的签名,使用特制的根证书和任意椭圆曲线参数进行验证。

2、如果再次使用其中包含的任何椭圆曲线参数,都将导致特制的根证书的签名被验证为自签名证书。

3、通过查找公钥的哈希值,在系统证书存储区中找到了与特制根证书匹配的值,从而将特制的根证书视为合法的根证书。

4、检查特制根证书和合法根证书的公钥哈希值,如果哈希值匹配,那么就不会再对特制根证书进行进一步验证,从而通过了特制根证书的全部验证过程。

经过对Microsoft的补丁程序进行分析,我们发现他们添加了对新函数ChainComparePublicKeyParametersAndBytes()的调用,替换了颁发者和受信任根公钥哈希值之间的简单比较,取而代之的是将受信任根证书与证书之间的公钥参数和字节进行了比较,以此来验证最终证书上的签名是否合法。

如果比较结果不匹配,则会调用CryptVerifySignatureEx(),使用实际的受信任根证书、参数等所有内容来重新验证最终证书上的签名,从而有效发现与实际受信任根证书加密参数不同的特制根证书。

安全建议与解决方案

我们建议个人和组织应该尽快安装Microsoft的最新补丁,以防止CurveBall漏洞的进一步利用。用户还可以通过漏洞评估工具确认是否受到CVE-2020-0601的威胁。

通过Trend Micro深度安全防护系统和漏洞防护解决方案,可以保护系统和用户免受该漏洞的威胁:

1010130-Microsoft Windows CryptoAPI Spoofing Vulnerability (CVE-2020-0601)

1010132-Microsoft Windows CryptoAPI Spoofing Vulnerability (CVE-2020-0601) – 1

使用MainlineDV过滤器,可以有效防范利用CVE-2020-0601的威胁和攻击:

36956: HTTP: Microsoft Windows CryptoAPI Spoofing Vulnerability

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章