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() – Calls CClfsRequest::Cleanup() and CClfsRequest::Close() in that order, freeing FsContext2.
  • DeviceIoControl() – Calls a specific function that uses FsContext2.

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 function ReserveAndAppendLog() or directly sending the control code 0x8007A827 via DeviceIoControl().
  • CClfsRequest::WriteRestart() can be reached by calling the user function WriteLogRestartArea() or directly sending the control code 0x8007281F via DeviceIoControl().
  • CClfsRequest::ReadArchiveMetadata() can be reached by sending the control code 0x80076856 via DeviceIoControl().

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 perform CClfsRequest::Cleanup() and CClfsRequest::Close().
  • Thread 2 would call either ReserveAndAppendLog(), WriteLogRestartArea() or DeviceIoControl() 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

blfCreateFile

Suggested Mitigations

Download and apply Microsoft updates from 8th April 2025 onwards.

You can check out the PoC here.

References: