disk.sys — Out-of-bounds read (CWE-125) and out-of-bounds write (CWE-787) in SCSI mode page parsing fixed
KB5094128
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):
- User-mode process opens a handle to a disk device (
\\.\PhysicalDriveN, volume, or the malicious device itself). - Issues
DeviceIoControl(IOCTL_DISK_GET_CACHE_INFORMATION = 0x740D4). disk!ClassDeviceControldispatch atDiskDeviceControl (sub_1c0001770)routes to the cache info handlerDiskIoctlGetCacheInformation (sub_1c000bd50).- Handler calls
DiskGetCacheInformation (sub_1c000cbf0). - Inside
DiskGetCacheInformation (sub_1c000cbf0):ExAllocatePoolWithTag→memset→ClassModeSense→ClassFindModePage→ OOB 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):
- User-mode process opens a handle to a disk device (
\\.\PhysicalDriveN, a volume, or the malicious device itself). - Issues
DeviceIoControl(IOCTL_DISK_SET_CACHE_INFORMATION = 0x7C088)with aDISK_CACHE_INFORMATIONinput buffer. DiskDeviceControl(unpatched0x1C0001770) routes to the set-cache handlerDiskIoctlSetCacheInformation (0x1C000F910).- That handler calls
DiskSetCacheInformation (0x1C0010C48). - Inside
DiskSetCacheInformation:ExAllocatePoolWithTag→memset→ClassModeSense→ClassFindModePage→ OOB 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
-
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 theAddDevicework-item path). -
IOCTL code.
IOCTL_DISK_GET_CACHE_INFORMATION = 0x740D4(METHOD_BUFFERED, FILE_ANY_ACCESS). Output buffer is a 24-byteDISK_CACHE_INFORMATION. -
Device-side payload. The attached storage device must answer SCSI
MODE SENSE(10)for page code 0x08 (caching) with a response that: - Has total transferred length ≥ 4 bytes (passes the minimum check at
0x1c000cc69). - Contains a valid mode page 8 header byte (
page code = 0x08) soClassFindModePagereturns a non-NULL pointer. -
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. -
No ordering or race constraints. The read is synchronous within the IRP handling path. The
AddDevicework-item path requires only that PnP enumerate the device. -
Observable confirmation.
- Without Driver Verifier: OOB bytes silently copied into the IOCTL output buffer; compare returned
DISK_CACHE_INFORMATIONfields (especiallyCacheSize,PrefetchSize,MaximumCacheSize,DiskCacheSize) against the device's true parameters — leaked pool residue will appear as anomalous 16-bit values. - With Special Pool enabled on
disk.sys: immediate BSOD —PAGE_FAULT_IN_NONPAGED_AREAorBUGCODE_INVALID_DRIVER_ACCESS— faulting address just past theScDCallocation. - 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 — confirmrcx= device extension,rdx= outputDISK_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 evaluater9+0xCvsrdi+ecxto predict OOB.disk+0xccad: first byte read from[r9]. If OOB, single-step and watch reads continue to+0xb.disk+0xcc82: pre-call toClassFindModePage. Inspectrcx(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
0x1c000cc82—call ClassFindModePage(returns mode page ptr inrax).0x1c000cc8e—mov r9, rax(pointer saved).0x1c000cc91–0x1c000cc94—test rax,rax; je— only 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 fshows theScDCpool block; bytes past its data region are leaked.- Under Driver Verifier with Special Pool on
disk.sys: immediate BSOD — typicallyPAGE_FAULT_IN_NONPAGED_AREA, faulting IPdisk+0xccad(or any of the read sites), faulting address slightly beyond theScDCallocation. - Without verifier:
dt _DISK_CACHE_INFORMATION @rbxat function return shows leaked 16-bit values inCacheSize/PrefetchSize/MaximumCacheSize/DiskCacheSizefields.
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 → KeGetCurrentIrqlswap; calls vulnerableDiskGetCacheInformation (sub_1c000cbf0), sim 0.86.DiskStartFdo (sub_1c000ca00)— cosmetic; calls vulnerableDiskGetCacheInformation (sub_1c000cbf0), sim 0.85.DisableWriteCache (sub_1c000eca0)— cosmetic; calls vulnerableDiskGetCacheInformation (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 vulnerableDiskGetCacheInformation (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 → ClassModeSense → ClassFindModePage → 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.