记录黑客技术中优秀的内容,传播黑客文化,分享黑客技术精华

CVE-2020-0601 ECC证书欺骗漏洞分析

2020-03-19 11:43

 

作者:Strawberry @ QAX A-TEAM

2020年1月15日,微软公布了1月份的补丁更新列表,其中包括一个椭圆曲线密码(ECC)证书检测绕过相关的漏洞(CVE-2020-0601),该漏洞由NSA发现并汇报给微软。攻击者可利用这个漏洞伪造合法证书,进一步可实现对恶意软件及恶意网站证书的签名。奇安信CERT第一时间发布了该漏洞的修补通告,并于次日对该漏洞进行原理分析及复现。本文主要参考Trend Micro于上月发布的文章《An In-Depth Technical Analysis of CurveBall (CVE-2020-0601)》对此漏洞进行进一步分析,如有不足之处,欢迎批评指正。

声明:本篇文章由 Strawberry@ QAX A-TEAM原创,仅用于技术研究,不恰当使用会造成危害,严禁违法使用 ,否则后果自负。

2020年1月15日,微软公布了1月份的补丁更新列表,其中包括一个椭圆曲线密码(ECC)证书检测绕过相关的漏洞(CVE-2020-0601),该漏洞由NSA发现并汇报给微软。攻击者可利用这个漏洞伪造合法证书,进一步可实现对恶意软件及恶意网站证书的签名。奇安信CERT第一时间发布了该漏洞的修补通告,并于次日对该漏洞进行原理分析及复现。本文主要参考Trend Micro于上月发布的文章《An In-Depth Technical Analysis of CurveBall (CVE-2020-0601)》对此漏洞进行进一步分析。

前置基础

一、证书结构

数字证书是用来认证公钥持有者身份合法性的电子文档,由 CA(Certifacate Authority)来负责签发。数字证书结构的ASN.1语法描述如下,应包括证书内容、签名算法标识符以及对证书内容的签名,其中,签名算法为CA对tbsCertificate进行签名时所使用的算法:

Certificate: :=SEQUENCE {

tbsCertificate TBSCertificate,

signatureAlgorithm AlgorithmIdentifier,

signatureValue BIT STRING

}

证书内容 (tbsCertificate) 是需要被CA签名的信息,其ASN.1语法描述如下:

TBSCertificate: :=SEQUENCE {

version[0] EXPLICIT Version DEFAULT v1,

serialNumber CertificateSerialNumber,

signature AlgorithmIdentifier,

issuer Name,

validity Validity,

subject Name,

subjectPublicKeyInfo SubjectPublicKeyInfo,

issuerUniqueID[1] IMPLICIT UniqueIdentifier OPTIONAL,

subjectUniqueID[2] IMPLICIT UniqueIdentifier OPTIONAL,

extensions[3] EXPLICIT Extensions OPTIONAL

}

证书的签发者 (issuer) 和证书主体 (subject) 分别标识了签发证书的CA实体和证书持有者实体。证书主体公钥信息(subjectPublicKeyInfo)给出了证书所绑定的加密算法和公钥。其ASN.1描述如下:

SubjectPublicKeyInfo::=SEQUENCE{

algorithm AlgorithmIdentifier,

subjectPublicKey BIT STRING

}

其中,algorithm中包含加密算法的OID以及可选的算法参数,OID为对象标识符,可以唯一地确定网络中的对象(不仅仅是加密算法)。比如1.2.840.10045.2.1对应了 id-ecPublicKey,表示椭圆曲线密码算法。

AlgorithmIdentifier: :=SEQUENCE {

algorithm OBJECT IDENTIFIER,

parameters ANY DEFINED BY algorithm OPTIONAL

}

在parameters中,可以通过指定“命名曲线”的方式来指定椭圆曲线参数(隐式指定)。

EcpkParameters: :=CHOICE {

ecParameters ECParameters,

namedCurve OBJECT IDENTIFIER,

implicitlyCA NULL

}

使用openssl工具可解析证书结构,这里使用Windows系统中的ECC证书Microsoft ECC Product Root Certificate Authority 2018。可以看出其 Subject 和 Issuer 字段内容是一致的,因为它是自签名的根证书。其公钥信息为04 c7 11 16 2a … f4 01 07 ac,指定的椭圆曲线为secp384r1:

➜  ~ openssl x509 -in cert.cer -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
14:98:26:66:dc:7c:cd:8f:40:53:67:7b:b9:99:ec:85
Signature Algorithm: ecdsa-with-SHA384
Issuer: C = US, ST = Washington, L = Redmond, O = Microsoft Corporation, CN = Microsoft ECC Product Root Certificate Authority 2018
Validity
Not Before: Feb 27 20:42:08 2018 GMT
Not After : Feb 27 20:50:46 2043 GMT
Subject: C = US, ST = Washington, L = Redmond, O = Microsoft Corporation, CN = Microsoft ECC Product Root Certificate Authority 2018
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (384 bit)
pub:
04:c7:11:16:2a:76:1d:56:8e:be:b9:62:65:d4:c3:
ce:b4:f0:c3:30:ec:8f:6d:d7:6e:39:bc:c8:49:ab:
ab:b8:e3:43:78:d5:81:06:5d:ef:c7:7d:9f:ce:d6:
b3:90:75:de:0c:b0:90:de:23:ba:c8:d1:3e:67:e0:
19:a9:1b:86:31:1e:5f:34:2d:ee:17:fd:15:fb:7e:
27:8a:32:a1:ea:c9:8f:c9:7e:18:cb:2f:3b:2c:48:
7a:7d:a6:f4:01:07:ac
ASN1 OID: secp384r1
NIST CURVE: P-384
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
43:EF:70:87:B8:9D:BF:EC:88:19:DC:C6:C4:6B:75:0D:75:34:33:08
1.3.6.1.4.1.311.21.1:
...
X509v3 Certificate Policies:
Policy: X509v3 Any Policy
Policy: 1.3.6.1.4.1.311.76.509.1.1
CPS: http://www.microsoft.com/pkiops/Docs/Repository.htm

Signature Algorithm: ecdsa-with-SHA384
30:66:02:31:00:a1:c0:49:44:5d:32:55:27:cc:3e:90:6e:25:
22:9d:24:5b:9b:51:35:c7:91:49:49:2a:a3:f9:6f:4f:1c:cd:
dd:9c:e1:b5:57:c9:9e:c2:22:45:9b:06:15:70:1c:45:bf:02:
31:00:c5:d3:28:eb:72:c7:3e:b0:ac:27:09:7f:62:3d:60:79:
e5:92:f1:45:2a:b9:a5:02:e4:60:bb:fe:7a:2b:9c:60:a7:b5:
99:14:f2:b0:be:f0:bb:05:96:56:56:8f:c1:68

二、ECC椭圆曲线密码算法

 ECC椭圆曲线密码(Elliptic curve cryptography)是由Miller和Koblitz提出的一种基于有限域上椭圆曲线的公钥密码学。ECC所基于的数学问题为椭圆曲线离散对数问题。椭圆曲线可形象地被描述为bizzaro台球游戏(https://blog.cloudflare.com/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/),椭圆曲线方程可看作台球边界。

E: y² = x³+ ax + b (mod p)

现有两个点A和B,如果要计算A+B,则将球放在A点,然后朝B点射击。当它撞击曲线,球将以平行于y轴的方向反弹至曲线的另一侧,得到结果C。通过相同的原理可计算出D(A+C)、E(A+D)。

 

如果要计算2A(也就是A+A),就需要选择非常靠近A的点A’并朝其射击。随着A’越来越靠近A,它们之间的连接线也越来越靠近A的切线。也就是说,则将球放在A点,沿着椭圆曲线过A点的切线射击,球在撞击曲线后反弹至曲线的另一侧,得到结果2A。以和上图同样的思路可以计算 A+2A = 3A、A+3A = 4A 等。

当球放在已知的点 G(称为生成器或基点)上时,玩家可选择一个次数d-1,然后按照以上规则从切线方向开始连续从点G射击d-1次(即计算d*G),最终球在停在Q点,其它人很难猜测到这个次数d-1,除非他一直在场。而且如果这个次数很大,那需要的观看时间相应就会很长。在椭圆曲线密码算法中,Q = d×G,d称为私钥,而Q称为公钥。通过d和G计算Q很容易,但是给定Q和G很难确定d(如果d已知,可以采用快速算法得到Q,而如果给定公钥Q和基点G去求d的话,只能从头开始挨个验证,当数据量很大时,是很难推测出私钥d的)。但这只在基点G确定的情况下才成立,因为如果起点G允许任意选取,猜测者可以选取任意的G’和d’,使得d’ ×G’ = Q。最简单的思路是,猜测者声称球根本没有移动,即最终球位置Q就是初始位置G’,令G’= Q、d’= 1,可使得 d’×G’= 1×Q = Q。有了这个新的基点,攻击者就拥有了和公钥Q相对应的新的私钥d’,如果系统不验证参数G,攻击者就可以使用这个假的私钥d’来伪造他人的签名。

ECC不仅支持隐式指定椭圆曲线参数,还允许自定义ecParameters,可以显式指定曲线的所有参数,这为伪造证书提供了基础。下面为ECParameters的ASN.1结构:

ECParameters: :=SEQUENCE {--Elliptic curve parameters

version ECPVer,

fieldID FieldID,

curve Curve,

base ECPoint,
--Base point G

order INTEGER,
--Order n of the base point

cofactor INTEGER OPTIONAL
}--The integer h = #E(Fq) / n

由于之前解析的根证书的ECC参数为secp384r1,我们来看一下该曲线的标准参数,然后对伪造的证书进行解析。比较两者的算法参数,发现除了基点G都可以匹配的上,而伪造证书中的参数G和其公钥值是一样的。所以可以通过检测ECC证书中的Generator (uncompressed)字段和其公钥值是否相同,来判定该证书是否是非法的:

Certificate:
Data:
Version: 3 (0x2)
Serial Number:
24:ef:d1:58:53:74:56:5e:41:4d:c0:88:3b:eb:89:61:a1:90:69:fe
Signature Algorithm: ecdsa-with-SHA256
Issuer: C = US, ST = Washington, L = Redmond, O = Microsoft Corporation, CN = Microsoft ECC Product Root Certificate Authority 2018
Validity
Not Before: Mar 3 12:18:09 2020 GMT
Not After : Apr 2 12:18:09 2020 GMT
Subject: C = US, ST = Washington, L = Redmond, O = Microsoft Corporation, CN = Microsoft ECC Product Root Certificate Authority 2018
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (384 bit)
pub:
04:c7:11:16:2a:76:1d:56:8e:be:b9:62:65:d4:c3:
ce:b4:f0:c3:30:ec:8f:6d:d7:6e:39:bc:c8:49:ab:
ab:b8:e3:43:78:d5:81:06:5d:ef:c7:7d:9f:ce:d6:
b3:90:75:de:0c:b0:90:de:23:ba:c8:d1:3e:67:e0:
19:a9:1b:86:31:1e:5f:34:2d:ee:17:fd:15:fb:7e:
27:8a:32:a1:ea:c9:8f:c9:7e:18:cb:2f:3b:2c:48:
7a:7d:a6:f4:01:07:ac
Field Type: prime-field
Prime:
00:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:fe:ff:ff:ff:ff:00:00:00:00:00:00:00:00:
ff:ff:ff:ff
A:
00:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:fe:ff:ff:ff:ff:00:00:00:00:00:00:00:00:
ff:ff:ff:fc
B:
00:b3:31:2f:a7:e2:3e:e7:e4:98:8e:05:6b:e3:f8:
2d:19:18:1d:9c:6e:fe:81:41:12:03:14:08:8f:50:
13:87:5a:c6:56:39:8d:8a:2e:d1:9d:2a:85:c8:ed:
d3:ec:2a:ef
Generator (uncompressed):
04:c7:11:16:2a:76:1d:56:8e:be:b9:62:65:d4:c3:
ce:b4:f0:c3:30:ec:8f:6d:d7:6e:39:bc:c8:49:ab:
ab:b8:e3:43:78:d5:81:06:5d:ef:c7:7d:9f:ce:d6:
b3:90:75:de:0c:b0:90:de:23:ba:c8:d1:3e:67:e0:
19:a9:1b:86:31:1e:5f:34:2d:ee:17:fd:15:fb:7e:
27:8a:32:a1:ea:c9:8f:c9:7e:18:cb:2f:3b:2c:48:
7a:7d:a6:f4:01:07:ac
Order:
00:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:c7:63:4d:81:f4:
37:2d:df:58:1a:0d:b2:48:b0:a7:7a:ec:ec:19:6a:
cc:c5:29:73
Cofactor: 1 (0x1)
Seed:
a3:35:92:6a:a3:19:a2:7a:1d:00:89:6a:67:73:a4:
82:7a:cd:ac:73
X509v3 extensions:
X509v3 Subject Key Identifier:
43:EF:70:87:B8:9D:BF:EC:88:19:DC:C6:C4:6B:75:0D:75:34:33:08
X509v3 Authority Key Identifier:
keyid:43:EF:70:87:B8:9D:BF:EC:88:19:DC:C6:C4:6B:75:0D:75:34:33:08

X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: ecdsa-with-SHA256
30:64:02:30:4e:9f:c7:5b:ca:1e:bf:fe:d9:4b:25:db:be:58:
d2:1e:32:e9:a5:a9:da:0c:e5:b1:00:25:82:6b:1e:9d:2c:55:
72:f2:a6:c6:4b:94:e1:f6:2a:b6:20:4b:4c:b9:ef:29:02:30:
32:84:a1:19:a2:ae:35:88:dd:b3:e0:77:fe:de:4f:dc:47:55:
45:42:bf:4e:76:3a:ef:d2:57:ae:45:a2:d6:7f:b0:c0:d1:a8:
6f:91:1d:11:c6:99:51:cd:d2:44:90:76

三、证书签名与验证

由于后面会涉及到证书的验证过程,所以在这里简单提一下相关算法及验证流程。

椭圆曲线数字签名算法(ECDSA)

椭圆曲线数字签名算法(ECDSA)是使用椭圆曲线密码(ECC)对数字签名算法(Digital Signature Algorithm,DSA)的模拟。整个签名过程与DSA类似,只是签名中采用的算法为ECC。ECC的简要原理在前面介绍过,下面来看一下数字签名算法DSA,DSA的安全性基于离散对数求解的困难性,一般用于数字签名和认证。以下为DSA签名算法流程:

  1. 发送方使用HASH算法将发送内容加密产生摘要;

  2. 发送方使用私钥对摘要进行再次加密得到数字签名;

  3. 发送方将原文和数字签名传给接收方;

  4. 接收方使用发送方的公钥对签名进行解密得到摘要 ,同时对收到的内容使用同样的HASH算法加密生成摘要;

  5. 接收方将解密后的摘要和使用HASH算法加密生成的摘要进行比对,如果两者一致,则说明传输过程的信息没有被破坏和篡改,签名验证成功。

证书链验证

数字证书采用信任链验证。数字证书的信任锚(信任的起点)就是根证书颁发机构。根证书(root certificate)一般为一个无签名或自签名的证书。证书链的验证一般如下图所示:

  1. 取上级证书的公钥,对下级证书的签名进行解密得出下级证书的摘要digest1;

  2. 对下级证书使用相同的哈希算法进行信息摘要得到digest2;

  3. 判断digest1和digest2是否相等,相等则说明这个下级证书没有被篡改且确实由上级证书签发,下级证书验证通过;

  4. 依次对各个相邻级别证书实施1~3步骤,直到根证书(或者可信任锚点[trusted anchor],此时已经证明这条trust chain上的证书都是正确且可信任的;

补丁对比

补丁对比可以帮助我们快速定位到关键函数或关键位置,可使用 bindiff 对补丁前后的crypt32.dll进行比对,就不截图了。本次补丁主要修改了ChainGetSubjectStatus函数、CCertObjectCache::FindKnownStoreFlags函数及CertDllVerifyMicrosoftRootCertificateChainPolicy函数,并添加了一些函数,如ChainComparePublicKeyParametersAndByte函数、IsRootEntryMatch函数、ChainIsNullOrNoParameters函数、CveEventWrite函数(导入函数)等等。具体情况看下面的对比吧:

ChainGetSubjectStatus函数

ChainGetSubjectStatus函数中增加了对ChainComparePublicKeyParametersAndBytes函数的调用,该函数是补丁中新增的函数。补丁前的代码看上去只比较了两处0x10大小的数据,前提是v35中设置了标志2。

  v35 = *(_DWORD *)(v8 + 0x14);
if ( v35 & 2 )
{
if ( !memcmp((const void *)(v8 + 0xF8), (const void *)(v37 + 0xE8), 0x10u) )
{
LABEL_11:
v16 = (char *)a4 + 4;
goto LABEL_12;
}
*(_DWORD *)a4 |= 8u;
}

补丁后的代码在该流程中加入了ChainComparePublicKeyParametersAndBytes函数,如果该函数返回值大于0,则有可能执行 ChainLogMSRC54294Error函数,在该函数内部会调用CveEventWrite来将检测到的对CVE-2020-0601漏洞的利用情况写入Windows事件日志,所以后续可以从ChainGetSubjectStatus这个函数开始分析。

  v36 = ChainComparePublicKeyParametersAndBytes(
*(int **)(v9 + 0x108),
*(_DWORD *)(v9 + 0x10C),
(int *)(*((_DWORD *)pvIssuer + 3) + 0x3C),
*((_DWORD *)pvIssuer + 3) + 0x44);
v26 = v36 == 0;
if ( v36 > 0 )
{
if ( CryptVerifyCertificateSignatureEx(0, 1u, 2u, pvSubject, 2u, pvIssuer, 0, 0) )
goto LABEL_40;
ChainLogMSRC54294Error((PCCERT_CONTEXT)pvIssuer, *(unsigned int **)(v9 + 0x108));
v26 = v36 == 0;
}

在新增的ChainComparePublicKeyParametersAndBytes函数中有两次比对,分别是对a2、a4相应结构的比对和对a1、a3相应结构的比对。这很可能增加了对ECC算法参数的比较。

signed int __fastcall ChainComparePublicKeyParametersAndBytes(int *a1, int a2, int *a3, int a4)
{
...
v9 = 0;
v10 = 0;
v4 = a1;
if ( a2
&& a4
&& *(_DWORD *)a2 == *(_DWORD *)a4
&& *(_DWORD *)a2
&& !memcmp(*(const void **)(a2 + 4), *(const void **)(a4 + 4), *(_DWORD *)a2) )
{
v5 = 1;
if ( !v4 )
v4 = &v9;
v6 = a3;
if ( !a3 )
v6 = &v9;
if ( *v4 == *v6 )
{
if ( !*v4 || !memcmp((const void *)v4[1], (const void *)v6[1], *v4) )
v5 = 0;
}

CCertObjectCache::FindKnownStoreFlags函数

CCertObjectCache::FindKnownStoreFlags函数中也引入了ChainComparePublicKeyParametersAndByte函数。这里函数的四个参数就比较直观了,补丁前只比较了两个结构体偏移0x44处的结构,加入该函数后,增加了对结构体偏移0x3c处的结构(后面证实了这个结构体偏移0x3c处为算法参数的_CRYPTOAPI_BLOB结构)的比较。

if ( v20 != v19 )
{
if ( *(_DWORD *)(v20 + 0x44) != *(_DWORD *)(v19 + 0x44)
|| (v4 = v17, memcmp(*(const void **)(v20 + 0x48), *(const void **)(v19 + 0x48), *(_DWORD *)(v20 + 0x44))) )
{
if ( !_InterlockedExchangeAdd((volatile signed __int32 *)i, 0xFFFFFFFF) )
{
v21 = *(_DWORD *)(i + 4);
v22 = *(void (__thiscall **)(_DWORD, _DWORD))(v21 + 4);
if ( v19 != v18 )
{
if ( ChainComparePublicKeyParametersAndBytes((int *)(v19 + 0x3C), v19 + 0x44, (int *)(v18 + 0x3C), v18 + 0x44) )
{
if ( !_InterlockedExchangeAdd((volatile signed __int32 *)v6, 0xFFFFFFFF) )
{
v20 = *(_DWORD *)(v6 + 4);
v21 = *(void (__thiscall **)(_DWORD, _DW

CertDllVerifyMicrosoftRootCertificateChainPolicy函数

在CertDllVerifyMicrosoftRootCertificateChainPolicy函数中引入了IsRootEntryMatch函数,补丁前只比较了公钥哈希与硬编码数据。

if ( !CryptHashCertificate2(
L"SHA256",
0,
0,
v8->SubjectPublicKeyInfo.PublicKey.pbData,
v8->SubjectPublicKeyInfo.PublicKey.cbData,
&pbComputedHash,
&pcbComputedHash)
|| pcbComputedHash != 0x20 )
{
LABEL_19:
v13 = 0x800B0109;
v14 = v22 - 1;
goto LABEL_16;
}
if ( v6 & 0x20000 )
{
if ( v6 & 0x10000 )
{
v13 = 0x80070057;
goto LABEL_15;
}
v16 = dword_5CF09EF8;
v17 = 0;
while ( memcmp(v16, &pbComputedHash, 0x20u) )

新加入的IsRootEntryMatch函数的第一个参数为硬编码数据,第二个参数为通过证书公钥计算出的SHA256散列值,第三个参数为SubjectPublicKeyInfo.Algorithm.Parameters结构。

    v9 = (int)&v8->pCertInfo->SubjectPublicKeyInfo.Algorithm.Parameters;
if ( v6 & 0x20000 )
{
if ( v6 & 0x10000 )
{
v10 = 0x80070057;
LABEL_34:
v14 = 0;
goto LABEL_19;
}
pcbComputedHash = 0;
v11 = &off_5CF03210;
v22 = &off_5CF03210;
while ( !IsRootEntryMatch((int)v11, (int)&pbComputedHash, v9) )

在IsRootEntryMatch函数中首先比较a1、a2指向的0x20大小的散列值是否相同,不同的话返回0。然后比较算法参数是否相同,比较长度为*(_DWORD *)a3,成功的话返回1。

signed int __fastcall IsRootEntryMatch(int a1, int a2, int a3)
{
int v3; // esi

v3 = 0;
while ( *(_DWORD *)(a1 + 4 * v3 + 4) == *(_DWORD *)(a2 + 4 * v3) )
{
if ( ++v3 == 8 )
{
if ( **(_DWORD **)a1 == *(_DWORD *)a3
&& !memcmp(*(const void **)(*(_DWORD *)a1 + 4), *(const void **)(a3 + 4), *(_DWORD *)a3) )
{
return 1;
}
return 0;
}
}
return 0;
}

 

漏洞分析

下面从ChainGetSubjectStatus函数开始分析,首先ChainGetSubjectStatus函数中会通过第一个和第三个参数获取pvIssuer和pvSubject,pvSubject和pvIssuer分别存放了证书及其签发者证书的CERT_CONTEXT结构,其中 pbCertEncoded指向了证书编码的缓冲区,cbCertEncoded为其长度。

0:000> dt cert_context 594980
combase!CERT_CONTEXT
+0x000 dwCertEncodingType : 1
+0x004 pbCertEncoded : 0x00568408 "0???"
+0x008 cbCertEncoded : 0x299
+0x00c pCertInfo : 0x00568b98 _CERT_INFO
+0x010 hCertStore : 0x0055ee70 Void

CERT_CONTEXT结构偏移0xc处为包含该证书信息的CERT_INFO结构,这个结构中的内容之前讲的证书结构相吻合。

0:000> dx -r1 ((combase!_CERT_INFO *)0x568b98)
((combase!_CERT_INFO *)0x568b98) : 0x568b98 [Type: _CERT_INFO *]
[+0x000] dwVersion : 0x2 [Type: unsigned long]
[+0x004] SerialNumber [Type: _CRYPTOAPI_BLOB]
[+0x00c] SignatureAlgorithm [Type: _CRYPT_ALGORITHM_IDENTIFIER]
[+0x018] Issuer [Type: _CRYPTOAPI_BLOB]
[+0x020] NotBefore [Type: _FILETIME]
[+0x028] NotAfter [Type: _FILETIME]
[+0x030] Subject [Type: _CRYPTOAPI_BLOB]
[+0x038] SubjectPublicKeyInfo [Type: _CERT_PUBLIC_KEY_INFO]
[+0x050] IssuerUniqueId [Type: _CRYPT_BIT_BLOB]
[+0x05c] SubjectUniqueId [Type: _CRYPT_BIT_BLOB]
[+0x068] cExtension : 0x4 [Type: unsigned long]
[+0x06c] rgExtension : 0x568c20 [Type: _CERT_EXTENSION *]

然后函数会调用ChainGetMatchInfoStatus函数判断证书的签发实体是否正确,验证证书的签发者字段(Issuer)和其签发者证书的证书主体字段(Subject )是否相同。其中,Issuer 和 Subject 均为CRYPTOAPI_BLOB结构,包含cbData(pbData的长度) 和 pbData字段(指向缓冲区):

if ( *(_DWORD *)(v12 + 0x18) != *(_DWORD *)(v7 + 0x30)
|| !*(_DWORD *)(v12 + 0x18)
|| memcmp(*(const void **)(v12 + 0x1C), *(const void **)(v7 + 0x34), *(_DWORD *)(v7 + 0x30)) )
{
v9 = 2;
goto LABEL_8;
}

例:Microsoft ECC Product Root Certificate Authority 2018证书的证书所有者信息的编码数据。

0:000> dx -r1 (*((combase!_CRYPTOAPI_BLOB *)0x568bb0))
(*((combase!_CRYPTOAPI_BLOB *)0x568bb0)) [Type: _CRYPTOAPI_BLOB]
[+0x000] cbData : 0x97 [Type: unsigned long]
[+0x004] pbData : 0x568437 : 0x30 [Type: unsigned char *]

0:000> db 0x568437 l97
00568437 30 81 94 31 0b 30 09 06-03 55 04 06 13 02 55 53 0..1.0 ..U....US
00568447 31 13 30 11 06 03 55 04-08 0c 0a 57 61 73 68 69 1.0...U....Washi
00568457 6e 67 74 6f 6e 31 10 30-0e 06 03 55 04 07 0c 07 ngton1.0...U....
00568467 52 65 64 6d 6f 6e 64 31-1e 30 1c 06 03 55 04 0a Redmond1.0...U..
00568477 0c 15 4d 69 63 72 6f 73-6f 66 74 20 43 6f 72 70 ..Microsoft Corp
00568487 6f 72 61 74 69 6f 6e 31-3e 30 3c 06 03 55 04 03 oration1>0<..U..
00568497 0c 35 4d 69 63 72 6f 73-6f 66 74 20 45 43 43 20 .5Microsoft ECC
005684a7 50 72 6f 64 75 63 74 20-52 6f 6f 74 20 43 65 72 Product Root Cer
005684b7 74 69 66 69 63 61 74 65-20 41 75 74 68 6f 72 69 tificate Authori
005684c7 74 79 20 32 30 31 38 ty 2018

由于第三个参数所指向的CCertObject结构体偏移0x14处的flag被初始化为0,所以第一次调用ChainGetSubjectStatus函数时不会去执行memcmp那段流程。

  v8 = *(_DWORD *)a3;
v34 = *(_DWORD *)(v8 + 0x14);
if ( v34 & 2 )
{
if ( !memcmp((const void *)(v8 + 0xF8), v36 + 0x3A, 0x10u) )
{
LABEL_11:
v17 = (_DWORD *)((char *)a4 + 4);
goto LABEL_12;

随后程序会调用CryptVerifyCertificateSignatureEx函数去验证证书签名。首先通过CryptDecodeObjectEx函数解码证书的pbCertEncoded结构获取StructInfo(长度为0x88),该结构中存放证书元数据、算法OID以及编码后的证书签名,其中OID 1.2.840.10045.4.3.2 代表 ecdsa-with-SHA2,这里同时指定了哈希算法SHA256和签名算法ECDSA。虽然原证书中使用的算法为ecdsa-with-SHA384,而伪造证书使用的是 ecdsa-with-SHA2,但这并不影响后续验证。

0:000> dd 005e9f48 l88/4
005e9f48 0000021f 0056840c 00568e80 00000000
005e9f58 00000000 00000067 005e9f68 00000000
005e9f68 30026530 d3a62109 ef573e49 c8247932
005e9f78 7adc3eb3 7b43c0e5 0761b4be d1e40fad
005e9f88 38d9607b ab746d99 bcbdd35e 48983ede
005e9f98 a1e9c699 ee003102 33beddd1 dfce2156
005e9fa8 d6d55979 680ee477 72cdba97 71805f78
005e9fb8 cd2b929f 3d40d072 fcc9ccac 518420b1
005e9fc8 b42c99a0 baacc899

0:000> db 00568e80
00568e80 31 2e 32 2e 38 34 30 2e-31 30 30 34 35 2e 34 2e 1.2.840.10045.4.
00568e90 33 2e 32 00 ab ab ab ab-ab ab ab ab 00 00 00 00 3.2.............

然后通过CryptFindOIDInfo函数获取散列算法相关的CRYPT_OID_INFO结构,通过CryptHashCertificate2计算证书的散列值,大小为0x20。

0:000> gu
eax=00000001 ebx=005e9f48 ecx=6e81a50e edx=00000001 esi=005e9ff8 edi=00000000
eip=74fe73ab esp=00cf85dc ebp=00cf8608 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
CRYPT32!I_CryptCNGVerifyCertificateSignedContent+0xcf:
74fe73ab 85c0 test eax,eax
0:000> dd 005e9ff8
005e9ff8 aa54c5ce 8ef563af 0ccb1ef9 12ce25a1
005ea008 6510dbbc 96dbbb9b 1aba9a30 532b2e50

然后通过CryptImportPublicKeyInfo函数获取签发者证书的公钥句柄,通过I_CertAllocAndDecodeObject函数以及PkiAsn1ReverseBytes函数解码及调整获取证书中的签名数据,最终调用BCryptVerifySignature函数验证散列值和签名是否匹配。这一流程符合证书的验证流程,即获取证书元数据和数字签名,使用同样的Hash算法计算证书元数据的散列值,使用 Issuer 的公钥对数字签名进行解密,得到解密后的散列值。比较这两个散列值是否相同,相同则通过验证。

memcpy(&v13[v10 - *(_DWORD *)v8], *(const void **)(v8 + 4), *(_DWORD *)v8);
v7 = pbSignature;
memcpy(&pbSignature[cbSignature - *(_DWORD *)(v8 + 8)], *(const void **)(v8 + 12), *(_DWORD *)(v8 + 8));
v9 = BCryptVerifySignature(hKey, 0, pbHash, cbHash, v7, cbSignature, 0);

如果签名验证成功,就将v13[0x3a](其实就是v13偏移0xE8)起始的0x10字节的数据复制到v14处(v12偏移0xF8)。经过溯源发现,v12来自*(DWORD *)a3,也就是证书的CCertObject结构,v13来自*(DWORD *)a1,也就是签发者证书的CCertObject结构。而通过这两个偏移进行的复制的位置恰好是当证书的CCertObject结构体偏移0x14处的flag中0x2被设置时要比较的位置。这个0x10长度的散列很容易让人联想到MD5,而且这个是从签发者证书结构向证书结构中复制的数据,那会不会就是传说中的证书公钥散列值,这个还需在后面进行分析。

if ( v11 )
{
v12 = v33;
CCertObject::SetWeakHash(v33, v27);
v13 = v36;
if ( !(v34 & 1) || (memcmp((char *)v12 + 0xF8, v36 + 0x3A, 0x10u) ? (v19 = -2) : (v19 = v34), !(v19 & 1)) )
{
v14 = (_DWORD *)((char *)v12 + 0xF8);
v29 = 0x10;
v30 = v14;
v15 = (char *)pvSubject;
*v14 = v13[0x3A];
v14[1] = v13[0x3B];
v14[2] = v13[0x3C];
v14[3] = v13[0x3D];
SetProperty(
0x18u,
v15 != 0 ? (unsigned int)(v15 - 48) : 0,
(struct _CONTEXT_ELEMENT *)0x80000000,
(unsigned int)&v29,
0,
v22,
(int)v24);
v12 = v33;
}
*((_DWORD *)v12 + 5) |= 3u;
v6 = v37;
goto LABEL_11;
}

随后调用SetProperty函数为证书添加属性,然后将偏移0x14处的flag设置为3,这个标志也是后面要讨论的。证书的属性链在X结构体偏移 0x2c 处(这个结构体还没分析到先叫它X吧),也就是CERT_CONTEXT结构偏移 -4 的位置。每个属性结构的首4字节表明不同的属性,偏移0x8和0xc处分别为该属性的内容及长度。偏移0x10和0x14处分别为该属性结构的前向指针和后向指针。

0:000> dd ecx
00568eb0 00000001 00000000 00000000 00000003
00568ec0 00568eb0 0055ee70 00000000 00568de0
00568ed0 0055ee70 00000000 00000000 00594788
00568ee0 00000001 00568408 00000299 00568b98
00568ef0 0055ee70 abababab abababab 00000000

0:000> dd 00594788
00594788 0000005c 80000000 0056b490 00000004
00594798 005946d8 00000000 abababab abababab

0:000> dd 005946d8
005946d8 00000019 80000000 005946b0 00000010
005946e8 00594650 00594788 abababab abababab

0:000> dd 00594650
00594650 00000014 80000000 005942d0 00000014
00594660 005942a0 005946d8 abababab abababab

0:000> dd 005942a0
005942a0 0000000f 80000000 00594268 00000020
005942b0 00593ef0 00594650 abababab abababab

0:000> dd 00593ef0
00593ef0 00000004 80000000 0058d278 00000010
00593f00 00000000 005942a0 abababab abababab

注意标志为0x19的属性,其大小也为0x10。经过对比发现,该属性的内容和a3指向的结构体偏移0xE8处0x10大小的数据相同。而本次SetProperty函数要设置的属性标志为0x18,对应于a3指向的结构体偏移0xF8。

0:000> dd 5946b0
005946b0 2848569c b80f667d 65cb78a4 11e4da77
005946c0 abababab abababab 00000000 00000000

0:000> dd 005940d8
005940d8 00000001 00000002 0056b7a8 00000004
005940e8 00000004 00000000 00000002 00000000
005940f8 00000000 00000000 00000003 00000000
00594108 00002000 00000000 00000000 00000000
00594118 00000000 00000000 00000000 00000000
00594128 00568ee0 00000000 00000000 00000000

0:000> dd 005940d8+e8
005941c0 2848569c b80f667d 65cb78a4 11e4da77
005941d0 1e7d9e7d 1da15d8d 074bc8c0 cbedec57

现在回顾一下,签名验证成功后,程序将签发者证书CCertObject结构偏移0xE8处的内容复制给证书CCertObject结构偏移0xF8处,并用此值设置了证书的0x18属性,并将证书CCertObject结构偏移0x14处的flag设置为3。后面我们又验证了证书CCertObject结构偏移0xE8处的内容和标志为0x19的属性是一致的,这说明每个证书都有0x19属性,程序先给签发者证书设置了0x19属性,然后同步0xE8处内容(也有可能先设置0xE8处内容,再设置0x19属性),然后又将该内容复制给其签发证书(通过验证)的CCertObject结构的0xF8处。如果ChainGetSubjectStatus函数再次被调用,且a3还是指向这个证书的CCertObject结构,由于其偏移0x14处的flag已经设置了2这个标志位,所以肯定会执行 memcmp((const void )((DWORD *)a3 + 0xF8), *(DWORD *)a1 + 0xE8, 0x10u),一旦比较成功,函数就会成功返回。

下面我们来看一下证书结构体偏移0xE8处存放的数据从何而来。在CCertObject::CCertObject函数中会初始化CCertObject结构体,后面会执行如下代码,证书的属性就是在这里初始化的。下面这段代码通过CertGetCertificateContextProperty函数获取对应的属性值放进结构体相应的偏移处。这里可以清楚的看到属性0x18、属性0x19以及属性0x14和结构体0xF8、0xE8以及0xE0偏移的对应关系。另外,如果存在0x18属性,就会将结构体偏移0x14处的flag设置1。所以推测flag 2 应该是证书的签名验证成功的标志。

  if ( !CertGetCertificateContextProperty(pCertContext, 0x14u, v12, (DWORD *)v7 + 0x38) )
goto LABEL_88;
pcbData = 16;
if ( !CertGetCertificateContextProperty(pCertContext, 0x19u, (char *)v7 + 0xE8, &pcbData) || pcbData != 0x10 )
goto LABEL_88;
pcbData = 16;
if ( CertGetCertificateContextProperty(pCertContext, 0x18u, (char *)v7 + 0xF8, &pcbData) && pcbData == 0x10 )
*((_DWORD *)v7 + 5) |= 1u;

CertGetCertificateContextProperty函数最终会调用GetProperty函数来获取对应的属性,该函数会调用DefaultHashCertificate函数来计算相应数据的散列值。如果要获取的属性标志为0x19(v10),则v25被设置为指向证书公钥的缓冲区,v24被设置为公钥长度,计算出的散列值及其长度分别存放在 pbComputedHash和pcbComputedHash中。由于v10为标志0x19,所以该函数第一个参数为0x8003。

else
{
cbEncoded = 0;
v26 = (struct _RTL_CRITICAL_SECTION *)DefaultHashCertificate(
(v10 == 3) + 0x8003,
(int)v25,
v24,
&pbComputedHash,
&pcbComputedHash);
v7 = pv;
}
goto LABEL_56;

下面为DefaultHashCertificate函数的流程,如果第一个参数为0x8003,则使用MD5算法计算a2指向数据的散列值,并存放在a4指向的内存中。

v10 = a2;
......
if ( a1 == 0x8003 )
{
v7 = 0x10;
if ( v6 >= 0x10 )
{
MD5Init(&v11);
if ( a3 )
MD5Update(&v11, v10, a3);
MD5Final(&v11);
*a4 = v12;
a4[1] = v13;
v9 = a4 + 2;
*v9 = v14;
v9[1] = v15;
v5 = a4;
}
}

然后通过SetProperty函数设置其0x19属性(公钥散列值),然后再次调用GetProperty将散列值复制到CCertObject结构偏移0xE8处。我们可以来验证一下,使用python计算系统Microsoft ECC Product Root Certificate Authority 2018证书公钥的MD5,和之前复制的数据相比较,再次验证了证书CCertObject结构偏移0xE8处存放的就是该证书公钥MD5值。

>>> import hashlib
>>> a = "\x04\xc7\x11\x16\x2a\x76\x1d\x56\x8e\xbe\xb9\x62\x65\xd4\xc3\xce\xb4\xf0\xc3\x30\xec\x8f\x6d\xd7\x6e\x39\xbc\xc8\x49\xab\xab\xb8\xe3\x43\x78\xd5\x81\x06\x5d\xef\xc7\x7d\x9f\xce\xd6\xb3\x90\x75\xde\x0c\xb0\x90\xde\x23\xba\xc8\xd1\x3e\x67\xe0\x19\xa9\x1b\x86\x31\x1e\x5f\x34\x2d\xee\x17\xfd\x15\xfb\x7e\x27\x8a\x32\xa1\xea\xc9\x8f\xc9\x7e\x18\xcb\x2f\x3b\x2c\x48\x7a\x7d\xa6\xf4\x01\x07\xac"
>>> hex(len(a))
'0x61'
>>> hashlib.md5(a).hexdigest()
'7d9e7d1e8d5da11dc0c84b0757ecedcb'

0:000> db 005941d0 l10
005941d0 7d 9e 7d 1e 8d 5d a1 1d-c0 c8 4b 07 57 ec ed cb }.}..]....K.W...

接下来就是0x14偏移处的flag了,这个值还是在CCertObject::CCertObject中被初始化为0,然后在ChainGetSubjectStatus中验证了证书签名并且设置了0x18属性之后被设置为了3。在后续流程中,由于标志2已被设置,所以在CChainPathObject::FindAndAddIssuers函数中跳出了原来的循环,之后又继续调用了CChainPathObject::FindAndAddIssuersByMatchType函数。

  while ( 1 )
{
v8 = *(int *)((char *)&dword_5CF10DBC + v7);
if ( (1 << (v8 - 1)) & v10 )
{
if ( !CChainPathObject::FindAndAddIssuersByMatchType(this, v8, a2, a3, a4) )
return v5;
if ( *(_DWORD *)(*((_DWORD *)this + 0x1A) + 4) && *(_BYTE *)(v6 + 0x14) & 2 )
break;
}
v7 = v11 + 4;
v11 = v7;
if ( v7 >= 0xC )
return 1;
}
if ( CChainPathObject::FindAndAddIssuersByMatchType(this, 4u, a2, a3, a4) )
return 1;

然后又调用CChainPathObject::FindAndAddIssuersFromStoreByMatchType函数,使用FindElementInCollectionStore函数搜索与之前比较的公钥散列值相匹配的系统信任证书(返回X结构)。

0:000> dd eax
005ea490 00000004 00000000 00010000 00000001
005ea4a0 00579240 005722d0 00000000 00000000
005ea4b0 005763f0 00000000 00000000 005ea468
005ea4c0 00000001 00579380 00000327 00579b40
005ea4d0 005722d0 abababab abababab 00000000
005ea4e0 00000000 00000000 79bed629 0000e6a4
005ea4f0 005ea158 0054a9d8 feeefeee feeefeee
005ea500 61bdd632 1800e6ab 00000003 005ea510

然后通过CCertObject::CCertObject为找到的根证书建立CCertObject结构,然后调用CCertIssuerList::AddIssuer函数,最终会再次调用ChainGetSubjectStatus函数,a1和a3参数分别为系统信任证书和伪造证书签发的证书的结构。由于标志被设置,所以只比较了系统信任证书和伪造证书的公钥散列值(从伪造证书CCertObject结构偏移0xE8处复制到用户证书CCertObject结构偏移0xF8处的数据),如果一样就成功返回。这个时候再来看一眼补丁,新增的ChainComparePublicKeyParametersAndBytes函数有四个参数,从第三、四个参数可以看出,这两个分别为系统信任证书的算法参数(Parameters)和公钥(PublicKey)结构。那么相应的前两个参数应该就是要比较的自签名证书(伪造证书)的算法参数和公钥结构了。

//ChainGetSubjectStatus
v37 = ChainComparePublicKeyParametersAndBytes(
*(int **)(v9 + 0x108),
*(_DWORD *)(v9 + 0x10C),
(int *)(*((_DWORD *)pvIssuer + 3) + 0x3C),
*((_DWORD *)pvIssuer + 3) + 0x44);
v26 = v37 == 0;
----------------------------------------------------------------------------------------------------------
0:000> dt cert_context 05ea4c0
combase!CERT_CONTEXT
+0x000 dwCertEncodingType : 1
+0x004 pbCertEncoded : 0x00579380 "0???"
+0x008 cbCertEncoded : 0x327
+0x00c pCertInfo : 0x00579b40 _CERT_INFO
+0x010 hCertStore : 0x005722d0 Void
0:000> dx -r1 ((combase!_CERT_INFO *)0x579b40)
((combase!_CERT_INFO *)0x579b40) : 0x579b40 [Type: _CERT_INFO *]
[+0x000] dwVersion : 0x2 [Type: unsigned long]
[+0x004] SerialNumber [Type: _CRYPTOAPI_BLOB]
[+0x00c] SignatureAlgorithm [Type: _CRYPT_ALGORITHM_IDENTIFIER]
[+0x018] Issuer [Type: _CRYPTOAPI_BLOB]
[+0x020] NotBefore [Type: _FILETIME]
[+0x028] NotAfter [Type: _FILETIME]
[+0x030] Subject [Type: _CRYPTOAPI_BLOB]
[+0x038] SubjectPublicKeyInfo [Type: _CERT_PUBLIC_KEY_INFO]
[+0x050] IssuerUniqueId [Type: _CRYPT_BIT_BLOB]
[+0x05c] SubjectUniqueId [Type: _CRYPT_BIT_BLOB]
[+0x068] cExtension : 0x5 [Type: unsigned long]
[+0x06c] rgExtension : 0x579bc0 [Type: _CERT_EXTENSION *]
0:000> dx -r1 (*((combase!_CERT_PUBLIC_KEY_INFO *)0x579b78))
(*((combase!_CERT_PUBLIC_KEY_INFO *)0x579b78)) [Type: _CERT_PUBLIC_KEY_INFO]
[+0x000] Algorithm [Type: _CRYPT_ALGORITHM_IDENTIFIER]
[+0x00c] PublicKey [Type: _CRYPT_BIT_BLOB]
0:000> dx -r1 (*((combase!_CRYPT_ALGORITHM_IDENTIFIER *)0x579b78))
(*((combase!_CRYPT_ALGORITHM_IDENTIFIER *)0x579b78)) [Type: _CRYPT_ALGORITHM_IDENTIFIER]
[+0x000] pszObjId : 0x568f47 : "1.2.840.10045.2.1" [Type: char *]
[+0x004] Parameters [Type: _CRYPTOAPI_BLOB]

在第一次调用ChainGetSubjectStatus函数时,如果下级证书的签名验证成功,会分别申请两块内存来存放其签发者证书的算法参数(如果参数存在的话)和公钥数据的结构及数据,即前4个字节为数据的长度,紧接着是指向缓存区的指针。然后分别放在该证书CCertObject结构偏移0x108和0x10C处,以便后续进行比较。

      v17 = PkiZeroAlloc(*(_DWORD *)(v16 + 0x3C) + 8);
*(_DWORD *)(v9 + 0x108) = v17;
if ( v17 )
{
v18 = v42;
*v17 = *(_DWORD *)(v42 + 0x3C);
v17[1] = v17 + 2;
memcpy(v17 + 2, *(const void **)(v18 + 0x40), *(_DWORD *)(v18 + 0x3C));
LABEL_30:
v19 = PkiZeroAlloc(*(_DWORD *)(v42 + 0x44) + 0xC);
*(_DWORD *)(v9 + 0x10C) = v19;
if ( v19 )
{
v20 = v42;
*v19 = *(_DWORD *)(v42 + 68);
v19[1] = v19 + 3;
memcpy(v19 + 3, *(const void **)(v20 + 0x48), *(_DWORD *)(v20 + 0x44));

然后在新增的ChainComparePublicKeyParametersAndBytes函数中分别比较了两个证书的公钥、算法参数以及它们的长度,如果成功的话就返回0。

signed int __fastcall ChainComparePublicKeyParametersAndBytes(int *a1, int a2, int *a3, int a4)
{
...
v9 = 0;
v10 = 0;
v4 = a1;
if ( a2
&& a4
&& *(_DWORD *)a2 == *(_DWORD *)a4
&& *(_DWORD *)a2
&& !memcmp(*(const void **)(a2 + 4), *(const void **)(a4 + 4), *(_DWORD *)a2) )
{
v5 = 1;
if ( !v4 )
v4 = &v9;
v6 = a3;
if ( !a3 )
v6 = &v9;
if ( *v4 == *v6 )
{
if ( !*v4 || !memcmp((const void *)v4[1], (const void *)v6[1], *v4) )
v5 = 0;
}

如果判断失败,则会再次调用CryptVerifyCertificateSignatureEx验证签名,这次就会验证该证书是否由系统信任证书签发,如果没有验证成功,就会进入ChainLogMSRC54294Error函数记录错误日志。

  if ( v37 > 0 )
{
if ( CryptVerifyCertificateSignatureEx(0, 1u, 2u, pvSubject, 2u, pvIssuer, 0, 0) )
goto LABEL_40;
v27 = *(_DWORD *)(v9 + 0x108);
ChainLogMSRC54294Error((PCCERT_CONTEXT)pvIssuer);
v26 = v37 == 0;
}

既然都看到这里了,就给你们展示一下算法参数吧,下面这个是编码后的自定义算法参数,来自伪造根证书(可以拿去和上面解析的证书参数进行比较):

0:000> dx -r1 (*((combase!_CRYPTOAPI_BLOB *)0x568cec))
(*((combase!_CRYPTOAPI_BLOB *)0x568cec)) [Type: _CRYPTOAPI_BLOB]
[+0x000] cbData : 0x15b [Type: unsigned long]
[+0x004] pbData : 0x5691d6 : 0x30 [Type: unsigned char *]
0:000> db 0x5691d6 l15b
005691d6 30 82 01 57 02 01 01 30-3c 06 07 2a 86 48 ce 3d 0..W...0<..*.H.=
005691e6 01 01 02 31 00 ff ff ff-ff ff ff ff ff ff ff ff ...1............
005691f6 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
00569206 ff ff ff ff fe ff ff ff-ff 00 00 00 00 00 00 00 ................
00569216 00 ff ff ff ff 30 7b 04-30 ff ff ff ff ff ff ff .....0{.0.......
00569226 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
00569236 ff ff ff ff ff ff ff ff-fe ff ff ff ff 00 00 00 ................
00569246 00 00 00 00 00 ff ff ff-fc 04 30 b3 31 2f a7 e2 ..........0.1/..
00569256 3e e7 e4 98 8e 05 6b e3-f8 2d 19 18 1d 9c 6e fe >.....k..-....n.
00569266 81 41 12 03 14 08 8f 50-13 87 5a c6 56 39 8d 8a .A.....P..Z.V9..
00569276 2e d1 9d 2a 85 c8 ed d3-ec 2a ef 03 15 00 a3 35 ...*.....*.....5
00569286 92 6a a3 19 a2 7a 1d 00-89 6a 67 73 a4 82 7a cd .j...z...jgs..z.
00569296 ac 73 04 61 04 c7 11 16-2a 76 1d 56 8e be b9 62 .s.a....*v.V...b
005692a6 65 d4 c3 ce b4 f0 c3 30-ec 8f 6d d7 6e 39 bc c8 e......0..m.n9..
005692b6 49 ab ab b8 e3 43 78 d5-81 06 5d ef c7 7d 9f ce I....Cx...]..}..
005692c6 d6 b3 90 75 de 0c b0 90-de 23 ba c8 d1 3e 67 e0 ...u.....#...>g.
005692d6 19 a9 1b 86 31 1e 5f 34-2d ee 17 fd 15 fb 7e 27 ....1._4-.....~'
005692e6 8a 32 a1 ea c9 8f c9 7e-18 cb 2f 3b 2c 48 7a 7d .2.....~../;,Hz}
005692f6 a6 f4 01 07 ac 02 31 00-ff ff ff ff ff ff ff ff ......1.........
00569306 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
00569316 c7 63 4d 81 f4 37 2d df-58 1a 0d b2 48 b0 a7 7a .cM..7-.X...H..z
00569326 ec ec 19 6a cc c5 29 73-02 01 01 ...j..)s...

下面就是真正的Microsoft ECC Product Root Certificate Authority 2018证书的算法参数啦,只有7个字节,这是椭圆曲线secp384r1的OID的编码形式,其OID为1.3.132.0.34。第一个字节06指明这是一个OID类型数据,然后后面的05代表OID编码后的长度,然后OID有个编码算法,这里不再展开说明,有兴趣的话可以自己查一下。新增的对算法参数的比较就是对pCertInfo->SubjectPublicKeyInfo.Algorithm.Parameters CRYPTOAPI_BLOB结构中内容的比较(包括参数编码长度)。看,长度明显不一样的。

0:000> dx -r1 (*((combase!_CRYPTOAPI_BLOB *)0x579b7c))
(*((combase!_CRYPTOAPI_BLOB *)0x579b7c)) [Type: _CRYPTOAPI_BLOB]
[+0x000] cbData : 0x7 [Type: unsigned long]
[+0x004] pbData : 0x579506 : 0x6 [Type: unsigned char *]
0:000> db 0x579506 l7
00579506 06 05 2b 81 04 00 22

由于ChainGetMatchInfoStatus函数和CryptVerifyCertificateSignatureEx函数关系着每一层证书的验证,所以可以在这两个函数下断点,从而推测整个证书链的验证过程。首先获取用户证书并为其建立CCertObject结构,通过ChainGetMatchInfoStatus函数来判断该证书的Issuer和Subject字段是否相同(目的是为了找到自签名证书)。应该有个大循环去判断这些,直到找到可以通过ChainGetMatchInfoStatus函数以及CryptVerifyCertificateSignatureEx函数验证的自签名证书,然后为其建立CChainPathObject结构(所以验证的方向应该是自根证书向下的)。然后调用ChainGetSubjectStatus函数(ChainGetSubjectStatus应该是验证Issuer对Subject的签名是否正确以及Issuer身份是否可信的一个函数)来验证自签名证书对证书的签名是否正确,正确的话设置属性和标志。当标志设置了之后又通过FindElementInCollectionStore函数寻找和自签名证书相匹配的系统信任证书,并为其建立CChainPathObject结构。之后再次调用ChainGetSubjectStatus函数,由于签名验证标志已经设置,所以只比较了伪造证书和找到的这个证书公钥散列值是否相同,相同就成功返回。然后循环这一过程,直到所有证书都验证通过。PS:可以通过CERT_INFO结构中的时间或公钥来判断是哪个证书。

当成功返回CCertChainEngine::CreateChainContextFromPathGraph函数并进行一些额外校验后会调用CChainPathObject::CreateChainContextFromPath函数来创建CERT_CHAIN_CONTEXT结构,其包含简单证书链和一个信任状态结构,可以看出这个链中的根证书是系统的根证书(我在调试的时候记住了这几个证书的cbCertEncoded的大小)。

0:000> dt CERT_CHAIN_CONTEXT @eax
combase!CERT_CHAIN_CONTEXT
+0x000 cbSize : 0x38
+0x004 TrustStatus : _CERT_TRUST_STATUS
+0x00c cChain : 1
+0x010 rgpChain : 0x005eac64 -> 0x005eac68 _CERT_SIMPLE_CHAIN
+0x014 cLowerQualityChainContext : 0
+0x018 rgpLowerQualityChainContext : (null)
+0x01c fHasRevocationFreshnessTime : 0n1
+0x020 dwRevocationFreshnessTime : 0xcd
+0x024 dwCreateFlags : 0x48000001
+0x028 ChainId : _GUID {00000000-0000-0000-0000-000000000000}

0:000> dt _CERT_SIMPLE_CHAIN 0x005eac68
combase!_CERT_SIMPLE_CHAIN
+0x000 cbSize : 0x20
+0x004 TrustStatus : _CERT_TRUST_STATUS
+0x00c cElement : 2
+0x010 rgpElement : 0x005eac88 -> 0x005eac90 _CERT_CHAIN_ELEMENT
+0x014 pTrustListInfo : (null)
+0x018 fHasRevocationFreshnessTime : 0n1
+0x01c dwRevocationFreshnessTime : 0xcd

0:000> dx -r1 ((combase!_CERT_CHAIN_ELEMENT * *)0x5eac88)
((combase!_CERT_CHAIN_ELEMENT * *)0x5eac88) : 0x5eac88 [Type: _CERT_CHAIN_ELEMENT * *]
0x5eac90 [Type: _CERT_CHAIN_ELEMENT *]
0:000> dx -r1 ((combase!_CERT_CHAIN_ELEMENT *)0x5eac90)
((combase!_CERT_CHAIN_ELEMENT *)0x5eac90) : 0x5eac90 [Type: _CERT_CHAIN_ELEMENT *]
[+0x000] cbSize : 0x20 [Type: unsigned long]
[+0x004] pCertContext : 0x568ee0 [Type: _CERT_CONTEXT *]
[+0x008] TrustStatus [Type: _CERT_TRUST_STATUS]
[+0x010] pRevocationInfo : 0x5eaf58 [Type: _CERT_REVOCATION_INFO *]
[+0x014] pIssuanceUsage : 0x5ea618 [Type: _CTL_USAGE *]
[+0x018] pApplicationUsage : 0x5e9d08 [Type: _CTL_USAGE *]
[+0x01c] pwszExtendedErrorInfo : 0x0 [Type: wchar_t *]

0:000> dx -r1 ((combase!_CERT_CONTEXT *)0x568ee0)
((combase!_CERT_CONTEXT *)0x568ee0) : 0x568ee0 [Type: _CERT_CONTEXT *]
[+0x000] dwCertEncodingType : 0x1 [Type: unsigned long]
[+0x004] pbCertEncoded : 0x568408 : 0x30 [Type: unsigned char *]
[+0x008] cbCertEncoded : 0x299 [Type: unsigned long]
[+0x00c] pCertInfo : 0x568b98 [Type: _CERT_INFO *]
[+0x010] hCertStore : 0x55ee70 [Type: void *]

0:000> dx -r1 ((combase!_CERT_CHAIN_ELEMENT * *)0x5eac88+1)
((combase!_CERT_CHAIN_ELEMENT * *)0x5eac88+1) : 0x5eac8c [Type: _CERT_CHAIN_ELEMENT * *]
0x5eacb0 [Type: _CERT_CHAIN_ELEMENT *]
0:000> dx -r1 ((combase!_CERT_CHAIN_ELEMENT *)0x5eacb0)
((combase!_CERT_CHAIN_ELEMENT *)0x5eacb0) : 0x5eacb0 [Type: _CERT_CHAIN_ELEMENT *]
[+0x000] cbSize : 0x20 [Type: unsigned long]
[+0x004] pCertContext : 0x5e9d60 [Type: _CERT_CONTEXT *]
[+0x008] TrustStatus [Type: _CERT_TRUST_STATUS]
[+0x010] pRevocationInfo : 0x0 [Type: _CERT_REVOCATION_INFO *]
[+0x014] pIssuanceUsage : 0x0 [Type: _CTL_USAGE *]
[+0x018] pApplicationUsage : 0x0 [Type: _CTL_USAGE *]
[+0x01c] pwszExtendedErrorInfo : 0x0 [Type: wchar_t *]

0:000> dx -r1 ((combase!_CERT_CONTEXT *)0x5e9d60)
((combase!_CERT_CONTEXT *)0x5e9d60) : 0x5e9d60 [Type: _CERT_CONTEXT *]
[+0x000] dwCertEncodingType : 0x1 [Type: unsigned long]
[+0x004] pbCertEncoded : 0x579380 : 0x30 [Type: unsigned char *]
[+0x008] cbCertEncoded : 0x327 [Type: unsigned long]
[+0x00c] pCertInfo : 0x579b40 [Type: _CERT_INFO *]
[+0x010] hCertStore : 0x5722d0 [Type: void *]

这个结构最终放在CertGetCertificateChain函数的第8个参数*ppChainContext中,其中CERT_TRUST_STATUS结构中存放了证书链的错误码和状态码。dwErrorStatus 为0,表示没有错误,证书是有效的。

0:000> dt PCCERT_CHAIN_CONTEXT 00cf8b4c
combase!PCCERT_CHAIN_CONTEXT
0x005eab28
+0x000 cbSize : 0x38
+0x004 TrustStatus : _CERT_TRUST_STATUS
+0x00c cChain : 1
+0x010 rgpChain : 0x005eab84 -> 0x005eab88 _CERT_SIMPLE_CHAIN
+0x014 cLowerQualityChainContext : 0
+0x018 rgpLowerQualityChainContext : (null)
+0x01c fHasRevocationFreshnessTime : 0n1
+0x020 dwRevocationFreshnessTime : 0xcd
+0x024 dwCreateFlags : 0x48000001
+0x028 ChainId : _GUID {00000000-0000-0000-0000-000000000000}

0:000> dx -r1 (*((combase!_CERT_TRUST_STATUS *)0x5eab2c))
(*((combase!_CERT_TRUST_STATUS *)0x5eab2c)) [Type: _CERT_TRUST_STATUS]
[+0x000] dwErrorStatus : 0x0 [Type: unsigned long]
[+0x004] dwInfoStatus : 0x0 [Type: unsigned long]

最后,通俗地总结一下这个漏洞吧。在非对称加密算法中,每个主体有一对密钥,即公钥和私钥。公钥是公开的,私钥是自己留下的。用公钥加密的信息可以使用对应的私钥来解密,而在另一个场景下,用私钥加密的信息可以使用对应的公钥来解密。在签名算法中,主体使用其私钥来进行签名,如果他人可成功使用该主体的公钥解开其签名的内容,那证明这个东西确实是该主体签的(因为理论上除了他自己,没人会知道他的私钥)。在ECC加密算法中,公钥Q 、私钥d和基点G的关系是Q = d×G,只给出公钥Q和基点G是很难推测出私钥d的。而在证书标准中是允许指定自定义算法参数的,这其中包括基点G,并且crypt32库中对自签名证书的验证主要是通过检查其与系统信任证书的公钥哈希值是否匹配(使用自签名证书对下级证书进行验证,然后再比较这个自签名证书和系统信任证书公钥散列值是否相同),而忽略了对算法参数的检查。这使得攻击者可以选取一对基点G’ 、私钥d’,使得 d’×G’= Q(要伪造证书的公钥)。攻击者可将要伪造的证书算法参数中的基点G修改为自己构造的G’(也可以修改其它参数,只要保证 d’×G’ = Q即可),然后同时也拥有了和伪造证书相匹配的私钥d’。攻击者可选择伪造系统信任的证书,并使用伪造的私钥对其他证书进行签名,然后进一步可对恶意程序等进行签名。由于签名具有不可否认性,存在此漏洞的系统就会认为这证书确实是由那个被伪造证书的主体签发的,然后恶意程序的数字签名就会被验证通过。另外,可通过提取并判断自签名证书中的算法参数来检测针对该漏洞的攻击,即该ECC证书提供了自定义算法参数,但并不能完全匹配标准的椭圆曲线参数,尤其在其公钥和已知证书相同的情况下。

参考文章

https://blog.trendmicro.com/trendlabs-security-intelligence/an-in-depth-technical-analysis-of-curveball-cve-2020-0601/

https://www.cnblogs.com/jiu0821/articles/4598352.html?from=singlemessage

https://tools.ietf.org/html/rfc3279

https://www.ietf.org/rfc/rfc5480.txt

https://www.secg.org/SEC2-Ver-1.0.pdf

https://medium.com/zengo/win10-crypto-vulnerability-cheating-in-elliptic-curve-billiards-2-69b45f2dcab6

https://www.shsxt.com/it/html5/653.html

https://www.zybuluo.com/blueGhost/note/805491

https://blog.csdn.net/weixin43899764/article/details/104029814

https://mp.weixin.qq.com/s/8ipXMA2XjFN-kuSm0gkIaw

https://mp.weixin.qq.com/s/8jlqBHWt4qBn28xEESutaA

https://mp.weixin.qq.com/s/6qcREQjEToLoGFiDvfvrg


知识来源: https://www.anquanke.com/post/id/201228

阅读:34759 | 评论:0 | 标签:漏洞 CVE

想收藏或者和大家分享这篇好文章→复制链接地址

“CVE-2020-0601 ECC证书欺骗漏洞分析”共有0条留言

发表评论

姓名:

邮箱:

网址:

验证码:

ADS

标签云

本页关键词