返回 TI 主页

Overview

Kaspersky has disclosed [1] that the 0day vulnerability CVE-2023-28252 is an out-of-bounds write (increment) vulnerability, which can be exploited to obtain system privileges in Windows when the target system attempts to extend a metadata block. The exploitation process starts by altering the base log file so that an offset value in a specific Common Log File System (CLFS) structure in memory is changed into an offset pointing towards a malicious structure controlled by attackers. The CLFS structure is part of CLFS general-purpose logging system consisting of physical log files, log streams, log records and more.

The privilege escalation vulnerability has been exploited in the wild by the Nokoyawa ransomware group to obtain system privileges on target hosts before deploying their ransomware.

Microsoft [2] patched the vulnerability on April Patch Tuesday and identifying it as CVE-2023-28252. The following screenshot shows the exploit achieving elevation of privilege before the patch was applied.


Exploit Sample Analysis

The sample itself is protected by Themida, so anti-debugging needs to be bypassed while debugging. After that, the sample analysis is similar to normal sample analysis. Through analysis of the exploit sample, it was found that the exploitation and code implementation of this vulnerability are very similar to last year's CVE-2022-37969. The following image shows that the corresponding working directory is cleared before the sample runs, then fun_osVersioncheck is called to obtain the system version. Corresponding kernel offsets for reading the system and the current process are also obtained through fun_osVersioncheck, followed by the initialization of a series of memories.

The implementation of fun_osVersioncheck here is basically consistent with CVE-2022-37969 exp, and even the key data structure for initialization has not changed much, as shown in the following image from the analysis of CVE-2022-37969 by Zscaler researchers [3].

Obtain addresses of function ClfsEarlierLsn, ClfsMgmtDeregisterManagedClient, RtlClearBit/PoFxProcessorNotification, SeSetAccessStateGenericMapping from clfs.sys/ntoskrnl.exe by dynamic address resolution. Among them, two functions ClfsMgmtDeregisterManagedClient and PoFxProcessorNotification were not used in CVE-2022-37969.

Allocate 0x1000000-bytes-long memory at position 0x5000000, note that the use of this address is consistent with CVE-2022-37969.

Next, obtain the address of the NtFsControlFile function and use ZwQuerySystemInformation to obtain the kernel object address of PipeAttributer. Allocate memory with a length of 4096 bytes at 0xFFFFFFFF , and deploy the system Process token to assist with the final memory write of ClfsEarlierLsn/SeSetAccessStateGenericMapping. If you are familiar with the exploitation of CVE-2022-37969, then you will know that this position is used for this purpose .

Enter the core part of the exploit, where the function fun_prepare creates the first log file using CreateLogFile. We call it trigger clfs. Then, fun_trigger is called repeatedly to create 10 more log files, which are called spray clfs[i] below.

Take a closer look at how the log files are constructed in fun_prepare/fun_trigger. The main part of the code in fun_prepare is as follows :

The text describes modifications made to the CLFS log block header record offsets array[12], as well as changes to 16 bytes of data in the “other data” section of both the base block and base block shadow. Note that the base block and base block shadow are consistent.

After writing to the CLFS file and fixing the corresponding CRC checksum value, AddLogContainer is called to add a log container. It should be noted that the kernel address para_clfsKerneladdress of the trigger CLFS base block is obtained by searching through ZwQuerySystemInformation for a 0x7a00-sized pool with tag labeled "Clfs". Similarly, the kernel address of pipeAttribute is also obtained through this method.

The modifications made in spray clfs[i] are more scattered

It should be noted that after generating spray clfs[i], function AddLogContainer isn’t called at this position.

The structure of spray clfs[i] is shown below. The key positions are the control block and control block shadow, both of which have been modified. In the control block shadow, 0x13 has been modified, and in addition, “cbsyblozone” in the base block has been set to 0x65c8 to remain consistent with its corresponding position.

After that, the code performs a series of memory spray operations. First, the position at trigger clfs kernel base block address + 0x30 is repeatedly assigned to an array (labeled as v93). Then, the function fun_pipeSpray is called twice with parameters 0x5000 and 0x4000 respectively.

fun_pipeSpray is a spray of pipes. It generates a specified number of pairs of pipes (read/write) based on the passed parameter. The first call to fun_pipeSpray passes in 0x5000, resulting in the generation of 0x5000 pairs of pipes (read/write), which are collectively referred to as pipeA. The second call generates 0x4000 pairs of pipes (read/write), which are collectively referred to as pipeB.

PipeA is traversed, and its pipes’ writepipe function are called to write the array v93 (containing address of trigger clfs base block + 0x30). After the traversal is completed, part of pipeA memory is released, starting from the 174th pair of pipes at offset 0x2000, releasing a total of 0x667 pairs of pipes.

After the release, immediately call CreateLogFiles through the loop of spray[i] clfs, which probably acts as memory placeholder since some object occurring in the invocation of CreateLogFiles could the released pipe pairs in pipeA. After the loop of using CreateLogFiles to set placeholders ends, traverse 0x4000 pairs of pipes in pipeB and calls their writepipe function to write array v93.

The memory structure is as follows after the series of operations described above.

start of pipeA(trigger clfs + 0x30)
...
spray clfs[i]
...
pipeB(trigger clfs + 0x30)
...
end of pipeA(trigger clfs + 0x30)

Once completing the memory spray, traverse the spray clfs[i] and call AddLogContainer for each one to add a log container, then place the memory layout at 0x5000000.

After completing the memory layout at 0x5000000, call CreateLogFile. At this time, the clfs object called is the trigger clfs. Once CreateLogFile is called, system process token can be obtained through fun_NtFsControlFile. It can be seen from here that the vulnerability should have been triggered after CreateLogFile was called, completing the same operation as CVE-2022-37969 exp did before, that is, executing the contents in memory 0x50000000 and completing the modification of the PipeAttribute kernel object, thereby enabling fun_NtFsControlFile to achieve arbitrary address reading.

Then repeat calling CreateLogFile to trigger the vulnerability and complete the replacement of the process token.

Additionally, the sample also supports modifying priviousMod to achieve privilege escalation through arbitrary address read and write.

By analyzing the exploit code above, it can be found that exploit of this vulnerability has many similarities in that of previous CLFS vulnerability CVE-2022-37969. The key is that the container pointer is suspected to have been modified by exploiting the vulnerability. In this vulnerability, the container pointer is suspected to be pointed to 0x5000000. The attack leverages the layout at 0x5000000 and relies on the following utility functions to achieve arbitrary address writing, which is similar to CVE-2022-37969. However, new functions has been added to this toolchain:

PoFxProcessorNotification ClfsMgmtDeregisterManagedClient

The final function invocation chain is:

PoFxProcessorNotification ClfsMgmtDeregisterManagedClient ClfsEarlierLsn SeSetAccessStateGenericMapping

The difference between this vulnerability and CVE-2022-37969 is that CVE-2022-37969 can be triggered easily, while this vulnerability requires more complex operations before triggering. Here, we summarize the operations in the exploit sample before triggering the vulnerability.

(1) Fun_prepare generated a trigger clfs with its corresponding position set to 0x5000000, and AddLogContainer is called.

(2) CreateLogFile creates 10 spray clfs[i]

(3) The address of base block in trigger clfs +0x30 has been pipe sprayed, specifically as follows :

(3-1) 0x5000 pairs of pipes (read/write), referred to as pipeA

(3-2) 0x4000 pairs of pipes (read/write), referred to as pipeB

(3-3) An array is written to pipeA. The array containing 12 elements with the same value (the address of trigger CLFS base block +0x30).

(3-4) releases from pipeA (offset 0x2000), the 174th pair of pipes onwards, a total of 0x667 pairs of pipes are released.

(3-5) 10 spray clfs call CreateLogFile again to occupy the memory released by 0x667 pairs of pipe in the previous step.

(3-6) Traverse pipeB and write an array containing 12 elements with the same value (the address of trigger CLFS base block +0x30). After spraying, the memory structure is roughly as follows:

PipeA
    0x2000
    ...
    spray clfs[n](size of 7a00) + 0xDB PipeB
    ...
    0xACDA(0x2000 + 0x667 * 16)
end

(4) Traverse and call AddLogContainer for each spray clfs[i]

(5) Call CreateLogFile for the trigger clfs

According to the above procedure, it is speculated that in step (4), the call of AddLogContainer for spray clfs[i] will lead to the destruction of trigger clfs base block memory through the adjacent pipeB (trigger clfs + 0x30) in the following memory structure. This will cause the container pointer called by trigger clfs when it calls CreateLogFile to point to 0x5000000 during step (5), which ultimately enters the memory controlled by the attacker and achieves arbitrary address writing through a series of auxiliary function chains.

start of pipeA(trigger clfs + 0x30)
...
spray clfs[i]
...
pipeB(trigger clfs + 0x30)
...
end of pipeA(trigger clfs + 0x30)

Vulnerability Analysis

The first step is to confirm whether our guess is correct, that is, whether the wrong container pointer is called, which points to 0x5000000. The simplest way to do this is to set a breakpoint on CLFS!ClfsEarlierLsn because this function is the beginning of the function call chain in memory at 0x5000000. By tracing it, we can find out how the vulnerability enters and executes at that address.

A breakpoint is set on the CLFS!ClfsEarlierLsn function, and after the CreatelogFile function is called, the kernel triggers entry into the CLFS!ClfsEarlierLsn call.

Tracing back, CLFS!ClfsEarlierLsn enters through position 0x5000000, and from the code, it is highly likely that the corresponding container pointer has been corrupted

Enter the CLFS!ClfsEarlierLsn call.

The function that triggers the execution of the code in memory at 0x5000000 is CLFS!CClfsBaseFilePersisted::CheckSecureAccess. It can be seen that the malicious container pointer comes from v29, and v29 comes from the function CLFS!CClfsBaseFile::GetSymbol.

The value of v29 in CLFS!CClfsBaseFile::GetSymbol is derived from v17, which is jointly determined by BaseLogRecord + v6. Here, BaseLogRecord is a fixed value, so we need to see where v6 comes from. According to the code, the value of v6 is passed as the second parameter to the CLFS!CClfsBaseFile::GetSymbol function.

Back to CLFS!CClfsBaseFilePersisted::CheckSecureAccess, we can see that the second parameter of CLFS!CClfsBaseFile::GetSymbol is poi(BaseLogRecord + 0xCA).

Set a breakpoint at CLFS!CClfsBaseFilePersisted::CheckSecureAccess, the value of poi(BaseLogRecord + 0xCA) passed before entering CLFS!CClfsBaseFile::GetSymbol is 0x1570.

This value is passed as the second parameter to CLFS!CClfsBaseFile::GetSymbol.

Calculating the return value of corresponding v29 as shown below. Offset 0x18 of the address pointed by the returned pointer is 0x5000000. Careful readers can see that content of this piece of memory is actually the same as that constructed in the "other data" field of trigger clfs.

Later, codes in clfs.sys will sequentially check whether several values near the pointer satisfy requirements. These checked fields are also part of trigger clfs which are constructed at the beginning.

The corresponding detection code is shown below:

After that, return to CLFS!CClfsBaseFilePersisted::CheckSecureAccess and look up the address 0x5000000 pointed by v29.

Obtain the pointer stored at address 0x5010000, which is controlled by the attacker. Therefore, ultimately enter CLFS!ClfsEarlierLsn address deployed by the attacker on 0x5010000 for execution.

As mentioned earlier, the key to the code execution is 0x1570, which causes an addressing error of the container pointer so that the data in the “other data” field constructed by the attacker is treated as the container pointer. Therefore, we need to know where 0x1570 comes from.

In the Exp, AddLogContainer is called, and the corresponding function in the kernel is CLFS!CClfsLogFcbPhysical::AllocContainer. The “this” pointer of this function points to the object CClfsLogFcbPhysical.

The offset 0x2b0 of CClfsLogFcbPhysical object points to CClfsBaseFilePersisted object, which stores a pointer at offset 0x30 and this pointer points to a 0x90 size heap memory. Here we call it is clfsheap, and offset 0x30 of clfsheap stores a pointer pointing to the base block.

Clfheap can be understood in the following form, which saves pointers to each block. This figure comes from the analysis of CVE-2022-37969 by security researchers at Zscaler [3].

The "base block" is a pool with a size of 0x7a00. In the Exp, the address of this pool is searched in the kernel through the function ZQuerySystemInformation based on the fixed size of this pool and the clfs tag.

Combining the base block fffa409cb25e000 and the structure of clfs in the above figure, it can be seen that 0x369 at 0x68 constructed in the trigger clfs corresponds to the record offset array[12]. This figure comes from the analysis of CVE-2022-37969 by security researchers at Zscaler [3].

The 0x1570 that caused the call at 0x5000000 is located in position 0x398 of the base block, that is, rgContainers. This figure comes from the analysis of CVE-2022-37969 by security researchers at Zscaler [3].

Here are breakpoints for reading and writing under the offsets trigger clfs base block(offset 0x398 an 0x68). When the write breakpoint is triggered, 0x398 is written with 0x1470.

The call stack at this time is shown below, and it can be seen that it is still in the AllocContainer function.

When AddLogContainer is executed and triggered by spray[i], the corresponding kernel function calls for CClfsLogFcbPhysical/CClfsBaseFilePersisted/base block are as follows:

Executing again, we can see that the read breakpoint is triggered, and the data at 0x369 in spray[i] clfs base block + 0x68 is read.

Immediately after, 0x369+r15 (which has a value of trigger clfs base block + 0x30) is incremented and triggers the previously configured write breakpoint, successfully modifying 0x1470 to 0x1570 at 0x398 of trigger clfs base block.

The call stack at this time is shown below, and it can be seen that it is still in AddContainer.

Similarly, it can be seen that if we address to 0x1470 in the trigger clfs base block, the final pointer found is actually a legitimate container pointer, but if we address to 0x1570, it eventually points to the attacker's arranged location of 0x5000000.

Returning to the function CClfsBaseFilePersisted::WriteMetadataBlock, which triggered the breakpoint. From the previous debugging, it can be seen that the two breakpoints that triggered the read and write operations are adjacent to each other. It is important to note that at this point, AddLogContainer is called by spray[i] clfs. Therefore, “this” pointer of the CClfsBaseFilePersisted::WriteMetadataBlock function should point to the CClfsBaseFilePersisted object of spray[i] clfs. However, in reality, the position of v9 obtained through the CClfsBaseFilePersisted object of spray[i] clfs points to trigger clfs +0x30, which is obviously unreasonable. Because v9 points to trigger clfs +0x30, the subsequent operation poi(poi(trigger clfs + 0x30) + 0x369)++ modifies 0x1470 at trigger clfs +398 to 0x1570. This ultimately leads to an addressing error in the container pointer during the subsequent AddLogContainer call of the trigger clfsc, entering the attacker's layout memory at 0x5000000.

So how is v9 generated here? As shown in the above figure, "(_QWORD )(((_QWORD )this + 6) + 24 * v4)" is used. This means taking the pointer from position 0x30 of the CClfsBaseFilePersisted object + 24*v4. In this calculation, all the values except for v4 are normal. The value of v4 comes from the second parameter of CClfsBaseFilePersisted::WriteMetadataBlock. It should be noted that the content pointed to by the pointer at position 0x30 of the CClfsBaseFilePersisted object mentioned earlier is a clfsheap with a length of 0x90. Here, "(_QWORD )(((_QWORD )this + 6) + 24 * v4)" requires the calculation of 24*v4. If the value of v4 is too large, it will cause out-of-bounds reads on the heap. This also corresponds to the memory structure of the spray described at the beginning of our analysis.

start of pipeA(trigger clfs + 0x30) ... spray clfs[i] ... pipeB(trigger clfs + 0x30) ... end of pipeA(trigger clfs + 0x30)

So we need to look at where the out-of-bound a2 comes from, which leads us back to the referencing function CClfsBaseFilePersisted::ExtendMetadataBlock of CClfsBaseFilePersisted::WriteMetadataBlock.

We can see that v5 comes from the second parameter of CClfsBaseFile::GetControlRecord.

It can be seen that the second parameter of CClfsBaseFile::GetControlRecord is a structure named CLFS_CONTROL_RECORD, and its generation method is shown below.

The first parameter of CClfsBaseFile::GetControlRecord is CClfsBaseFilePersisted. As analyzed above, its offset 0x30 points to a clfsheap with a length of 0x90.

Continuing the execution, the pointer at offset 0x0 of clfsheap is obtained. This pointer actually corresponds to the control block of CLFS, while 0x30 is the base block mentioned earlier.

The value at offset 0x28 of the control block is obtained, which is added to the control block to calculate and return the CLFS_CONTROL_RECORD.

CClfsBaseFilePersisted
    +0x30 heap block
        0x0 _CLFS_CONTROL_RECORD
            CLFS_METADATA_RECORD_HEADER(size 0x70)

CLFS_CONTROL_RECORD as blow:

typedef struct _CLFS_CONTROL_RECORD
{   
    CLFS_METADATA_RECORD_HEADER hdrControlRecord; 70    
    ULONGLONG ullMagicValue;              
    UCHAR Version;                            
    CLFS_EXTEND_STATE eExtendState;       
    USHORT iExtendBlock;                   
    USHORT iFlushBlock;                  
    ULONG cNewBlockSectors;               
    ULONG cExtendStartSectors;            
    ULONG cExtendSectors;                 
    CLFS_TRUNCATE_CONTEXT cxTruncate;  
    USHORT cBlocks;   
    ULONG cReserved;   
    CLFS_METADATA_BLOCK rgBlocks[ANYSIZE_ARRAY];
} CLFS_CONTROL_RECORD, *PCLFS_CONTROL_RECORD;

It can be seen that the returned data is actually the position after skipping hdrControlRecord (0x70) in CLFS_CONTROL_RECORD. The data set by spray[i] when constructing the CLFS is located at the position starting at offset 0x10 from this position.

Continue to execute down to CClfsBaseFilePersisted::WriteMetadataBlock. At this time, the data at position 0x1a is addressed through the returned pointer, which happens to be the data 0x13 constructed in the spray[i] CLFS, corresponding to the CLFS_CONTROL_RECORD structure above. Here, ullMagicValue is fixed to 0xc1f5c1f500005f1c, so this position should be iFlushBlock.

Here, it is necessary to iterate over the spray[i] that conforms to the trigger structure. At this time, the corresponding CClfsBaseFilePersisted address of the spray[i] is ffff9087fbd87000.

The clfsheap corresponding to the spray[i] CLFS CClfsBaseFilePersisted is shown below:

Calculate the offset based on the parameter 2 (0x13) passed in, and finally get the data at clfsheap+0x1c8.

However, it should be noted that the actual length of clfsheap is only 0xa0, so addressing according to 0x1c8 will definitely cause out-of-bounds reading.

The data at 0x1c8 happens to be the array written by occupying pipeB during our previous spraying, and this array contains 12 addresses of trigger clfs base block + 0x30. Therefore, directly accessing this data causes out-of-bounds reading.

Afterwards, the code performs arithmetic through trigger clfs + 0x30 using the formula (poi(poi(trigger clfs + 0x30) + 0x369)++) which causes the data at 0x1470 offset of the triger clfs base block to be modified to 0x1570. And finally, when AddLogContainer is called in trigger clfs, the wrong container pointer is addressed through 0x1570, and the code directly runs into the attacker's malicious memory layout at 0x5000000.


Summary

The key point in the fun_trigger function is to modify the corresponding iFlushBlock in the control block of the spray clfs[i], resulting in an out-of-bounds read when calling AddLogContain for spray clfs[i] later in CClfsBaseFilePersisted::WriteMetadataBlock, which exceeds the size of clfsheap 0x90. The following memory layout is formed through spray pipe.

start of pipeA(trigger clfs + 0x30) ... spray clfs[i] ... pipeB(trigger clfs + 0x30) ... end of pipeA(trigger clfs + 0x30)

By reading out of bounds, the corresponding spray clfs[i] clfsheap structure is used to access the trigger clfs + 0x30 in the pipB array. The position at 0x58 of trigger clfs is set to 0x369 when initialized by log. WriteMetadataBlock continues to execute and performs the following code calculation by accessing trigger clfs + 0x30 through out-of-bounds reading:

poi(trigger clfs + 0x30 + poi(trigger clfs + 0x30 + 0x28))++

This ultimately causes the value in trigger clfs offset 0x398 to be changed from 0x1470 to 0x1570. Afterwards, CreateLogFile is called through trigger clfs, and Getsymbol is called in CClfsBaseFilePersisted::CheckSecureAccess. Trigger clfs obtains the corresponding container pointer through trigger clfs offset 0x398. Since the 0x1470 has been modified to 0x1570, the obtained container pointer is the malicious container set by the attacker during the initialization of trigger clfs' log, whose corresponding pointer is 0x5000000. Finally, eip executes at 0x5000000, entering the function call chain laid out by the attacker.

The final privilege escalation sample provides two ways to achieve arbitrary address writing by deploying the following function sequences on 0x5000000: (ClfsEarlierLsn/PoFxProcessorNotification/ClfsMgmtDeregisterManagedClient/SeSetAccessStateGenericMap). Arbitrary writing modifies the pipe Attribute, while arbitrary reading is implemented through NtFsControlFileread, which replaces the current process token to achieve privilege escalation.

The core of this text is actually ClfsEarlierLsn and SeSetAccessStateGenericMap. After the execution of ClfsEarlierLsn, rdx will be assigned as 0xffffffff, while the pipe Attributer kernel object is deployed at that address.

SeSetAccessStateGenericMap will write the malicious data deployed at rcx+48 to the pointer pointed by rdx, which is the AttributeValueSize field of pipe Attributer, thus allowing arbitrary address reading through NtFsControlFileread.

This exploit does not simply call the combination of ClfsEarlierLsn/SeSetAccessStateGenericMap like in the previous CVE-2022-37969, but inserts two functions in between them. First is PoFxProcessorNotification, which will be called with the function pointer at an offset of 0x68 from the first parameter and the parameter at an offset of 0x48.

The second inserted function is ClfsMgmtDeregisterManagedClient, which will be called through the position at an offset of 8/0x28 from the first parameter, with the parameter itself as the first parameter. The main calling flow of the vulnerability exploitation entering 0x5000000 is PoFxProcessorNotification->ClfsMgmtDeregisterManagedClient, with ClfsEarlierLsn/SeSetAccessStateGenericMap called sequentially in ClfsMgmtDeregisterManagedClient.

The actual code execution is also in the red box part, not at (**v15)(v15).

The second privilege escalation method in the sample modifies PriviousMod by deploying the function sequence ClfsMgmtDeregisterManagedClient/RtlClearBit at 0x5000000, and finally achieves global memory read/write through NtWriteVirtualMemory/NtReadVirtualMemory.


Patch Comparison

The patch mainly deals with the following two functions CClfsBaseFilePersisted::WriteMetadataBlock/CClfsBaseFile::GetControlRecord .

First, in CClfsBaseFile::GetControlRecord, it checks the returned _CLFS_CONTROL_RECORD to prevent returning incorrect offsets that may cause out-of-bounds access to clfsheap.

Secondly, in CClfsBaseFilePersisted::WriteMetadataBlock, it checks the returned v9 to prevent accessing attacker's constructed data out of bounds.

The specific verification logic is shown as follows.


Reference link

[1]. https://securelist.com/nokoyawa-ransomware-attacks-with-windows-zero-day/109483/

[2]. https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-28252

[3]. https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part

[4]. https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part2-exploit-analysis

CVE-2023-28252 VULNERABILITY ANALYSIS