A Deep Dive into Windows Cryptographic Services Vulnerability: CVE-2023-23416
Abstract
This article provides an in-depth study of CVE-2023-23416, a Windows Cryptographic Services vulnerability that can be triggered by importing a malicious certificate into Trusted Root Certification Authorities store. We first identified the CertUtil modules that had updated timestamps following the installation of the cumulative update. Patch analysis was then undertaken to ascertain the modified functions, eventually leading to the discovery of an internal function housed within ncrypt.dll. Utilising a call graph to depict the required execution path, an input certificate was gradually constructed to access the target function and induce a system crash. The main issue causing the vulnerability was found to be the declaration of fixed-size arrays to store information for only up to 8 Security Identifiers (SIDs), without including additional checks to ensure that only a maximum of 8 SIDs would be processed. As a result, a buffer overflow would occur when a SID-protected certificate containing more than 8 AND-combined SIDs was imported. Investigation was done on the different kinds of crash behaviour that could result from the buffer overflow. Based on the findings, we assessed that it would be difficult for an attacker to exploit this vulnerability.
Introduction
As a Cyber Security Researcher specialising in Applied Cryptography, I often study CVEs that are associated with cryptographic implementations to gain in-depth understanding of the nature of the vulnerabilities and their corresponding mitigation methods. This process enables me to recognise common vulnerability patterns and continuously enhance my knowledge of vulnerability discovery and reverse engineering techniques. Consequently, I can conduct threat and impact assessments with heightened proficiency, contributing to a more robust cybersecurity landscape.
During March’s Patch Tuesday 2023, Microsoft released a fix for CVE-2023-23416 that was titled “Windows Cryptographic Services Remote Code Execution Vulnerability”. At Microsoft Security Response Centre (MSRC) website, this CVE was given a max severity rating of Critical and a CVSS v3.1 base score of 7.8. The fix for this CVE was included within a cumulative update which could be directly downloaded from the MSRC website, with a different cumulative update provided for each Windows OS version. The following comments were found in the FAQ section:
For successful exploitation, a malicious certificate needs to be imported on an affected system. An attacker could upload a certificate to a service that processes or imports certificates, or an attacker could convince an authenticated user to import a certificate on their system.
The word Remote in the title refers to the location of the attacker. This type of exploit is sometimes referred to as Arbitrary Code Execution (ACE). The attack itself is carried out locally. This means an attacker or victim needs to execute code from the local machine to exploit the vulnerability.
Some relevant information could be obtained from the CVSS metrics section:
- The attack complexity is low, and an attacker can expect repeatable success against the vulnerable component.
- There is a total loss of confidentiality, integrity and availability should the vulnerability be successfully exploited.
- No exploit code is available, or an exploit is theoretical.
This vulnerability affects Windows 10, 11 and Windows Server 2012 to 2022, so it should be located within a binary that is common on all the systems. At the time of my research, there was little information available elsewhere besides what was reported by MSRC.
The goal of this study is to identify the vulnerability associated with CVE-2023-23416 and assess whether a malicious actor can easily abuse it. To prepare for this study, I set up 2 Windows 10 x64 Enterprise 21H2 systems, with one running the affected OS and the other running the patched OS, so that I would be able to compare the differences between the affected processes and binaries easily. The cumulative update used to patch this Windows OS version is KB5023696. During the later part of my study, I also set up a Windows Server 2016 system installed with Active Directory Domain Services (AD DS) and used it to export several types of certificates.
Windows Cryptographic Services
Windows Cryptographic Services is a Network Service that starts up by default. It provides three management services:
- Catalog Database Service: Confirms the signatures of Windows files and allows new programs to be installed.
- Protected Root Service: Adds and removes Trusted Root Certification Authority certificates from a computer.
- Automatic Root Certificate Update Service: Retrieves root certificates from Windows Update and enable scenarios such as SSL.
Comparing the description for Protected Root Service with the attack vector mentioned in the FAQ of CVE-2023-23416, it seemed like one would need to import a malicious certificate into the Trusted Root Certification Authorities (“root”) store to trigger the vulnerability.
The library associated with Cryptographic Services is C:\Windows\System32\cryptsvc.dll. The timestamp of this file was unchanged after applying the cumulative update. This implied that the vulnerability was not located within this DLL but instead located in some other DLL used during the certificate import process.
Certificates can be imported on Windows using either CertMgr (certmgr.msc), CertUtil (certutil.exe) or via PowerShell commands such as Import-Certificate. I decided to choose CertUtil for the analysis as it is a native program and can be directly executed using a debugger. CertUtil also allows input parameters to be specified on the command line, avoiding the need for user interaction while debugging the process.
Finding the Relevant Modules
To identify the modules that are associated with the certificate import process, a debugger such as WinDbg or x64dbg was used to run CertUtil. The Symbols tab in x64dbg would display all the modules that are used by CertUtil when importing a sample certificate into root store. From this list of modules, there were only 6 DLLs with updated timestamps after installing the patch, which meant that the vulnerability was likely located within one or more of these 6 DLLs.
Finding the Relevant Functions
After narrowing down the modules, the BinDiff plugin for IDA Pro was used to compare the changes between the pre- and post-patched versions of the DLLs to identify the functions that had been modified. The results from BinDiff showed that rpcrt4.dll had the highest number of modified functions. The full list of functions is provided below:
Since these were the only functions modified by the cumulative update, the vulnerability was likely located within one or more of these functions. Given that the patch is a cumulative update that contained fixes for different issues, not all the modifications may be related to CVE-2023-23416. Further analysis on the code would be required to pinpoint the modifications that are related to CVE-2023-23416.
Study on rpcrt4.dll
As it was tedious to perform analysis on all the modified functions, my initial approach was to set break points on all the modified functions and let the debugger tell me which ones were traversed when importing a certificate into root store. This resulted in having break point hits on 3 functions, all of which were from rpcrt4.dll.
- InitializeRpcServer
- LRPC_CASSOCIATION::Bind
- SIMPLE_DICT::Insert
I started doing patch analysis on SIMPLE_DICT::Insert as it had the shortest code. Below diagrams show the pre- and post-patched pseudocode after decompilation by IDA Pro.
The difference between the pre- and post-patched versions is highlighted in the red boxes.
- In the pre-patched version, SIMPLE_DICT::Insert multiplies unsigned int v4 by 2 and the result is assigned to the 2nd argument of SIMPLE_DICT::ExpandToSize.
- In the patched version, SIMPLE_DICT::Insert first multiplies unsigned int v4 by 2 using ULongMult. If the operation is successful, the result is assigned to the 2nd argument of SIMPLE_DICT::ExpandToSize.
The ULongMult function performs overflow and underflow checks when multiplying one value of type ULONG by another.
- If the operation results in a value that overflows or underflows the capacity of the type, the function returns INTSAFE_E_ARITHMETIC_OVERFLOW (0x80070216).
- If the operation succeeds, the function returns S_OK (0).
It appeared that SIMPLE_DICT::Insert contained an integer overflow bug that could potentially result in an out-of-bounds write. This was then fixed by using ULongMult to perform the multiplication safely. However, I was not confident that the bug was associated with CVE-2023-23416. When checking the cross references to the SIMPLE_DICT::Insert function, I noticed that it was called by many other functions within rpcrt4.dll. The bug would have impacted many other RPC-related processes and it would be strange to label it as a Windows Cryptographic Services vulnerability. Furthermore, there were a few other CVEs related to RPC Runtime that were fixed by the cumulative update. Although few details were published on those RPC Runtime vulnerabilities, this bug was likely associated with one of them. The same argument would also apply to other bugs found in rpcrt4.dll.
[Afternote: A blog post subsequently published by Akamai Technologies revealed that the integer overflow bug was indeed associated with other RPC Runtime vulnerabilities that were fixed by the cumulative update.]
Study on ncrypt.dll
I decided to change my approach to target other DLLs that are more relevant to Cryptographic Services. The natural candidate was ncrypt.dll since it contained functions associated with private key encryption, storage, and retrieval.
Ncrypt.dll contained only 1 modified function I_GetDescriptorFromAndCombiner. This is an internal function that was not reached during the certificate import process. I suspected that only certain types of certificates or certain command-line options would enable the execution flow to reach the function. To analyse this hypothesis further, the “Xrefs graph to” option in IDA Pro was used to trace out CertUtil call graph terminating at NCrypt.I_GetDescriptorFromAndCombiner. This provided a visual representation of the execution path required by CertUtil to reach NCrypt.I_GetDescriptorFromAndCombiner. The call graph is displayed below. None of the functions in the call graph were reached when importing a sample certificate into root store.
Reaching CertUtil.myPFXImportCertStore
The name of the functions in certutil.exe and crypt32.dll made it obvious that a PFX or Personal Information Exchange certificate was required as input. A PFX certificate is one that contains the corresponding private key for the certificate. This private key is usually encrypted with a user-specified password.
After creating a PFX certificate and importing it with CertUtil, the execution flow was able to reach the functions in certutil.exe and crypt32.dll but stopped short of proceeding into ncrypt.dll.
Reaching NCrypt.NCryptUnprotectSecret
Microsoft documentation for NCryptUnprotectSecret mentions that this function decrypts data to a specified protection descriptor. The documentation on Protection Descriptors indicates that one of the methods to protect keys is to use Security Identifiers (SID) to control which Active Directory user or group are authorised to access the key, which in our context would mean the private key in the PFX certificate.
Looking through the list of command-line options that can be specified with CertUtil, I noticed a “-ProtectTo SAMnameandSIDlist” option which allows the user to specify a comma-separated SAM name/SID list. This option can be used together with CertUtil’s “-exportPFX” option to export a PFX certificate containing the private key. It looks like CertUtil also supports the use of Active Directory user or group to protect the private key in a PFX certificate instead of a user-specified password.
To test this theory further, I set up Windows Server 2016 with Active Directory Domain Services (AD DS) and carried out the following steps to generate an SID-protected PFX certificate:
- Import a PFX certificate containing the private key into root store on the Windows server.
- Use CertUtil’s “-exportPFX” and “-ProtectTo mydomain\user1” to export the PFX certificate from root store, where “mydomain\user1” refers to an existing user “user1” in “mydomain” domain.
When the SID-protected PFX certificate was brought over to the Windows client and imported by CertUtil, the execution flow reached NCrypt.NCryptUnprotectSecret and its descendants, up to the penultimate function.
Reaching NCrypt.I_GetDescriptorFromAndCombiner
There were some clues on how to reach the target function. The function name itself seems to imply that there is another protection method related to the use of “And” operator to “Combine” things. In fact, the Protection Descriptors documentation mentions that AND or OR operators can be used with protection descriptors to encrypt data for multiple parties or establish multiple conditions for decryption. An example of a protection descriptor rule string using the AND operator is “SID=S-1–5–21–4392301 AND SID=S-1–5–21–3101812”.
Since CertUtil’s “-ProtectTo SAMnameandSIDlist” option allows a comma-separated SAM name/SID list to be specified, I tried specifying 2 users when exporting the PFX certificate from the Windows server. After the PFX certificate was exported, either user was able to access the private key, confirming that the SID permissions in the PFX certificate are correctly set with dual user permissions. However, the PFX certificate still failed to trigger the target function when importing it on the Windows client. There was no option available in CertUtil or CertMgr for the user to select the AND or OR operator to combine multiple SIDs. These observations indicated that the OR operator must have been used by default during the certificate export process.
I then proceeded to debug CertUtil’s PFX certificate export process with multiple SIDs specified in the “-ProtectTo” option. This led me to the CertUtil.adminGetSIDProtector function where I noticed that the OR operator had been hardcoded to concatenate multiple SIDs during the preparation of the protection descriptor rule string. The problematic code in CertUtil.adminGetSIDProtector is highlighted below.
To use the AND operator, I set a break point before the hardcoded region and patched the bytes in memory from “ OR SID=” to “ AND SID=” before continuing with the execution. CertUtil completed successfully and created an SID-protected PFX certificate that used the AND operator to combine multiple SIDs. When this certificate was imported into the Windows client, the execution flow could finally reach the target function NCrypt.I_GetDescriptorFromAndCombiner.
Summarising the steps taken to reach the target function, I needed to first create a SID-protected PFX certificate containing at least 2 AND-combined SIDs from a Windows AD DS server, before importing the certificate on an affected system.
Triggering the Vulnerability
The next step was to determine how to trigger the vulnerability. BinDiff was again used to compare the pre- and post-patched versions of NCrypt.I_GetDescriptorFromAndCombiner. The pseudocode highlighted in red below was added to the patched version.
The highlighted code checks if *(v31 + 1) is less than or equal to 8 before executing the code from line 101 onwards. Otherwise, the execution would go directly to the end of the function and return an error. In the pre-patched version, the same code from line 92 onwards would always run regardless of the value in *(v31 + 1). Performing dynamic analysis on the code region showed that *(v31 + 1) holds the number of SIDs used to protect the private key in the certificate. This meant that a SID-protected PFX certificate containing more than 8 AND-combined SIDs is required to trigger the vulnerability.
Crafting the Vulnerable Certificate
I initially thought I could simply specify more than 8 SIDs in the “-ProtectTo” option when exporting the PFX certificate on the Windows server. However, both CertUtil and CertMgr did not allow a certificate to be protected by more than 8 SIDs. The programs would simply fail with a “Bad Data” error message when more than 8 SIDs were specified. Analysing the decompiled code within the certificate export flow, I noted that the arrays used to store information associated with the SIDs were declared with a size of 8. There were also checks in place to ensure that the number of SIDs did not exceed 8. Patching the code to allow for more than 8 SIDs was non-trivial since the storage buffers were not dynamically allocated and multiple code regions would also have to be modified.
I decided to generate a SID-protected PFX certificate that contained exactly 8 AND-combined SIDs and study whether it was possible to manually modify it to include more than 8 SIDs. Since the contents of the PFX certificate were encoded in ASN.1 format, I needed to first decode it to make sense of the data and manipulate it. Using an open-source tool that could perform decoding and encoding of ASN.1 data, the following steps were taken to recreate a SID-protected PFX certificate containing 9 AND-combined SIDs.
- Perform ASN.1 decoding on a SID-protected PFX certificate that contains 8 AND-combined SIDs.
- Locate the 8 SID entries in the decoded output.
- Duplicate any one of the SID entries.
- Perform ASN.1 encoding on the resulting data to obtain a SID-protected PFX certificate that contains 9 AND-combined SIDs.
Below is the partially extracted data decoded from a SID-protected PFX certificate with AND-combiner, showing where the SID entries are located.
The definitions of the OIDs (Object Identifier) were extracted from the data section of certutil.exe. In the below diagram, the line following each numeric OID is its definition. It looked like these were the only key protection descriptors supported by certutil.exe.
When the crafted certificate was imported into root store on the affected OS, CertUtil would crash with a “heap corruption” error code. When the import was done on the patched OS, CertUtil would terminate with a wrong password error message. This confirmed that the crafted certificate could trigger the vulnerability and that the patch was able to mitigate it. Notice that the method to trigger the vulnerability was remarkably similar to the description of the attack vector in CVE-2023-23416, making it highly likely that this vulnerability was indeed associated with CVE-2023-23416.
Understanding the Vulnerability
With the vulnerability confirmed to be located within NCrypt.I_GetDescriptorFromAndCombiner and triggered using my crafted certificate, I proceeded to analyse the code further to locate and understand the vulnerability.
At the start of NCrypt.I_GetDescriptorFromAndCombiner, there are 4 arrays (hMem, v37, v38, Dst) declared and initialised to zeroes. The arrays have fixed capacities to hold up to 8 or 32 address pointers only.
The following diagram of the local stack from low to high memory address shows that the arrays are adjacent to each other. The stack canary is also located just after the Dst array.
The function would decode certain portions of the certificate, extract information from all the SID entries, and store the address pointers referencing the information into the arrays hMem, v37 and Dst. The relevant code is shown below.
The main issue causing the vulnerability is that NCrypt.I_GetDescriptorFromAndCombiner wrongly assumes that an input certificate would only contain a maximum of 8 SIDs. It declares fixed-size arrays to store information for only up to 8 SIDs and does not include additional checks to ensure that only a maximum of 8 SIDs would be processed. When presented with a certificate containing more than 8 SIDs, the function would process all the SIDs without checking for a buffer overflow. As a result, there is an out-of-bounds write in some of the arrays when the additional SIDs are processed. This corrupts the stack memory and eventually leads to a crash during a later part of the function.
Note that hMem and v37 are handled by RtlAsn1DecodeAndAllocate and they store handles that are returned by a memory allocation function. On the other hand, the pointers stored in Dst are assigned from other variables.
[Afternote: On 8 June 2023, the researcher who discovered CVE-2023-23416 posted a short article which confirmed that the vulnerability was located in NCrypt.I_GetDescriptorFromAndCombiner. He briefly described the method used to trigger the vulnerability and it was also similar to ours. However, he did not reveal details on the procedure to create the malicious certificate or provide analysis of the vulnerable code and resulting crash.]
Crash Analysis
Through experimentation on different types of certificates and binaries, I was able to trigger 3 types of crash behaviour during the certificate import process even though they all originated from a buffer overflow. The reasons for each crash are listed below, and I will discuss each of them in the following sections.
- Freeing an Invalid Handle
- Double Free
- Write into Inaccessible Memory
Analysis of Crash 1: Freeing an Invalid Handle
The following diagram shows the stack memory for NCrypt.I_GetDescriptorFromAndCombiner after processing 8 SIDs. Arrays hMem, v37 and Dst are fully filled with 8-byte address pointers, while array v38 remains empty.
The following diagram shows the memory state after processing the 9th SID. The modified bytes are highlighted in red.
The following is what happened when the 9th SID is processed.
- A pointer associated with the 9th SID is to be written at hMem[8]. Since hMem can only store 8 entries, the value is written into v37[0].
- Another pointer associated with the 9th SID is to be written at v37[8]. Since v37 can only store 8 entries, the value is written into v38[0].
- Another pointer associated with the 9th SID is to be written at Dst[32]. Since Dst can only store 8 sets of entries, the value is written into the stack canary.
At the end of the function, a do-while loop containing 3 I_DefaultExtrenalFree functions attempts to free the pointers stored in hMem, v37 and v38 if they are not empty. The loop counter starts with the pointers associated with the 9th SID and decrements down to the 1st SID. Notice that entries in the Dst array are not freed.
The following happens when the pointers associated with the 9th SID are being freed.
- hMem[8] is freed successfully. This is the pointer overwritten into v37[0].
- V37[8] is freed successfully. This is the pointer overwritten into v38[0].
- When attempting to free v38[8], an exception with error code 0xc0000374 is encountered.
An illustration of the array entries being freed is provided below.
V38[8] refers to the same location as Dst[0], which contains a pointer assigned during the processing of the 1st SID. A trace of the pointer address will reveal that it references the string “SID”, denoting the protection descriptor type. Further analysis of the string buffer shows that it is located within a larger memory chunk that was dynamically allocated. The string pointer establishes a direct access to the string “SID” within the larger memory chunk. As it is not a valid handle directly returned from a memory allocation function, it is not supposed to be directly freed.
Analysis of I_DefaultExtrenalFree shows that it uses Microsoft’s LocalFree function to perform the free operation. There are multiple checks within the LocalFree function (specifically ntdll.RtlpFreeHeapInternal) to determine if an address is directly referencing a valid memory chunk allocated from the heap before attempting to free it. An exception is raised when an invalid handle is detected, and the program would proceed to terminate. The heap is not corrupted since the invalid handle will not be freed. The call stack during the crash is shown below.
If this exception is bypassed using a debugger, the program would then terminate upon exiting NCrypt.I_GetDescriptorFromAndCombiner due to the corrupted stack canary.
Analysis of Crash 2: Double Free
The analysis done in Crash 1 used the 64-bit CertUtil program to import the certificate. When using the 32-bit CertUtil program to import the crafted certificate, the program would crash due to a double-free issue instead. This is because the order of declaring the 4 arrays in the 32-bit NCrypt.I_GetDescriptorFromAndCombiner function has been inverted, resulting in a different layout within the stack memory. This new layout causes the double-free to be encountered first.
The following diagram shows the stack memory for the 32-bit NCrypt.I_GetDescriptorFromAndCombiner after processing 8 SIDs. The arrays hMem, v37 and Dst are fully filled up with 4-byte address pointers.
The following diagram shows the memory state after processing the 9th SID. The modified bytes are highlighted in red.
The following is what happened when the 9th SID is processed.
- A pointer associated with the 9th SID is written at hMem[8], overwriting the stack canary.
- Another pointer associated with the 9th SID is written at v37[8], overwriting hMem[0].
- Another pointer associated with the 9th SID is written at Dst[33], overwriting v38[0].
During the free loop at the end of the function, the pointers in hMem, v37 and 38 are freed from the 9th to 1st SID if they are not empty.
- When processing the 9th SID, hMem[8], v37[8] and v38[8] are freed successfully.
- When processing the 8th to 2nd SID, only entries in hMem and v37 are freed since v38 contains null addresses. Note that the entries in Dst are never accessed since it lies above the other arrays.
- When processing the 1st SID, attempting to free hMem[0] causes an exception as it is the same location as v37[8], which has already been freed and is no longer a valid handle. hMem[0], or v37[8], is located at address 0x0083F164 in the diagram above.
The LocalFree function contains validation code to detect that the address is an invalid handle and proceeds to terminate the program without freeing the address. Therefore, the heap will not be corrupted.
Analysis of Crash 3: Write into Inaccessible Memory
Crafting a vulnerable certificate containing an exceptionally large number of SIDs would induce a different kind of crash behaviour. When such certificate is imported, the stack memory is corrupted more drastically and becomes overwhelmed with address pointers associated with the additional SIDs. Note that these address pointers are automatically generated by memory allocation functions such as LocalAlloc and are not controlled by the user.
Using a certificate with less than around 200 SIDs does not change the crash behaviour. The execution flow would reach the free loop at the end of the function and encounter an exception due to either the first or second crash behaviour, depending on whether 64-bit or 32-bit CertUtil is used. However, when there are around 200 SIDs or more, the program will eventually attempt to write into an inaccessible memory region, resulting in process termination. This happens during the allocation of pointers into the arrays, which takes place before reaching the free loop at the end of the function.
Threat Assessment
Recall that the patch for NCrypt.I_GetDescriptorFromAndCombiner checks that there are not more than 8 SIDs in the SID-protected PFX certificate using the AND combiner. This effectively mitigates the vulnerability because the code that causes the buffer overflow will be bypassed when there are more than 8 SIDs detected in the certificate. For systems that have not been patched, importing the crafted certificate would cause a buffer overflow within the stack memory, which eventually causes the program (CertUtil or CertMgr) to terminate unexpectedly.
According to the description for CVE-2023-23416, an attacker may be able to achieve Arbitrary Code Execution when a malicious certificate is imported on an affected system. For the vulnerability to be exploited, the attacker would need to overcome one or more of the challenges listed below:
- There are validation checks present within the LocalFree function to prevent an invalid handle from being freed, making heap exploitation techniques less likely to succeed.
- Since the pointers that are written into the affected arrays are automatically generated, the attacker may not be able to perform arbitrary write on any out-of-bound locations.
- Assuming the invalid handle exception can be bypassed, the corrupted stack canary would terminate the program upon function exit, preventing any exploit chain to be developed further.
- Increasing the number of SIDs further does not result in unintended execution flow. Program simply terminates upon writing into the inaccessible memory region.
Despite the seemingly high difficulty for an attacker to exploit this vulnerability, it is still important to patch this CVE as soon as possible, as there may be other possible threat vectors that have not been discovered in this study.
Conclusion
In this article, we identified and analysed the vulnerability in CVE-2023-23416. The vulnerability was located by identifying the modules used by CertUtil and checking which ones had updated timestamps after installing the cumulative update. Patch analysis was conducted to determine which functions had been modified. We were able to narrow it down to an internal function located within ncrypt.dll. Using a call graph to visualise the required execution path, an input certificate was progressively crafted to reach the target function and trigger a crash. We discovered that the root cause of the vulnerability involved the use of fixed-size arrays without checking if the maximum capacity would be exceeded, resulting in a buffer overflow when a maliciously crafted certificate was imported. We then explored different kinds of crash behaviour that could result from the vulnerability and assessed that it may be difficult for an attacker to exploit it.
Additionally, this study has provided me valuable insights into various protection modes utilised to safeguard a PFX certificate. It shed light on the critical role played by the ncrypt.dll library in managing these protection mechanisms effectively. Overall, it has broadened my knowledge and understanding of vulnerabilities related to certificate security, underscoring the significance of robust cybersecurity practices in today’s digital landscape.
—
If you are interested in a career in cybersecurity research, find out more about our available research roles at CSIT.