A Technical Analysis of CurveBall (CVE-2020-0601)
A code-level root cause analysis of CVE-2020-0601 in the context of how applications are likely to use CryptoAPI to handle certificates — more specifically in the context of applications communicating via Transport Layer Security (TLS).
The first Microsoft patch Tuesday of 2020 contained fixes for CVE-2020-0601, a vulnerability discovered by the United States’ National Security Agency (NSA) that affects how cryptographic certificates are verified by one of the core cryptography libraries in Windows that make up part of the CryptoAPI system. Dubbed CurveBall or “Chain of Fools,” an attacker exploiting this vulnerability could potentially create their own cryptographic certificates that appear to originate from a legitimate certificate that is fully trusted by Windows by default.
Within a couple days of the disclosure, public Proof of Concepts (PoCs) began surfacing. Along with this came several explanations regarding the Elliptic Curve Cryptography (ECC) concepts involved with the vulnerability.
In contrast, this post will primarily highlight the code-level root cause analysis of the vulnerability in the context of how applications are likely to use CryptoAPI to handle certificates — more specifically in the context of applications communicating via Transport Layer Security (TLS).
However, before diving into the code, let’s take a high level look at certificates and elliptic curve cryptography.
Certificates
X.509 is an International Telecommunication Union (ITU) standard specifying the structure of public-key certificates using the ASN.1 notation. Basically, a certificate is a sequence containing three “first layer” items: the certificate, a signature algorithm identifier, and the signature verifying the authenticity of the certificate. This structure is expressed by the following ASN.1 snippet:
The certificate is itself a sequence of several components that include additional nested items:
Of particular importance here is the SubjectPublicKeyInfo item, which is a sequence that contains information regarding the algorithm used for the public key, followed by the actual public key:
The AlgorithmIdentifier structure, which is used to store information and parameters for both public key and signature algorithms, consists of an Object Identifier (OID) and optional parameters depending on the specific algorithm identified by the OID:
In the context of a public key, the algorithm field may be one of many OIDs such as rsaEncryption, which has an OID of 1.2.840.113549.1.1.1, dsa (1.2.840.10040.4.1), and ecPublicKey (1.2.840.10045.2.1). When the OID corresponds to ecPublicKey, it means that the public key is based on elliptic curve cryptography. If this is the case, the parameters field is set to one of the choices corresponding to EcpkParameters, as defined in RFC 3279:
In other words, an elliptic curve may be defined either by supplying an OID corresponding to one of the well-known “named curves” (which implicitly specifies the parameters for the curve), or by explicitly defining curve parameters in ecParameters. The vulnerability itself occurs when an attacker presents a crafted certificate with ecParameters instead of a named curve. But exactly why this happens requires a quick introduction to elliptic curves.
Elliptic Curves
Elliptic curves are defined by the following equation:
y^2 = x^3 + ax + b
In elliptic curve cryptography, solutions to this equation are calculated within the context of a finite field. A finite field, also known as a Galois field, is a set with a finite number of elements, typically created by performing all operations on the field modulo using a prime number or the power of a prime number, which can be denoted as GF(pn). Points in a given elliptic curve consist of x and y coordinates in the range of {0,1,2,…,p-1} for prime fields (i.e., where the power n is 1). The number of elements in the set is known as the order of the field, and so the order of an elliptic curve consists of all the points on the curve.
Elliptic curves used for ECC define what is known as a base or generator point, which is a specific point on the curve that can be used to “generate” any other point on the curve. This is accomplished by multiplying the point by some integer in the range of the order of the finite field. For a named curve, the coefficients a and b, the field identifier (typically the prime p), and the base point are all predetermined and documented in the official standards for each curve.
However, if the certificate defines explicit ecParameters, all parameters for the curve are explicitly chosen and presented in the certificate, as seen in the ASN.1 structure of ECParameters:
In the context of the vulnerability, these parameters may be manipulated in a way that allows an attacker to generate a certificate using a private key he created; the public key is identical to an existing certificate’s public key. This is generally performed by modifying the base point and leaving all other curve parameters the same as the predetermined parameters for the named curve used on the original certificate.
An attacker may then use this newly crafted “trusted” certificate and private key to sign an additional certificate, then present both the crafted certificate and additional certificate, known as the “end” certificate, to a target. The target will then attempt to validate the certificate chain using a combination of the certificates presented by the attacker as well as trusted certificates contained in the Windows certificate stores. This validation process is where the vulnerability lies, so let’s take a deep look at the process in Crypto API by first delving into the changes applied in the patch.
Examining the patch changes
While the Windows CryptoAPI includes several different libraries, the vulnerability itself is present in crypt32.dll. A binary diff between the latest unpatched version of the DLL (10.0.18362.476) and the patched version (10.0.18362.592) using BinDiff shows slight changes between these two files:
Figure 1. Using BinDiff on the unpatched and patched versions of the DLL file show very minor differences
What has changed in the DLL?
At a high level, there are five potentially interesting new functions added to the new version and five existing functions that were changed. In cases like this where we see several new functions added, the preexisting functions have likely been updated to take advantage of these new functions. Thankfully, since Microsoft provides debugging symbols for the vast majority of Windows components, we can derive a lot of information just from inspecting the names of the functions.
First, we let’s take at the names of the changed functions:
Figure 2. The names of the changed functions in crypt32.dll
We can see that both the constructor and destructor of the “CCertObject” class have been changed slightly, but our largest changes are present in ChainGetSubjectStatus() and CCertObjectCache::FindKnownStoreFlags().
Next, we have the names of the new functions:
Figure 3. The names of the new functions in crypt32.dll
Several of the new functions stand out and one in particular serves as a good starting point for analysis. The ChainLogMSRC54294Error() function is a new logging function added to facilitate Windows event logging for potential exploitation attempts. Its purpose can be identified through the presence of the following block:
Figure 4. Block showing the purpose of the ChainLogMSRC54294Error() function
The block passes a string containing the CVE reference associated with this vulnerability to an external library function called CveEventWrite, which is a relatively new Event Tracing API function that writes a CVE-based event to the Windows Event Log.
Using this information, we can inspect the cross-references to this new function to discover more details about the context in which this event was logged. In this case, the only direct reference to ChainLogMSRC54294Error() is in the function ChainGetSubjectStatus(), which happens to have the largest changes among the updated functions:
Figure 5. Examining the function ChainGetSubjectStatus() to check references to ChainLogMSRC54294Error()
A quick look at the surrounding context of the call to the new logging function reveals that the function is called due to specific results in calls to CryptVerifyCertificateSignatureEx() and ChainComparePublicKeyParametersAndBytes(), the latter of which is one of the new functions added in the patch. ChainGetSubjectStatus() is therefore a good target to focus on, both by analyzing the changes in the function and the context in which it is called.
The inner workings of certificate validation in CryptoAPI
In order to understand how ChainGetSubjectStatus() is involved, we must first look at how a typical program uses CryptoAPI to handle certificates.
In this case, the analysis was performed on Powershell’s Invoke-Webrequest cmdlet, but other TLS clients will likely behave in a similar manner. When Powershell is first loaded, handles to the system certificate stores containing explicitly trusted certificates are obtained with calls to CertOpenStore so that the stores may be used when necessary. These certificate stores are then added to a “collection,” which effectively behaves as one large merged certificate store.
When a command such as Invoke-Webrequest is used to send an HTTP request over TLS to a server, the server will present a TLS certificate handshake message containing an end certificate and potentially additional certificates that will be used to validate the certificate chain. Upon receipt of these certificates, an additional “in memory” store will be created with an additional call to CertOpenStore(). The received certificates are then added to this new store via the function CertAddEncodedCertificateToStore(), which creates a CERT_CONTEXT structure that contains a handle to the certificate store, a pointer to the original encoded certificate, and a pointer to a CERT_INFO structure that essentially corresponds to the ASN.1 structure of a certificate.
For example, these are the structures created as a result of calling CertAddEncodedCertificateToStore() for the end certificate.
Figure 6. The structures created from calling CertAddEncodedCertificateToStore()
A few of these items are particularly important. The Issuer should be an exact match of the Subject for the certificate belonging to the signer of this certificate, while SubjectPublicKeyInfo is a structure containing the precise information contained in the ASN.1 structure of the same name. For example, we can see that our end certificate public key uses elliptic curves due to the algorithm identifier OID of 1.2.840.10045.2.1:
Figure 7. End certificate public key showing the algorithm identifier OID
We can also quickly identify whether a named curve or explicit curve parameters are provided by inspecting the first byte of the Parameters:
Figure 8. Using the first byte of the Parameters to determine if named or explicit curve parameters are specified
In this case, 0x6 is the DER encoding tag for an object identifier, implying that a named curve is specified. If explicit ecParameters were provided, that tag would have a value of 0x30, corresponding to a sequence.
Once all received certificates have been added to the in-memory store, Powershell calls the CertGetCertificateChain() function with the CERT_CONTEXT structure for the end certificate in order to construct the certificate chain context — including any intermediate certificates received, going back to a trusted root if possible.
More specifically, CertGetCertificateChain() will return a CERT_CHAIN_CONTEXT structure, which is an array of certificate chains, and a trust status structure containing data regarding the validity of the chains. This is primarily handled within the CCertChainEngine::GetChainContext() function that will ultimately return a CERT_CHAIN_CONTEXT structure containing the verification status of the certificate chain.
CCertChainEngine::GetChainContext() calls CCertChainEngine::CreateChainContextFromPathGraph(), which first creates a new collection that includes the collections previously created from the system certificate stores. It then locates the end certificate from the collection (i.e., all stores belonging to the collection) and calls ChainCreateCertObject() with the CERT_INFO structure of the end certificate. ChainCreateCertObject() attempts to locate an existing CCertObject in the creation cache, and if none is found, a new CCertObject is instantiated.
The constructor for CcertObject does the following: it initializes numerous fields, copies a variety of properties from the CERT_INFO structure such as the signature hash, key identifier, looks for policies set in any certificate extensions, and checks whether any extensions marked as critical are unsupported by CryptoAPI.
The constructor then calls ChainGetIssuerMatchInfo() to retrieve a CERT_AUTHORITY_KEY_ID_INFO structure, which identifies the key used to sign the certificate. It then checks whether the certificate is self-signed, and whether the public key uses the RSA algorithm with a length that is considered weak.
Once the CCertObject for the end certificate has been created, a CChainPathObject is created. After the main initialization of the CChainPathObject has been performed, the constructor will call CChainPathObject::FindAndAddIssuers(), which eventually leads to a call to CChainPathObject::FindAndAddIssuersFromStoreByMatchType() in order to look for a certificate that matches the issuer of the end certificate — typically based on the hash of the subject that matches the hash of the end certificate’s issuer. If an additional certificate is found in the memory store (i.e., it was sent with the end certificate), ChainCreateCertObject() is once again called, this time with the CERT_INFO structure of the certificate for the issuer of the end certificate.
This process is repeated recursively until a self-signed certificate (i.e. root) certificate is encountered, which will lead to a call of ChainGetSelfSignedStatus(). But first, it will check to see if the subject and issuer match and if they do, will verify the signature on the certificate by calling CryptVerifyCertificateSignatureEx(), given the public key and key algorithm information provided in the CERT_PUBLIC_KEY_INFO structure from the CERT_INFO structure for the certificate.
Much of the validation is performed within the function I_CryptCNGVerifyEncodedSignature(). If the certificate provides explicit elliptic curve parameters as opposed to a named curve, the function I_ConvertPublicKeyInfoToCNGECCKeyBlobFull() is called to populate a BCRYPT_ECCFULLKEY_BLOB structure containing the curve parameters, a structure which is not formally documented but can be located in the bcrypt.h header of the Windows SDK. These parameters are then used in a call to CNGECCVerifyEncodedSignature(), which calls the bcrypt library function BCryptVerifySignature() to perform the actual verification given the parameters and signature.
If the signature is successfully verified, the CCertIssuerList::AddIssuer() function is called which eventually leads to the creation of a new CChainPathObject. The CChainPathObjects for the end certificate and the self-signed certificate are then used in a call to CCertIssuerList::CreateElement(), which first performs some initialization, and then calls ChainGetSubjectStatus() with the two CChainPathObjects.
ChainGetSubjectStatus() first calls ChainGetMatchInfoStatus() with the CCertObjects associated with each of the CChainPathObjects, which tests to see if the subjects are the same between the two. It then checks a flag in the CCertObject associated with the end certificate.
Figure 9. Calling ChainGetMatchInfoStatus() and testing the flag in CCertObjects associated with the end certificate
This flag is initially set to 0 when the CCertObject is created. This leads to a call to CryptVerifyCertificateSignatureEx() in order to verify that the self-signed certificate is a valid issuer of the end certificate.
Figure 10. Calling CryptVerifyCertificateSignatureEx() for certificate verification
If the signature is deemed valid, the CERT_ISSUER_PUBLIC_KEY_MD5_HASH property is added to the end certificate and the aforementioned flag is set to 3.
Figure 11. Adding the CERT_ISSUER_PUBLIC_KEY_MD5_HASH property to the end certificate
Once CCertIssuerList::AddIssuer() returns and all other functions return back to the original CChainPathObject::FindAndAddIssuers() call, the aforementioned flag in the end certificate is checked and, if set, will once again call CChainPathObject::FindAndAddIssuersFromStoreByMatchType() and CChainPathObject::FindAndAddIssuersFromStoreByMatchType().
This time, the collection store containing the system certificate trust list is searched, providing a CERT_STORE_PROV_FIND_INFO structure containing the parameters of the search to FindElementInCollectionStore(), which iterates through the stores in the collection, searching each store for a certificate with an MD5 hash that matches the public key of the self-signed certificate.
If the public key hash matches a certificate found in the system store, ChainCreateCertObject() is once again called, this time with the CERT_CONTEXT structure for the certificate retrieved from the system store, creating a new CCertObject, and in the process applying flags due to the fact that the certificate originated from a "known store." The object is then added as an issuer object to the CCertObjectCache, and added to the issuer list, once again leading to the creation of a new CChainPathObject and a call to CCertIssuerList::CreateElement() with the CChainPathObjects for the self-signed certificate and the trusted certificate retrieved from the store.
When ChainGetSubjectStatus() is called, the aforementioned flag is now set, causing the previously set CERT_ISSUER_PUBLIC_KEY_MD5_HASH on the self-signed certificate to be checked against the MD5 hash of the public key in the certificate from the trusted store.
Figure 12. Checking the CERT_ISSUER_PUBLIC_KEY_MD5_HASH on the self-signed certificate against the MD5 hash of the public key in the certificate from the trusted store
If there is a match, no further verification of the provided self-signed certificate is performed with respect to the certificate retrieved from the trusted store. This implies that the certificate provided is a trusted self-signed certificate due to the fact it sufficiently matches the certificate retrieved from the certificate store based on the public key hash. In addition, the provided self-signed certificate was successfully used to verify the signature of the end certificate. In cases where an attacker provides a certificate with a public key that is identical to the trusted root certificate and was crafted with explicitly defined elliptic curve parameters, the end certificate signature is effectively trusted as if it were signed by the legitimate root certificate.
This trust is exhibited once all the functions return to CCertChainEngine::CreateChainContextFromPathGraph(). Each object in the certification path (i.e., the end certificate and crafted root certificate) performs additional checks, such as certificate validity with respect to the current time and revocation status. The CERT_CHAIN_CONTEXT structure is then created with a call to CChainPathObject::CreateChainContextFromPath() and the CERT_TRUST_STATUS structure contained within is set to reflect the validity of the certificate chain.
Once the CertGetCertificateChain() function returns, the main application may check the verification status of the certificate chain contained within the CERT_CHAIN_CONTEXT structure, which will show as valid in the described attack scenario.
Figure 13. Verifying the status of the certificate chain contained within the CERT_CHAIN_CONTEXT structure
Conclusion
In summation, our analysis of CVE-2020-0601 brings up the following points:
- The signature of the end certificate is verified using the crafted root certificate and any elliptic curve parameters included.
- The signature of the crafted root certificate is verified as a self-signed certificate, again using any elliptic curve parameters included.
- A matching certificate for the crafted root certificate is located in the system certificate store by using the hash of the public key, which is identical for both the crafted and legitimate root certificate.
- The hashes of the public key for both the crafted and legitimate root certificate are checked, and if the hashes match, no further verification of the crafted root certificate is performed with respect to the legitimate root certificate, leading to successful verification of the crafted end certificate.
How did Microsoft’s patch resolve the vulnerability? In Figure 5 we see that they added a call to the new function ChainComparePublicKeyParametersAndBytes(), replacing the simple comparison between the issuer and trusted root public key hash, which compares the public key parameters and bytes between the trusted root certificate and the certificate that was actually used to verify the signature on the end certificate. If that comparison fails, CryptVerifySignatureEx() is called to re-verify the signature on the end certificate using the actual trusted root certificate, parameters and all, catching any crafted root certificates with cryptographic parameters that differ from those on the actual trusted certificate.
Recommendations and Trend Micro solutions
We encourage both individuals and organizations to apply the latest patch from Microsoft as soon as possible to prevent further exploit of CurveBall. Users can also check whether they’re at risk from CVE-2020-0601 via this Vulnerability Assessment Tool.
The Trend Micro™ Deep Security™ and Vulnerability Protection solutions also protect systems and users from threats that exploit CVE-2020-0601 via the following rules:
- 1010130-Microsoft Windows CryptoAPI Spoofing Vulnerability (CVE-2020-0601)
- 1010132-Microsoft Windows CryptoAPI Spoofing Vulnerability (CVE-2020-0601) – 1
Trend Micro™ TippingPoint® customers are protected from threats and attacks that may exploit CVE-2020-0601 via the following MainlineDV filter:
- 36956: HTTP: Microsoft Windows CryptoAPI Spoofing Vulnerability