1. Overview

Field Value
Unpatched binary disk_unpatched.sys
Patched binary disk_patched.sys
Overall similarity 0.9074
Matched functions 140
Changed functions 65
Identical functions 69
Unmatched (unpatched → patched) 0
Unmatched (patched → unpatched) 0

Verdict: The patch adds the same missing bounds check in two SCSI mode page 8 (Caching Mode Page) routines that share the exact same parsing prologue. In the read path (DiskGetCacheInformation) it closes a medium-severity out-of-bounds read that can disclose a small amount of adjacent kernel pool memory to user mode or fault the system. In the write path (DiskSetCacheInformation) it closes a high-severity out-of-bounds write of caller-supplied cache fields into non-paged pool past the located mode page descriptor. Both are triggerable via a malformed storage-device MODE SENSE response; the write path additionally requires the caller to issue IOCTL_DISK_SET_CACHE_INFORMATION.


2. Vulnerability Summary

Finding 1 — Out-of-Bounds Read in Mode Page 8 Parser

Attribute Value
Severity Medium
Class CWE-125 — Out-of-bounds read
Function DiskGetCacheInformation (unpatched 0x1C000CBF0, patched 0x1C000DBA4)
Entry point IOCTL_DISK_GET_CACHE_INFORMATION (0x740D4) via DeviceIoControl
Primitive Bounded kernel pool memory disclosure / potential fault (DoS)

Root cause. DiskGetCacheInformation allocates a 0xC0-byte NonPagedPoolNxCacheAligned buffer (tag ScDC), zeroes it with memset, fills the first data_length bytes with ClassModeSense (page code 8, caching), and calls ClassFindModePage to locate the Caching Mode Page descriptor inside the buffer. The search length passed to ClassFindModePage is the device-derived value min(data_length, buffer[0]+1), so the returned descriptor pointer can legitimately land near the end of the transferred data. The unpatched code performs only a NULL check on that pointer and then unconditionally reads bytes at offsets +0x0, +0x2..+0x9, and conditionally +0xA/+0xB (a fixed window spanning 12 bytes, +0x0 through +0xB) to populate a DISK_CACHE_INFORMATION structure. If the device returns a truncated MODE SENSE response where the mode page 8 descriptor sits near the tail of the buffer with fewer than 12 bytes remaining, those reads walk past the valid data. Because the allocation is zero-initialized, reads that stay within the 0xC0-byte block return zeroes; only reads that cross the end of the allocation (descriptor pointer within ~11 bytes of P+0xC0) reach adjacent pool contents. The captured bytes are copied into the IOCTL output buffer and returned to the caller.

Why it matters. The over-read data flows directly back to user mode via the IOCTL output buffer. A controlled or malicious storage device (USB gadget, iSCSI LUN, virtual disk) that positions the mode page 8 descriptor near the tail of a full-length response can disclose up to ~11 bytes of adjacent non-paged pool through the returned cache fields. If the over-read reaches an unmapped page, the read faults and the system bugchecks (DoS).

What the patch does. Inserts a single bounds check immediately after ClassFindModePage returns:

if (mode_page_ptr + 0xC > buffer_start + search_len) {   // search_len = min(data_length, buffer[0]+1)
    status = STATUS_UNSUCCESSFUL (0xC0000001);
    goto error;
}

This guarantees the full 12-byte field window lies within the region that was populated and searched before any field access.

Call chain (attacker → flaw):

  1. User-mode process opens a handle to a disk device (\\.\PhysicalDriveN, volume, or the malicious device itself).
  2. Issues DeviceIoControl(IOCTL_DISK_GET_CACHE_INFORMATION = 0x740D4).
  3. disk!ClassDeviceControl dispatch at DiskDeviceControl (sub_1c0001770) routes to the cache info handler DiskIoctlGetCacheInformation (sub_1c000bd50).
  4. Handler calls DiskGetCacheInformation (sub_1c000cbf0).
  5. Inside DiskGetCacheInformation (sub_1c000cbf0): ExAllocatePoolWithTagmemsetClassModeSenseClassFindModePageOOB reads at offsets +0x0…+0xB → data copied to output struct → returned via IRP completion.

Alternate reach: The same parser is invoked from DiskStartFdo (sub_1c000ca00) and DisableWriteCache (sub_1c000eca0). These are device start/enumeration-time paths, so a malicious device can reach the parser during start without a user-mode IOCTL.

Finding 2 — Hardening: IOCTL Rejection on Uninitialized Device State

Attribute Value
Severity Low (defense-in-depth)
Class State/initialization hardening
Function DiskDeviceControl (unpatched 0x1C0001770, patched 0x1C00010C0)
Entry point Multiple storage IOCTLs

The patch adds an early rejection returning STATUS_NOT_SUPPORTED (0xC00000BB) for IOCTL codes 0x74080, 0x7c088, 0x7c084, 0x4d02c, 0x4d030 when the device sub-object referenced by [dev+0x208] has its state field at +0x1C equal to 0x11 (patched cmp dword ptr [rax+1Ch], 11h at 0x1C000113F → dispatch to the reject block at 0x1C0002E4C; no such gate exists in the unpatched build). This blocks processing of those storage IOCTLs while the device is in that state. Not directly exploitable from the diff alone.

Finding 3 — Out-of-Bounds Write in Mode Page 8 Writer

Attribute Value
Severity High
Class CWE-787 — Out-of-bounds write
Function DiskSetCacheInformation (unpatched 0x1C0010C48, patched 0x1C001315C)
Entry point IOCTL_DISK_SET_CACHE_INFORMATION (0x7C088) via DeviceIoControl
Primitive Bounded non-paged pool out-of-bounds write of caller-influenced bytes

Root cause. DiskSetCacheInformation is the write counterpart of DiskGetCacheInformation and shares the identical parsing prologue: it allocates the same 0xC0-byte NonPagedPoolNxCacheAligned buffer (tag ScDC), memset-zeroes it, calls ClassModeSense (page code 8, caching) to fill it, then calls ClassFindModePage with search length min(data_length, buffer[0]+1) to locate the Caching Mode Page descriptor inside the buffer. Where the read function reads a fixed 12-byte field window (+0x0..+0xB) from the returned descriptor pointer, this function writes that window: it merges the caller-supplied DISK_CACHE_INFORMATION input (arg2) into the descriptor bytes and hands the result to DiskModeSelect to push back to the device. The unpatched code performs only a NULL check on the returned pointer (test rax,rax; jnz at 0x1C0010D84/0x1C0010D87) and then writes offsets +0x0 (and byte ptr [rax],7Fh), +0x2, +0x3, +0x4..+0x9, and conditionally +0xA/+0xB — a 12-byte window mirroring the read function's field layout — with no verification that the descriptor plus 12 bytes stays inside the region that was searched. If the device returns a MODE SENSE response where the mode page 8 descriptor sits near the tail of the transferred data with fewer than 12 bytes remaining, these stores walk past the end of the 0xC0-byte allocation into adjacent non-paged pool.

Why it matters. A kernel-mode out-of-bounds write into non-paged pool is materially more dangerous than the read counterpart: the overwritten bytes can corrupt an adjacent pool allocation's header or an adjacent kernel object, which is a well-understood stepping stone toward elevation of privilege, and at minimum causes pool corruption and a bugcheck (DoS). The offset of the write is device-controlled (the malicious device positions the mode page 8 descriptor near the tail of a full-length response) and the written data is partly caller-controlled — the values come from the DISK_CACHE_INFORMATION structure the IOCTL caller supplies ([rbx+1], [rbx+2], [rbx+4], [rbx+8], [rbx+0Ch]..[rbx+15h]), combined with the existing byte via mask/XOR.

What the patch does. Inserts the identical bounds check the read path received, immediately after the NULL check and before any store:

if (mode_page_ptr + 0xC > buffer_start + search_len) {   // search_len = min(data_length, buffer[0]+1)
    status = STATUS_UNSUCCESSFUL (0xC0000001);
    goto error;
}

This guarantees the full 12-byte write window lies within the populated/searched region before any field is written back.

Call chain (attacker → flaw):

  1. User-mode process opens a handle to a disk device (\\.\PhysicalDriveN, a volume, or the malicious device itself).
  2. Issues DeviceIoControl(IOCTL_DISK_SET_CACHE_INFORMATION = 0x7C088) with a DISK_CACHE_INFORMATION input buffer.
  3. DiskDeviceControl (unpatched 0x1C0001770) routes to the set-cache handler DiskIoctlSetCacheInformation (0x1C000F910).
  4. That handler calls DiskSetCacheInformation (0x1C0010C48).
  5. Inside DiskSetCacheInformation: ExAllocatePoolWithTagmemsetClassModeSenseClassFindModePageOOB writes at offsets +0x0…+0xB into the returned descriptor → DiskModeSelect.

Alternate reach: DiskSetCacheInformation is also called from DiskStartFdo (0x1C000CA00) and DisableWriteCache (0x1C000ECA0), both device start/enumeration-time paths, so a malicious device can reach the writer during start.

Unpatched assembly (annotated) — DiskSetCacheInformation @ 0x1C0010C48:

; --- ClassFindModePage returns descriptor pointer ---
00000001C0010D75  call    cs:__imp_ClassFindModePage  ; CLASSPNP
00000001C0010D81  mov     rsi, rax                    ; rsi = mode_page_ptr
00000001C0010D84  test    rax, rax
00000001C0010D87  jnz     short loc_1C0010D93         ; *** ONLY NULL CHECK ***
00000001C0010D89  mov     ebp, 0C00000BBh             ; NULL -> STATUS_DEVICE_DATA_ERROR
;  *** MISSING: mov ecx,search_len; add rax,0Ch; add rcx,P; cmp rax,rcx; jbe safe ***
; --- writes begin, no bounds check ---
00000001C0010D93  and     byte ptr [rax], 7Fh         ; *** OOB WRITE +0x0 ***
00000001C0010DA4  mov     [rsi+2], cl                 ; *** OOB WRITE +0x2 ***
00000001C0010DE6  mov     [rsi+3], al                 ; *** OOB WRITE +0x3 ***
00000001C0010E0A  mov     [rsi+4], al                 ; *** OOB WRITE +0x4 ***
00000001C0010E10  mov     [rsi+5], al                 ; *** OOB WRITE +0x5 ***
00000001C0010E16  mov     [rsi+6], al                 ; *** OOB WRITE +0x6 ***
00000001C0010E1C  mov     [rsi+7], al                 ; *** OOB WRITE +0x7 ***
00000001C0010E22  mov     [rsi+8], al                 ; *** OOB WRITE +0x8 ***
00000001C0010E28  mov     [rsi+9], al                 ; *** OOB WRITE +0x9 ***
00000001C0010E2B  test    byte ptr [rsi+2], 2
00000001C0010E34  mov     [rsi+0Ah], al               ; *** OOB WRITE +0xA (conditional) ***
00000001C0010E3A  mov     [rsi+0Bh], al               ; *** OOB WRITE +0xB (conditional) ***

Patched equivalent — inserted bounds check at 0x1C00132B1:

00000001C001329E  mov     rdi, rax                    ; rdi = mode_page_ptr
00000001C00132A1  test    rax, rax
00000001C00132A4  jnz     short loc_1C00132B1
00000001C00132A6  mov     r14d, 0C00000BBh            ; NULL -> bail
00000001C00132B1  mov     ecx, r14d                   ; ecx = search_len (min(data_length, buffer[0]+1))
00000001C00132B4  add     rax, 0Ch                    ; rax = mode_page_ptr + 12
00000001C00132B8  add     rcx, rbp                    ; rcx = buffer_start + search_len
00000001C00132BB  cmp     rax, rcx                    ; *** BOUNDS CHECK ***
00000001C00132BE  jbe     short loc_1C0013300         ; ok -> proceed to the writes
00000001C00132F5  mov     r14d, 0C0000001h            ; STATUS_UNSUCCESSFUL
00000001C00132FB  jmp     loc_1C0013485               ; bail before any write

The added block is the entire fix — the same add ptr,0xC; add base,search_len; cmp; jbe guard applied to the write path, turning an unconditional trust of the device-supplied descriptor position into a verified access before the field stores.


3. Pseudocode Diff

DiskGetCacheInformation (sub_1c000cbf0) — Mode Page Cache Parser

// ===== SHARED PROLOGUE (identical) =====
P = ExAllocatePoolWithTag(NonPagedPoolNxCacheAligned, 0xC0, 'ScDC');
memset(P, 0, 0xC0);
data_length = ClassModeSense(device_ext->DeviceObject, P, 0xC0, 8);

if (data_length < 4) { /* retry path, omitted */ }

header_len = (UINT8)P[0] + 1;
search_len = min(data_length, header_len);
mode_page_ptr = ClassFindModePage(P, search_len, 8, 1);

if (mode_page_ptr == NULL) {
    status = STATUS_UNSUCCESSFUL;
    goto error;
}

// ===== UNPATCHED — proceeds directly to field reads =====
out->WriteCacheEnabled     = mode_page_ptr[0] >> 7;            // +0x0
out->WriteRetentionPolicy  = (~mode_page_ptr[2]) & 1;          // +0x2
out->FlushCacheEnabled     = (mode_page_ptr[2] >> 2) & 1;      // +0x2
out->WriteCacheType        = mode_page_ptr[3] >> 4;            // +0x3
out->WriteCachePolicy      = mode_page_ptr[3] & 0xF;           // +0x3
out->CacheSize             = BE16(mode_page_ptr + 4);          // +0x4, +0x5  *** OOB ***
out->PrefetchSize          = BE16(mode_page_ptr + 6);          // +0x6, +0x7  *** OOB ***
out->MaximumCacheSize      = BE16(mode_page_ptr + 8);          // +0x8, +0x9  *** OOB ***
out->DiskCacheSize         = BE16(mode_page_ptr + 0xA);        // +0xA, +0xB  *** OOB ***

// ===== PATCHED — bounds check inserted before any read =====
if ((mode_page_ptr + 0xC) > (P + search_len)) {      // <-- ADDED  (search_len = min(data_length, P[0]+1))
    status = STATUS_UNSUCCESSFUL (0xC0000001);        // <-- ADDED
    goto error;                                        // <-- ADDED
}                                                     // <-- ADDED
// ... then the same field reads as above, now safe

The critical change is the added block at 0x1C000DD1F: load the capped search length, compute mode_page_ptr + 12, compute P + search_len, compare, and bail with STATUS_UNSUCCESSFUL (0xC0000001) if the field window overruns the searched region.


4. Assembly Analysis

DiskGetCacheInformation (sub_1c000cbf0) — Unpatched Assembly (annotated)

; --- Prologue / allocation ---
0x1c000cbf0: mov     [rsp+0x8], rbx
0x1c000cbfa: mov     [rsp+0x18], rsi
0x1c000cc08: sub     rsp, 0x70
0x1c000cc0c: mov     rbx, rdx              ; rbx = output DISK_CACHE_INFORMATION
0x1c000cc0f: mov     r14, rcx              ; r14 = device extension
0x1c000cc12: mov     ebp, 0xc0             ; buffer size
0x1c000cc17: mov     r8d, 0x43446353       ; 'ScDC'
0x1c000cc1f: mov     ecx, 0x204            ; NonPagedPoolNxCacheAligned
0x1c000cc24: call    [ExAllocatePoolWithTag]
0x1c000cc32: mov     rdi, rax              ; rdi = P (buffer base)
0x1c000cc38: je      alloc_fail

; --- memset + ClassModeSense ---
0x1c000cc46: call    memset(P, 0, 0xc0)
0x1c000cc58: call    ClassModeSense(dev, P, 0xc0, 8)
0x1c000cc64: mov     ecx, eax              ; ecx = transferred data length
0x1c000cc66: cmp     eax, 4
0x1c000cc69: jb      retry_path            ; need >= 4 bytes

; --- ClassFindModePage call ---
0x1c000cc6f: movzx   edx, byte [rdi]       ; mode parameter header byte 0
0x1c000cc75: inc     edx                   ; header_len + 1
0x1c000cc7c: cmovbe  edx, ecx             ; min(data_length, header_len+1)
0x1c000cc82: call    [ClassFindModePage]   ; returns mode page 8 ptr in rax
0x1c000cc8e: mov     r9, rax               ; r9 = mode_page_ptr  <-- ATTACKER INFLUENCED

; --- VULNERABILITY: only NULL check, no size check ---
0x1c000cc91: test    rax, rax
0x1c000cc94: je      page_not_found
;  *** MISSING: add rax,0xc; add rcx,rdi; cmp rax,rcx; jbe safe ***
;  *** Patched binary inserts the above 4 instructions right here   ***

; --- Begin reading 12 bytes from r9 (potential OOB) ---
0x1c000cc9a: xor     eax, eax
0x1c000cca2: mov     [rbx], rax            ; zero output[0..7]
0x1c000cca5: mov     [rbx+0x8], rax
0x1c000cca9: mov     [rbx+0x10], rax
0x1c000ccad: mov     al, byte [r9]         ; *** OOB READ +0x0 ***
0x1c000ccb3: mov     [rbx], al             ; out->WriteCacheEnabled
0x1c000ccb5: mov     r11b, byte [r9+0x2]   ; *** OOB READ +0x2 ***
0x1c000ccc0: mov     [rbx+0x1], r11b
0x1c000ccc4: mov     bpl, byte [r9+0x2]    ; *** OOB READ +0x2 ***
0x1c000ccd0: mov     [rbx+0x2], bpl
0x1c000ccd4: mov     cl, byte [r9+0x3]     ; *** OOB READ +0x3 ***
0x1c000ccf6: mov     [rbx+0x4], r10d
0x1c000cd1f: movzx   r15d, byte [r9+0x4]   ; *** OOB READ +0x4 ***
0x1c000cd2b: movzx   eax, byte [r9+0x5]    ; *** OOB READ +0x5 ***
0x1c000cd37: mov     [rbx+0xc], r15w       ; out->CacheSize (BE16)
0x1c000cd3c: movzx   eax, byte [r9+0x7]    ; *** OOB READ +0x7 ***
0x1c000cd41: movzx   r12d, byte [r9+0x6]   ; *** OOB READ +0x6 ***
0x1c000cd51: mov     [rbx+0x10], r12w
0x1c000cd56: movzx   r13d, byte [r9+0x8]   ; *** OOB READ +0x8 ***
0x1c000cd5b: movzx   eax, byte [r9+0x9]    ; *** OOB READ +0x9 ***
0x1c000cd68: mov     [rbx+0x12], r13w

; --- Conditional tail reads (+0xA, +0xB) ---
0x1c000cd6d: test    byte [r9+0x2], 0x2    ; *** OOB READ +0x2 (bit test) ***
0x1c000cd72: jne     0x1c000e2f7
...
0x1c000e2fe: movzx   eax, byte [r9+0xa]    ; *** OOB READ +0xA ***
0x1c000e305: movzx   eax, byte [r9+0xb]    ; *** OOB READ +0xB *** (12th byte)
0x1c000e310: mov     [rbx+0x14], dx        ; out->DiskCacheSize

; --- Cleanup ---
0x1c000cd87: mov     rcx, rdi
0x1c000cd8a: call    [ExFreePoolWithTag]
0x1c000cd96: mov     eax, esi              ; return status

Patched Equivalent — Inserted Bounds Check

; After ClassFindModePage returns mode_page_ptr in rax -> r8
mov     r8, rax
test    rax, rax
jne     0x1c000dd1f
...
mov     ecx, ebp              ; ecx = search_len = min(data_length, P[0]+1)
add     rax, 0xc              ; rax = mode_page_ptr + 12
add     rcx, rdi              ; rcx = P + search_len (searched region end)
cmp     rax, rcx              ; *** BOUNDS CHECK ***
jbe     0x1c000dd6c           ; ok, proceed to parse
; fall through -> rsi = 0xC0000001, goto error

The added block is the entire fix — it turns an unconditional trust of the device-supplied descriptor position into a verified access.


5. Trigger Conditions

  1. Obtain device reachability. Open a handle with CreateFileW(L"\\\\.\\PhysicalDriveN", GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL), or trigger PnP enumeration of the malicious device (which fires the bug automatically via the AddDevice work-item path).

  2. IOCTL code. IOCTL_DISK_GET_CACHE_INFORMATION = 0x740D4 (METHOD_BUFFERED, FILE_ANY_ACCESS). Output buffer is a 24-byte DISK_CACHE_INFORMATION.

  3. Device-side payload. The attached storage device must answer SCSI MODE SENSE(10) for page code 0x08 (caching) with a response that:

  4. Has total transferred length ≥ 4 bytes (passes the minimum check at 0x1c000cc69).
  5. Contains a valid mode page 8 header byte (page code = 0x08) so ClassFindModePage returns a non-NULL pointer.
  6. Truncates the page so that mode_page_ptr + 12 > P + data_length. Example: total response = 16 bytes, mode page 8 header at offset 12 → only 4 bytes of page data, but parser reads 12 → 8 bytes OOB.

  7. No ordering or race constraints. The read is synchronous within the IRP handling path. The AddDevice work-item path requires only that PnP enumerate the device.

  8. Observable confirmation.

  9. Without Driver Verifier: OOB bytes silently copied into the IOCTL output buffer; compare returned DISK_CACHE_INFORMATION fields (especially CacheSize, PrefetchSize, MaximumCacheSize, DiskCacheSize) against the device's true parameters — leaked pool residue will appear as anomalous 16-bit values.
  10. With Special Pool enabled on disk.sys: immediate BSOD — PAGE_FAULT_IN_NONPAGED_AREA or BUGCODE_INVALID_DRIVER_ACCESS — faulting address just past the ScDC allocation.
  11. If the read crosses a real page boundary (no Special Pool): bugcheck PAGE_FAULT_IN_NONPAGED_AREA.

6. Exploit Primitive

Primitive. A bounded out-of-bounds read of NonPagedPoolNxCacheAligned memory into a user-visible IOCTL output buffer. The read window is a fixed 12 bytes (+0x0..+0xB) from the descriptor pointer. Because the 0xC0-byte buffer is memset-zeroed before ClassModeSense runs, reads that stay inside the allocation return zeroes; only the portion of the window that extends past the end of the 0xC0-byte allocation reads adjacent pool contents. That out-of-allocation portion is at most ~11 bytes and surfaces in the CacheSize / PrefetchSize / MaximumCacheSize / DiskCacheSize fields of the returned DISK_CACHE_INFORMATION.

Reachability constraints. Triggering the out-of-allocation read requires a storage device whose SCSI responses the attacker controls (physical USB device, iSCSI LUN, or virtual/emulated disk). A normal disk does not return a truncated mode page 8 descriptor positioned at the tail of a full-length response, so the bug is not reachable against unmodified hardware. The disclosed bytes are whatever currently occupies the pool immediately after the ScDC allocation; their content is not controlled by the attacker beyond triggering the read.

Impact bound. Two outcomes are demonstrable from the binaries: (1) disclosure of a small, bounded amount of adjacent non-paged pool into the IOCTL output, and (2) a denial of service if the over-read reaches an unmapped address. This report does not claim a full exploit chain; any escalation beyond these two outcomes is not supported by the diff.


7. Debugger PoC Playbook

Assume WinDbg/KD attached to the unpatched disk.sys. Use lm disk to confirm the loaded base and add the offsets below.

Finding 1 — OOB Read in DiskGetCacheInformation (sub_1c000cbf0)

Breakpoints

bp disk!DiskGetCacheInformation (sub_1c000cbf0)
bp disk+0xcbf0              ; same, if symbols are stripped — substitute real base
bp disk+0xcc8e              ; after ClassFindModePage returns — THE critical point
bp disk+0xccad              ; first OOB read instruction
bp disk+0xcc82              ; ClassFindModePage call site
  • disk+0xcbf0: function entry — confirm rcx = device extension, rdx = output DISK_CACHE_INFORMATION.
  • disk+0xcc8e: mov r9, rax — r9 now holds the attacker-influenced mode page pointer. This is where the patch inserts the missing check. Manually evaluate r9+0xC vs rdi+ecx to predict OOB.
  • disk+0xccad: first byte read from [r9]. If OOB, single-step and watch reads continue to +0xb.
  • disk+0xcc82: pre-call to ClassFindModePage. Inspect rcx (buffer P), edx (search length), r8b (page code 8), r9b (subpage 1).

What to inspect at each breakpoint

Stop Register / expression Why
+0xcbf0 entry rcx, rdx device extension / output struct
+0xcc24 pool tag, rax after call confirm ScDC, 0xC0-byte allocation
+0xcc64 eax / ecx data_length returned by ClassModeSense
+0xcc8e r9, rdi, ecx compute r9+0xC vs rdi+ecx — if greater, OOB
+0xcc8e !pool r9 f visualize the ScDC allocation boundary
+0xccad onward db r9 l0xc dump the 12 bytes being read; bytes past rdi+ecx-1 are OOB
+0xcd96 (return) dt _DISK_CACHE_INFORMATION @rbx parsed output — compare to known device params

Key offsets

  • 0x1c000cc82call ClassFindModePage (returns mode page ptr in rax).
  • 0x1c000cc8emov r9, rax (pointer saved).
  • 0x1c000cc91–0x1c000cc94test rax,rax; jeonly a NULL check; this is the gap the patch fills.
  • 0x1c000cc9a — parsing begins (no size verification).
  • 0x1c000ccad — first OOB-prone read ([r9]).
  • 0x1c000e305 — last OOB-prone read ([r9+0xb], the 12th byte).
  • Patched equivalent around 0x1c000dd00–0x1c000dd20: add rax,0xc; add rcx,rdi; cmp rax,rcx; jbe.

Trigger setup

Method A — direct IOCTL from a privileged/admin user-mode process (any user with access to the disk handle):

HANDLE h = CreateFileW(L"\\\\.\\PhysicalDrive0",
    GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE,
    NULL, OPEN_EXISTING, 0, NULL);
DISK_CACHE_INFORMATION out;
DWORD br;
DeviceIoControl(h, 0x740D4, NULL, 0, &out, sizeof(out), &br, NULL);

For an unmodified real disk this returns normally; to fire the bug the target device must return a truncated mode page 8 response.

Method B — programmable USB / virtual SCSI device. Configure the device emulator so its MODE SENSE(10) response for page code 8 has total length, say, 16 bytes with the mode page 8 header at offset 12 (leaving 4 bytes of page data). Plugging the device triggers AddDevice → IoQueueWorkItem → DisableWriteCache (sub_1c000eca0) → DiskGetCacheInformation (sub_1c000cbf0) automatically.

Method C — crafted VHD / virtual disk mount with the same SCSI emulation. Open the resulting volume and issue the IOCTL.

Expected observation

  • At disk+0xcc8e, evaluate ?? r9+0xc > rdi+ecx — if true, the next instructions read OOB.
  • !pool r9 f shows the ScDC pool block; bytes past its data region are leaked.
  • Under Driver Verifier with Special Pool on disk.sys: immediate BSOD — typically PAGE_FAULT_IN_NONPAGED_AREA, faulting IP disk+0xccad (or any of the read sites), faulting address slightly beyond the ScDC allocation.
  • Without verifier: dt _DISK_CACHE_INFORMATION @rbx at function return shows leaked 16-bit values in CacheSize / PrefetchSize / MaximumCacheSize / DiskCacheSize fields.

Struct / offset notes

typedef struct _DISK_CACHE_INFORMATION {   // sizeof = 0x18 (24 bytes)
    BOOLEAN WriteCacheEnabled;             // +0x0
    BOOLEAN WriteRetentionPolicy;          // +0x1
    BOOLEAN FlushCacheEnabled;             // +0x2
    BOOLEAN Pad0;                          // +0x3 (padding)
    ULONG   WriteCacheType;                // +0x4
    ULONG   WriteCachePolicy;              // +0x8
    USHORT  CacheSize;                     // +0xc
    USHORT  Pad1;                          // +0xe
    USHORT  PrefetchSize;                  // +0x10
    USHORT  MaximumCacheSize;              // +0x12
    USHORT  DiskCacheSize;                 // +0x14
} DISK_CACHE_INFORMATION;

Buffer allocation: ExAllocatePoolWithTag(NonPagedPoolNxCacheAligned (0x204), 0xC0, 'ScDC'). Note that ASLR slides the disk module base — rebase every offset using lm disk.


8. Changed Functions — Full Triage

Security-relevant

Function Similarity Change Note
DiskGetCacheInformation (unpatched 0x1C000CBF0, patched 0x1C000DBA4) 0.9151 Security Mode page 8 parser — adds the bounds check fixing the OOB read. Primary finding.
DiskSetCacheInformation (unpatched 0x1C0010C48, patched 0x1C001315C) n/a Security Mode page 8 writer — adds the identical bounds check fixing an OOB write (CWE-787) of caller-supplied cache fields. See Finding 3.
DiskDeviceControl (unpatched 0x1C0001770, patched 0x1C00010C0) 0.88 Hardening Early STATUS_NOT_SUPPORTED (0xC00000BB) for storage IOCTLs 0x74080/0x7c088/0x7c084/0x4d02c/0x4d030 when the device sub-object state field ([dev+0x208]+0x1C) equals 0x11. Defense-in-depth against IOCTL processing on a device in that state.

Behavioral (non-exploitable from diff)

Function Similarity Change Note
DiskInitFdo (sub_1c000be40) 0.82 Behavioral Failure-prediction init moved from inline synchronous to IoAllocateWorkItem + IoQueueWorkItem (async). PnP reliability improvement.

Cosmetic / register-allocation (collapsed)

The following functions changed only due to compiler recompilation, CRT optimization, or register reassignment. No new vulnerability and no exploitable behavioral change:

  • RtlStringCchPrintfW (sub_1c0002464) — CRT refactoring, sim 0.28.
  • memmove (sub_1c0002840) — SIMD strategy change, sim 0.31.
  • memset (sub_1c0002b80) — SIMD strategy change, sim 0.45.
  • DiskFdoProcessError (sub_1c0001e50) — bounds checks refactored, sim 0.89.
  • DriverEntry (sub_1c0014078) — function pointer table updated by recompile, sim 0.87.
  • DiskIoctlGetCacheInformation (sub_1c000bd50)CR8 → KeGetCurrentIrql swap; calls vulnerable DiskGetCacheInformation (sub_1c000cbf0), sim 0.86.
  • DiskStartFdo (sub_1c000ca00) — cosmetic; calls vulnerable DiskGetCacheInformation (sub_1c000cbf0), sim 0.85.
  • DisableWriteCache (sub_1c000eca0) — cosmetic; calls vulnerable DiskGetCacheInformation (sub_1c000cbf0), sim 0.83.
  • DiskIoctlGetLengthInfo (sub_1c000b220)CR8 → KeGetCurrentIrql, sim 0.86.
  • DiskIoctlSmartReceiveDriveData (sub_1c000faf4) — register reallocation, sim 0.87.
  • DiskPerformSmartCommand (sub_1c000b4a4) — register reallocation, sim 0.86.
  • DiskEnableDisableFailurePredictPolling (sub_1c000d5e4) — register reallocation, sim 0.85.
  • DiskCreateSymbolicLinks (sub_1c000c51c) — register reallocation, sim 0.84.
  • DiskCreateGuidSymbolicLink (sub_1c000ce08) — register reallocation, sim 0.84.
  • DiskIoctlSmartSendDriveCommand (sub_1c000ff0c) — register reallocation, sim 0.85.
  • McGenControlCallbackV2 (sub_1c00045f0) — register reallocation, sim 0.86.
  • GetSrbScsiData (sub_1c0002050) — bounds-check refactor, no new vuln, sim 0.87.

Note: three cosmetic callers (DiskIoctlGetCacheInformation (sub_1c000bd50), DiskStartFdo (sub_1c000ca00), DisableWriteCache (sub_1c000eca0)) reach the vulnerable DiskGetCacheInformation (sub_1c000cbf0). Although their changes are non-functional, they expand the attacker's reachability surface — keep them in mind when tracing the call graph.


9. Unmatched Functions

None added or removed. The patch is purely a modification of existing functions, with no new sanitizers inserted as standalone routines and no mitigations removed.


10. Confidence & Caveats

Confidence: High. The diff shows an unambiguous bounds-check insertion immediately after a ClassFindModePage call, with the unpatched side performing only a NULL check before reading a fixed 12-byte window. The data flow (device response → ClassModeSenseClassFindModePage → field reads → IOCTL output buffer) is consistent across pseudocode, assembly, and the call chain. The pool tag, allocation size, and IOCTL code all corroborate.

Assumptions made: - The call chain (DiskDeviceControl (sub_1c0001770)DiskIoctlGetCacheInformation (sub_1c000bd50)DiskGetCacheInformation (sub_1c000cbf0)) is reconstructed from naming and dispatch-table analysis; cross-reference confirmation with independent static analysis is recommended. - ClassFindModePage is assumed to return a pointer derived purely from device-controlled length fields (standard Storport behavior). A defensive ClassFindModePage implementation that already validates the descriptor length would partially mitigate, but the patch's existence strongly implies the unpatched version relied on the caller for bounds enforcement. - The AddDevice work-item path (DisableWriteCache (sub_1c000eca0)DiskGetCacheInformation (sub_1c000cbf0)) was inferred from similarity and the work-item refactor in DiskInitFdo (sub_1c000be40); confirm with !wdfkd.wdfdriver or !devnode traces.

Verify before writing the PoC: 1. Confirm lm disk rebased offsets match the unpatched target. 2. Confirm ClassFindModePage's exact return semantics on the target Windows build (does it ever return a pointer past P + data_length by itself?). 3. Build or emulate a USB mass-storage / iSCSI LUN that returns the truncated MODE SENSE response and verify the OOB pattern in the debugger before relying on it. 4. If exploiting for info leak rather than DoS, verify pool-residue availability by inspecting !poolused and !pool near the ScDC allocation under the target's actual pool configuration.