In April 2025, Microsoft patched a vulnerability that had become a key component in sophisticated ransomware attack chains. CVE-2025-29824
, an use-after-free bug in the Windows Common Log File System (CLFS) driver, wasn’t the initial entry point for attackers. Instead, threat actors first compromised Cisco ASA firewalls, then used this Windows kernel vulnerability as the crucial privilege escalation step that transformed limited network access into complete system domination. This multi-stage approach represents the evolution of modern ransomware operations: sophisticated threat actors chaining together network infrastructure vulnerabilities with Windows kernel bugs to devastating effect.
TL;DR
So my mentor dropped this assignment on me: “CVE-2025-29824 was used in-the-wild to pwn Windows machines - figure out how they did it. Oh, and there are no public samples, so you’re going in blind. Good luck!” 😅
Turns out, it’s a use-after-free vulnerability in the Windows Common Log File System (CLFS) driver. When a log file handle is closed, the FsContext2
structure is incorrectly freed in CClfsRequest::Cleanup()
, while another IRP request can still be in progress. The attackers chained this with a Cisco ASA exploit.
The challenge: Reverse engineer how this actually works, understand why Microsoft’s reference counting went sideways, and build a working PoC from scratch.
Spoiler alert: it involves a lot more assembly diving and crash dump analysis than I initially signed up for! 🔍💥
Technical Deep Dive Into The Vulnerability
Summary:
Product | Microsoft Windows |
---|---|
Vendor | Microsoft |
Severity | High |
Affected Versions | Windows 10-11, Windows Server 2008-2025 |
Tested Versions | Windows 11 23H2 |
Impact | Elevation of Privilege |
CVE ID | CVE-2025-29824 |
CWE | CWE-416: Use After Free |
PoC available? | Yes |
Patch available? | Yes |
Exploit available? | No |
CVSSv3.1 Scoring System:
Base Score: 7.8 (HIGH)
Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Metric | Value |
---|---|
Attack Vector (AV) | Local |
Attack Complexity (AC) | Low |
Privileges Required (PR) | Low |
User Interaction (UI) | None |
Scope (S) | Unchanged |
Confidentiality Impact (C) | High |
Integrity Impact (I) | High |
Availability Impact (A) | High |
How Microsoft Fixed the Bug
Patch Analysis - Before vs. After
To analyze the vulnerability fix, we compared two versions of clfs.sys
:
Before the patch:
- Version:
10.0.22621.5097
- SHA256:
3FB7DA92269380F7BC72A1AD1B06D9BF1CB967AC3C5C7010504C841FCCC6108E
After the patch:
- Version:
10.0.22621.5192
- SHA256:
969AC3B22CA9A22D73E8CC0BC98D9CF8A969B7867FAF2EE1093B0B0695D3439F
The security fixes are implemented within the CClfsLogCcb::Cleanup()
and CClfsRequest::Close()
functions.
CClfsRequest::Cleanup() Implementation
The cleanup process follows this sequence:
CClfsRequest::Cleanup(PIRP Irp){
...
CClfsLogCcb::AddRef(FsContext2);
CClfsLogCcb::Cleanup(FsContext2);
CClfsLogCcb::Release(FsContext2);
...
}
CClfsLogCcb::AddRef()
performs atomic increment operations on the FsContext2
reference counter:
CClfsLogCcb::AddRef(FsContext2){
return _InterlockedIncrement(FsContext2->reference_count);
}
CClfsLogCcb::Release()
atomically decrements the FsContext2
reference counter and triggers cleanup when the count reaches zero, invoking the destructor CClfsLogCcb::~CClfsLogCcb()
for proper resource deallocation.
CClfsLogCcb::Release(FsContext2){
refcount = _InterlockedDecrement(&FsContext2->reference_count);
if ( !refcount && FsContext2 ){
CClfsLogCcb::~CClfsLogCcb(FsContext2);
...
}
return refcount;
}
CClfsLogCcb::Cleanup() Changes
Pre-patch implementation:
CClfsLogCcb::Cleanup(FsContext2)
{
...
CClfsLogCcb::Release(FsContext2);
}
Post-patch implementation:
CClfsLogCcb::Cleanup(FsContext2)
{
...
if(!PatchFlag()){
CClfsLogCcb::Release(FsContext2);
}
}
This patch makes it such that CClfsLogCcb::Cleanup()
no longer calls CClfsLogCcb::Release(FsContext2)
. This has instead been moved into CClfsRequest::Close()
.
CClfsRequest::Close() Changes
Pre-patch implementation:
CClfsRequest::Close(PIRP Irp){
CClfsLogFcbCommon *ReservedContext;
...
boolean = ExAcquireResourceExclusiveLite(&ReservedContext->m_resLockFcb, 1u);
CClfsLogFcbCommon::Close(ReservedContext);
if (boolean){
ExReleaseResourceForThreadLite(&ReservedContext->m_resLockFcb, (ERESOURCE_THREAD)KeGetCurrentThread());
}
...
return 0;
}
Post-patch implementation:
CClfsRequest::Close(PIRP Irp){
CClfsLogFcbCommon *ReservedContext;
...
FileObject = CurrentStackLocation->FileObject;
if (PatchFlag()){
FsContext2 = (CClfsLogCcb *)CurrentStackLocation->FileObject->FsContext2;
v10 = FsContext2; //save FsContext2
if (FsContext2){
CClfsLogCcb::AddRef(FsContext2); //increment reference count to FsContext2
CClfsLogCcb::Close(FsContext2); //decrement reference count to FsContext2
}
}
...
boolean = ExAcquireResourceExclusiveLite(&ReservedContext->m_resLockFcb, 1u);
CClfsLogFcbCommon::Close(ReservedContext);
if (boolean){
ExReleaseResourceForThreadLite(&ReservedContext->m_resLockFcb, (ERESOURCE_THREAD)KeGetCurrentThread());
FsContext2 = v10; //load FsContext2
}
...
if (PatchFlag()){
FileObject->FsContext = 0;
FileObject->FsContext2 = 0;
if (FsContext2){
CClfsLogCcb::Release(FsContext2);
}
}
...
return 0;
}
Cleanup VS Close
The reason the patch fixes the vulnerability is that it moves CClfsLogCcb::Release(FsContext2)
from CClfsRequest::Cleanup()
into CClfsRequest::Close()
, which are called via the control codes IRP_MJ_CLEANUP
and IRP_MJ_CLOSE
respectively.
IRP_MJ_CLEANUP
is sent when the last usermode handle for a file object that is associated with the target device object is closed (but, due to outstanding I/O requests, might not have been released).
IRP_MJ_CLOSE
is sent when the last reference to the file object is released, and all outstanding I/O requests have been completed or canceled.
Thus, after the patch, when FsContext2
is being released in CClfsRequest::Close()
, there should be no other requests that are using it. This means that there should be no opportunities to exploit it.
FsContext2 Structure Deep-Dive
According to the official Microsoft documentation, FsContext2
belongs to the structure FILE_OBJECT
, which is used by the system to represent a file object. FsContext2
is a pointer to whatever additional state a driver maintains about a file object, otherwise it is NULL
. Simply put, FsContext2
is different depending on the type of file being represented.
Through some reversing and referencing existing documentation done by others, FsContext2
in the case of CLFS takes the form of the following undocumented structure, CClfsLogCcb
. Where possible, the structure’s members have been identified and named accordingly.
00000000 struct __unaligned __declspec(align(8)) CClfsLogCcb // sizeof=0x110
00000000 {
00000000 _BYTE gap0[8];
00000008 _LIST_ENTRY list_entry;
00000018 ULONG reference_count;
0000001C _DWORD flag;
00000020 _BYTE gap20[8];
00000028 unsigned int m_cArchiveRef;
0000002C _BYTE gap2C[4];
00000030 _QWORD m_hPhysicalHandle;
00000038 _QWORD qword38;
00000040 char *field_40;
00000048 PFILE_OBJECT m_foFileObj;
00000050 _QWORD m_foFileObj2;
00000058 CLFS_LSN clfs_lsn58;
00000060 CLFS_LSN clfs_lsn60;
00000068 _QWORD qword68;
00000070 __int64 field_70;
00000078 // padding bytes
00000080 CClfsBaseFileSnapshot *field_80;
00000088 _BYTE gap88[16];
00000098 ERESOURCE m_Resource;
00000100 struct CClfsLogCcb::ILifetimeListener *m_pltlLifeTimeListener;
00000108 _QWORD qword108;
00000110 };
This 0x110-byte
structure contains all the state information for a CLFS log file handle. The reference_count
at offset 0x18
and m_Resource
at offset 0x98
are critical for understanding the vulnerability.
How to Trigger the Bug
Triggering the Vulnerability
The vulnerability here lies in improper synchronization between cleanup and control operations within the CLFS driver. Specifically, race conditions exist between the following:
CloseHandle()
– CallsCClfsRequest::Cleanup()
andCClfsRequest::Close()
in that order, freeingFsContext2
.DeviceIoControl()
– Calls a specific function that usesFsContext2
.
CClfsRequest::Cleanup()
and CClfsRequest::Close()
are called in that order when the user calls the function CloseHandle()
with a log file handle as an argument.
DeviceIoControl()
allows users to directly send control codes to a specified driver to invoke a function that corresponds to the control code sent. In this case we want to send control codes to the CLFS driver to trigger the vulnerability.
Finding the Right Functions
To find out which CLFS driver function we want to call, we would have to reverse all the functions that are reachable via DeviceIoControl()
. Thus, we turn to searching CClfsRequest::Dispatch()
, which is in charge of processing the control codes sent to the CLFS driver and calling the relevant function. The goal here is to find CClfsRequest functions that use FsContext2
.
The first step is to understand how data is being passed between the various functions. We find that CClfsRequest::Cleanup()
, CClfsRequest::Close()
and CClfsRequest::Dispatch()
all take an Irp
data structure as an argument, which represents an I/O request packet. By reversing CClfsRequest::Cleanup()
, we can see how they access FsContext2
.
CClfsRequest::Cleanup(PIRP Irp){
...
CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation; //offset 0x78h
...
//offsets are FileObject (0x30h), FsContext2 (0x20h)
FsContext2 = CurrentStackLocation->FileObject->FsContext2;
...
}
Now we look inside CClfsRequest::Dispatch()
to see how Irp
is being passed into the functions. It is stored at offset 0x30h
of the structure CClfsRequest
, which is then passed into these functions.
CClfsRequest::Dispatch(CClfsRequest *this, PIRP Irp, struct _DEVICE_OBJECT *a3){
this->m_pIrp = Irp; //offset 0x30h
...
switch ( LowPart ){
case 0x8007A85C:
appended = CClfsRequest::AdvanceLogBase(this);
goto LABEL_9;
case 0x8007A810:
appended = CClfsRequest::SetArchiveTail(this);
goto LABEL_9;
...
}
An example of a function that we are looking for:
CClfsRequest::UsefulFunction(this){
//offsets: this->0x30->0x78->0x30->0x20
FsContext2 = this->m_pIrp->Tail.Overlay.CurrentStackLocation->FileObject->FsContext2;
}
Vulnerable Functions Found
Now that we know the offset of FsContext2
relative to the argument being passed into these functions, we look through each of these functions to find those that access FsContext2
.
All of these functions loads and dereferences FsContext2
as some point in its execution:
CClfsRequest::ReserveAndAppendLog()
CClfsRequest::WriteRestart()
CClfsRequest::ReadArchiveMetadata()
__int64 __fastcall CClfsRequest::ReserveAndAppendLog(CClfsRequest *this){
...
Irp = this->m_pIrp; //offset 0x30h
CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation; //offset 0x78h
...
FsContext2 = CurrentStackLocation->FileObject->FsContext2;
...
v29 = FsContext2[13]; // Potential FsContext2 dereference
...
}
__int64 __fastcall CClfsRequest::WriteRestart(CClfsRequest *this){
...
Irp = this->m_pIrp; //offset 0x30h
CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation; //offset 0x78h
...
this->m_Ccb = CurrentStackLocation->FileObject->FsContext2;
...
if ( this->field_F0 + this->m_Ccb->qword68 >= 0 ){ // Potential FsContext2 dereference
...
}
__int64 __fastcall CClfsRequest::ReadArchiveMetadata(CClfsRequest *this){
...
Irp = this->m_pIrp; //offset 0x30h
CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation; //offset 0x78h
...
FsContext2 = CurrentStackLocation->FileObject->FsContext2;
...
CClfsLogCcb::ReadArchiveMetadata(FsContext2, ...);
...
if ( FsContext2 ){
CClfsLogCcb::Release(FsContext2);
}
}
__int64 __fastcall CClfsLogCcb::ReadArchiveMetadata(FsContext2, ...){
...
p_m_Resource = &FsContext2->m_Resource; // Potential FsContext2 dereference
...
if ( FsContext2->m_cArchiveRef ) // Potential FsContext2 dereference
...
}
Attack Vectors
These functions can be used in order to dereference FsContext2
and trigger the vulnerability.
CClfsRequest::ReserveAndAppendLog()
can be reached by calling the user functionReserveAndAppendLog()
or directly sending the control code0x8007A827
viaDeviceIoControl()
.CClfsRequest::WriteRestart()
can be reached by calling the user functionWriteLogRestartArea()
or directly sending the control code0x8007281F
viaDeviceIoControl()
.CClfsRequest::ReadArchiveMetadata()
can be reached by sending the control code0x80076856
viaDeviceIoControl()
.
Both ReserveAndAppendLog()
and WriteLogRestartArea()
additional setup of a separate variable called a Marshal
that will be passed as an argument into these functions while CClfsRequest::ReadArchiveMetadata()
can be called via DeviceIoControl()
without the need to setup any additional variables. Thus, CClfsRequest::ReadArchiveMetadata()
is used in the proof of concept due to simplicity and ease of use.
Step-by-Step Exploitation
In order to trigger this vulnerability, one could do the following:
- Get a file handle to a log file by calling
CreateLogFile()
. - Create 2 threads, passing the file handle to both threads.
- Thread 1 would call
CloseHandle()
on the file handle, thus telling the CLFS driver to performCClfsRequest::Cleanup()
andCClfsRequest::Close()
. - Thread 2 would call either
ReserveAndAppendLog()
,WriteLogRestartArea()
orDeviceIoControl()
on the file handle. - If the timing is just right,
FsContext2
would be dereferenced by Thread 2 just after Thread 1 releases it, leading to a use-after-free vulnerability.
Proof-of-Concept
The general idea of the Proof of Concept is to create 2 threads and attempt to induce a race condition where CloseHandle()
and CClfsRequest::ReadArchiveMetadata()
execute at the same time, thus causing a crash as a result of use after free vulnerability.
The Proof of Concept has a high likelihood of triggering this vulnerability and causing a crash that results in a blue screen error of IRQL_NOT_LESS_OR_EQUAL
in ntoskrnl.exe
. This error is a memory related error that appears if a system process or a driver attempts to access an incorrect or corrupted pointer.
Crash Context
# Child-SP RetAddr Call Site
00 fffffa83`f58c9a38 fffff802`1e9668e2 nt!DbgBreakPointWithStatus
01 fffffa83`f58c9a40 fffff802`1e965fa3 nt!KiBugCheckDebugBreak+0x12
02 fffffa83`f58c9aa0 fffff802`1e816c07 nt!KeBugCheck2+0xba3
03 fffffa83`f58ca210 fffff802`1e82c4e9 nt!KeBugCheckEx+0x107
04 fffffa83`f58ca250 fffff802`1e827a34 nt!KiBugCheckDispatch+0x69
05 fffffa83`f58ca390 fffff802`1e6fc805 nt!KiPageFault+0x474
06 fffffa83`f58ca520 fffff802`1c348b85 nt!ExDeleteResourceLite+0x125
07 fffffa83`f58ca570 fffff802`1c347ee2 CLFS!CClfsLogCcb::~CClfsLogCcb+0x31
08 fffffa83`f58ca5b0 fffff802`1c36290c CLFS!CClfsLogCcb::Release+0x32
09 fffffa83`f58ca5e0 fffff802`1c3485b5 CLFS!CClfsRequest::ReadArchiveMetadata+0x164
0a fffffa83`f58ca650 fffff802`1c34785e CLFS!CClfsRequest::Dispatch+0x369
0b fffffa83`f58ca6a0 fffff802`1c3477a7 CLFS!ClfsDispatchIoRequest+0x8e
0c fffffa83`f58ca6f0 fffff802`1e6ebef5 CLFS!CClfsDriver::LogIoDispatch+0x27
0d fffffa83`f58ca720 fffff802`1eb40060 nt!IofCallDriver+0x55
0e fffffa83`f58ca760 fffff802`1eb41a90 nt!IopSynchronousServiceTail+0x1d0
0f fffffa83`f58ca810 fffff802`1eb41376 nt!IopXxxControlFile+0x700
10 fffffa83`f58caa00 fffff802`1e82bbe5 nt!NtDeviceIoControlFile+0x56
11 fffffa83`f58caa70 00007ffb`add2f454 nt!KiSystemServiceCopyEnd+0x25
12 000000e9`084ff678 00007ffb`ab34664b 0x7ffbadd2f454
13 000000e9`084ff680 00000000`00000000 0x7ffbab34664b
In the context of the Proof of Concept, Release(FsContext2)
is being called by CClfsLogCcb::ReadArchiveMetadata()
. However, FsContext2
has already been freed at that point due to CClfsRequest::Cleanup()
calling Release(FsContext2)
, thus leading to an invalid memory access.
Variant Analysis
Now we know permanently releasing things in Cleanup() is dangerous because other IOCTLs can still be dispatched, we briefly explore if there are any other objects released or fields modified in CClfsRequest::Cleanup()
.
No other fields or objects are released, as the only call to CClfsLogCcb::Release()
inside CClfsRequest::Cleanup()
is for FsContext2
.
Fields or objects that could be modified inside CClfsRequest::Cleanup()
are as follows:
FsContext2->flag
(offset 0x1C)FsContext2->gap20[4]
(offset 0x20)FsContext2->m_pltlLifeTimeListener
(offset 0x100)
Enhanced Detection Strategies
Points of detection for this vulnerability would be:
- Creation of unknown log files with the extension
.blf
. - Monitor System Event ID 11 for suspicious activity
Suggested Mitigations
Download and apply Microsoft updates from 8th April 2025 onwards.
You can check out the PoC here.