CVE-2024-30090 — Kernel Streaming Event Double Fetch + Arbitrary Address Increment
Last updated: 2026-04-12
Severity: High
Component: ksthunk.sys (Kernel Streaming WOW Thunk)
Bug Class: Access Mode Mismatch + TOCTOU / Double Fetch → Arbitrary Increment
Privilege Escalation: User → SYSTEM
Patch: July 2024 Patch Tuesday
Vulnerability Summary
A double fetch vulnerability in ksthunk.sys’s WoW64 conversion path for IOCTL_KS_ENABLE_EVENT, combined with the same “Proxying to Kernel” access mode mismatch bug class as CVE-2024-35250. When a WoW64 process sends IOCTL_KS_ENABLE_EVENT with KSEVENT_TYPE_QUERYBUFFER, ksthunk re-issues the IOCTL via KsSynchronousIoControlDevice (KernelMode). A race allows changing the event type flag to KSEVENT_TYPE_ENABLE between validation and use, enabling the attacker to provide kernel objects to ks.sys’s event handler — resulting in an arbitrary kernel address increment-by-one primitive.
Root Cause Analysis
Step 1: ThunkEnableEventIrp Double Fetch
// ksthunk CKSAutomationThunk::ThunkEnableEventIrp:
if (Flags == KSEVENT_TYPE_QUERYBUFFER) { // [READ 1 — validation]
newinputbuf = ExAllocatePoolWithTag(..., inputbuflen + 8, 'bqSK');
memcpy(newinputbuf, Type3InputBuffer, 0x28); // copies first 0x28 bytes
KsSynchronousIoControlDevice(FileObject, 0 /*KernelMode*/,
IOCTL_KS_ENABLE_EVENT, newinputbuf, ...); // [2] KernelMode IOCTL
}
Neither I/O again: Type3InputBuffer stays in user memory. The Flags field is read once (step 1) then the buffer is copied. An attacker races to change Flags from KSEVENT_TYPE_QUERYBUFFER to KSEVENT_TYPE_ENABLE between step 1 and step 2 (the new KernelMode IOCTL).
Step 2: KernelMode Allows Kernel Objects in KspEnableEvent
When ks.sys receives the IOCTL_KS_ENABLE_EVENT with RequestorMode == KernelMode and KSEVENT_TYPE_ENABLE:
// ks.sys KspEnableEvent:
switch (KSEVENTDATA.NotificationType) {
case KSEVENTF_EVENT_HANDLE: // user: wait on event handle
case KSEVENTF_SEMAPHORE_HANDLE: // user: signal semaphore
case KSEVENTF_KSWORKITEM: // KERNEL ONLY: increment work item counter
case KSEVENTF_DPC: // KERNEL ONLY: queue DPC
...
}
With KernelMode, KSEVENTF_KSWORKITEM is reachable. The attacker provides an arbitrary kernel address disguised as a KSWORKITEM pointer.
Step 3: Arbitrary Increment
// ks.sys KsGenerateEvent:
case KSEVENTF_KSWORKITEM:
KsIncrementCountedWorker(eventdata->KsWorkItem.KsWorkerObject);
// Internally: InterlockedIncrement on a field of KsWorkerObject
// → increments 4 bytes at (attacker_address + field_offset)
Result: Arbitrary kernel address increment by 1 (at a specific offset into the attacker-provided address).
Exploitation Technique
Arbitrary Increment → EoP
Challenge: Traditional increment-based EoP paths (token privilege bits, IoRing) have issues here:
- Token privilege bits: need 0x10 increments × 2 fields = 32 race conditions → unstable
- IoRing RegBuffers: zero-value problem —
KsQueueWorkItemis called when value goes from 0 → 1, causing BSoD
Novel Technique: SeDebugPrivilege LUID Modification
nt!SeDebugPrivilege is a global LUID variable in a writable section of ntoskrnl.exe:
nt!SeDebugPrivilege = {LowPart = 0x14, HighPart = 0} (default)
SeChangeNotifyPrivilege = {LowPart = 0x17, HighPart = 0}
The target: increment nt!SeDebugPrivilege.LowPart from 0x14 to 0x17 (3 increments).
Why this works:
NtOpenProcesscallsSeSinglePrivilegeCheck(SeDebugPrivilege, ...)- This reads
nt!SeDebugPrivilegeto get the LUID value (0x14) - Checks if the current token has bit
0x14set inPrivileges.Enable/Present - After modification, reads LUID =
0x17→ checks bit0x17instead SeChangeNotifyPrivilege(LUID0x17) is enabled for all users by default- Result: any user can open any process (except PPL) with
PROCESS_ALL_ACCESS
Why it’s stable:
- The target address (
nt!SeDebugPrivilege) never holds value0— starts at0x14 KsQueueWorkItemzero-check issue is avoided- Only 3 increments needed (each triggered separately)
Additional LUID Targets
SeTcbPrivilege = 0x7 → can target other LUIDs for different escalation paths
SeTakeOwnershipPrivilege = 0x9
SeLoadDriverPrivilege = 0xa
Post-EoP
After nt!SeDebugPrivilege → 0x17:
OpenProcess(PROCESS_ALL_ACCESS, FALSE, winlogon_pid); // succeeds for any user
DuplicateTokenEx(winlogon_token, ...); // or parent process spoofing
Restore nt!SeDebugPrivilege to 0x14 after completing escalation (3 decrements using another primitive, or write directly if AAW available).
Key Primitives Used
KsSynchronousIoControlDevicewith KernelMode (same as CVE-2024-35250)- KSEVENTF_KSWORKITEM kernel-mode event path
nt!SeDebugPrivilegeLUID modification (novel technique)SeChangeNotifyPrivilege(universal) as proxy forSeDebugPrivilegecheck
Proof-of-Concept Notes
- Requires WoW64 (32-bit process on 64-bit Windows) to reach ksthunk’s conversion path
- MSKSSRV or any device supporting KS events works
- 3 separate increment operations needed; each requires winning a race
- Presented at HEXACON 2024 by Angelboy (DEVCORE)
Patch Analysis
Patched July 2024. Fix validates the event flags in ksthunk before (or after) the copy to prevent race-substitution, or re-validates flags from the captured buffer rather than user memory.
References
- Angelboy (DEVCORE), “Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part II”, devco.re, 2024-10-05 (HEXACON 2024)
- MSRC: CVE-2024-30090 — msrc.microsoft.com/update-guide/vulnerability/CVE-2024-30090
- See also: Cve 2024 35250 (sibling Proxying to Kernel bug)
- See also: Kernel Streaming, Primitives
