hvcrash.sys — Uninitialized stack bytes in crash-dump writer descriptor (CWE-908) zeroed / fixed
KB5078752
1. Overview
- Unpatched Binary:
hvcrash_unpatched.sys - Patched Binary:
hvcrash_patched.sys - Overall Similarity: 0.983
- Diff Statistics: 50 functions matched by symbol (46 identical, 4 changed), 1 function added in the patched build (
Diagnostics::Initialize), 0 removed. - Verdict: The patch zero-initializes an 8-byte tail of a 16-byte stack structure in
HvCrashWriter::Initializebefore that structure is handed to a WDF I/O target request. In the unpatched build those 8 bytes carry stale kernel-stack contents into the input buffer of an internal device I/O control request sent down the driver stack. This is a genuine uninitialized-memory hardening fix, but it is confined to the crash-dump writer setup path and the data stays inside kernel mode; there is no demonstrable attacker-controlled trigger or cross-privilege leak. Severity is Low. The other three changed functions and the one added function are a debug-logging feature (a registry-gatedDbgPrint), not security fixes.
2. Vulnerability Summary
- Severity: Low
- Vulnerability Class: Use of uninitialized stack memory (CWE-908 / CWE-200)
- Affected Function:
HvCrashWriter::Initialize(unpatched@0x1C0002E68; patched@0x1C0002FE0)
Root Cause:
HvCrashWriter::Initialize builds a 16-byte structure on the kernel stack (_DWORD v14[4] at rbp+0x37). In the unpatched build it writes only the first two DWORDs (v14[0]=6 at rbp+0x37 and v14[1]=0x12 at rbp+0x3b) and leaves the last 8 bytes (v14[2]/v14[3], at rbp+0x3f through rbp+0x46) uninitialized, so they retain whatever was previously on the stack. The structure is wrapped in a WDF_MEMORY_DESCRIPTOR (type = buffer, pointer = v14, length = 16) and passed as the input buffer of a synchronous internal device I/O control request (control code 0x3EC080) issued through the KMDF function table. Those 8 stale bytes therefore travel into the request sent to the next driver in the device stack. The patch zero-initializes the whole 16-byte structure first, then fills the two DWORD fields, so the trailing 8 bytes are defined (zero).
Reachability:
This runs during crash-dump writer initialization, reached through the driver's WDF device-control (EvtIoDeviceControl) handler. It is part of the crash/dump-preparation machinery, not a routine exposed to an ordinary untrusted caller. The recipient of the leaked bytes is a lower kernel-mode driver in the device stack (all ring 0). There is no demonstrated path by which an unprivileged user, or a party outside the guest, controls this trigger or observes the leaked bytes.
Call chain:
1. A device I/O control request with control code 0x2D164B reaches the driver's WDF I/O queue.
2. HvCrashEvtDeviceAdd (@0x1C00080E0) registered HvCrashDeviceControl (@0x1C00088D0) as the EvtIoDeviceControl callback.
3. HvCrashDeviceControl switches on the control code; case 0x2D164B (decimal 2954827) calls CrashdumpShim::GetDumpInfo.
4. CrashdumpShim::GetDumpInfo (@0x1C00019E8) retrieves a 0x48-byte request input buffer, validates its fields, allocates pool, and calls HvCrashWriter::Initialize.
5. HvCrashWriter::Initialize (@0x1C0002E68) builds the partially initialized 16-byte structure and passes it as the input buffer of the internal IOCTL sent down the device stack.
3. Pseudocode Diff
The change is in the stack-buffer initialization of HvCrashWriter::Initialize.
// UNPATCHED HvCrashWriter::Initialize @0x1C0002E68
__int64 HvCrashWriter::Initialize(HvCrashWriter *this, WDFDEVICE__ *a2, const _GUID *a3) {
_DWORD v14[4]; // 16-byte struct at rbp+0x37
v14[0] = 6; // rbp+0x37 (4 bytes)
v14[1] = 18; // rbp+0x3b (4 bytes) (0x12)
// v14[2], v14[3] (rbp+0x3f..rbp+0x46, 8 bytes) LEFT UNINITIALIZED
v8[0] = 1; // WDF_MEMORY_DESCRIPTOR: type = buffer
v8[1] = v14; // buffer pointer
v8[2] = 16; // length = 16 bytes (all 16 are consumed)
...
// WdfFunctions_01015 + 1488 (table offset 0x5d0): synchronous internal
// device IOCTL 0x3EC080; v8 is the INPUT memory descriptor
v5 = (*(...)(WdfFunctions_01015 + 1488))(WdfDriverGlobals, v3, 0, 4112512, v8, v7, 0, 0);
}
// PATCHED HvCrashWriter::Initialize @0x1C0002FE0
__int64 HvCrashWriter::Initialize(HvCrashWriter *this, WDFDEVICE__ *a2, const _GUID *a3) {
_DWORD v14[4];
// FIX: whole 16-byte struct zeroed first
*(_QWORD*)&v14[0] = 0; // zeros rbp+0x37..rbp+0x3e
*(_QWORD*)&v14[2] = 0; // zeros rbp+0x3f..rbp+0x46 (the fix)
v14[0] = 6; // then fields are set
v14[1] = 18;
...
v5 = (*(...)(WdfFunctions_01015 + 1488))(WdfDriverGlobals, v3, 0, 4112512, v8, v7, 0, 0);
}
4. Assembly Analysis
Frame base: after push rbp, lea rbp, [rsp-57h], so a variable shown as [rbp+57h+var_XX] is at rbp + 0x57 - 0xXX. The 16-byte structure is var_20 (= rbp+0x37); its uninitialized tail is var_18 (= rbp+0x3f).
Unpatched initialization sequence (HvCrashWriter::Initialize @0x1C0002E68):
00000001C0002E88 xor eax, eax
00000001C0002E8A mov [rbp+57h+var_20], 6 ; dword [rbp+0x37] = 6
00000001C0002E91 mov [rbp+57h+var_58], rax ; [rbp-0x1] = 0
00000001C0002E95 mov [rbp+57h+var_48], rax ; [rbp+0xf] = 0
00000001C0002E99 mov [rbp+57h+var_1C], 12h ; dword [rbp+0x3b] = 0x12
00000001C0002EA0 lea r8d, [rax+1]
00000001C0002EA4 mov dword ptr [rbp+57h+var_48], 10h ; length = 16
00000001C0002EAB lea rax, [rbp+57h+var_20] ; &struct (rbp+0x37)
00000001C0002EB3 mov [rbp+57h+var_50], rax ; descriptor buffer ptr at rbp+0x7
; bytes rbp+0x3f..rbp+0x46 (var_18) are NEVER written
Patched initialization sequence (HvCrashWriter::Initialize @0x1C0002FE0):
00000001C0003000 xor eax, eax
00000001C0003002 mov [rbp+57h+var_20], rax ; NEW: qword [rbp+0x37] = 0
00000001C0003006 mov [rbp+57h+var_58], rax
00000001C000300A mov [rbp+57h+var_48], rax
00000001C000300E lea r8d, [rax+1]
00000001C0003012 mov [rbp+57h+var_18], rax ; FIX: qword [rbp+0x3f] = 0
00000001C0003016 lea rax, [rbp+57h+var_20]
00000001C000301A mov dword ptr [rbp+57h+var_20], 6 ; then dword [rbp+0x37] = 6
00000001C0003048 mov dword ptr [rbp+57h+var_20+4], 12h ; dword [rbp+0x3b] = 0x12
00000001C0003053 mov dword ptr [rbp+57h+var_48], 10h ; length = 16
The net effect: the patched build additionally writes qword [rbp+0x3f] = 0 (var_18), defining the 8 bytes the unpatched build left stale.
The request that carries the buffer:
00000001C0002EF2 mov rcx, cs:WdfFunctions_01015
00000001C0002EF9 mov r9d, 3EC080h ; internal IOCTL control code
00000001C0002F11 mov r10, [rcx+5D0h] ; KMDF table entry (offset 0x5d0)
00000001C0002F24 lea rcx, [rbp+57h+var_58] ; WDF_MEMORY_DESCRIPTOR (input) -> struct
00000001C0002F34 call cs:__guard_dispatch_icall_fptr ; request issued
The indirect target is an entry in WdfFunctions_01015, the KMDF (Kernel-Mode Driver Framework) function dispatch table, at table offset 0x5d0; the preceding call at table offset 0x150 obtains the I/O target handle. These are WDF framework calls, not an inter-partition/VMBus interface.
5. Trigger Conditions
- Entry: a WDF device I/O control request with control code
0x2D164Breaches the driver's I/O queue, dispatched byHvCrashDeviceControl. - Payload constraints (validated in
CrashdumpShim::GetDumpInfo): the retrieved request input buffer is0x48bytes, with: - DWORD at offset
0x0equal to0x48; - DWORD at offset
0x4equal to0x1; - DWORD at offset
0x8non-zero and< 0x10, and not equal to0xf(0xfreturns early without reaching the writer). - Execution: on a passing payload the driver allocates the writer object and calls
HvCrashWriter::Initialize, which builds the descriptor and issues the internal IOCTL. - Effect (unpatched): the input buffer of that internal IOCTL contains a 16-byte structure whose last 8 bytes hold stale kernel-stack data rather than defined values.
6. Impact & Limitations
- Primitive: 8 bytes of uninitialized kernel-stack memory placed into the input buffer of an internal device I/O control request that is sent to the next driver in the device stack.
- Boundary: the source and the recipient are both kernel-mode (ring 0). There is no verified path making the trigger reachable from an unprivileged user process, and no verified path by which the 8 bytes are returned to, or observed by, a lower-privileged or external party. The value of the 8 bytes is whatever the prior stack frames left; no specific sensitive content is demonstrable.
- Nature: defense-in-depth hardening against use of uninitialized memory (CWE-908) on the crash-dump writer setup path. This is why the severity is Low rather than a directly exploitable information-disclosure primitive.
7. Debugger Notes
Attach a kernel debugger to a system running the unpatched hvcrash.sys.
Breakpoints (RVAs; add the actual load base):
; vulnerable function entry
bp hvcrash+0x2E68
; the request-issuing call, just before the buffer leaves the function
bp hvcrash+0x2F34
What to inspect at +0x2F34:
- The descriptor buffer pointer is stored at [rbp+0x7] and points to the 16-byte structure at rbp+0x37.
- dq poi(rbp+7) L2 — the first QWORD is 0x0000001200000006; the second QWORD is the trailing 8 bytes. In the unpatched build it may be non-zero (stale stack); in the patched build it is zero.
8. Changed Functions — Full Triage
HvCrashWriter::Initialize(unpatched@0x1C0002E68, patched@0x1C0002FE0; Security Relevant):- Change: adds
mov qword [rbp+0x37], 0andmov qword [rbp+0x3f], 0(fromxor eax,eax) to zero the full 16-byte stack structure before setting the two DWORD fields. Defines the previously uninitialized 8-byte tail. This is the fix. CrashdumpShim::GetDumpInfo(unpatched@0x1C00019E8, patched@0x1C0001AF8; Behavioral, low security relevance):- Change: caller of the fixed function. The substantive change is that the patched build adds three
WdfRequestCompletecalls (KMDF table offset0x838): on the0x148pool-allocation failure, theHvCrashWriter::Initializefailure, and theRtlGetVersionfailure paths. In the unpatched build these three paths return without completing the WDF request, so the request is left pending (caller hang / resource leak) when one of those failures occurs. The unpatched build already completes the request on the earlier input-validation and0xC0-allocation failures, and on the success path. The remaining differences are compiler register-allocation, global/string reference offsets shifted by the newly added globals, and updated debug line-number constants. The request-buffer validation logic is identical between builds. This is a robustness fix on abnormal (allocation / version-query) failure paths, not a directly attacker-controlled primitive. Diagnostics::NtStatus_ResultErrorCallback(@0x1C00010BC; Behavioral, not security):- Change: the error-reporting helper. Its
DbgPrintcall is now guarded by a new global flagDiagnostics::g_DbgPrintOnError(set from aDebugFlagsregistry value); in the unpatched build it was unconditional. Blocks reordered and the stack-cookie check target relocated. A debug-logging feature, not a security fix. DriverEntry(@0x1C000A008; Behavioral, not security):- Change: adds a call to the new
Diagnostics::Initialize(which reads theDebugFlagsregistry value and populates the debug-logging globals). Stack layout and a debug line-number constant shift. A debug feature, not a security fix.
9. Unmatched Functions
- Added (patched only):
Diagnostics::Initialize(@0x1C00011F8) — reads theDebugFlagsregistry value and sets the debug-logging globals used byDiagnostics::NtStatus_ResultErrorCallback. Not security relevant. - Removed: none.
10. Confidence & Caveats
- Confidence: High that the mechanism is a real uninitialized-memory fix: both builds and both the decompilation and disassembly show the same 16-byte structure, the missing 8-byte initialization in the unpatched build, and the two added zeroing writes in the patched build.
- Severity rationale: Low. The routine is on the crash-dump writer setup path, the trigger is a device-control request handled by the crash-dump shim rather than an ordinary untrusted surface, and the leaked bytes move between kernel-mode components. No cross-privilege or guest-boundary disclosure is demonstrable from the binaries.
- Not claimed: there is no evidence in the binaries that the trigger is a VMBus message, that the data is transmitted to the Hyper-V host, or that specific sensitive values (pointers, cookies) are disclosed. Such claims are not supported and are not made here.