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 — KsQueueWorkItem is 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:

  • NtOpenProcess calls SeSinglePrivilegeCheck(SeDebugPrivilege, ...)
  • This reads nt!SeDebugPrivilege to get the LUID value (0x14)
  • Checks if the current token has bit 0x14 set in Privileges.Enable/Present
  • After modification, reads LUID = 0x17 → checks bit 0x17 instead
  • SeChangeNotifyPrivilege (LUID 0x17) 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 value 0 — starts at 0x14
  • KsQueueWorkItem zero-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!SeDebugPrivilege0x17:

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

  • KsSynchronousIoControlDevice with KernelMode (same as CVE-2024-35250)
  • KSEVENTF_KSWORKITEM kernel-mode event path
  • nt!SeDebugPrivilege LUID modification (novel technique)
  • SeChangeNotifyPrivilege (universal) as proxy for SeDebugPrivilege check

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