Threatlabz
Part 1
On September 2, 2022, Zscaler Threatlabz captured an in-the-wild 0-day exploit in the Windows Common Log File System Driver (CLFS.sys) and reported this discovery to Microsoft. In the September Tuesday patch, Microsoft fixed this vulnerability that was identified as CVE-2022-37969, which is a Windows Common Log File System Driver elevation of privilege vulnerability. An attacker who successfully exploits this vulnerability may gain SYSTEM privileges. The 0-day exploit can execute the privilege escalation successfully on Windows 10 and Windows 11 prior to the September patch. The cause of the vulnerability is due to the lack of a strict bounds check on the field cbSymbolZone in the Base Record Header for the base log file (BLF) in CLFS.sys. If the field cbSymbolZone is set to an invalid offset, an out-of-bound write will occur at the invalid offset. In this two-part blog series, we will demystify the vulnerability and the 0-day exploit discovered in-the-wild. The blogs consist of two parts: an analysis of the root cause, and an analysis of the exploit. In this blog, we first present a detailed analysis of the root cause for CVE-2022-37969.
Debugging Environment
All analysis and debugging in this two-part blog series are conducted in the following environment.
Windows 11 21H2 version 22000.918
CLFS.sys 10.0.22000.918
Introduction to CLFS Internals
The Common Log File System (CLFS) is a general-purpose logging subsystem that can be used by applications running in both kernel mode and user mode for building high-performance transaction logs, and is implemented in the driver CLFS.sys. The Common Log File System generates transaction logs in a base log file (BLF). The concepts and terminology introductions for CLFS are specified in the official documentation from Microsoft.
Prior to using CLFS, a log file is created using the CreateLogFile API. A log file is composed of a base log file that consists of metadata blocks, and several containers that store the actual data. The AddLogContainer API is used to add a container to the physical log that is associated with the log handle.
Figure 1 illustrates the BLF format based on the official CLFS documentation and Alex Ionescu’s unofficial documentation.
Figure 1. Base Log File (BLF) Format
A base log file is made up of six different metadata blocks, which are the control block, base block, and truncate block along with their corresponding block shadows. The three types of records (Control Record, Base Record, and Truncate Record) can reside in these blocks. This blog only focuses on the Base Record that is relevant to this vulnerability. The Base Record comprises the symbol tables that store information on the client contexts, container contexts, and security contexts associated with the base log file.
Every log block begins with a log block header, with the structure defined below:
typedef struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
CLFS_CLIENT_ID ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
} CLFS_LOG_BLOCK_HEADER, *PCLFS_LOG_BLOCK_HEADER;
The memory layout of the CLFS_LOG_BLOCK_HEADER structure whose size is 0x70 bytes has been illustrated in Figure 1. The SignaturesOffset field is the offset of an in-memory array that is used to store all sector signatures. The array should be located on the last sector in each block. The sector signature is located at the end of every sector (size: 0x200) and consists of a Sector Block Type (1 byte) and Usn (1 byte). The following enumerates the types of a sector.
const UCHAR SECTOR_BLOCK_NONE = 0x00;
const UCHAR SECTOR_BLOCK_DATA = 0x04;
const UCHAR SECTOR_BLOCK_OWNER = 0x08;
const UCHAR SECTOR_BLOCK_BASE = 0x10;
const UCHAR SECTOR_BLOCK_END = 0x20;
const UCHAR SECTOR_BLOCK_BEGIN = 0x40;
In Figure 1, the Base Block starts at offset 0x800 and ends at offset 0x71FF in a .BLF file and starts with a Log Block Header (0x70 bytes), followed by the Base Record Header, followed by records. The Base Record Header can be represented by the CLFS_BASE_RECORD_HEADER structure described in Figure 2.
Figure 2. The definition of the CLFS_BASE_RECORD_HEADER structure
The structure layout of the CLFS_BASE_RECORD_HEADER structure is illustrated in Figure 3.
Figure 3. The structure layout of Base Record Header in a .BLF file
The Base Record begins with a header (CLFS_BASE_RECORD_HEADER) whose size is 0x1338 bytes, followed by related context data. In the CLFS_BASE_RECORD_HEADER structure, some important fields related to this vulnerability are described below:
- rgClients represents the array of offsets that point to the Client Context object.
- rgContainers represents the array of offsets that point to the Container Context object.
- cbSymbolZone represents the next free available offset for a new symbol in the symbol zone.
In the Base Record, the Client Context, Container Context, and Shared Security Context are represented by symbols, which are preceded by the CLFSHASHSYM structure defined below:
typedef struct _CLFS_NODE_ID {
ULONG cType;
ULONG cbNode;
} CLFS_NODE_ID, *PCLFS_NODE_ID;
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;
The memory layout of the CLFSHASHSYM structure is illustrated in Figure 4, followed by various context objects.
Figure 4. CLFSHASHSYM structure (symbol header)
In the Base Record, the client context is used to identify a client for a log file. At least one client context can be created in a base log file. The client context is represented by the CLFS_CLIENT_CONTEXT structure defined below:
typedef struct _CLFS_CLIENT_CONTEXT
{
CLFS_NODE_ID cidNode;
CLFS_CLIENT_ID cidClient;
USHORT fAttributes;
ULONG cbFlushThreshold;
ULONG cShadowSectors;
ULONGLONG cbUndoCommitment;
LARGE_INTEGER llCreateTime;
LARGE_INTEGER llAccessTime;
LARGE_INTEGER llWriteTime;
CLFS_LSN lsnOwnerPage;
CLFS_LSN lsnArchiveTail;
CLFS_LSN lsnBase;
CLFS_LSN lsnLast;
CLFS_LSN lsnRestart;
CLFS_LSN lsnPhysicalBase;
CLFS_LSN lsnUnused1;
CLFS_LSN lsnUnused2;
CLFS_LOG_STATE eState;
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
} CLFS_CLIENT_CONTEXT, *PCLFS_CLIENT_CONTEXT;
The eState field is located at offset 0x78 in the CLFS_CLIENT_CONTEXT structure, and can be one of the following values:
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
In the Base Record, the container context is related to adding a container file for a base log file, which is represented by the CLFS_CONTAINER_CONTEXT structure defined below:
typedef struct _CLFS_CONTAINER_CONTEXT
{
CLFS_NODE_ID cidNode; //8 bytes
ULONGLONG cbContainer; //8 bytes
CLFS_CONTAINER_ID cidContainer; // 4 bytes
CLFS_CONTAINER_ID cidQueue; // 4 bytes
union
{
CClfsContainer* pContainer; //8 bytes
ULONGLONG ullAlignment;
};
CLFS_USN usnCurrent;
CLFS_CONTAINER_STATE eState;
ULONG cbPrevOffset; //4 bytes
ULONG cbNextOffset; //4 bytes
} CLFS_CONTAINER_CONTEXT, *PCLFS_CONTAINER_CONTEXT;
The field pContainer is a kernel pointer to the CClfsContainer object representing the container at runtime, which is located at offset 0x18 in the CLFS_CONTAINER_CONTEXT structure. Figure 5 shows the memory layout of the CLFS_CONTAINER_CONTEXT structure.
Figure 5. The memory layout of the CLFS_CONTAINER_CONTEXT structure
Reproduce BSOD Crash
In order to determine the root cause of CVE-2022-37969, ThreatLabz developed a Proof-of-Concept (PoC) that triggers a “blue screen of death” (BSOD) crash stably. Figure 6 shows detailed crash information after triggering the vulnerability.
Figure 6. CVE-2022-37969 crash information in WinDbg
As shown in Figure 6, the register rdi points to an invalid memory address. The register rdi stores a pointer to the CClfsContainer object. In the CLFS_CONTAINER_CONTEXT structure described before, the field pContainer is a pointer to the CClfsContainer object and located at offset 0x18 in the memory layout. Based on the location of the crash, the field pContainer in the CLFS_CONTAINER_CONTEXT structure has been corrupted. This leads to a BSOD crash when this pointer is dereferenced.
Figure 7 shows a comparison between a properly structured base log file (.BLF) and a specially crafted base log file that is used to trigger the CVE-2022-37969 vulnerability.
Figure 7. A specially crafted Base Log File (BLF) for CVE-2022-37969
After this base log file is created, specific bytes including the field SignatureOffset, client context offset array, cbSymbol, a fake client context, etc must be modified accordingly. As shown in Figure 7, all mutated bytes are located in the Base Log Record (offset: 0x800 ~ 0x81FF in the .blf file). The modifications to the .blf file are listed in Figure 8.
Figure 8. Modifications to the .BLF file to trigger CVE-2022-37969
Proof-of-Concept code to trigger CVE-2022-37969 is shown in Figure 9.
Figure 9. Proof-of-Concept code snippet for CVE-2022-37969
The Proof-of-Concept of CVE-2022-37969 involves the following steps.
- Create a base log file MyLog.blf in the folder C:\Users\Public\ via the CreateLogFile API
- Create dozens of base log files named MyLog_xxx.blf. It’s crucial to make the offset between the two subsequently created memory regions representing the Base Block constant (where the constant offset equals 0x11000 bytes). The original 0-day sample leveraged an advanced method to produce the offset constant. Part 2 of this blog series will cover this technique. The ThreatLabz PoC uses a constant count to create base log files, and therefore, might not make the offset between the two subsequently created memory regions constant. Hence, the constant value has to be tweaked for this situation.
- Modify a couple of bytes at specific offsets in MyLog.blf, recalculate the new CRC32 checksum for the Base Block, then write the new checksum value at offset 0x80C. Then, open the existing base log file MyLog.blf.
- Call the CreateLogFile API to create a base log file MyLxg_xxx.blf in the folder C:\Users\Public\.
- Call the AddLogContainer API to add a log container for the base log file MyLxg_xxx.blf created in Step 4.
- Call GetProcAddress(LoadLibraryA("ntdll.dll"), "NtSetInformationFile") to obtain the function address of the NtSetInformationFile API.
- Call the AddLogContainer API to add a log container for the base log file MyLog.blf opened in Step 3.
- Call NtSetInformationFile(v55, (PIO_STATUS_BLOCK)v33, v28, 1, (FILE_INFORMATION_CLASS)13), where the last parameter is the type of FileInformationClass. When the value is FileDispositionInformation (13), the function will delete the file when it is closed or will cancel a previously requested deletion.
- Call the CloseHandle API to close the handle of the base log file MyLxg_xxx.blf, to trigger this vulnerability.
Root Cause Analysis
Now that Proof-of-Code has been introduced, the root cause can be analyzed. In Figure 9, Step 3 calls the CreateLogFile API whose 5th parameter is 4 (OPEN_ALWAYS), which opens an existing file or creates the file if it does not exist. In this case, the existing base log file MyLog.blf is opened. In Step 4, the code calls the CreateLogFile API to create a new base log file named MyLxg_xxx.blf. In Steps 5 and 7, respectively, the code calls AddLogContainer to add a container to the physical log that is associated with the log handle.
First, let’s take a closer look at how the CLFS driver handles the request of adding a log container when the AddLogContainer() function is called in user space. The following breakpoint can be set to trace the process of handling this request.
bu CLFS!CClfsRequest::AllocContainer
In CLFS.sys, the CClfsRequest class is responsible for handling the requests from user space. The CClfsRequest::AllocContainer function is used to handle the request of adding a container to the physical log. The CClfsRequest::AllocContainer function calls CClfsLogFcbPhysical::AllocContainer whose declaration is shown below:
CClfsLogFcbPhysical::AllocContainer(CClfsLogFcbPhysical *this, _FILE_OBJECT
*,_UNICODE_STRING *,unsigned __int64 *)
Next, the breakpoint at CClfsLogFcbPhysical::AllocContainer is set as follows:
bu CLFS!CClfsLogFcbPhysical::AllocContainer
In Step 5, when the code calls the AddLogContainer function, the breakpoint at CClfsLogFcbPhysical::AllocContainer is triggered. When the breakpoint is hit, let’s inspect the this pointer of the CClfsLogFcbPhysical class. The this pointer points to the CClfsLogFcbPhysical object. As shown in Figure 10, the register rcx stores the this pointer of the CClfsLogFcbPhysical class.
Figure 10. Inspection of the this pointer for the CClfsLogFcbPhysical class at CClfsLogFcbPhysical::AllocContainer
The address of vftable in the CClfsLogFcbPhysical class is stored at offset 0x00. At offset this+0x30, a pointer to the log name is stored. At offset this+0x2B0, the this pointer to CClfsBaseFilePersisted class is stored. Once in memory, a CLFS Base Log File is represented by a CClfsBaseFile class, which can be further extended by a CClfsBaseFilePersisted class. In the this pointer of the CClfsBaseFilePersisted class, at offset 0x30 a pointer to a heap buffer is stored whose size is 0x90 bytes. Furthermore, in the heap buffer, a pointer to the Base Block is stored at offset 0x30. Additionally, in the this pointer of CClfsBaseFilePersisted, a pointer to the CClfsContainer object is stored at offset 0x1C0. After a container is added successfully, we can check the CLFS_CONTAINER_CONTEXT structure described in Figure 5 in memory as shown in Figure 11.
Figure 11. The CLFS_CONTAINER_CONTEXT structure after a container is added successfully
At offset 0x1C0 in the CClfsBaseFilePersisted object, a pointer to the CClfsContainer object is stored which comes from the field pContainer in the CLFS_CONTAINER_CONTEXT structure. At this point, a memory write breakpoint at CLFS_CONTAINER_CONTEXT+0x18 can be set to trace when the pointer to the CClfsContainer object in the CLFS_CONTAINER_CONTEXT structure is corrupted. Another memory write breakpoint at offset 0x1C0 in the CClfsBaseFilePersisted object can be set as follows:
1: kd> ba w8 ffffc80c`cc86a4f0 //CLFS_CONTAINER_CONTEXT: +0x18
1: kd> ba w8 ffffb702`3cf251c0 //CClfsBaseFilePersisted: +0x1C0
In Step 7, when the code calls the AddLogContainer function, the breakpoints at CLFS!CClfsRequest::AllocContainer and CLFS!CClfsLogFcbPhysical::AllocContainer are hit again. At this point, let’s inspect the this pointer (see Figure 12) of the CClfsLogFcbPhysical class.
Figure 12. Inspection of the this pointer for CClfsLogFcbPhysical class
In WinDbg, let’s continue to run the code. The memory write breakpoint “ba w8 ffffc80c`cc86a4f0” will be hit, and the CLFS_CONTAINER_CONTEXT structure in the Base Record produces an out-of-bound write, which leads to a corrupted pointer in the CClfsContainer object shown in Figure 13.
Figure 13. Corrupting the pointer to the CClfsContainer object in CLFS_CONTAINER_CONTEXT structure
Based on the above back stack trace, the memset function was called to trigger the memory write breakpoint in the CClfsBaseFilePersisted::AllocSymbol function. Figure 14 shows the pseudocode of the CClfsBaseFilePersisted::AllocSymbol function.
Figure 14. The pseudocode of the CClfsBaseFilePersisted::AllocSymbol function
This function is described as follows:
- The variable v9 is the value of the field cbSymbolZone in the Base Record Header. The field cbSymbolZone was set to be an abnormal value described in Figure 8. This value was modified from 0x000000F8 to 0x0001114B.
- Validate the cbSymbolZone field. In order to perform an out-of-bound write in Step 4, this validation must be bypassed. The SignaturesOffset field, which was originally 0x00000050 in the crafted MyLog.blf file described in Figure 7, has been overwritten with a large number (0xFFFF0050) in memory. Therefore, even when the cbSymbolZone field is set to an abnormal value, the validation for the cbSymbolZone field can still be bypassed. Determining the reason that the SignaturesOffset field is set to 0xFFFF0050 from 0x00000050 is the root cause of CVE-2022-37969.
- The variable v10 is equal to v9 plus the BaseLogRecord plus 0x1338, and the variable v9 is equal to 0x0001114b, which leads to an invalid offset at v10.
- The function calls memset() which causes an out-of-bound write at an invalid offset v10, which falls into the CLFS_CONTAINER_CONTEXT structure in the Base Record for MyLxg_xxx.blf, leading to a corrupted CClfsContainer pointer at offset 0x18 in the CLFS_CONTAINER_CONTEXT structure.
Figure 15 shows how the out-of-bound write occurs, leading to a corrupted pointer in the CClfsContainer object.
Figure 15. Explanation of the out-of-bound write caused by CVE-2022-37969
So far, we have discussed why an out-of-bound write occurred and how the pointer to CClfsContainer object in the CLFS_CONTAINER_CONEXT structure was corrupted. Next, let’s take a look at when the corrupted CClfsContainer pointer will be dereferenced. When the CloseHandle function is called in user space, CClfsRequest::Close(PIRP Irp) is responsible for handling this request. In the kernel, another memory breakpoint (0x1c0+CClfsBaseFilePersisted) is hit in the ClfsBaseFilePersisted::WriteMetadataBlock function as shown in Figure 16.
Figure 16. Memory breakpoint(0x1c0+CClfsBaseFilePersisted) hit in ClfsBaseFilePersisted::WriteMetadataBlock
The corrupted pointer is copied to the CClfsContainer object in the CLFS_CONTAINER_CONTEXT structure in the Base Record to the offset 0x1c0 in the CClfsBaseFilePersisted object.
Figure 17 shows the pseudocode of the ClfsBaseFilePersisted::WriteMetadataBlock function after the corrupted pointer to CClfsContainer is stored at offset 0x1c0 in the CClfsBaseFilePersisted object. The code zeros out the field of the pointer to the CClfsContainer object in the CLFS_CONTAINER_CONTEXT structure. After decoding the block, the pointer to the CClfsContainer object is restored in the CLFS_CONTAINER_CONTEXT structure from the offset 0x1c0 in the CClfsBaseFilePersisted object.
Figure 17. The pseudocode of the ClfsBaseFilePersisted::WriteMetadataBlock function
Finally, the breakpoint at CLFS!CClfsBaseFilePersisted::RemoveContainer can be set to trace when the corrupted pointer to the CClfsContainer object in the CLFS_CONTAINER_CONTEXT structure in the Base Record is dereferenced.
Figure 18 shows the pseudocode of the CClfsBaseFilePersisted::RemoveContainer function.
Figure 18.The pseudocode of the CClfsBaseFilePersisted::RemoveContainer function
The following steps are executed in the CClfsBaseFilePersisted::RemoveContainer function.
- Obtains the container context offset at offset 0x398 in the Base Record.
- Calls CClfsBaseFile::GetSymbol to obtain the pointer to the CLFS_CONTAINER_CONTEXT structure and stores it in the 4th parameter.
- Assigns the value of the pointer to the CClfsContainter object in the CLFS_CONTAINER_CONTEXT structure obtained in Step 2 to the local variable v13.
- Zeros out the pointer to the CClfsContainter object in the CLFS_CONTAINER_CONTEXT structure.
- Calls CClfsContainer::Remove and CClfsContainer::Release, in turn, to remove the associated container log file and release the CClfsContainer object.
Dereferencing the corrupted pointer to the CClfsContainter object leads to a memory violation. Figure 19 shows the crash information in WinDbg, consequently producing the BSOD crash.
Figure 19. Dereferencing the corrupted pointer to the CClfsContainter object
As described in Figure 14, an out-of-bound write occurred in the CClfsBaseFilePersisted::AllocSymbol function. Before calling the memset function, the validation for the cbSymbolZone field has been bypassed due to the SignaturesOffset field in memory being overwritten with 0xFFFF0050. The SignaturesOffset field in memory in the base block can be overwritten with 0xFFFF0050 in the process of handling the request of calling the CreateLogFile API to open the specially crafted base log file MyLog.blf described in Step 3 in Figure 9.
When the CreateLogFile function is called in user space, CLFS!CClfsRequest::Create is responsible for handling this request. When the CreateLogFile function is used to open an existing base log file, the function CClfsLogFcbPhysical::Initialize is called in CLFS.sys. Figure 20 shows the pseudo-code snippet of the CClfsLogFcbPhysical::Initialize function.
Figure 20. The pseudo-code snippet of the CClfsLogFcbPhysical::Initialize
This function is described as follows:
1. Call the CClfsBaseFilePersisted::OpenImage function to create a bigpool (size: 0x7a00) for the base block in a base log file. The following function calls can be followed to enter the CClfsBaseFilePersisted::ReadMetadataBlock function.
CClfsBaseFilePersisted::OpenImage -> CClfsBaseFilePersisted::ReadImage -> CClfsBaseFile::AcquireMetadataBlock -> CClfsBaseFilePersisted::ReadMetadataBlock
Figure 21 shows the pseudo-code snippet of the CClfsBaseFilePersisted::ReadMetadataBlockfunction, where the ExAllocatePoolWithTag(PagedPoolCacheAligned, 0x7a00, 0x73666C43u) is called to create a bigpool to store the base block, followed by initializations, and then the ClfsDecodeBlock function is called to decode the block. In the ClfsDecodeBlock function, the ClfsDecodeBlockPrivate function is called to parse the sector signatures array that is located at offset 0x50 (the value of SignaturesOffset) in the base block. Two bytes are required to overwrite the sector signature of each sector. These two bytes 0x0050 at offset 0x68 in the base block can be overwritten to the offset 0x19FE (0xC*0x200+0x1FE) where the sector signature of the 13th section is stored. At this point, we can set two memory write breakpoints which are located at base_block+0x68 and base_block+0x200*0xE-0x8. The purpose of setting these two memory write breakpoints is to trace when the sector signature of the 14th sector in the base block is overwritten, and the SignaturesOffset field in the base block is overwritten to 0xFFFF0050.
1: kd> ba w8 ffffd08b`51c03000+0x68 //base_block+0x68
1: kd> ba w8 ffffd08b`51c03000+0x200*0xE-0x8 // base_block+0x200*0xe-0x8
Figure 21. The pseudo-code snippet of the CClfsBaseFilePersisted::ReadMetadataBlock function
2. Call the CClfsBaseFile::AcquireClientContext function to acquire the client context from the base block. As shown in Figure 7, the first offset in the Client Context offset array located at offset 0x9A8 in the base log file is specially crafted. A fake Client Context is located at offset 0x23A0 in the base log file. The eState field located at offset 0x78 in the fake Client Context structure is set to 0x20 (CLFS_LOG_SHUTDOWN).
3. Check if the eState field is not CLFS_LOG_SHUTDOWN or the base log is a multiplexed log.
4. Call the CClfsLogFcbPhysical::ResetLog function due to the condition being false in Step 3. Figure 22 shows the pseudo-code snippet of the CClfsLogFcbPhysical::ResetLog function. The 8 bytes located at base_block+0x1BF8 are set to 0xFFFFFFFF00000000. The sector signature is located at offset base_block+0x1BFC. Therefore, the sector signature is overwritten with 0xFFFF. Figure 23 demonstrates that the sector signature is overwritten in WinDbg.
Figure 22. The pseudo-code snippet of the CClfsLogFcbPhysical::ResetLog function
Figure 23. The sector signature is overwritten with 0xFFFF
5. Call the CClfsLogFcbPhysical::FlushMetaData function. The following function calls can be followed to enter the CLFS!ClfsEncodeBlockPrivate function.
CLFS!CClfsLogFcbPhysical::FlushMetadata ->
CLFS!CClfsBaseFilePersisted::FlushImage ->
CLFS!CClfsBaseFilePersisted::WriteMetadataBlock ->
CLFS!ClfsEncodeBlock -> CLFS!ClfsEncodeBlockPrivate
Figure 24 shows the pseudo-code snippet of the CLFS!ClfsEncodeBlockPrivate function.
Figure 24. The pseudo-code snippet of the CLFS!ClfsEncodeBlockPrivate function
The code above acquires the sector signature from each sector in the base block and overwrites the sector signature array with the sector signature. The sector signature array is located at offset 0x50 and overlaps the SignaturesOffset field in the base block. The sector signature of the 14th sector has been set to 0xFFFF as shown in Figure 23. Therefore, two bytes (0xFFFF) are overwritten at offset 0x6C (0x50+0xE*2) in the base block. At this time, the SignaturesOffset field has the value 0xFFFF0050 as shown in Figure 25.
Figure 25. The SignaturesOffset field is overwritten with 0xFFFF0050
In the end, we summarize the process of overwriting the SignaturesOffset field in Figure 26.
Figure 26. The process of overwriting the SignaturesOffset field
Conclusion
In this blog, ThreatLabz presented a detailed root cause analysis for CVE-2022-37969, which is due to improper bounds checking for the SignaturesOffset field in the Base Block for the base log file (BLF) in CLFS.sys. A specially crafted client context array and a fake Client Context in the base log file, can exploit CLFS to overwrite the SignaturesOffset field with an abnormal value. This leads to a validation bypass for the cbSymbolZone field when a Symbol is allocated. Thus, an invalid cbSymbolZone field can produce an out-of-bound write at an arbitrary offset. Therefore, the pointer to the CClfsContainer object can be corrupted. When dereferenced, the corrupted pointer to the CClfsContainer object causes a memory violation that triggers a BSOD crash. In Part 2, we will present the analysis of the 0-day exploit that leverages this vulnerability for privilege escalation. Stay tuned!
References
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-37969
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://i.blackhat.com/USA-22/Thursday/us-22-Jin-The-Journey-Of-Hunting-ITW-Windows-LPE-0day.pdf
https://www.slideshare.net/PeterHlavaty/deathnote-of-microsoft-windows-kernel
https://www.pixiepointsecurity.com/blog/nday-cve-2022-24521.html
https://learn.microsoft.com/en-us/previous-versions/windows/desktop/clfs/log-types
https://learn.microsoft.com/en-us/previous-versions/windows/desktop/clfs/creating-a-log-file
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/clfs-terminology
Part 2
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.918CLFS.sys 10.0.22000.918
Windows 10 21H2 version 19044.1949CLFS.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 NtFsControlFileAPI, 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 (0x5000000~0x5100000), 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(0x5000000~0x5100000)
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
Exploitation on Windows 10
We broke down the process of exploitation targeting Windows 11 into 7 steps. For Windows 10, the process of exploitation is still classified into 7 steps, Steps 1-5 are the same as Windows 11. Performing an arbitrary write primitive and token replacement are different from the steps in Windows 11. Figure 31 shows the code snippet to perform an arbitrary write and token replacement on Windows 10.
Figure 31. Perform arbitrary write primitive and token replacement on Windows 10
Performing an arbitrary write primitive and token replacement for Windows 10 involves the following steps.
1. Perform heap spraying to set up the memory, where 0x0018000000000800 is written at offset 0xFFFFFFFF, and 0x000F000000000000 is written at offset 0x100000007. Figure 32 shows the memory around 0xFFFFFFFF.
Figure 32. Arrange the memory region (0xFFFFFFFF ~ 0x10000000E)
In addition, Figure 33 shows the layout of the memory region (0x10000~0x1010000). The PreviousMode field is located at offset 0x232 in the _ETHREAD structure. The value (addr_of_PreviousMode - 8) is set up at offset N*8+8 in the memory region starting at 0x10000.
Figure 33. The layout of the memory region (0x10000~0x1010000)
2. Arrange the memory region (0x5000000~0x5100000), where the address of the ClfsEarlierLsn function is stored at 0x5000008, and the address of the SeSetAccessStateGenericMapping function is stored at 0x0x5000018 (see Figure 34).
Figure 34. The memory region (0x5000000~0x5100000)
3. Trigger the CLFS vulnerability. The CClfsBaseFilePersisted::RemoveContainer function will be hit. Figure 35 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 35. Dereference the corrupted pointer 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 36 shows what the SeSetAccessStateGenericMapping function does and how to perform an arbitrary write.
Figure 36. Perform an arbitrary write on PreviousMode on Windows 10
At the end of the SeSetAccessStateGenericMapping function, the PreviousMode in the _ETHREAD object is set to 0, which means that it allows an unconstrained read/write operation across the whole kernel memory using NtReadVirtualMemory and NtWriteVirtualMemory. This is a powerful method to enable the read/write. At this point, the exploit is ready to perform an arbitrary write.
4. Call the NtWriteVirtualMemory function to overwrite the buffer pointed by a local pointer variable with the value of the Token field in _EPROCESS for System (PID 4). Next, the code calls the NtWriteVirtualMemory function again to overwrite the Token field in _EPROCESS for the current process with the data (the value of the Token field in _EPROCESS for System), which is stored in the buffer pointed by this local pointer variable, which completes the token replacement. Figure 37 demonstrates that the Token field in _EPROCESS for the current process has been replaced with the Token field in _EPROCESS for the process System (PID 4).
Figure 37. Token replacement on Windows 10
5. Call the NtWriteVirtualMemory to overwrite the PreviousMode field in the _ETHREAD object in order to complete PreviousMode restoration, which can prevent the newly launched process with the SYSTEM privilege from crashing.
6. Spawn a command prompt (cme.exe) with the SYSTEM privilege as shown in Figure 38.
Figure 38. Spawn a cmd with the SYSTEM privilege on Windows 10
In the end, the process of performing arbitrary write on PreviousMode and token replacement on Windows 10 is demonstrated in Figure 39.
Figure 39. The process of performing arbitrary write on PreviousMode and token replacement on Windows 10
Additionally, ThreatLabz also tested the exploitation code targeting Windows 10 on Windows 11 via patching the binary of the exploit. After the CLFS vulnerability is triggered, the _EThread.PreviousMode is overwritten with 0, which leads to the following crash in Figure 40. Windows 11 appears to have mitigated the PreviousMode exploitation technique, and therefore, the exploitation technique targeting Windows 11 in the exploit is different from the exploitation technique for Windows 10.
Figure 40. A crash occurs after _EThread.PreviousMode is set to 0 on Windows 11
Summary
In this blog, we analyzed the process to exploit CVE-2022-37969 on Windows 10 and Windows 11. For Windows 11, the exploit first triggers the CLFS vulnerability to perform an arbitrary write for the PipeAttribute object. Then the exploit triggers the CLFS vulnerability a second time to perform token replacement. For Windows 10, the exploit only needs to trigger the CLFS vulnerability once and leverages the PreviousMode technique to implement an arbitrary write primitive, and then completes the token replacement by calling the NtWriteVirtualMemory function.
Acknowledgments
Special thanks to the Zscaler ThreatLabz team's Nirmal Singh for capturing this 0-day exploit sample and performing the initial analysis!
References
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/eprocess
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/previousmode
https://www.gaijin.at/en/infos/windows-version-numbers
https://www.lifewire.com/windows-version-numbers-2625171
https://googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2022/CVE-2022-24521.html
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-37969