CVE-2024-38238 — Kernel Streaming Forgotten MDL Lock → Arbitrary Physical Memory Write
Last updated: 2026-04-12
Severity: High
Component: ks.sys / ksthunk.sys (Kernel Streaming)
Bug Class: Uninitialized PFN Array in MDL → Arbitrary Physical Memory Write
Privilege Escalation: User → SYSTEM
Patch: August 2024 Patch Tuesday
Vulnerability Summary
A vulnerability in ks.sys’s MDL handling during WoW64 frame processing. When a frame with KSSTREAM_HEADER_OPTIONSF_PERSIST_SAMPLE cache flag is submitted alongside a non-cached frame, ksthunk allocates and locks an MDL for the non-cached frame but skips MDL allocation for the cached frame. Then ks.sys’s CKsMdlcache::MdlCacheHandleThunkBufferIrp allocates a new MDL for the cached frame via IoAllocateMdl but does not call MmProbeAndLockPages. Subsequently, KsProbeStreamIrp only checks the lock status of the first MDL in the chain and calls MmMapLockedPagesSpecifyCache on all MDLs — including the unlocked, uninitialized one. Since IoAllocateMdl allocates from NonPagedPoolNx without zero-initialization, the PFN array contains attacker-controlled stale memory (via pool spray), enabling arbitrary physical memory writes.
Root Cause Analysis
MDL Lifecycle Bug
Normal MDL usage:
IoAllocateMdl → MmProbeAndLockPages → MmMapLockedPagesSpecifyCache → ... → MmUnlockPages → IoFreeMdl
Vulnerable path:
IoAllocateMdl → [SKIP MmProbeAndLockPages] → MmMapLockedPagesSpecifyCache
↑ accesses uninitialized PFN array!
Code Path
// ks.sys CKsMdlcache::MdlCacheHandleThunkBufferIrp:
while (TotalSize >= sizeof(KSSTREAM_HEADER)) {
if (OptionsFlag & KSSTREAM_HEADER_OPTIONSF_PERSIST_SAMPLE) {
IoAllocateMdl(header->Data, header->FrameExtent, ..., Irp); // [1] no lock!
} else {
return KsProbeStreamIrp(irp, a3, 0); // [2] for non-cached frames
}
}
// After loop:
for (MDL = irp->MdlAddress; MDL; MDL = MDL->Next) {
MmProbeAndLockPages(MDL, ...); // [3] locks ALL MDLs — but only for standard path
}
// KsProbeStreamIrp:
if ((MDL->MdlFlags & is_locked_and_nonpaged) != 0) { // [4] checks FIRST MDL only
while (MDL) {
MmMapLockedPagesSpecifyCache(MDL, ...); // [5] maps ALL MDLs!
MDL = MDL->Next;
}
}
Setup: Frame 1 = cache flag SET, Frame 2 = cache flag CLEAR
ksthunk: allocates and locks MDL for Frame 2 (no cache); skips Frame 1ks.sys CKsMdlcache: allocates (but doesn’t lock) MDL for Frame 1 (cache); callsKsProbeStreamIrpfor Frame 2KsProbeStreamIrp: checks Frame 2’s MDL (locked — from ksthunk) → proceeds → maps ALL MDLs including Frame 1’s unlocked MDL
Exploitation via Pool Spray
IoAllocateMdl uses POOL_FLAG_UNINITIALIZED:
- The PFN array at the end of the MDL structure contains stale pool data
- Attacker sprays
NonPagedPoolNxwith crafted data at the exact MDL size before triggering the vulnerability - When
IoAllocateMdlreuses sprayed memory, PFN array = attacker-controlled page frame numbers MmMapLockedPagesSpecifyCacheuses these PFNs to create a virtual mapping- Worker thread writes device data to these physical pages → arbitrary physical memory write
Spray technique: Named pipes for NonPagedPoolNx spray (documented by Alex Ionescu). Size must match exactly: sizeof(MDL) + ceil(FrameExtent/PAGE_SIZE) * sizeof(PFN).
Exploitation Technique
Step 1: Pool Spray
Spray NonPagedPoolNx with crafted PFN values. Calculate MDL size based on chosen frame size (e.g., 1 page → MDL is sizeof(_MDL) + 8 = 0x38 bytes on x64).
Step 2: Trigger Vulnerability
Submit two frames via IOCTL_KS_READ_STREAM (WoW64):
- Frame 1: small size,
KSSTREAM_HEADER_OPTIONSF_PERSIST_SAMPLEset - Frame 2: any size, no cache flag
IoAllocateMdl reuses sprayed pool → PFNs point to target physical pages.
Step 3: Controlled Data Write (via Buffered Mode)
Without buffered mode, we can’t control what’s written (it’s device data). With KSSTREAM_HEADER_OPTIONSF_BUFFEREDTRANSFER:
Device data → intermediate kernel buffer (normal pool)
↓
KS copies: memmove(frame_buffer_mapped_to_physical_target, intermediate_buf, size)
Attacker controls frame timing and intermediate buffer — device writes to intermediate buffer, then KS copies to target physical address. In practice: wait for device to write known data, use timing, OR directly control intermediate buffer content via other means.
Simpler: ntoskrnl.exe physical base is predictable on many Windows 24H2 systems (0x100400000). Write to a security check in PsOpenProcess — same SeDebugPrivilege→SeChangeNotifyPrivilege technique (see CVE-2024-30090).
Key Primitives Used
- Uninitialized
NonPagedPoolNxPFN array via pool spray (named pipes) MmMapLockedPagesSpecifyCacheon unlocked MDL- Arbitrary physical memory write
- Buffered mode for controlled data transfer
- Predictable ntoskrnl.exe physical base on Win24H2
Mitigations Bypassed
- kASLR (physical address predictability bypasses KASLR for physical write target)
- kCFG (not involved — data-only attack)
- HVCI (data-only physical write technique works even with HVCI)
Related CVEs
- CVE-2024-38237: Similar MDL mismatch (different trigger path)
- CVE-2024-38241: Another forgotten lock variant
- CVE-2025-21375: MDL mismatch
- CVE-2025-24046: Double free variant (MDL freed without clearing IRP pointer)
- CVE-2025-24066: Another forgotten lock
References
- Angelboy (DEVCORE), “Frame by Frame, Kernel Streaming Keeps Giving Vulnerabilities”, devco.re, 2025-05-17 (OffensiveCon 2025)
- MSRC: CVE-2024-38238
- Alex Ionescu, “Understanding Pool Spraying with Named Pipes”, alex-ionescu.com
- See also: Kernel Streaming, Cve 2024 38245
