1. Overview

Item Value
Unpatched binary tcpip_unpatched.sys
Patched binary tcpip_patched.sys
Overall similarity 0.9924
Matched functions 6053
Changed functions 2
Identical functions 6051
Unmatched (unpatched / patched) 0 / 0

Verdict: The patch closes a critical, remotely-reachable kernel pool out-of-bounds write in FseProcessIncomingMessages (the FSE framed-message stream reassembler) by adding the missing > 0x400 bounds guards and an integer-overflow guard around three memcpy sites that copy attacker-controlled data into a fixed 1024-byte pool buffer. It matters because the write length and contents are fully attacker-controlled and the code is reached directly from the network receive path.

Of the 2 changed functions, only 1 is security-relevant. FseProcessIncomingMessages (similarity 0.6679) carries the actual fix. IppCreateForwardPath (similarity 0.8227) is a benign feature-flag reordering of a flag-set (Entry+0x70 |= 1) and a field store — no allocation/bounds/lifetime change — and is not the patched vulnerability. The real fix is concentrated entirely in the FSE reassembly accumulator and is gated behind the staged velocity flag Feature_3924026683__private_IsEnabledDeviceUsageNoInline().


2. Vulnerability Summary

2.1 CRITICAL — Kernel pool out-of-bounds write in FSE reassembly (CWE-787 / CWE-120; accumulation path also CWE-190 -> CWE-787)

  • Affected function: FseProcessIncomingMessages (sub_1401bd224) (unpatched), FseProcessIncomingMessages (sub_1401bd318) (patched) — reassembles a length-prefixed FSE message stream into a fixed-size pool buffer inside the FSE context object.
  • Root cause: The function copies an attacker-controlled number of inbound bytes into a fixed 0x400-byte buffer without ever bounding the copy length to the buffer capacity. Object/field layout of the FSE context (ctx = *(*(arg1+0x20)+0xa8)):
  • ctx+0x88 — reassembly buffer base (fixed capacity 0x488 − 0x88 = 0x400 = 1024 bytes)
  • ctx+0x488 — 32-bit accumulated-byte counter (rbp_1)
  • Inbound data pointer: rsi = *(arg1+0x30)
  • Inbound length: rdi = *(arg2+0x38) (attacker-controlled)

The only length check present pre-patch validates the internal per-message length field, not the raw fragment size:

0x1401bd2e0  mov  r14d, dword [r15]      ; internal per-message length
0x1401bd2e3  lea  eax, [r14-0x21]
0x1401bd2e7  cmp  eax, 0x3df             ; validates msg len to [0x21,0x400] ONLY

The trailing-fragment write is unbounded:

memcpy(rbx + 0x88, rsi, rdi);   // rdi never checked <= 0x400
*(rbx + 0x488) = rdi;           // counter set past capacity, no cap
  • Why exploitable: The attacker controls both the copy length (rdi = *(arg2+0x38)) and the copied contents (the inbound network buffer). The destination is a fixed 1024-byte pool allocation embedded in the FSE context. A fragment longer than 0x400, or a partial message (rbp_1) combined with a new fragment such that rbp_1 + rdi > 0x400, writes attacker bytes linearly past the buffer into adjacent pool memory — a controlled contiguous pool overflow. The accumulate counter *(ctx+0x488) += rdi is a 32-bit add that can also wrap.
  • What the patch does: Adds explicit upper-bound guards before the buffering/accumulate copies:
  • if (*r14 > 0x400 || rsi > 0x400) for the trailing-fragment path
  • r8_6 = *(ctx+0x488) + rsi; if (r8_6 < result || r8_6 > 0x400) — integer-overflow AND 0x400 cap for the accumulate path

On violation the code drops the data (result = 0) and emits WPP trace ids 0x2b/0x2f/0x32. All guards are gated behind Feature_3924026683 (call at sub_1401bd084); if the feature is disabled the old unchecked path can still run. - Attacker-reachable entry point: Inbound data on the FSE transport channel → registered receive-completion callback OnReceiveComplete (sub_1401bdf50) → call at 0x1401bdfb9. - Call chain: 1. Network transport receive indication (inbound bytes) 2. OnReceiveComplete (sub_1401bdf50) — receive-completion callback, invokes vuln at 0x1401bdfb9 3. FseProcessIncomingMessages (sub_1401bd224) 4. memcpy at 0x1401bd4e4 / 0x1401bd307 / 0x1401bd38b / 0x1401bd419 (OOB write)


3. Pseudocode Diff

3.1 FseProcessIncomingMessages (sub_1401bd224)

// UNPATCHED — FseProcessIncomingMessages (sub_1401bd224)
uint64_t rbp_1 = *(rbx + 0x488);            // accumulated bytes so far
// internal per-message length validated only:
// if (r14_1 - 0x21 > 0x3df) { error }      // [0x21,0x400] on the message header

// trailing-fragment path (loop label_1401bd48a):
while (rdi) {
    int64_t* rdx_11 = rsi;
    if (rdi > 0x20) {
        int32_t r8_12 = *rsi;               // next-message 4-byte length prefix (attacker)
        if (r8_12 <= rdi) {
            FseProcessIncomingMessage(rbx, rdx_11, r8_12);
            result = *rsi; rsi += result; rdi = rdi - result; continue;
        }
    }
    memcpy(rbx + 0x88, rdx_11, rdi);        // <-- OOB WRITE: rdi never checked <= 0x400
    *(rbx + 0x488) = rdi;                   // counter set past capacity
    break;
}

// accumulate path:
memcpy(rbx + 0x88 + rbp_1, rsi, rdi);       // <-- OOB WRITE: rbp_1+rdi never checked <= 0x400
*(rbx + 0x488) += rdi;                      // 32-bit counter can wrap
// PATCHED — FseProcessIncomingMessages (sub_1401bd318)
if (Feature_3924026683__private_IsEnabledDeviceUsageNoInline()) {
    // trailing-fragment guard
    if (*r14 > 0x400 || rsi > 0x400) {      // reject oversize fragment
        result = 0;                         // drop data
        WppTrace(0x32);                     // (also 0x2b / 0x2f on related drops)
        goto drop;
    }
    // accumulate guard (integer-overflow AND cap)
    r8_6 = *(ctx + 0x488) + rsi;
    if (r8_6 < result || r8_6 > 0x400) {    // wrap OR exceeds 0x400
        result = 0;
        WppTrace(0x2f);
        goto drop;
    }
}
// only reached when within bounds:
memcpy(rbx + 0x88 + rbp_1, rsi, rdi);
*(rbx + 0x488) += rdi;

Key changes: - Unpatched: memcpy(rbx+0x88, rsi, rdi) with rdi unbounded, and memcpy(rbx+0x88+rbp_1, rsi, rdi) with rbp_1+rdi unchecked. - Patched: rejects any fragment whose size exceeds 0x400 and any accumulation that overflows or crosses the 0x400 capacity; on failure it zeroes result, drops the data, and traces 0x2b/0x2f/0x32. - The entire guard set is behind Feature_3924026683 — if the velocity flag is off, the unchecked path remains reachable (fallback risk).


4. Assembly Analysis

4.1 FseProcessIncomingMessages (sub_1401bd224)

Unpatched — context load and the trailing-fragment OOB write:

0x1401bd247  mov  rax, qword [rcx+0x20]
0x1401bd24b  mov  rsi, qword [rcx+0x30]        ; rsi = inbound data ptr (arg1+0x30)
0x1401bd24f  mov  edi, dword [rdx+0x38]        ; edi = inbound length (arg2+0x38) ATTACKER
0x1401bd252  mov  rbx, qword [rax+0xa8]        ; rbx = FSE ctx (buf @ +0x88, count @ +0x488)
...
0x1401bd48c  mov  rdx, rsi
0x1401bd48f  cmp  edi, 0x20
0x1401bd492  jbe  0x1401bd4da
0x1401bd494  mov  r8d, dword [rsi]             ; next-message 4-byte length prefix
0x1401bd497  cmp  r8d, edi
0x1401bd49a  ja   0x1401bd4da                  ; prefix > remaining => incomplete => buffer remainder
0x1401bd4da  mov  r8d, edi                     ; r8 = rdi (UNBOUNDED length)
0x1401bd4dd  lea  rcx, [rbx+0x88]              ; dst = 0x400-byte buffer base
0x1401bd4e4  call 0x1401c2880                 ; memcpy(rbx+0x88, rsi, rdi)  <-- OOB WRITE
0x1401bd4e9  mov  dword [rbx+0x488], edi        ; *(ctx+0x488)=rdi (no 0x400 cap)

Annotations: - 0x1401bd2e7 cmp eax, 0x3df: the ONLY length bound present pre-patch — validates the internal per-message header to [0x21,0x400], not the raw fragment copy size. - 0x1401bd49a ja 0x1401bd4da: decides "incomplete fragment" (next prefix > rdi) and routes to the unbounded buffering memcpy. - 0x1401bd4e4 call memcpy: the primary OOB write, rdi copy length with no 0x400 cap; 0x1401bd4e9 then records the oversize length in ctx+0x488.

Unpatched — accumulate-path OOB write:

0x1401bd2ca  mov  ebp, dword [rbx+0x488]       ; rbp_1 = accumulated bytes
0x1401bd2f1  lea  rcx, [rbx+0x88]
0x1401bd2f8  sub  edx, ebp
0x1401bd2fa  add  rcx, rbp                     ; dst = rbx+0x88+rbp_1
0x1401bd2fd  cmp  edi, edx
0x1401bd2ff  jae  0x1401bd336
0x1401bd301  mov  r8, rdi
0x1401bd304  mov  rdx, rsi
0x1401bd307  call 0x1401c2880                 ; memcpy(rbx+0x88+rbp_1, rsi, rdi) <-- OOB WRITE
0x1401bd30c  add  dword [rbx+0x488], edi        ; *(ctx+0x488)+=rdi (no cap, 32-bit wrap)

Annotations: - 0x1401bd2fa add rcx, rbp: destination is offset by the previously-buffered byte count, with no check that rbp_1 + rdi <= 0x400. - 0x1401bd307 call memcpy: second OOB write site; 0x1401bd30c blindly adds rdi to the 32-bit counter (integer wrap).

Patched — safe path (guards gated by Feature_3924026683):

; conceptual layout of inserted guards (from diff_function_code, side=patched):
call 0x1401bd084                              ; Feature_3924026683 gate
test <flag>, <flag>
je   <old_unchecked_path>                      ; feature OFF -> legacy path
mov  eax, 0x400
cmp  r8d, eax                                  ; fragment length > 0x400 ?
ja   0x1401bd705                              ; -> drop (result=0, WPP 0x2b/0x2f/0x32)
lea  r8d, [rax+rsi]                            ; acc + incoming
cmp  r8d, eax                                  ; > 0x400 ?  (and jb below covers wrap)
jb   <overflow-check>                          ; only proceed when within capacity

Annotations: - call 0x1401bd084: the Feature_3924026683 velocity gate — guards only active when enabled. - cmp r8d, 0x400 / ja <drop>: rejects any single fragment larger than the buffer. - lea r8d,[rax+rsi] / cmp / jb: the integer-overflow-and-cap check on the accumulated length; the drop path emits WPP ids 0x2b/0x2f/0x32.


5. Trigger Conditions

  1. Reach the target. Establish an FSE channel/connection over the connected tcpip transport so inbound bytes drive OnReceiveComplete (sub_1401bdf50) and thus FseProcessIncomingMessages.
  2. Deliver a crafted inbound, length-prefixed FSE byte stream. Framing: 4-byte length prefix at buffer start; internal message length is checked only against [0x21,0x400].
  3. Trailing-fragment overflow (primary): Send a stream whose consumed framed messages leave a trailing remainder of length rdi in the range 0x401 .. (pool slack), with rdi > 0x20, and set the remainder's leading 4-byte length prefix *rsi strictly greater than rdi (so the message is "declared but not fully present").
  4. This forces the ja 0x1401bd4da branch, routing to memcpy(ctx+0x88, remainder, rdi) with rdi > 0x400 — the boundary is crossed and the 1024-byte buffer is exceeded.
  5. The single firing action is the receive completion of that trailing fragment; the copy at 0x1401bd4e4 runs immediately.
  6. Observable effect. Adjacent pool corruption; dword [ctx+0x488] holds a value > 0x400 after return. Subsequent pool operations bugcheck — typically BAD_POOL_HEADER (0x19), KERNEL_MODE_HEAP_CORRUPTION (0x13A), or DRIVER_OVERRAN_STACK_BUFFER (0x139) if the cookie is tripped — with the fault trail into tcpip!FseProcessIncomingMessages.

Alternative — accumulation overflow: First send a fragment that leaves ctx+0x488 partially filled (e.g., near 0x400), then send a second fragment of length rdi so rbp_1 + rdi > 0x400 while each per-message header still passes the [0x21,0x400] check; memcpy(ctx+0x88+rbp_1, data, rdi) at 0x1401bd307 overflows, and *(ctx+0x488) += rdi can 32-bit-wrap.

Notes: The overflow length and content are fully attacker-controlled. Reliable exploitation requires pool grooming so a useful victim object sits adjacent to the FSE context allocation. Because the fix is behind Feature_3924026683, on builds where the flag is disabled even the patched binary can still reach the unchecked path.


6. Exploit Primitive & Development Notes

  • Primitive: Linear kernel pool out-of-bounds write with fully attacker-controlled length (rdi, up to the fragment size) and contents (the inbound bytes), starting at ctx+0x88 and running past the 1024-byte buffer into adjacent pool memory. The accumulate variant additionally offers a 32-bit counter wrap at ctx+0x488.
  • Turning it into a full exploit:
  • Groom the pool so that the FSE context allocation is immediately followed by a controllable victim object (another pool block with a function pointer, LIST_ENTRY, or object header of interest).
  • Overflow into and corrupt the adjacent object — e.g., overwrite a function pointer or a LIST_ENTRY used in a later dereference — with attacker bytes, converting the linear overflow into a write-what-where or hijacked control transfer.
  • Defeat kASLR with a separate info leak (remote overflow gives no leak by itself); target values that survive KASLR randomization once a base is known.
  • Redirect execution (via a corrupted call target) or perform data-only privilege escalation (e.g., corrupt a token/security field reachable through the adjacent object).
  • Mitigations to consider:
  • kASLR: Full impact — a remote-only overflow yields no base leak; a companion info-leak primitive is required to place absolute pointers.
  • SMEP / SMAP: Standard kernel mitigations; a data-only corruption path (fixing an adjacent structure, not jumping to user memory) avoids them.
  • CFG / CET: Constrains control-flow hijack via corrupted indirect call targets; prefer data-only corruption or ROP within valid targets.
  • HVCI / VBS: No unsigned kernel code execution; a data-only variant (privilege escalation via corrupted kernel data) survives.
  • Pool hardening: Modern pool cookies/quarantine make header corruption prone to detected bugchecks; prefer corrupting adjacent object fields rather than the pool header itself.
  • Realistic outcome: Remote kernel crash / DoS is straightforward; remote LPE/code execution is plausible but requires a separate info leak and careful pool grooming.

7. Debugger PoC Playbook

Assume WinDbg/KD attached to the UNPATCHED binary.

7.1 FseProcessIncomingMessages (sub_1401bd224)

Breakpoints:

bp tcpip!FseProcessIncomingMessages          ; 0x1401bd224 (RVA 0x1bd224) - entry, capture args/ctx
bp tcpip+0x1bd4e4                             ; trailing-fragment memcpy - watch r8d vs 0x400
bp tcpip+0x1bd307                             ; accumulate memcpy - watch rcx dst, r8 length
bp tcpip!OnReceiveComplete                    ; 0x1401bdf50 - see network buffer marshalling

Rebase note: the analysis base is 0x140000000; if the live module base differs, subtract 0x140000000 from the addresses above to get the RVA and add the live base.

Inspect at each breakpoint: - At FseProcessIncomingMessages (entry, 0x1401bd224): rcx = arg1, rdx = arg2. Read the data pointer and length, and resolve the context:

r @rcx, @rdx
dq @rcx+0x30 L1              ; rsi = inbound data ptr
dd @rdx+0x38 L1             ; edi = attacker-controlled inbound length
dq poi(@rcx+0x20)+0xa8 L1   ; rbx = FSE context base
  • At +0x1bd4e4 (trailing-fragment memcpy): watch r8d (copy length) and rcx (= rbx+0x88). If r8d > 0x400 the copy overflows. Step to 0x1401bd4e9 to see *(rbx+0x488) set to the oversize length.
r r8, rcx
dd @rbx+0x488 L1            ; accumulated counter (before/after)
  • At +0x1bd307 (accumulate memcpy): watch rcx (= rbx+0x88+rbp_1) and r8 (length). Overflow if rbp_1 (dword [rbx+0x488] on entry) + rdi > 0x400. 0x1401bd30c performs the unchecked add dword [rbx+0x488], edi.
  • At OnReceiveComplete (0x1401bdf50): observe how the inbound network buffer/length are placed into arg1+0x30 / arg2+0x38 before the call at 0x1401bdfb9.

Trigger setup (from user mode / peer): 1. Bring up the FSE channel that feeds OnReceiveComplete (an inband framed-message stream over the connected tcpip transport). 2. From the peer, send a message stream so that after the framed messages are consumed a trailing remainder of length > 0x400 remains, with that remainder's leading 4-byte length prefix set greater than the remainder length (forces the incomplete-fragment buffering path):

[complete framed messages ...][trailing remainder]
trailing remainder layout:
  +0x00  DWORD  prefix   = value > (remainder_len)   ; forces *rsi > rdi
  +0x04  ...    filler   = attacker bytes, total remainder_len > 0x400
  1. Alternative: send a first fragment leaving dword [ctx+0x488] near 0x400, then a second fragment whose length pushes ctx+0x488 past 0x400.

Expected observation: At 0x1401bd4e4 (or 0x1401bd307) the memcpy length register r8d exceeds 0x400 while rcx is inside the ctx+0x88 buffer; stepping over the call corrupts adjacent pool memory. Expect a pool-corruption BSOD (0x19 BAD_POOL_HEADER, 0x13A KERNEL_MODE_HEAP_CORRUPTION, or 0x139 if the cookie trips) on the next allocation/free, faulting back into tcpip!FseProcessIncomingMessages. A value > 0x400 left in dword [ctx+0x488] after return is direct confirmation the bound was never enforced.

Struct / offset notes:

FSE context = *(*(arg1+0x20)+0xa8)   ; rbx
  +0x088  BYTE[0x400]  reassembly buffer (capacity 0x488-0x88 = 0x400 = 1024)
  +0x488  DWORD        accumulated-byte counter (rbp_1; 32-bit, can wrap)
arg1+0x30 = inbound data pointer (rsi)
arg2+0x38 = inbound fragment length (edi/rdi)
Only pre-patch bound: cmp eax,0x3df @0x1401bd2e7 (internal msg len [0x21,0x400], NOT the copy)
Route to unbounded memcpy: cmp r8d,edi / ja @0x1401bd49a (next prefix > remaining)
Helpers: FseProcessIncomingMessage (single-msg) = 0x1401bd108
         OnReceiveComplete = 0x1401bdf50 (invokes vuln at 0x1401bdfb9)
Patched gate: call 0x1401bd084 (Feature_3924026683) + cmp ...,0x400 + WPP 0x2b/0x2f/0x32

8. Changed Functions — Full Triage

  • FseProcessIncomingMessages (sub_1401bd224) -> FseProcessIncomingMessages (sub_1401bd318) (similarity 0.6679, security_relevant): The FSE reassembly accumulator gained > 0x400 fragment-size guards and an integer-overflow-plus-cap check on the accumulated length (ctx+0x488) before three memcpy sites (0x1401bd4e4, 0x1401bd307, 0x1401bd38b). Pre-patch these copied attacker-controlled rdi/rbp_1+rdi bytes into a fixed 1024-byte pool buffer with no size limit — a linear pool OOB write. Fix is gated behind Feature_3924026683 and drops data (WPP 0x2b/0x2f/0x32) on violation.
  • IppCreateForwardPath (sub_...) -> IppCreateForwardPath (sub_...) (similarity 0.8227, behavioral): Only relocates a flag-set (*(Entry+0x70) |= 1) into two Feature_277588282-gated sites (before/after RtlRestructureHashTable) and moves the *(Entry+0x78) = gsbase+0x1a4 store earlier; net effect preserved. Remainder is pure register renaming. No allocation/bounds/lifetime change — not a memory-safety fix.

Collapsed note on non-security changes: Aside from the FSE fix, the only other diff is a benign feature-flag reordering and register renaming in IppCreateForwardPath; the sole meaningful, security-relevant change is FseProcessIncomingMessages.


9. Unmatched Functions

  • Removed (unpatched only): None
  • Added (patched only): None
  • Implication: The patch is purely additive/in-place: the fix consists of inserted bounds/overflow checks inside the existing FseProcessIncomingMessages, gated behind the Feature_3924026683 velocity flag rather than a new sanitizer routine. No functions were added or removed. Because the guards are feature-flag-gated, the unchecked legacy path may still be reachable on builds where the flag is disabled.

10. Confidence & Caveats

  • Confidence: critical
  • Rationale:
  • The buffer geometry is unambiguous: capacity 0x488 − 0x88 = 0x400, counter at ctx+0x488, three memcpy sites copying rdi/rbp_1+rdi bytes with no 0x400 bound in the unpatched code.
  • The patch is a textbook fix idiom — added cmp ...,0x400 / ja <drop> and lea r8d,[rax+rsi] / cmp / jb overflow-and-cap guards gated by a feature flag, dropping data with WPP trace codes.
  • Data flow from arg1+0x30 / arg2+0x38 through the FSE context to the copy sites is fully traced in the disassembly.
  • Assumptions made:
  • The FSE channel is reachable such that attacker-influenced bytes drive OnReceiveComplete and thus set arg1+0x30 (data) and arg2+0x38 (length) — the exact network conditions/protocol to bring up an FSE channel are not fully enumerated here.
  • The pool type of the FSE context allocation and the identity/adjacency of a useful victim object are not determined and must be established for a controlled overflow.
  • To verify before a PoC:
  • Confirm the FSE context object layout (buffer at +0x88, counter at +0x488) and its pool allocation size/type against a live/unpatched target.
  • Reproduce the trailing-fragment path: craft a stream leaving a trailing remainder > 0x400 with next-prefix > rdi, break at 0x1401bd4e4, and confirm r8d > 0x400 and adjacent pool corruption / bugcheck.