返回 TI 主页

Overview

This vulnerability was first discovered and exploited in the wild by the Microsoft Threat Intelligence Center and fixed on the April 2025 patch day. Qi'anxin Threat Intelligence Center captured the wild sample uploaded to vt on May 30, 2025. The sample MD5 is as follows:

881a60702d4876db65098176a3ec7e3a

At that time, there were only 23 engines checking.

After running, the corresponding system shell pops up.


Cause of the vulnerability

Initially, we were unsure of the CVE number for this vulnerability sample. After some testing, we determined that the vulnerability was fixed in the April 2025 patch. Among the vulnerabilities fixed in April was a CLFs-related exploit in the wild, CVE- 2025-29824 . The patch comparison results are shown below. Combined with the numerous closehandle function calls in the sample and the vulnerability type of CVE-2025-29824 use-after-free, the CClfsRequest::Close and CClfsRequest::Cleanup functions came into our view.

The modified code in CClfsRequest::Clos e is as follows. You can see that the patch code detects the reference at Irp->Tail.Overlay.CurrentStackLocation->FileObject->FsContext2 and then releases it through CClfsLogCcb::Release.

CClfsRequest::Cleanup is shown below. Here, a check for the IsEnableDeviceUsage function is added before calling ClfsLogCcb::Release to prevent the corresponding release. Combined with the previous speculation about the CVE-2025-29824 vulnerability, it is basically confirmed that this is a use-after-free vulnerability related to the FsContext2 object. The operation of CClfsLogCcb::Release has also been moved from CClfsRequest::Cleanup to CClfsRequest::Close.

CClfsRequest::Cleanup's superior CClfsRequest::Cleanup , after the patch, CClfsLogCcb::Release will not be called because the initial CClfsLogCcb::AddRef adds a reference to FsContext2. The subsequent CClfsRequest::Cleanup patch code restricts the call of CClfsLogCcb::Release. Therefore, the CClfsLogCcb::AddRef will cause the object reference to be non-0 when CClfsLogCcb::Release is called in CClfsRequest::Cleanup. Therefore, CClfsLogCcb::Release here will never be called.

Here, CClfsLogCcb::Release determines that the reference is decremented to 0 and the corresponding object is not empty, and then releases FsContext2 through ExFreeToNPagedLookasideList.

ExFreeToNPagedLookasideList does not release the specified Entry every time it is called, but only when the number meets L.Depth.

So how do we trigger the call of CClfsRequest::Cleanup/CClfsRequest::Close ? When we call closehandle through the application layer to close a clfs file object, it will send IRP_MJ_CLEANUP/IRP_MJ_CLOSE to CClfsRequest::Cleanup/CClfsRequest::Close in the kernel , as shown below:

IRP_MJ_CLEANUP is used to clean up files, and IRP_MJ_CLOSE is used to close files. However, there is a sequence between the two. IRP_MJ_CLEANUP is sent when the last user-mode handle to the file object managed by the target device object is closed. However, at this time, since there may still be unfinished I/O, the file object may not have been released. IRP_MJ_CLOSE is sent when the last reference to the file object is released and all I/O requests have been completed. It can be seen that there is a time difference between the two, which is consistent with the order in which CClfsRequest::Cleanup/CClfsRequest::Close are triggered after a closehandle call in the figure below.

The vulnerability here is because CClfsRequest::Cleanup calls CClfsLogCcb::Release in advance to release the additional state FsContext2 of the corresponding file object, but the file object has not been released in the kernel at this time. The attacker can use the file object to trigger the use of the additional state FsContext2 again in some way. If the attacker can reuse this memory when ClfsLogCcb::Release releases the additional state FsContext2 of the corresponding file object, then calling the function involving the use of the additional state FsContext2 again may enter the attacker-controlled process, resulting in the privilege escalation of the UAF vulnerability. In fact, there are many similar functions in clfs to trigger the reuse of the additional state FsContext2. Some of them are listed in the previous startlab article, such as

  • CClfsRequest::ReserveAndAppendLog()
  • CClfsRequest::WriteRestart()
  • CClfsRequest::ReadArchiveMetadata()


Exploitation

Now that we understand the cause of the vulnerability, let's look at how the vulnerability exploit sample in the actual attack achieves privilege escalation. The sample first checks whether there is a blf file generated by a previous run in the SkyPDF working directory, and checks the version of the target system to determine whether the vulnerability can be triggered.

Next are two key threads. You can see that after these two threads are called, they will start to search for the Winlogon process. Therefore, we can confirm that the core of the vulnerability exploitation lies in the callback functions of these two threads. Both callback functions pass in the same parameter Parameter, which is a huge array containing multiple data involved in synchronization and exchange between the two threads.

Then, the handle of the target Winlogon process is obtained through DuplicateHandle, and a remote thread is created using the handle of the Winlogon process to load the subsequent payload file, thereby achieving the Winlogon process loading and injection Dll.

First, let's look at threadProc1. This function is quite complex. It initially performs a series of checks using a local configuration table. The configuration table is then saved in the parameter Parameter passed into the function. It then calls fun_getRtlSetAllBits/fun_CreatePipe in sequence. Similarly, the data in the Parameter array is passed into both functions as parameters.

fun_getRtlSetAllBits function allocates two memory segments A and B with lengths of E0 and 5E0 respectively, and attempts to write some data. A is filled with 0x11, while B is more complicated.

The B data after some operations is shown below. You can see that each section is constructed as follows and referenced by pointers in turn.

Get the address of the function NtQuerySystemInformation.

The Privieges ( _SEP_TOKEN_PRIVILEGES ) location of the kernel _EPROCESS object corresponding to the current process is obtained through NtQuerySystemInformation. The core location will be written to the previously allocated memory section F. VirtualAlloc is called again to allocate a 0x1000 length address C at the address 0x10000000.

Get the address of the function RelSetAllBits. This function is an excellent writing tool function and is used in multiple Windows privilege escalation vulnerabilities. The offset of the RelSetAllBits function address is written to the previously allocated memory 0x10000000. After that, the memory addresses A, B, and C previously allocated in the function are also recorded in the parameter object Parameter passed to the thread function.

The parameters at this time are as follows. Sections A, B, C, D, and E in address B are referenced in sequence. A call stack for RtSetAllBits is constructed in F, and the parameter passed in is PreviousMode.

fun_CreatePipe function is relatively simple, creating two sets of pipe pairs, each with 0x400 readpipe/writepipe pairs. Four arrays containing 0x400 readpipe/writepipe pairs are also saved in the passed parameter object Parameter.

After the fun_getRtlSetAllBits/fun_CreatePipe call is completed, the content of the core object Parameter is roughly as follows:

Afterwards, piperWrite2/pipeRead2 are called multiple times to repeatedly write to the pipe pair. This is mainly to prepare the memory layout before the vulnerability is triggered, so as to facilitate the subsequent UAF memory preemption.

Create 0x10 clfs log file objects through CreateFileW.

CloseHandle is repeatedly called to close the previously created clfs log file object, triggering CClfsRequest::Cleanup to call ClfsLogCcb::Release to release Irp->Tail.Overlay.CurrentStackLocation->FileObject->FsContext2. Then, WriteFile is used to write our malicious fake CClfsManagedLogClientUser data, which corresponds to the section A with a length of 0xE0 in the previous Parameter, in order to reuse the released Irp->Tail.Overlay.CurrentStackLocation->FileObject->FsContext2.

It should be noted that CloseHandle is called four times before entering the exploit loop. This is essentially because of the ExFreeToNPagedLookasideList function requires a certain L.Depth. For Fscontext2 in the exploit, this depth is 4. Therefore, the exploit ensures at least 4 CloseHandle calls before entering the core exploit loop.

The data structure used to maintain paged memory management dependencies in ExFreeToNPagedLookasideList is PPAGED_LOOKASIDE_LIST, which is the L in the previous L.Depth. The Depth has been mentioned before and is used to record the current amount of paged memory that needs to be released. When L.Depth is reached, it is released uniformly. The size in it marks the size of the paged memory block maintained by the corresponding List.

ExFreeToNPagedLookasideList is ExAllocateFromPagedLookasideList, which is used to allocate a corresponding paged memory block.

FsContext2 released when the vulnerability is triggered has a size of 0x110. To achieve the corresponding release, the number of CClfsLogCcb in the freelist needs to reach four.

The memory allocation for a CClfsLogCcb object is as follows. Besides the actual size of 0x110, it also includes a 0x10 header. The actual allocated memory size is 0x120. Therefore, to reuse the freed FsContext2 (CClfsLogCcb) object, we need to allocate 0x120 of memory.

From the previous analysis, we know that the attack code achieves this goal by writing data of size 0xE0 through the pipe. This operation causes the actual memory size allocated in the kernel memory to be 0xE0 (actual size) + 0x40 (header size) = 0x120, which is exactly the same as the size of the released FsContext2 (CClfsLogCcb) object.

Next is threadPro2. This function is relatively simple. After waiting for the event/synchronization barrier in the Parameter object to be triggered through WaitForSingObject and EnterSynchronizationBarrier, and the position judgment condition in the parameter Parameter is met, it calls DeviceIoControl to send the 0x80077028 ioctl code.

0x80077028 corresponds to the function CClfsManagedLogClientUser::ReadNotification in the clfs.sys driver. Note that the first parameter of this function comes from FsContext2 , which is exactly the location released and reused earlier. Its offset + 0x108 is passed as the first parameter of ReadNotification.

Since CClfsManagedLogClientUser is a fake CClfsManagedLogClientUser constructed and reused by the attacker , the RtlSetAllBits constructed and referenced in the fake CClfsManagedLogClientUser at the following location in the function will be triggered and called.

directly on the RtlSetAllBits function, we can see that the corresponding RtlSetAllBits is called in CClfsManagedLogClientUser::ReadNotification to achieve privilege escalation.


Dynamic debugging

Let’s take a closer look at the vulnerability exploit during debugging.

the value of FsContext2 at the corresponding CClfsRequest::Close through the following breakpoints:

bp CLFS!CClfsRequest::Close+0x5c ".if(poi(poi(rax+0x20)+0x108)){.printf\"current FsContext2 of irp is:%p\\n\",poi(poi(rax+0x20)+0x108);gc}.else{gc}"

It can be seen that throughout the entire exploitation process, until the Readnotification call, the CClfsManagedLogClientUser object pointed to in the FsContext2 corresponding to the irp in CLFS!CClfsRequest::Close+0x5c is empty and is reused afterwards.

The reused memory at this time is as follows. In the ClfsMgmtDispatchIo function , irp is fffffc782997fd790, and the reused FsContext2 is ffffc78293d88710, which points to the A section we constructed. The length of A section is 0xE0, but its actual kernel memory needs to add an additional structure of 0x30. Therefore, the B section address at the offset of 0xD8 in the original A section should be 0x30+0xD8 = 0x108 for the A section object.

CClfsManagedLogClientUser object we forged is used as the first parameter passed into CClfsManagedLogClientUser::ReadNotification , which corresponds to section B.

corresponding to the CClfsManagedLogClientUser ::ReadNotification call has been completely reused and modified to the fake CClfsManagedLogClientUser constructed by the attacker in fun_getRtlSetAllBits.

ClfsManagedLogClientUser + 0xE0 is shown in the following figure:

Call CClfsManagedLogClientUser::ReadNotification in sequence, and finally call the RtlSetAllBits pointer maliciously referenced in fake CClfsManagedLogClientUser.

CClfsManagedLogClientUser::ReadNotification
    CLFS\!CClfsMdlReference::AddRef
        CLFS\!CClfsRequest::ReadMgmtNotificationInProgress
            nt\!RtlSetAllBits

The BitMapHeader parameter of the RtlSetAllBits call is also constructed from the F section of the fake CClfsManagedLogClientUser and points to the token->priviliege of the current process. As shown below, it is ultimately set using memset, resulting in the current process's privilege escalation.

The writing process in RtlSetAllBits is as follows:

It can be seen that the process of the privilege escalation sample has been set to system permissions.

Now let's review the Parameter object from before. We fill in the corresponding pipe occupying position 0xE0 to write malicious memory. The calling relationship in actual exploitation is shown below.

The final complete exploit diagram is shown below:


References

[1] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-29824

[2] https://starlabs.sg/bl og/2025/07-my-blind-date-with-cve-2025-29824/

0DAY CVE-2025-29824 VULNERABILITY