1. Overview

  • Unpatched Binary: fcvsc_unpatched.sys
  • Patched Binary: fcvsc_patched.sys
  • Overall Similarity Score: 0.9622
  • Diff Statistics: 73 matched functions (61 identical, 12 changed), 0 unmatched functions in either direction.
  • Verdict: The patch fixes a genuine critical flaw in the Hyper-V crash-dump storage client (fcvsc.sys): the driver used a raw kernel SRB pointer as the VMBus transaction ID and then dereferenced whatever value the host echoed back, giving a malicious host a controlled kernel-pointer write into the guest. The patch replaces the raw-pointer scheme with a bounded index-into-table lookup. The same patch adds several defense-in-depth checks (a hard cap on the SCSI sense length, validation of the host-negotiated ring-buffer size, and validation of completion-packet structure). Those secondary checks harden paths that were already bounded, so they are lower severity than the pointer fix.

2. Vulnerability Summary

Finding 1: Untrusted Pointer Dereference via VMBus Transaction ID

  • Severity: Critical
  • CWE: CWE-822 (Untrusted Pointer Dereference)
  • Affected Functions: CrashVscSendSrbRequest (origin of the transaction ID), CrashVscHwInterrupt (ISR that trusts the echoed value), CrashVscSrbCompletion (the sink that reads and writes through the pointer)
  • Root Cause: In the crash-dump send path the driver passes the raw kernel SRB pointer to the host as the VMBus transaction ID. On completion, the ISR takes the value the host echoes back and uses it directly as the SRB pointer argument to CrashVscSrbCompletion, with no validation. A malicious host can echo any 64-bit value; the guest kernel then treats it as an SRB structure — writing to fields at +0x03, +0x04, +0x10 and dereferencing the pointer stored at +0x20 as the destination of a copy. This is a host-controlled kernel write (and read) primitive inside the guest.
  • Attacker-Reachable Entry Point & Data Flow:
  • The guest sends an SRB request; CrashVscSendSrbRequest passes the raw SRB pointer as the VMBus transaction ID.
  • The malicious host returns a completion packet of type 3 whose transaction-ID field holds an attacker-chosen value.
  • CrashVscHwInterrupt reads that value from the received packet and passes it directly as the second argument (the "SRB pointer") to CrashVscSrbCompletion.
  • CrashVscSrbCompletion writes through and dereferences that attacker-controlled pointer.

Finding 2: Added hard cap on SCSI sense-data length (defense in depth)

  • Severity: Low (hardening)
  • Affected Functions: CrashVscSrbCompletion, StorChannelCopyPacketDataToSrb
  • Nature of change: Both builds already gate the sense-data copy on the SRB's own sense-buffer length field: the copy only runs when SenseInfoBufferLength (SRB+0x0B) >= host_sense_len (packet+0x15), and the copied size is the smaller host value. That is a matched buffer/length pair — the destination SRB+0x20 is bounded by the guest-owned length at SRB+0x0B. The patch adds an additional upfront check that fail-fasts (int 0x29) if the host sense length exceeds 0x14. Because the copy was already bounded by the guest's own SRB length field, this added cap is defense in depth, not the fix for a reachable overflow. In the unpatched crash path, the only way the destination and length become attacker-influenced is through Finding 1 (the SRB pointer itself is attacker-controlled); in the StorChannel path the SRB is a legitimate guest object.

Finding 3: Host-negotiated ring-buffer size validation (defense in depth)

  • Severity: Low
  • Affected Functions: StorChannelEstablishCommunications, CrashVscEstablishCommunications
  • Nature of change: During channel negotiation the driver stores a size value taken from the host's response. The patch adds if ((size - 1) > 0x7fffff) fail-fast before storing/using it. This bounds a host-controlled quantity; the demonstrable impact of an out-of-range value is a guest bugcheck, so this is a hardening change.

Finding 4: Completion-packet structure validation (defense in depth)

  • Severity: Low
  • Affected Function: StorChannelKmclPacketCompletionRoutine
  • Nature of change: The patch adds a prologue check that fail-fasts when a success completion (status >= 0) arrives with a NULL buffer or a length below 0x30, before the buffer is used.

Finding 5: IOCTL input-buffer size correction

  • Severity: Low
  • Affected Function: VscGetCrashInterfaceWorker
  • Nature of change: The IoBuildDeviceIoControlRequest input-buffer length is changed from 8 to 0x10, and the patched build initializes the additional input fields. This is an internal kernel-to-kernel IOCTL (not attacker-controllable). It corrects the buffer sizing and ensures the full structure is initialized before it is sent.

3. Pseudocode Diff

CrashVscSendSrbRequest & CrashVscHwInterrupt (Untrusted Pointer Fix)

// === UNPATCHED ===
NTSTATUS CrashVscSendSrbRequest(adapter, srb_ptr, extra) {
    // VULNERABILITY: the raw kernel SRB pointer is the transaction ID
    channel_send(channel, /*type*/2, /*transaction_id*/ srb_ptr, packet, 0x40, extra);
}

BOOLEAN CrashVscHwInterrupt(adapter) {
    // receive packet into local buffer; arg_10 = transaction ID echoed by host
    if (packet_type == 3)
        // VULNERABILITY: host-echoed value used directly as the SRB pointer
        CrashVscSrbCompletion(adapter, /*srb_ptr*/ arg_10, packet);
}

// === PATCHED ===
NTSTATUS CrashVscSendSrbRequest(adapter, srb_ptr, extra) {
    // scan adapter+0x18 table (0x3e8 slots) for a free entry
    for (i = 0; i < 0x3e8; i++)
        if (adapter->SrbTable[i] == 0) { adapter->SrbTable[i] = srb_ptr; index = i + 2; break; }
    if (!found) return 0xC0000001;
    // FIX: pass the bounded index as the transaction ID, not a pointer
    channel_send(channel, /*type*/2, /*transaction_id*/ index, packet, 0x40, extra, flags);
}

BOOLEAN CrashVscHwInterrupt(adapter) {
    if (packet_size < 0x40) fail_fast();          // FIX: minimum size
    if (packet_type == 3) {
        id = host_transaction_id;                 // arg_10
        if ((id - 2) > 0x3e7) fail_fast();        // FIX: index bounds check
        srb_ptr = adapter->SrbTable[id];          // look up the real pointer
        adapter->SrbTable[id] = NULL;             // clear slot (no reuse)
        if (!srb_ptr) fail_fast();
        if (srb_ptr->field_10 < host_value) fail_fast();
        CrashVscSrbCompletion(adapter, srb_ptr, packet);
    }
}

CrashVscSrbCompletion (added sense-length cap)

// === UNPATCHED ===
void CrashVscSrbCompletion(adapter, srb_ptr, packet) {
    // ... on the fixed-format sense branch (packet[0x0f] == 2):
    uint8_t host_len = packet[0x15];
    // EXISTING BOUND: copy only when the SRB's own sense length covers it
    if (srb_ptr->SenseInfoBufferLength /*+0x0b*/ >= host_len)
        memmove(srb_ptr->SenseInfoBuffer /*+0x20*/, &packet[0x1c], host_len);
}

// === PATCHED ===
void CrashVscSrbCompletion(adapter, srb_ptr, packet) {
    if (packet[0x0f] == 2) {
        uint8_t host_len = packet[0x15];
        if (host_len > 0x14) fail_fast();          // ADDED: hard cap (defense in depth)
        // ... same SenseInfoBufferLength gate then memmove
    }
}

4. Assembly Analysis

Untrusted pointer dereference

The unpatched send path puts the raw SRB pointer (rbp = arg2) into the transaction-ID argument.

; UNPATCHED CrashVscSendSrbRequest
00000001C00012E4  mov     rcx, [rsi+8]              ; channel object
00000001C00012ED  mov     [rsp+var_60], r14         ; extra data
00000001C00012F2  mov     r8, rbp                   ; VULNERABLE: raw SRB pointer as transaction ID
00000001C00012F5  mov     edx, 2                    ; packet type
00000001C00012FA  mov     [rsp+var_68], 40h         ; packet size
00000001C0001309  call    cs:__guard_dispatch_icall_fptr   ; channel send

In the ISR the unpatched driver hands the host-returned value straight to the completion routine as the SRB pointer.

; UNPATCHED CrashVscHwInterrupt (type 3 path)
00000001C00011C6  mov     rdx, [rbp+arg_10]         ; VULNERABLE: host-echoed transaction ID
00000001C00011CA  lea     r8, [rbp+var_40]          ; received packet
00000001C00011CE  mov     rcx, rbx                  ; adapter
00000001C00011D1  call    CrashVscSrbCompletion     ; rdx used as SRB pointer

Patch Change: CrashVscSendSrbRequest builds a 0x3e8-slot table at adapter+0x18, stores the SRB pointer, and sends the slot index (index = slot + 2) instead of the pointer:

; PATCHED CrashVscSendSrbRequest
00000001C0001354  lea     rcx, [rbp+18h]            ; table base at adapter+0x18
00000001C000135A  cmp     qword ptr [rcx], 0        ; scan for a free slot
00000001C0001367  cmp     rax, 3E8h                 ; up to 1000 slots
00000001C0001371  lea     rdi, [rax+2]              ; index = slot + 2
00000001C0001375  mov     [rbp+rax*8+18h], r15      ; store SRB pointer in table[slot]
00000001C00013B5  mov     r8, rdi                   ; FIX: send the index, not a pointer

CrashVscHwInterrupt now validates the returned index, looks up the real pointer, and clears the slot:

; PATCHED CrashVscHwInterrupt (type 3 path)
00000001C0001205  cmp     [rbp+arg_0], esi          ; packet size >= 0x40
00000001C0001208  jb      loc_1C0001292             ; else fail-fast
00000001C000121B  mov     rcx, [rbp+arg_10]         ; host-returned index
00000001C000121F  lea     rax, [rcx-2]
00000001C0001223  cmp     rax, 3E7h                 ; FIX: (index-2) <= 0x3e7
00000001C0001229  ja      short loc_1C0001238
00000001C000122B  mov     rdx, [rbx+rcx*8+8]        ; look up SRB pointer from table
00000001C0001230  and     qword ptr [rbx+rcx*8+8], 0 ; clear slot
00000001C000123D  jz      short loc_1C0001292       ; NULL pointer -> fail-fast
00000001C0001242  cmp     [rdx+10h], eax            ; validate SRB field before use
00000001C000124E  call    CrashVscSrbCompletion

Sense-data length (added cap over an existing bound)

The unpatched copy is already gated: it only runs when the SRB's own sense length (cl = SRB+0x0b) is at least the host-provided length (al = packet+0x15), and it copies that host value.

; UNPATCHED CrashVscSrbCompletion (fixed-format sense branch)
00000001C00015A3  mov     al, [r8+15h]              ; host sense length (packet+0x15)
00000001C00015A7  cmp     cl, al                    ; cl = SRB+0x0b (SenseInfoBufferLength)
00000001C00015A9  jb      short loc_1C00015C1       ; skip copy if SRB length < host length
00000001C00015AB  mov     rcx, [rsi+20h]            ; dest = SRB+0x20 sense buffer
00000001C00015AF  mov     rdx, rbx                  ; src = packet+0x1c
00000001C00015B2  movzx   r8d, al                   ; size = host length (<= cl)
00000001C00015B6  call    memmove

Patch Change: the patched routine adds an upfront hard cap that fail-fasts before any sense processing, keeping the same SRB+0x0b >= host_len gate afterwards:

; PATCHED CrashVscSrbCompletion
00000001C0001542  mov     bpl, [r8+15h]             ; host sense length
00000001C0001546  cmp     bpl, 14h
00000001C000154A  jbe     short loc_1C0001553       ; ok if <= 0x14
00000001C000154C  mov     ecx, 3Ah
00000001C0001551  int     29h                       ; RtlFailFast otherwise

StorChannelCopyPacketDataToSrb carries the identical existing gate (0x1C0003049: mov al,[rdi+15h]; cmp dl,al; jb skip) and gains the same > 0x14 fail-fast (0x1C0003025: cmp bpl, 14h; ja fail-fast).


5. Trigger Conditions

To exercise the untrusted-pointer flaw (Finding 1) as a malicious Hyper-V host:

  1. Ensure the guest crash-dump storage client (fcvsc) is loaded and its VMBus channel is established.
  2. Wait for the guest to issue an SRB via CrashVscSendSrbRequest; in the unpatched build the transaction ID it sends is the raw kernel SRB pointer.
  3. Return a completion packet of type 3 whose transaction-ID field (arg_10) is an attacker-chosen value instead of the original pointer, and whose size is at least 0x40 bytes.
  4. On receipt, CrashVscHwInterrupt passes that value directly to CrashVscSrbCompletion as the SRB pointer.
  5. CrashVscSrbCompletion writes packet+0x18 to ptr+0x10, packet+0x0e to ptr+0x03, packet+0x0f to ptr+0x04, and (on the sense branch) reads ptr+0x0b/ptr+0x20 and copies into the buffer pointed to by ptr+0x20. With a chosen pointer value this is a controlled kernel write in the guest.
  6. Observable effect on an invalid pointer: a guest bugcheck (for example PAGE_FAULT_IN_NONPAGED_AREA) when the pointer is dereferenced. With a valid attacker-chosen address, the write occurs silently.

6. Exploit Primitive & Development Notes

  • Provided Primitive (Finding 1): A malicious host controls the 64-bit value that the guest kernel uses as an SRB pointer. Through CrashVscSrbCompletion this yields writes of host-derived bytes to attacker-chosen offsets (+0x03, +0x04, +0x10) of an attacker-chosen base, plus a copy into the buffer whose pointer is read from base+0x20. That is a host-to-guest kernel write primitive with host-chosen target.
  • Reachability: The value flows from the VMBus completion callback (CrashVscHwInterrupt) with no bounds check in the unpatched build. It requires the host to be malicious or compromised; the attack direction is host-to-guest.
  • Constraints: The primitive is gated by the packet-type check (type 3) and the field layout that CrashVscSrbCompletion reads. The report does not assert a specific full-chain kernel code-execution technique for this build; the demonstrable, binary-supported primitive is the controlled pointer write described above.

7. Debugger PoC Playbook

To validate Finding 1 on an unpatched target VM with a kernel debugger:

  • Breakpoints: text bp fcvsc_unpatched!CrashVscHwInterrupt bp fcvsc_unpatched!CrashVscSrbCompletion
  • What to inspect:
  • At CrashVscHwInterrupt after the receive call returns: [rbp+arg_8] is the packet type (3 = SRB completion) and [rbp+arg_10] is the host-returned transaction ID (a raw SRB pointer in the unpatched build).
  • At the call site 0x1C00011D1: rdx is the value being passed as the SRB pointer — an attacker fully controls it as the host.
  • At CrashVscSrbCompletion entry (0x1C0001420): rcx = adapter, rdx = the SRB pointer (host-controlled in the unpatched build), r8 = received packet. db r8+15 L1 reads the sense-length byte; the destination pointer is read from rdx+0x20.
  • Key Instructions/Offsets:
  • 0x1C00012F2 (mov r8, rbp): the raw SRB pointer is sent as the transaction ID.
  • 0x1C00011C6 (mov rdx, [rbp+arg_10]): the host value is loaded as the SRB pointer.
  • 0x1C00015B6 (call memmove): the sense copy, bounded by SRB+0x0b; note the destination rdx+0x20 is itself read from the host-controlled pointer in the unpatched build.
  • Trigger Setup: From the host side, after the guest sends an SRB, return a type-3 completion whose transaction-ID field is an attacker-chosen value with packet size >= 0x40.
  • Expected Observation: With an unmapped pointer, an immediate fault/bugcheck when CrashVscSrbCompletion writes through rdx. With a valid attacker-chosen address, the guest kernel memory at that address is modified without a crash.

8. Changed Functions — Full Triage

  • CrashVscSendSrbRequest: Replaced raw-pointer transaction IDs with an index into a 0x3e8-slot table at adapter+0x18; sends slot+2 instead of the pointer; clears the slot on send failure. Also computes transfer-length flags from the SRB direction bits and passes them as a new argument. Security-relevant (part of Finding 1).
  • CrashVscHwInterrupt: Added a packet-size check (>= 0x40), bounds check on the returned index ((index-2) <= 0x3e7), table lookup of the real SRB pointer, slot clearing, and SRB-field validation before completion. Security-relevant (Finding 1).
  • CrashVscSrbCompletion: Added an upfront hard cap (host_sense_len <= 0x14, else fail-fast) over the existing SenseInfoBufferLength (SRB+0x0b) >= host_len gate. Defense in depth (Finding 2). Also the sink for the Finding 1 pointer.
  • StorChannelCopyPacketDataToSrb: Added the same <= 0x14 sense-length cap and an SRB data-length capacity check (SRB+0x10 >= value, else fail-fast) over the existing sense-length gate. Defense in depth (Finding 2). In this path the SRB is a legitimate guest object.
  • StorChannelKmclPacketCompletionRoutine: Added validation that a success completion (status >= 0) has a non-NULL buffer of length >= 0x30 before use; now reads a length argument the unpatched build ignored. Defense in depth (Finding 4).
  • StorChannelEstablishCommunications: Added (ring_size - 1) <= 0x7fffff validation before storing the host size at context+0x54; added a bounce-buffer allocation and __security_check_cookie. Defense in depth (Finding 3).
  • StorChannelSendSrbRequest: Changed completion-context layout and added transfer-direction flags passed to the send call. Behavioral refactor, not security-relevant.
  • CrashVscEstablishCommunications: Added the same (ring_size - 1) <= 0x7fffff validation before storing the host size. Defense in depth (Finding 3).
  • CrashVscSendPacketSync: Added a 7th argument (0) to match the updated send signature; converted a silent unexpected-packet return into a fail-fast. Hardening/behavioral.
  • VscStartIO: Caps the maximum transfer length at 0x8000 ([rsi+0x50] clamped) and changes an inquiry-response word at [rsi+0x6a] from 0x8000 to 0x8003. Behavioral/hardening.
  • VscGetCrashInterfaceWorker: Changed the IoBuildDeviceIoControlRequest input length from 8 to 0x10, initialized the added input fields, and added __security_check_cookie. Internal IOCTL correctness (Finding 5).
  • CrashVscHwFindAdapter: Added a memset that zero-initializes the new table region at adapter+0x18 (0x1f40 bytes), and changed the configuration word at adapter-config+0x59 from 0x100 to 0x103. Infrastructure for Finding 1's table; not itself a vulnerability.

9. Unmatched Functions

There were no added or removed functions between the patched and unpatched binaries. All changes were implemented within existing function bodies.

10. Confidence & Caveats

  • Confidence Level: High for Finding 1: the unpatched send path uses the raw SRB pointer as the transaction ID and the ISR dereferences the host-echoed value with no validation; the patch introduces a bounded index-table indirection. The sense-length, ring-buffer, completion-structure, and IOCTL changes are confirmed in the binaries and are correctly characterized as defense-in-depth / correctness hardening.
  • Assumptions: The threat model is a malicious or compromised Hyper-V host attacking a guest. The reverse direction is not applicable, since a guest cannot spoof host VMBus responses.
  • Scope note: Finding 2's sense-data copy is bounded in both builds by the SRB's own SenseInfoBufferLength field (a matched buffer/length pair); the added <= 0x14 cap does not, by itself, close an otherwise-reachable overflow. Where the destination and length become attacker-influenced in the unpatched crash path, it is because of Finding 1 (the SRB pointer itself is host-controlled).