fcvsc.sys — untrusted host transaction ID dereferenced as kernel SRB pointer (CWE-822) fixed
KB5073723
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,+0x10and dereferencing the pointer stored at+0x20as 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;
CrashVscSendSrbRequestpasses 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.
CrashVscHwInterruptreads that value from the received packet and passes it directly as the second argument (the "SRB pointer") toCrashVscSrbCompletion.CrashVscSrbCompletionwrites 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 destinationSRB+0x20is bounded by the guest-owned length atSRB+0x0B. The patch adds an additional upfront check that fail-fasts (int 0x29) if the host sense length exceeds0x14. 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-fastbefore 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 below0x30, before the buffer is used.
Finding 5: IOCTL input-buffer size correction
- Severity: Low
- Affected Function:
VscGetCrashInterfaceWorker - Nature of change: The
IoBuildDeviceIoControlRequestinput-buffer length is changed from8to0x10, 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:
- Ensure the guest crash-dump storage client (
fcvsc) is loaded and its VMBus channel is established. - 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. - 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 least0x40bytes. - On receipt,
CrashVscHwInterruptpasses that value directly toCrashVscSrbCompletionas the SRB pointer. CrashVscSrbCompletionwritespacket+0x18toptr+0x10,packet+0x0etoptr+0x03,packet+0x0ftoptr+0x04, and (on the sense branch) readsptr+0x0b/ptr+0x20and copies into the buffer pointed to byptr+0x20. With a chosen pointer value this is a controlled kernel write in the guest.- 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
CrashVscSrbCompletionthis 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 frombase+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
CrashVscSrbCompletionreads. 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
CrashVscHwInterruptafter 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:rdxis the value being passed as the SRB pointer — an attacker fully controls it as the host. - At
CrashVscSrbCompletionentry (0x1C0001420):rcx= adapter,rdx= the SRB pointer (host-controlled in the unpatched build),r8= received packet.db r8+15 L1reads the sense-length byte; the destination pointer is read fromrdx+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 bySRB+0x0b; note the destinationrdx+0x20is 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
CrashVscSrbCompletionwrites throughrdx. 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 a0x3e8-slot table atadapter+0x18; sendsslot+2instead 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 existingSenseInfoBufferLength (SRB+0x0b) >= host_lengate. Defense in depth (Finding 2). Also the sink for the Finding 1 pointer.StorChannelCopyPacketDataToSrb: Added the same<= 0x14sense-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>= 0x30before use; now reads a length argument the unpatched build ignored. Defense in depth (Finding 4).StorChannelEstablishCommunications: Added(ring_size - 1) <= 0x7fffffvalidation before storing the host size atcontext+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) <= 0x7fffffvalidation 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 at0x8000([rsi+0x50]clamped) and changes an inquiry-response word at[rsi+0x6a]from0x8000to0x8003. Behavioral/hardening.VscGetCrashInterfaceWorker: Changed theIoBuildDeviceIoControlRequestinput length from8to0x10, initialized the added input fields, and added__security_check_cookie. Internal IOCTL correctness (Finding 5).CrashVscHwFindAdapter: Added amemsetthat zero-initializes the new table region atadapter+0x18(0x1f40bytes), and changed the configuration word atadapter-config+0x59from0x100to0x103. 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
SenseInfoBufferLengthfield (a matched buffer/length pair); the added<= 0x14cap 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).