In Part 1 of this blog series, we analyzed the root cause for CVE-2022-37969. In this blog, we will present an in-the-wild exploit that was discovered by Zscaler ThreatLabz that successfully leveraged CVE-2022-37969 for privilege escalation on Windows 10 and Windows 11.
Debugging Environment
The analysis and debugging for the exploitation was conducted in the following environment.
Windows 11 21H2 version 22000.918\
CLFS.sys 10.0.22000.918
Windows 10 21H2 version 19044.1949\
CLFS.sys 10.0.19041.1865
Prerequisite
Before starting to analyze the exploitation of CVE-2022-37969, we'd like to introduce some key structures in the kernel related to this exploit.
The _EPROCESS structure is an opaque structure that represents the process object for a process in the kernel. In other words, each process running on Windows has its corresponding _EPROCESS object somewhere in the kernel. Figure 1 shows the layout of the _EPROCESS structure in the kernel for Windows 11. This layout might change significantly between Windows versions.

Figure 1. The _EPROCESS structure on Windows 11
The Token field is stored at offset 0x4B8 in the _EPROCESS structure. The _EPROCESS.Token points to an _EX_FAST_REF structure rather than a _TOKEN structure. Based on the layout of the _EX_FAST_REF structure, its three fields (Object, RefCnt, Value) have the same offset, the last 4 digits of the _EX_FAST_REF object represents the RefCnt field that denotes the reference to this token. Therefore, we can zero the last 4 digits out and get the actual address of the _TOKEN structure.
The _TOKEN structure is a kernel structure that describes the security context of a process and contains information such as the token id, token privileges, session id, token type, logon session, etc. Figure 2 shows the structure layout of the _TOKEN structure in the kernel.

Figure 2. The _Token structure on Windows 11
In general, manipulating the _Token object can be used to execute privilege escalation in the kernel. Two general techniques are involved, one is token replacement which means that a low-privileged token associated with a process is replaced with a high-privileged token associated with another process. The second technique is token privilege adjustment which means that more privileges are added and enabled to an existing token. The exploit captured by ThreatLabz for CVE-2022-37969 leveraged the token replacement technique.
In user space, a user can use the CreatePipe function to create an anonymous pipe, which returns handles to the read and write ends of the created pipe.
BOOL CreatePipe(\
[out] PHANDLE hReadPipe,\
[out] PHANDLE hWritePipe,\
[in, optional] LPSECURITY_ATTRIBUTES lpPipeAttributes,\
[in] DWORD nSize\
);
The user is able to add attributes to the pipe. The attributes are a key-value pair and stored in a linked list. The PipeAttribute structure is the representation of the attributes in kernel space, which is allocated in the PagedPool. The PipeAttribute structure is defined below.
struct PipeAttribute {\
LIST_ENTRY list ;\
char * AttributeName;\
uint64_t AttributeValueSize;\
char * AttributeValue;\
char data [0];\
};
*typedef struct _LIST_ENTRY {\
struct _LIST_ENTRY *Flink;\
struct _LIST_ENTRY *Blink;\
} LIST_ENTRY, PLIST_ENTRY, PRLIST_ENTRY;
The memory layout of the PipeAttribute structure is illustrated in Figure 3.

Figure 3. PipeAttribute structure
A pipe attribute can be created on a pipe using the NtFsControlFile API, where the 6th parameter FsControlCode is set to 0x11003C. The attribute value can be read using the NtFsControlFile API, where the 6th parameter FsControlCode is set to 0x110038.
The _ETHREAD structure is an opaque structure that represents the thread object for a thread in the kernel. Figure 4 shows the layout of the _ETHREAD structure. The PreviousMode field is located at offset 0x232 in the _ETHREAD structure.

Figure 4. The _ETHREAD structure
Regarding the PreviousMode, Microsoft's documentation states "When a user-mode application calls the Nt or Zw version of a native system services routine, the system call mechanism traps the calling thread to kernel mode. To indicate that the parameter values originated in user mode, the trap handler for the system call sets the PreviousMode field in the thread object of the caller to UserMode."
We take the NtWriteVirtualMemory function as an example. Figure 5 shows the implementation of the NtWriteVirtualMemory function. When PreviousMode is set to 1 (UserMode) the call of the NT or Zw version function comes from user space, where it conducts address validation. In this case, an arbitrary write across the whole kernel memory will fail. On the contrary, when PreviousMode is set to 0 (KernelMode), the address validation is skipped and the arbitrary kernel memory address can be written. The exploit targeting Windows 10 for CVE-2022-37969 leverages PreviousMode to implement an arbitrary write primitive.

Figure 5. The implementation of the NtWriteVirtualMemory function
Exploitation on Windows 11
In the previous section, we introduced the key structures that will be involved in the process of exploitation. Let's deep dive into the sample of the exploit. The exploit involves the following steps.
0x01 Check Windows OS version
The exploit first checks if the Windows operating system (OS) version running the sample is supported. Figure 6 shows the pseudo-code snippet for checking the Windows OS version.

Figure 6. The pseudo-code snippet checking the Windows OS version
Figure 7 demonstrates a Windows OS Build Number that consists of the OS build number and UBR.

Figure 7. Windows OS Build Number
The exploit first obtains the _PEB object via NtCurrentTeb()->ProcessEnvironmentBlock, then gets the OS Build Number from the OSBuildNumber field at offset 0x120 in the _PEB structure. The UBR can be obtained via querying the value of UBR in the registry key HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion. Once the exploit confirms that the targeting Windows is supported, the code stores the offset of the Token field for the _EPROCESS structure in a global variable. In our debugging environment for Windows 11 (21H2) 10.0.22000.918, the offset was equal to 0x4B8.
Based on the code in Figure 6, we summarize the supported Windows OS version in Figure 8.

Figure 8. The supported Windows OS version (before patching)
Zsclaer's ThreatLabz verified the exploit in the following versions, where a local privilege escalation can be performed successfully. Other vulnerable Windows OS versions in Figure 8 have not been verified by ThreatLabz at the time of publication.
Windows 10 21H2 version 19044.1949, Windows 10 Enterprise\
Windows 10 20H2 version 19042.1949, Windows 10 Enterprise\
Windows 11 21H2 version 22000.918, Windows 11 Pro x64
0x02 Retrieve _EPROCESS and _TOKEN
Next, the exploit obtains the key data structures _EPROCESS and _TOKEN for the current process and the System process (always PID 4) owning the SYSTEM privilege via calling the NtQuerySystemInformation API with the appropriate parameters. The NtQuerySystemInformation API is used to retrieve the specified system information based on the first parameter, with the declaration shown below.
__kernel_entry NTSTATUS NtQuerySystemInformation(\
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,\
[in, out] PVOID SystemInformation,\
[in] ULONG SystemInformationLength,\
[out, optional] PULONG ReturnLength\
);
Figure 9 shows the pseudo-code snippet to obtain the corresponding address of the _EPROCESS and _TOKEN objects for the current process and the System process.

Figure 9. The pseudo-code snippet to obtain the corresponding address of the _EPROCESS and _TOKEN objects
This function is described as follows:
1. Obtain the function address of the NtQuerySystemInformation API.
2. Call the NtQuerySystemInformation API, where the first parameter is set with SystemExtendedHandleInformation (0x40). If the function returns an NTSTATUS success, the retrieved information is stored at the second parameter SystemInformation, which is a pointer to the SYSTEM_HANDLE_INFORMATION_EX structure whose memory layout is illustrated in Figure 10. Next, the code locates the _EPROCESS object associated with the current process. Finally, the address of the _EPROCESS object is stored in a global variable.

Figure 10. The memory layout of the SYSTEM_HANDLE_INFORMATION_EX structure
3. Call the NtQuerySystemInformation API again, where the first parameter is set with SystemExtendedHandleInformation (0x40). If the function returns an NTSTATUS success, the code locates the _EPROCESS object associated with the System process (PID 4). Finally, the address of the System _EPROCESS object is stored in a global variable.
4. Obtain the address of the Token field for the _EPROCESS object representing the current process and the address of the Token field of the _EPROCESS object representing the System process. Both addresses are stored in corresponding global variables.
The exploit also stores some key data structures in global variables. We summarize these global variables and what they represent in Figure 11.

Figure 11. The key global variables in the exploit
0x03 Check access token
The exploit calls the OpenProcessToken function to open the access token associated with the current process. A pointer to a handle that identifies the newly opened access token is stored at the third parameter when the OpenProcessToken function returns. Then, the exploit calls the NtQuerySystemInformation API, where the first parameter is set with SystemHandleInformation (0x10). If it returns an NTSTATUS success, it checks if the handle that identifies the newly opened access token exists in the system handle list. Figure 12 shows the pseudo-code snippet for checking the access token.
Figure 12. The pseudo-code snippet for checking the access token
0x04 Obtain the constant offset between the two bigpools representing the Base Block
As shown in Figure 9 in Part 1, the Proof-of-Concept code for CVE-2022-37969 first creates a base log file MyLog.blf via the CreateLogFile API. Then the code creates dozens of base log files named MyLog_xxx.blf, where Zscaler ThreatLabz's PoC uses a constant count. The exploit code uses the following advanced technique to ensure the offset between the two subsequently created bigpools representing the Base Block constant. Figure 13 shows the code snippet to obtain the constant value between two adjacent bigpools representing the Base Block.
Figure 13. The code snippet to obtain the constant value between two adjacent bigpools representing the Base Block
After every new base file named MyLog_xxx.blf is created, the code calls the ZwQuerySystemInformation API, where the first parameter is set with SystemBigPoolInformation(0x42). If the function returns an NTSTATUS success, the retrieved information is stored at the second parameter SystemInformation, which is a pointer to the SYSTEM_BIGPOOL_ENTRY structure that holds all bigpool memory at runtime. Then it locates the bigpool representing the Base Block of a base log file, where the size of the bigpool must be 0x7a00, and the tag name of the bigpool is "Clfs". The address of the bigpool is stored in a local array. Next, in a loop, the code checks if the offset between the base block of the N-th BLF and the base block of the N+1-th BLF is constant. The code will jump out of the loop until the offset is constant. In our debugging environment, the constant value is 0x11000. The constant value plus 0x14B is set to the cbSymbolZone field in the Base Record Header.
0x05 Craft the base log file
Part 1 of this blog series describes the process of crafting the base log file in detail. Before crafting the base log file, the exploit code performs heap spraying to set up the controlled memory.
Figure 14 shows the process of heap spraying.
Figure 14. Perform heap spraying to set up memory
Figure 15 shows the memory layout after performing heap spraying.
Figure 15. The memory layout after performing heap spraying
0x06 Module-gadget performing arbitrary write primitive
Figure 16 shows the code snippet for performing an arbitrary write on the PipeAttribute object.
Figure 16. The code snippet for performing an arbitrary write on the PipeAttribute object
This code snippet is described as follows:
1. Call the CreatePipe function to create an anonymous pipe, and add a pipe attribute on the pipe using the NtFsControlFile API, where the 6th parameter FsControlCode is set to 0x11003C. Then the code calls the ZwQuerySystemInformation API, where the first parameter is set with SystemBigPoolInformation(0x42). After the function returns an NTSTATUS success, the retrieved information is a pointer to the SYSTEM_BIGPOOL_ENTRY structure that holds all bigpool memory at runtime. Finally, the exploit locates the bigpool representing the newly created PipeAttribute object. The variable v30 stores the kernel address of this PipeAttribute object. Figure 17 shows the memory layout of this created PipeAttribute object in the kernel.
Figure 17. The memory layout of this created PipeAttribute object in the Windows kernel
2. The global variable qword_1400A8108 stores the kernel address of the _EPROCESS object for the System (PID 4) process. Then the exploit performs heap spraying shown in Figure 18. The address of the AttributeValueSize field in the PipeAttribute object is set at offset N*8+8 in the memory region (0x10000 ~ 0x1010000). The result of addr_EPROCESS_System & 0xfffffffffffff000 is written at offset 0xFFFFFFFF, and 0x414141414141005A is written at offset 0x100000007.
Figure 18. CVE-2022-37969 exploit performs heap spraying
3. Arrange the memory region (0x50000000x5100000), where the address of the ClfsEarlierLsn function is stored at 0x5000008, and the address of the SeSetAccessStateGenericMapping function is stored at 0x5000018 (see Figure 19).
Figure 19. The memory region(0x50000000x5100000)
4. Trigger the CLFS vulnerability. The CClfsBaseFilePersisted::RemoveContainer function will be hit. Figure 20 shows the location of dereferencing a corrupted pointer to a fake CClfsContainer object in CLFS.sys. The data that the address being dereferenced points to can be controlled and manipulated by heap spraying in user space.
Figure 20. Dereference the corrupted point to the CClfsContainer object
The fake vftable in the fake CClfsContainer object points to 0x5000000, where the address of the ClfsEarlierLsn function is stored at 0x5000008, and the address of the SeSetAccessStateGenericMapping function is stored at 0x0x5000018. The subsequent code will call the ClfsEarlierLsn function and the nt!SeSetAccessStateGenericMapping function successively. After the ClfsEarlierLsn function returns, the register RDX is equal to 0xFFFFFFFF. Figure 21 shows what the SeSetAccessStateGenericMapping function does and how to perform an arbitrary write.

Figure 21. Perform an arbitrary write on the PipeAttribute object
At the end of the SeSetAccessStateGenericMapping function, the AttributeValue field in the PipeAttribute object has been overwritten with addr_EPROCESS_System & 0xfffffffffffff000. The addr_EPROCESS_System represents the address of the _EPROCESS object for the System process (PID 4).
5. Read the pipe attribute on a pipe using the NtFsControlFile API, where the 6th parameter FsControlCode is set to 0x110038. This obtains the pipe attribute from the address that the overwritten AttributeValue field points to and copies the kernel data into a heap buffer in user space. The overwritten AttributeValue field points to the address addr_EPROCESS_System & 0xfffffffffffff000. Then, the code obtains the Token field in the _EPROCESS object for the System (PID 4) process based on the offset of the Token field. Finally, the value of the Token field for the System process (PID 4) is stored in a global variable qword_1400A8128.

Figure 22. Store the value of the Token field for the System process (PID 4)
0x07 Token replacement
Figure 23 shows the code snippet for performing token replacement on Windows 11.

Figure 23. The code snippet for performing token replacement
In order to complete the token replacement, the exploit triggers the CLFS vulnerability for the second time and performs the following actions.
1. Arrange the memory via heap spraying. The resulting address of the Token field for the current process minus 8 is set up at offset N*8+8 in the memory region (0x10000 ~ 0x1010000). The value of the Token field in the _EPROCESS object for the System process (PID 4) is written at offset 0xFFFFFFFF as shown in Figure 24.

Figure 24. Arrange the memory via heap spraying
2. Trigger the CLFS vulnerability to complete token replacement. The CClfsBaseFilePersisted::RemoveContainer function will be hit. Figure 25 shows the location of dereferencing a corrupted pointer to a fake CClfsContainer object in CLFS.sys. The data that the address being dereferenced points to can be controlled and manipulated by heap spraying in user space.

Figure 25. Dereference the corrupted point to the CClfsContainer object
Again, the subsequent code will call the ClfsEarlierLsn function and the nt!SeSetAccessStateGenericMapping function successively. After the ClfsEarlierLsn function returns, the register RDX is equal to 0xFFFFFFFF. Figure 26 shows what the SeSetAccessStateGenericMapping function does and how to perform an arbitrary write.

Figure 26. Perform an arbitrary write primitive to complete token replacement
At the end of the SeSetAccessStateGenericMapping function, the token replacement has been completed in Figure 27. The Token for the current process has been replaced with the Token for the System process (PID 4). This means that the current process has successfully elevated privileges to SYSTEM.
Figure 27. Gain the SYSTEM privilege successfully.
3. Spawn a command prompt (cmd.exe) with the newly obtained SYSTEM privilege. Figure 28 shows that the exploit spawns cmd.exe with the SYSTEM privilege.

Figure 28. Spawn a cmd with SYSTEM privilege
We have summarized the flow of the exploitation targeting Windows 11 in Figure 29.

Figure 29. The flow of the exploitation targeting Windows 11
Further, the process of performing an arbitrary write on a PipeAttribute object and token replacement is demonstrated in Figure 30.

Figure 30. The process of performing arbitrary write on a PipeAttribute object and token replacement on Windows 11