[CVE-2023-24949] Windows Kernel Elevation of Privilege Vulnerability Analysis

0. Introduction

This article introduces the CVE-2023-24949 vulnerability, whose full English name is “Windows Kernel Elevation of Privilege Vulnerability”, indicating that the vulnerability is located in ntoskrnl.exe. According to the official advisory, an attacker can exploit this vulnerability to achieve local privilege escalation.

This post describes the vulnerability based on my personal analysis workflow. Through patch comparison, function analysis, and function debugging, the vulnerability mechanism and trigger method were determined, and the vulnerability was successfully triggered. Finally, a brief summary is provided.

1. Patch Comparison

Checking ntoskrnl.exe before and after the patch, a total of three functions were modified.

Among them:

IoInitSystemPreDrivers adjusted some code structures, reducing nested conditional statements, and refactored some code into a new function, with no functional changes;

PopTransitionSystemPowerStateEx also had no observable functional changes;

The issue resides in the MiCaptureRetpolineRelocationTables function:

Note: For ease of understanding, I am directly pasting the version of the function where variable names and data structures have been cleaned up after performing the functional analysis.

Note that the position enclosed in the yellow box is where the patch was applied. Before the patch, it first added three numbers together, then checked if the addition result was greater than one of the addends to determine if an overflow occurred. After the patch, the addition of the three numbers is split into two steps, and each addition operation is performed by calling RtlULongAdd. This function contains a check to see if the addition result is greater than the augend:

NTSTATUS __stdcall RtlULongAdd(ULONG ulAugend, ULONG ulAddend, ULONG *pulResult)
{
  ULONG v3; // eax
  ULONG result; // edx
  NTSTATUS status; // eax

  v3 = ulAugend + ulAddend;
  result = -1;
  if ( v3 >= ulAugend )
    result = v3;
  status = v3 < ulAugend ? 0xC0000095 : 0;
  *pulResult = result;
  return status;
}

From this, it can be seen that this is actually an integer overflow issue. Before the patch, it only checked whether the addend imageDynamicRelocationRva overflowed after being added to the other two numbers, but did not check whether the addition of the addends baseRelocSize and 12 overflowed.

Note the green box in the screenshot. In the subsequent code, baseRelocSize + 12 is used as the size parameter for memory allocation and data copying. Although not visible in the screenshot, the three addends in the addition operation are originally 4-byte data, and the result of the addition is also 4-byte data. However, on 64-bit systems, the registers storing this data are actually 64-bit, and the parameters representing data size in the MiAllocatePool function and the memmove function are of type size_t, which are also 64-bit. Therefore, if the size parameter baseRelocSize + 12 overflows, it can bypass the curRetRelocRva > endOfTable check above, resulting in memmove accessing data far beyond the legitimate range.

2. Functional Analysis of the Function

The screenshot above shows the variables renamed and relevant data structures imported. However, at the beginning of the analysis, all variable names were formatted as v*, making the function’s purpose completely opaque. Therefore, after analyzing the patch, I first analyzed the functionality of the function.

2.1 Initial Attempts

Judging from the function name, this function is related to Retpoline, which is a mechanism used to mitigate Spectre vulnerability variants. Furthermore, according to the function references, it is called by MiParseImageLoadConfig, which seems related to the loading of executable files, but I wasn’t certain.

I had some superficial knowledge of these two types of vulnerabilities, but I didn’t know the relationship between Retpoline and executable loading, so I began some aimless searches.

While searching for keywords like “Retpoline” and “relocation table”, I discovered an introduction to the Dynamic Value Relocation Table (DVRT) [1] and its linked documentation for the Load Configuration Directory [2]. Everything suddenly became clear.

Spectre is a hardware-related vulnerability that exploits the CPU’s branch prediction mechanism to leak sensitive data. Retpoline is a CPU mitigation mechanism developed by Google to address this issue.

In the original CPU branch prediction mechanism, the CPU predicts the target address of a branch jump based on previously executed instructions and performs speculative execution. However, an attacker can construct specific code sequences to cause the CPU to mispredict the branch, thereby accessing protected data.

The Retpoline mechanism uses a pattern similar to a jump table to convert branch jump operations into indirect, non-speculative jumps, thereby avoiding the use of CPU branch prediction.

The Retpoline translation is executed dynamically when the kernel module is loaded into memory. But how does the operating system know where to perform this translation? The answer is that all the information is stored in the Dynamic Value Relocation Table (DVRT), which can be retrieved through the binary’s load config directory.

The DVRT is embedded into the binary during compilation. The compiler stores all metadata related to indirect jumps/calls into the .reloc section of the file. When the file runs, the kernel parses the DVRT data and performs translations for all saved branch jumps.

First is the format of the DVRT itself. I have only listed two data structures, as they can be imported into IDA to help analyze the vulnerable function:

// DVRT Header
struct ImageDynamicRelocationTable
{
    uint32_t version;   // Currently only version 1 exists
    uint32_t size;      // Size of the retpoline information data following the header
};

// Based on different retpoline types, there is a header at the start of each block
struct ImageDynamicRelocation
{
    uint64_t symbol;          // Represents the retpoline type, optional values are 3, 4, 5
    uint32_t baseRelocSize;   // Size of the retpoline information data of this type following the header
};

Next is the _IMAGE_LOAD_CONFIG_DIRECTORY64 structure, which is used to locate the DVRT. It can also be imported into IDA. The relevant fields will be explained when we use this data structure later:

struct _IMAGE_LOAD_CONFIG_CODE_INTEGRITY {
   WORD Flags;           
   WORD Catalog;         
   DWORD CatalogOffset;  
   DWORD Reserved;       
};

struct _IMAGE_LOAD_CONFIG_DIRECTORY64 {
   DWORD Size;           
   DWORD TimeDateStamp; 
   WORD MajorVersion;    
   WORD MinorVersion;    
   DWORD GlobalFlagsClear;
   DWORD GlobalFlagsSet;   
   DWORD CriticalSectionDefaultTimeout; 
   ULONGLONG DeCommitFreeBlockThreshold; 
   ULONGLONG DeCommitTotalFreeThreshold;
   ULONGLONG LockPrefixTable;
   ULONGLONG MaximumAllocationSize;
   ULONGLONG VirtualMemoryThreshold; 
   ULONGLONG ProcessAffinityMask;
   DWORD ProcessHeapFlags; 
   WORD CSDVersion;     
   WORD DependentLoadFlags;
   ULONGLONG EditList;        
   ULONGLONG SecurityCookie;  
   ULONGLONG SEHandlerTable;   
   ULONGLONG SEHandlerCount;   
   ULONGLONG GuardCFCheckFunctionPointer; 
   ULONGLONG GuardCFDispatchFunctionPointer;
   ULONGLONG GuardCFFunctionTable;
   ULONGLONG GuardCFFunctionCount;
   DWORD GuardFlags;     
   _IMAGE_LOAD_CONFIG_CODE_INTEGRITY CodeIntegrity;
   ULONGLONG GuardAddressTakenIatEntryTable;
   ULONGLONG GuardAddressTakenIatEntryCount;
   ULONGLONG GuardLongJumpTargetTable;
   ULONGLONG GuardLongJumpTargetCount;
   ULONGLONG DynamicValueRelocTable;
   ULONGLONG CHPEMetadataPointer;
   ULONGLONG GuardRFFailureRoutine;
   ULONGLONG GuardRFFailureRoutineFunctionPointer;
   DWORD DynamicValueRelocTableOffset;
   WORD DynamicValueRelocTableSection;
   WORD Reserved2;
   ULONGLONG GuardRFVerifyStackPointerFunctionPointer;
   DWORD HotPatchTableOffset;
   DWORD Reserved3;
   ULONGLONG EnclaveConfigurationPointer;
   ULONGLONG VolatileMetadataPointer;
   ULONGLONG GuardEHContinuationTable;
   ULONGLONG GuardEHContinuationCount;
 };

2.4 Summary

Based on the above information, we can determine that the vulnerable function is used to retrieve the DVRT during the load config phase. By looking at its callers and the other function parameters executed before the call, we can determine the types and functions of most variables in the vulnerable function, ultimately obtaining the function state shown in the screenshot at the beginning of the article.

3. Vulnerability Triggering

3.1 Determining the Approach

To trigger the vulnerability, we first need to know how to reach the vulnerable function. When checking references to MiCaptureRetpolineRelocationTables, we find two functions call it:

ChatGPT’s explanation of these two functions is as follows:

MiParseImageLoadConfig is a function in the NT kernel of the Windows operating system used to parse the load configuration information of executable files. In Windows, executable files usually contain load configuration information that instructs the operating system how to load the file into memory and how to initialize the data and code inside. The MiParseImageLoadConfig function is primarily used during the kernel module loading process. It reads the load configuration information from the executable file and performs appropriate memory allocation and initialization based on this information. Specifically, MiParseImageLoadConfig can complete the following tasks:

  1. Parse the load configuration information in the executable, including base address, size, checksum, etc.
  2. Calculate the required memory size and location based on the load configuration information, and allocate the corresponding virtual memory area.
  3. Copy code, data, and other parts from the executable file to the allocated memory area, and perform necessary relocation and initialization work.

Note that MiParseImageLoadConfig is a low-level function in the Windows kernel and is not usually called directly by applications. It is mainly called by other kernel module-related functions to complete the loading and initialization of executables.

MiCaptureBootDriverRetpolineInfo is a function in the NT kernel of the Windows operating system used to capture Retpoline information of boot drivers. In Windows, Retpoline is a special code transformation technique used to mitigate security risks caused by Spectre and similar CPU vulnerabilities. Specifically, when a driver is loaded into memory, MiCaptureBootDriverRetpolineInfo scans its code section and generates a corresponding jump target address table for the Retpoline calls contained within. This jump target address table can be used to replace the original Retpoline jumps, thereby improving code execution efficiency and security. Note that MiCaptureBootDriverRetpolineInfo is only called during the boot process. It is mainly used to capture Retpoline information of boot drivers for use in subsequent operations. Retpoline information for other drivers is captured and processed during loading.

It seems that one function is called when executables are loaded, and the other is called during the system boot process. Obviously, the former is easier to analyze.

Despite thinking this way, I was not sure about the accuracy of the above information, nor did I know if my understanding was correct. Therefore, I set breakpoints on both functions separately and tried to interact with the operating system to see if they would break.

Subsequently, I found that when browsing files in Explorer, the system could easily trigger breakpoints multiple times in MiParseImageLoadConfig, but no breakpoints were hit in MiCaptureBootDriverRetpolineInfo. Therefore, I chose the MiParseImageLoadConfig call chain as the target of analysis.

3.2 Locating the DVRT

Currently, there is no public documentation describing the PE file structure containing DVRT information. Moreover, when testing system breakpoints earlier, I also set a breakpoint on the vulnerable function itself, but it was never hit. Thus, I could not determine the location of the DVRT information by analyzing existing files.

Note: Later I found that it was actually relatively easy to hit the breakpoint in the vulnerable function in the system folder, but because there were too many files, I couldn’t be sure which file contained the DVRT information.

Initially, I tried to compile a program with DVRT directly using Visual Studio features, but failed. Unsure if VS supports this functionality natively, I decided to create an arbitrary program and directly modify the file content using a hex editor, so that the system would reach the vulnerable location when processing this file.

I used Visual Studio to create an Empty Project (C++), with arbitrary content. I only wrote an empty main function and compiled it to generate the .exe file.

First, I tested with this file to see how far the system code could execute. Setting a breakpoint on MiCaptureRetpolineRelocationTables did not trigger any hits, but setting a breakpoint on MiParseImageLoadConfig triggered multiple hits, making it impossible to determine which hit was caused by the test file.

By analyzing in IDA, I found that before calling MiCaptureRetpolineRelocationTables, the system first calls MiCaptureDynamicRelocationTableRva. Only if this function succeeds will execution proceed. Therefore, I set a breakpoint at the call location of this function and placed the test file in an isolated folder. When I opened this folder in File Explorer, the system hit the breakpoint at the call of MiCaptureDynamicRelocationTableRva, indicating that this normally generated test file could at least reach MiCaptureDynamicRelocationTableRva.

MiCaptureDynamicRelocationTableRva is defined as follows:

Note: The type of the variable configDirec was my guess based on the function’s purpose and the DVRT-related data structures. After setting it, I found that the _IMAGE_LOAD_CONFIG_DIRECTORY64 type indeed satisfies the function’s requirements.

Note the comments in the image: to make this function execute successfully, we need the DynamicValueRelocTableSection field to be non-zero.

But how do we determine the location of DynamicValueRelocTableSection in the file? We can use the dumpbin.exe tool provided by Visual Studio. The output is as follows:

PS C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x86> ./dumpbin.exe /loadconfig [Redacted File Path]
Microsoft (R) COFF/PE Dumper Version 14.35.32216.1
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file [Redacted File Path]

File Type: EXECUTABLE IMAGE

  Section contains the following load config:

            00000140 size
                   0 time date stamp
                0.00 Version
                   0 GlobalFlags Clear
                   0 GlobalFlags Set
                   0 Critical Section Default Timeout
                   0 Decommit Free Block Threshold
                   0 Decommit Total Free Threshold
     0000000000000000 Lock Prefix Table
                    0 Maximum Allocation Size
                    0 Virtual Memory Threshold
                    0 Process Affinity Mask
                    0 Process Heap Flags
                    0 CSD Version
                 0000 Dependent Load Flag
     0000000000000000 Edit List
     0000000140003008 Security Cookie
     0000000140002190 Guard CF address of check-function pointer
     00000001400021A0 Guard CF address of dispatch-function pointer
     0000000000000000 Guard CF function table
                    0 Guard CF function count
             00000100 Guard Flags
                        CF instrumented
                 0000 Code Integrity Flags
                 0000 Code Integrity Catalog
             00000000 Code Integrity Catalog Offset
             00000000 Code Integrity Reserved
     0000000000000000 Guard CF address taken IAT entry table
                    0 Guard CF address taken IAT entry count
     0000000000000000 Guard CF long jump target table
                    0 Guard CF long jump target count
     0000000000000000 Dynamic value relocation table
     0000000000000000 Hybrid metadata pointer
     0000000000000000 Guard RF address of failure-function
     0000000000000000 Guard RF address of failure-function pointer
             00000000 Dynamic value relocation table offset
                 0000 Dynamic value relocation table section
.....

Note that both Dynamic value relocation table offset and Dynamic value relocation table section in the file are 0. This is what we plan to modify.

To locate this content in the file, we can search for the 0000000140003008 Security Cookie field, as its value is relatively unique and unlikely to repeat. We eventually locate it at the following position:

According to the structure of _IMAGE_LOAD_CONFIG_DIRECTORY64, we determine the starting position of the configuration information in the red box and locate the DVRT information to be modified. The first four bytes represent the offset, and the next four bytes represent the section. We need to modify the last four bytes to make them non-zero. Since this binary has 6 sections, this value cannot be greater than 6. We can simply change this value to 1.

The modified file cannot reach the vulnerability point yet, but at this stage, we can debug first to verify the values of the relevant variables. Combined with the screenshot of the MiCaptureDynamicRelocationTableRva function above, after the system calculates the sectionHeader, checking this value reveals that it points to the first section header .text:

// sectionHeader
nt!MiCaptureDynamicRelocationTableRva+0xeb:
fffff805`7e9249f7 493bf8          cmp     rdi,r8
0: kd> db r8
fffff805`d4090208  2e 74 65 78 74 00 00 00-0c 0d 00 00 00 10 00 00  .text...........
fffff805`d4090218  00 0e 00 00 00 04 00 00-00 00 00 00 00 00 00 00  ................
fffff805`d4090228  00 00 00 00 20 00 00 60-2e 72 64 61 74 61 00 00  .... ..`.rdata..
fffff805`d4090238  b4 0e 00 00 00 20 00 00-00 10 00 00 00 12 00 00  ..... ..........
fffff805`d4090248  00 00 00 00 00 00 00 00-00 00 00 00 40 00 00 40  ............@..@
fffff805`d4090258  2e 64 61 74 61 00 00 00-38 06 00 00 00 30 00 00  .data...8....0..
fffff805`d4090268  00 02 00 00 00 22 00 00-00 00 00 00 00 00 00 00  ....."..........
fffff805`d4090278  00 00 00 00 40 00 00 c0-2e 70 64 61 74 61 00 00  ....@....pdata..
// Calculated DVRT RVA
0: kd> p
nt!MiCaptureDynamicRelocationTableRva+0x100:
fffff805`7e924a0c 41890e          mov     dword ptr [r14],ecx
0: kd> r rcx
rcx=0000000000001000

Continuing execution leads into the MiCaptureRetpolineRelocationTables function. The DVRT obtained based on the calculated RVA value is as follows:

0: kd> p
nt!MiCaptureRetpolineRelocationTables+0x76:
fffff805`7e9254de 4a8b043b        mov     rax,qword ptr [rbx+r15]
0: kd> p
nt!MiCaptureRetpolineRelocationTables+0x7a:
fffff805`7e9254e2 4889442420      mov     qword ptr [rsp+20h],rax
2: kd> db rbx+r15
fffff805`d4091000  b8 01 00 00 00 c3 cc cc-cc cc cc cc cc cc cc cc  ................
fffff805`d4091010  cc cc cc cc cc cc 66 66-0f 1f 84 00 00 00 00 00  ......ff........
fffff805`d4091020  48 3b 0d e1 1f 00 00 75-10 48 c1 c1 10 66 f7 c1  H;.....u.H...f..
fffff805`d4091030  ff ff 75 01 c3 48 c1 c9-10 e9 aa 02 00 00 cc cc  ..u..H..........
fffff805`d4091040  40 53 48 83 ec 20 b9 01-00 00 00 e8 be 0b 00 00  @SH.. ..........
fffff805`d4091050  e8 db 06 00 00 8b c8 e8-e8 0b 00 00 e8 cb 06 00  ................
fffff805`d4091060  00 8b d8 e8 0c 0c 00 00-b9 01 00 00 00 89 18 e8  ................
fffff805`d4091070  44 04 00 00 84 c0 74 73-e8 37 09 00 00 48 8d 0d  D.....ts.7...H..

Searching for this data in the test file, we find that it is the starting content of the .text section:

Based on the introduction to the DVRT data structure above, we know that if this data is parsed as a DVRT, it starts with an 8-byte ImageDynamicRelocationTable structure, followed by a 12-byte ImageDynamicRelocation structure.

At this point, although we do not know the exact structure of a PE file that genuinely contains a DVRT, we can successfully spoof the system into believing our test file has a DVRT, and we know the location where the system expects the DVRT to reside in the file.

3.3 Analyzing Trigger Conditions & Attempting to Trigger the Vulnerability

Let’s look at the details of the MiCaptureRetpolineRelocationTables function to determine the requirements the DVRT must meet:

Note the parts enclosed in the yellow boxes, and modify the data in the test file accordingly:

At this point, the test file should be modified. However, when testing again, the system did not crash, and the debugger showed the following message:

SXS: BasepCreateActCtx() NtCreateSection() failed. Status = 0xc000009a

Stepping through MiCaptureRetpolineRelocationTables again, we find that an integer overflow indeed occurs when the system executes the addition of the three values:

3: kd> p
nt!MiCaptureRetpolineRelocationTables+0xfe:
fffff805`7e925566 4503f4          add     r14d,r12d
3: kd> r r14d
r14d=1014
3: kd> r r12d
r12d=ffffffff
3: kd> p
nt!MiCaptureRetpolineRelocationTables+0x101:
fffff805`7e925569 443bf2          cmp     r14d,edx
3: kd> r r14d
r14d=1013

However, when the system executed the MiAllocatePool function, the allocation failed. Let’s look closely at the arguments for this function call:

3: kd> p
nt!MiCaptureRetpolineRelocationTables+0x181:
fffff805`7e9255e9 e832ddb1ff      call    nt!MiAllocatePool (fffff805`7e443320)
3: kd> r rcx
rcx=0000000000000100
3: kd> r rdx
rdx=000000010000000b

rdx should be the allocation size. As we suspected, due to the integer overflow, it attempts to allocate an extremely large memory block. This is where the issue lies; because my virtual machine did not have enough RAM, this function call failed.

After increasing the virtual machine’s memory, the allocation succeeded, and execution proceeded to the data copying operation:

0: kd> p
nt!MiCaptureRetpolineRelocationTables+0x19d:
fffff805`7e925605 e83619d0ff      call    nt!memcpy (fffff805`7e626f40)
0: kd> r rcx
rcx=ffffd6001c000000
0: kd> r rdx
rdx=fffff805e5631008
0: kd> r r8
r8=000000010000000b

It likewise attempts to copy a massive amount of data, with the source address fffff805e5631008 and the size 10000000b. Due to accessing invalid memory space, the system crashed.

4. Summary

Based on the analysis above, CVE-2023-24949 is an integer overflow vulnerability. Up to the point of my analysis, this vulnerability can achieve an out-of-bounds memory read. Further analysis is required to achieve local privilege escalation.

It is worth noting that the trigger condition for this vulnerability is extremely simple. Assuming sufficient memory, merely viewing the test file in File Explorer can trigger the vulnerability. If the test file is placed on the Desktop, the system will fail to boot normally. If combined with phishing attacks, this vulnerability could achieve more impact. Given this, the PoC file will not be directly published on GitHub; interested readers can attempt replication themselves based on this article.

5. References

  1. Dynamic Value Relocation Table (DVRT) details
  2. Windows PE32 load config directory
  3. How Meltdown and Spectre haunt Anti-Cheat
  4. https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-24949