1. Overview

Item Value
Unpatched binary tcpip_unpatched.sys
Patched binary tcpip_patched.sys
Overall similarity 0.9925
Matched functions 5746
Changed functions 2
Identical functions 5744
Unmatched (unpatched / patched) 0 / 0

Verdict: The patch closes a network-reachable, kernel-pool out-of-bounds write in FseProcessIncomingMessages. The function reassembles 4-byte length-prefixed messages into a fixed 1024-byte (0x400) buffer embedded inside a pool-allocated flow-context object, and the streaming/leftover buffering path copies the full attacker-controlled incoming segment length with no bound against the buffer size.

Of the two changed functions, only one is the memory-safety fix. FseProcessIncomingMessages (similarity 0.697) receives the actual bounds-check additions and is the subject of this writeup. IppCreateForwardPath (similarity 0.8814) is an unrelated concurrency/lifetime hardening change (reordering of a "ready" flag under a lock) with no buffer-size or copy-length impact and is not attacker-triggerable as a memory-safety bug. The real fix is concentrated entirely in the streaming/append paths of FseProcessIncomingMessages and is gated behind Feature_4058244411__private_IsEnabledDeviceUsage().


2. Vulnerability Summary

2.1 HIGH — Out-of-bounds write / pool buffer overflow (CWE-787 -> CWE-120)

  • Affected function: FseProcessIncomingMessages (sub_1c01d1214) (unpatched), FseProcessIncomingMessages (sub_1c01d1214) (patched) — reassembles 4-byte length-prefixed messages arriving on the tcpip fast-path/segment engine into a fixed reassembly buffer inside the flow-context object.
  • Root cause: The per-message length prefix is range-validated to [0x21,0x400] only in the header-parse branch. The streaming/leftover buffering path, reached when the buffered byte-count is 0, copies the entire remaining incoming segment length into the 0x400 buffer with no size check. Relevant object layout:
  • flow_ctx = *(*(arg1+0x20)+0xa8) — pool-allocated flow-context object
  • flow_ctx+0x88 — start of the fixed reassembly buffer, size 0x400 (1024 bytes), ends at +0x488
  • flow_ctx+0x488uint32 buffered byte-count
  • arg1+0x30 — incoming data pointer (rsi)
  • arg2+0x38 — incoming byte length (edi, attacker-controlled)

The exact flawed operation:

0x1c01d14cc: mov  r8d, edi          ; copy length = full leftover, UNBOUNDED
0x1c01d14cf: lea  rcx, [rbx+0x88]   ; dest = flow_ctx+0x88 (0x400 buffer)
0x1c01d14d6: call 0x1c00d4d40       ; memcpy(rbx+0x88, rsi, edi)  <== OOB WRITE
0x1c01d14db: mov  dword [rbx+0x488], edi ; count = edi (can exceed 0x400)
  • Why exploitable: edi (the incoming length from *(arg2+0x38)) is fully attacker-controlled and is never compared against 0x400. An attacker who sends a single segment longer than 1024 bytes whose leading 4-byte length prefix declares a value larger than the delivered segment length forces the "incomplete message" branch, resulting in a controlled-length, controlled-content copy that overflows the 0x400 buffer, clobbering the trailing count field at +0x488 and any adjacent non-paged pool allocation.
  • What the patch does: Wraps every append memcpy in guards gated by Feature_4058244411__private_IsEnabledDeviceUsage(). Before each copy it performs a checked RtlUIntAdd(current_count, incoming_len, &sum) (call to 0x1c00d490c) and explicit sum > 0x400 / next_msg_len > 0x400 || incoming_len > 0x400 comparisons, taking a reject path (result=0) and emitting WPP trace events (0x2c0x31) when the accumulated size would exceed the buffer. Note the fix is feature-flag gated; if the feature is disabled the old path could still execute.
  • Attacker-reachable entry point: tcpip.sys FSE (fast-path/segment engine) receive indication — network-driven. Caller passes arg1 with incoming data pointer at +0x30 and flow-context at *(arg1+0x20)+0xa8, and arg2 with the incoming byte length at +0x38.
  • Call chain:
  • Network receive indication (NBL) into tcpip.sys fast-path
  • FSE receive/dispatch handler (indicates buffered segment data to the segment engine)
  • FseProcessIncomingMessages (sub_1c01d1214)
  • memcpy(flow_ctx+0x88, data, len) at 0x1c01d14cc / 0x1c01d14d6 (unbounded); also the header-branch partial append at 0x1c01d12f7

3. Pseudocode Diff

3.1 FseProcessIncomingMessages (sub_1c01d1214)

// UNPATCHED — FseProcessIncomingMessages (sub_1c01d1214)
void* rsi = *(arg1 + 0x30);            // incoming data ptr
uint64_t rdi = *(arg2 + 0x38);         // incoming length (ATTACKER-CONTROLLED)
void* rbx = *(*(arg1 + 0x20) + 0xa8);  // flow ctx; buffer @ rbx+0x88, count @ rbx+0x488

if (!*(rbx + 0x488)) goto label_1c01d1478;   // buffered count == 0 -> streaming loop
// ... header-accumulation path omitted ...

label_1c01d1478:
    if (!rdi) { result = 1; }
    else while (true) {
        void* rdx_11 = rsi;
        if (rdi > 0x20) {
            uint64_t r8_9 = *(uint32_t*)rsi;  // next declared message length, range UNVALIDATED
            if (r8_9 <= rdi) {                // complete message -> consume it
                FseProcessIncomingMessage(rbx, rdx_11, r8_9);
                rsi += r8_9; rdi -= r8_9;
                continue;
            }
        }
        // incomplete trailing message -> buffer whatever remains:
        memcpy(rbx + 0x88, rdx_11, rdi);      // <== OOB: rdi NEVER checked <= 0x400
        *(uint32_t*)(rbx + 0x488) = rdi;      // count set to rdi (can exceed 0x400)
        break;
    }

// header-branch partial-append variant (rbp>=4, valid in-range len) is BOUNDED here:
//   memcpy(rbx+0x88+rbp, rsi, rdi); *(rbx+0x488) += rdi;
//   reached only when rdi < declared_len - rbp, and declared_len is range-checked to
//   [0x21,0x400], so rbp+rdi < declared_len <= 0x400. Not an independent overflow.
// PATCHED — FseProcessIncomingMessages (sub_1c01d1214)
// Every append is now preceded by Feature_4058244411-gated bounds checks:
if (Feature_4058244411__private_IsEnabledDeviceUsage()) {
    uint32_t next_msg_len = *(uint32_t*)rsi;
    if (next_msg_len > 0x400 || rdi > 0x400) {   // reject over-length units
        WPP_trace(0x2c /* .. 0x31 */);
        result = 0; goto reject;
    }
    uint32_t sum = 0;
    if (RtlUIntAdd(*(uint32_t*)(rbx + 0x488), rdi, &sum) < 0) { // overflow-checked add
        WPP_trace(...); result = 0; goto reject;
    }
    if (sum > 0x400) {                           // accumulated size exceeds buffer
        WPP_trace(0x2e /* 0x2f / 0x31 */);
        result = 0; goto reject;
    }
}
memcpy(rbx + 0x88, rsi, rdi);   // only reached when count + rdi <= 0x400
*(uint32_t*)(rbx + 0x488) = rdi;

Key changes: - The dangerous operation is memcpy(rbx+0x88, rsi, rdi) at 0x1c01d14cc/0x1c01d14d6 with rdi = full incoming leftover length, never bounded to 0x400. - The streaming-loop copy (the OOB site) is guarded directly: the patch rejects when next_msg_len > 0x400 or incoming_len > 0x400 before the copy (no checked-add on this path). The checked-add idiom RtlUIntAdd(current_count, incoming_len, &sum) with a sum > 0x400 reject is added to the header-accumulation partial-append paths (count < 4) as defense-in-depth; those paths were already bounded. - Fallback caveat: the fix is gated by Feature_4058244411__private_IsEnabledDeviceUsage(). When the feature is disabled, execution can still reach the old unbounded append — verify feature default state before assuming the fix is universally active.


4. Assembly Analysis

4.1 FseProcessIncomingMessages (sub_1c01d1214)

Unpatched — routing to the streaming loop and the unbounded append:

0x1c01d123b: mov  rsi, qword [rcx+0x30]        ; rsi = incoming data ptr
0x1c01d123f: mov  edi, dword [rdx+0x38]        ; edi = incoming length (ATTACKER)
0x1c01d1242: mov  rbx, qword [rax+0xa8]        ; rbx = flow ctx (buf @ +0x88, count @ +0x488)
0x1c01d1282: cmp  dword [rbx+0x488], 0x0       ; buffered count
0x1c01d1289: je   0x1c01d1478                  ; count==0 -> streaming loop
0x1c01d1478: test edi, edi
0x1c01d147a: je   0x1c01d14e1                  ; nothing to do -> return 1
0x1c01d147c: mov  rdx, rsi
0x1c01d147f: cmp  edi, 0x20
0x1c01d1482: jbe  0x1c01d14cc                  ; <=0x20 leftover -> buffer it
0x1c01d1484: mov  r8d, dword [rsi]             ; next message length (*rsi), UNVALIDATED
0x1c01d1487: cmp  r8d, edi
0x1c01d148a: ja   0x1c01d14cc                  ; declared len > leftover -> buffer it
0x1c01d14cc: mov  r8d, edi                     ; VULN: copy length = full leftover, no cap
0x1c01d14cf: lea  rcx, [rbx+0x88]              ; dest = flow_ctx+0x88 (0x400 buffer)
0x1c01d14d6: call 0x1c00d4d40                  ; memcpy(rbx+0x88, rsi, edi)  <== OOB WRITE
0x1c01d14db: mov  dword [rbx+0x488], edi       ; store count = edi (can exceed 0x400)

Annotations: - 0x1c01d1282: the only routing check — buffered count == 0 sends control into the streaming loop where no size validation exists. - 0x1c01d14840x1c01d148a: the "incomplete message" decision. Setting *rsi (declared length) greater than edi (leftover) forces the ja 0x1c01d14cc branch into the buffering path. - 0x1c01d14cc0x1c01d14d6: the vulnerable instruction sequence — r8d = edi (unbounded), then memcpy into the fixed 0x400 buffer. edi > 0x400 overflows immediately. - 0x1c01d14db: writes the out-of-range count into +0x488, corrupting the length field itself.

The only per-message range check (header path, NOT the loop):

0x1c01d12d0: mov  r14d, dword [r15]            ; buffered message length (r15 = rbx+0x88)
0x1c01d12d3: lea  eax, [r14-0x21]
0x1c01d12d7: cmp  eax, 0x3df                   ; only [0x21,0x400] range check
0x1c01d12dc: ja   0x1c01d1339                  ; out of range -> reject
; header-branch partial append (bounded by the [0x21,0x400] check above and rdi < declared_len - rbp):
0x1c01d12f1: mov  r8, rdi                       ; partial append length = rdi
0x1c01d12f4: mov  rdx, rsi
0x1c01d12f7: call 0x1c00d4d40                   ; memcpy(rbx+0x88+rbp, rsi, rdi)
0x1c01d12fc: add  dword [rbx+0x488], edi        ; count += rdi

Annotations: - 0x1c01d12d30x1c01d12dc: lea eax,[r14-0x21]; cmp eax,0x3df validates the declared header length to [0x21,0x400] — but this constraint is absent from the streaming loop path at 0x1c01d14cc. - 0x1c01d12f7: the header-branch partial append copies rdi bytes at rbx+0x88+rbp, with count += rdi. This copy is bounded: it is only reached when rdi < declared_len - rbp (0x1c01d12ed cmp edi,edx; jnb), and declared_len was already range-checked to [0x21,0x400] at 0x1c01d12d3, so count+rdi < declared_len <= 0x400. It is not an independent overflow (the patch still adds a defense-in-depth RtlUIntAdd/<=0x400 guard to the analogous count<4 paths).

Patched — added safe checks at the append site:

; Feature_4058244411 gate wraps the append. Representative added sequence:
; mov  eax, 0x400
; cmp  r8d, eax                 ; next_msg_len > 0x400 ?
; ja   <reject>
; lea  r8, [rbp-0x30]
; and  dword [rbp-0x30], 0
; call 0x1c00d490c              ; RtlUIntAdd(count, len, &sum)
; js   <reject>                 ; carry/overflow -> reject
; mov  eax, 0x400
; cmp  dword [rbp-0x30], eax    ; sum > 0x400 ?
; ja   <reject>                 ; take error path (result=0), WPP 0x2c-0x31

Annotations: - call 0x1c00d490c (RtlUIntAdd): computes count + incoming_len with overflow detection; js bails on carry. - cmp dword [rbp-0x30], 0x400; ja <reject>: the accumulated total must be <= 0x400 before any copy; otherwise the reject path runs (returns 0, emits WPP events 0x2e/0x2f/0x31). - The memcpy thunk was relocated 0x1c00d4d40 -> 0x1c00d4e40 and an rbp frame pointer was introduced — incidental to the added checks. The fix path is gated on Feature_4058244411__private_IsEnabledDeviceUsage().


5. Trigger Conditions

  1. Reach the target. Establish a network flow that drives the tcpip.sys FSE receive fast-path such that a flow-context object is created and FseProcessIncomingMessages is invoked on received bytes.
  2. Ensure the flow-context buffered-count at flow_ctx+0x488 is 0 — a fresh flow state that routes execution at 0x1c01d1282 into the streaming loop at 0x1c01d1478.
  3. Deliver a single segment whose total length edi = *(arg2+0x38) is greater than 0x400 (must exceed 1024 bytes to overflow the buffer).
  4. Set the leading 4-byte length prefix at the start of the payload (*rsi) to a value greater than the delivered length edi. This makes the loop treat the segment as an "incomplete trailing message" (ja 0x1c01d148a) rather than consuming it, so control falls into the buffering path.
  5. On receipt, the loop reaches memcpy(rbx+0x88, rsi, edi) at 0x1c01d14cc/0x1c01d14d6 and copies edi > 0x400 bytes into the 1024-byte buffer.
  6. Observable effect. The edi bytes beyond 0x400 land past the buffer end, clobbering flow_ctx+0x488 (the count field) and the adjacent pool allocation. Expect a kernel bugcheck such as BAD_POOL_HEADER / 0x139 (KERNEL_SECURITY_CHECK_FAILURE) on the next pool operation, or an immediate access violation if the write runs off the allocation.

Notes: The header-accumulation partial-append at 0x1c01d12f7 is not a second overflow: it is gated by the [0x21,0x400] header-length range check plus the leftover < declared_len - count condition, so count + len stays < declared_len <= 0x400. The single out-of-bounds write is the streaming-loop memcpy at 0x1c01d14cc/0x1c01d14d6. The fix is feature-flag gated (Feature_4058244411), so on a build where the feature is disabled the vulnerable path may still be reachable even on the "patched" binary. Reachability depends on the specific FSE feature/offload being active on the target.


6. Exploit Primitive & Development Notes

  • Primitive: Controlled-length, controlled-content out-of-bounds write into kernel non-paged pool starting at flow_ctx+0x88, spilling past flow_ctx+0x488. The attacker controls both the length (edi, up to a 32-bit value) and the full contents (the segment payload). Overflow begins exactly at the 0x400 boundary of the reassembly buffer.
  • Turning it into a full exploit:
  • Groom the non-paged pool so a controllable victim object sits immediately after the flow-context allocation (spray flow contexts / adjacent same-size allocations, then free a hole in front of the victim).
  • Overwrite an interesting field in the adjacent object — a function pointer, a LIST_ENTRY, or an object-header/size field — with attacker data. Because contents are fully controlled, precise field values are achievable.
  • Defeat KASLR via a separate info leak (this bug itself is a blind write). Corrupting the +0x488 count and any embedded length/size fields may enable a follow-on relative read/write to bootstrap a leak.
  • Drive code execution by hijacking a corrupted function pointer or callback, or pursue a data-only escalation by corrupting a token/privilege structure reachable from the neighboring object.
  • Mitigations to consider:
  • kASLR: The write is blind; you need a separate leak to build a full exploit. As a pure DoS, KASLR is irrelevant.
  • SMEP / SMAP: Do not block the pool write itself; matter only when redirecting execution to a controlled buffer — favor data-only or ROP/JOP within kernel image.
  • CFG / CET: Forward-edge (kCFG) and shadow-stack constrain function-pointer hijacks; prefer corrupting data structures or CFG-valid targets.
  • HVCI / VBS: Blocks unsigned code; a data-only variant (token/privilege or LIST_ENTRY corruption) survives.
  • Pool hardening: Overwriting a pool header risks immediate bugcheck; prefer corrupting in-object fields of a groomed adjacent allocation over the header itself.
  • Realistic outcome: Remote kernel pool corruption / DoS (bugcheck) is straightforward; remote or local privilege escalation 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_1c01d1214)

Breakpoints:

bp tcpip!FseProcessIncomingMessages        ; 0x1c01d1214 entry: capture arg1/arg2, flow_ctx, edi
bp tcpip!FseProcessIncomingMessages+0x2b8  ; 0x1c01d14cc: unbounded copy length set (r8d/edi)
bp tcpip!FseProcessIncomingMessages+0x2c2  ; 0x1c01d14d6: the memcpy call (dest rcx, len edi)
bp tcpip!FseProcessIncomingMessages+0xe3   ; 0x1c01d12f7: header-branch partial append memcpy

Rebase note: the analysis base for these addresses is 0x1c0000000. If the loaded module base differs, subtract 0x1c0000000 from each analysis address to get the RVA (e.g. 0x1c01d14cc -> +0x1d14cc).

Inspect at each breakpoint: - At entry (+0x0, 0x1c01d1214): rcx = arg1, rdx = arg2. Read rsi_data = poi(rcx+0x30), flow_ctx = poi(poi(rcx+0x20)+0xa8), and edi = dwo(rdx+0x38) (attacker length). Record flow_ctx to watch flow_ctx+0x88 (buffer) and flow_ctx+0x488 (count).

r @rcx, @rdx
dq poi(@rcx+0x20)+0xa8 L1        ; flow_ctx
dd poi(poi(@rcx+0x20)+0xa8)+0x488 L1  ; current buffered count
dd @rdx+0x38 L1                  ; incoming length
  • At +0x2b8 (0x1c01d14cc): r8d/edi = copy length (full leftover, unbounded), rcx = flow_ctx+0x88 (start of 0x400 buffer), rsi = source. If edi > 0x400 the following memcpy overflows.
  • At +0x2c2 (0x1c01d14d6): the call 0x1c00d4d40 (memcpy). Read edi (length) and rcx (dest). edi > 0x400 confirms the OOB write. Step over and inspect memory past flow_ctx+0x488.
  • At +0xe3 (0x1c01d12f7): header-branch partial append; r8 = rdi (copy length), rcx = flow_ctx+0x88+ebp. This copy is bounded (count+rdi < declared_len <= 0x400); it is a secondary copy site of interest, not the OOB write.

Trigger setup (from user mode / network): 1. Open a socket / establish the connection that lands on the tcpip FSE fast-path (segment-engine flow), so a flow-context is created and buffered-count +0x488 is 0. 2. Send one message unit whose declared 4-byte length prefix is larger than the delivered length, with the delivered length exceeding 1024 bytes:

payload = <declared_len: uint32 LE, e.g. 0x0000FFFF>  ; > delivered length
        + <filler bytes so total delivered length > 0x400, e.g. 0x800 bytes>

This forces edi > 0x400 into memcpy(flow_ctx+0x88, data, edi). 3. Note: the header-accumulation partial-append at 0x1c01d12f7 cannot be primed into an overflow — it is bounded by the [0x21,0x400] length check and the leftover < declared_len - count condition. The only OOB write is the streaming-loop copy above.

Expected observation: The memcpy at 0x1c01d14d6 writes edi (>0x400) bytes starting at flow_ctx+0x88, clobbering the count field at +0x488 and the following pool allocation. Typical results: BAD_POOL_HEADER, DRIVER_OVERRAN_STACK_BUFFER, or 0x139 on the next pool operation, or an immediate access violation. On the patched binary the same input takes the reject path (returns 0, WPP event 0x2e/0x2f/0x31) with no memcpy.

Struct / offset notes:

+0x088  BYTE[0x400]  reassembly buffer (ends at +0x488)
+0x488  UINT32       buffered byte-count
arg1+0x30            incoming data pointer
*(arg1+0x20)+0xa8    flow-context object base
arg2+0x38            incoming byte length (attacker-controlled)
memcpy thunk         0x1c00d4d40 (unpatched) / 0x1c00d4e40 (patched)
RtlUIntAdd (patched) 0x1c00d490c
FseProcessIncomingMessage (singular) 0x1c01d10f8

8. Changed Functions — Full Triage

  • FseProcessIncomingMessages (sub_1c01d1214) -> FseProcessIncomingMessages (sub_1c01d1214) (similarity 0.697, security_relevant): The unbounded reassembly append is the vulnerability. The patch inserts Feature_4058244411-gated bounds checks — a checked RtlUIntAdd(count, len, &sum) plus sum > 0x400 and next_msg_len/incoming_len > 0x400 comparisons before every append memcpy — rejecting oversized input (result=0) and emitting WPP trace IDs 0x2c0x31. Register/stack-frame reshuffle (rbp frame pointer, memcpy thunk relocation 0x1c00d4d40 -> 0x1c00d4e40) is incidental.
  • IppCreateForwardPath (sub_...) -> IppCreateForwardPath (sub_...) (similarity 0.8814, behavioral): Feature_411806010-gated reordering of *(Entry+0x50) |= 1 (path "valid/ready" flag) so the flag is set before IppPrunePathSetUnderLock/IppRestructureHashTableUnderLock rather than after, both still under the scalable write lock. Concurrency/lifetime hardening only — no buffer-size, copy-length, or bounds changes. Not the memory-safety fix and not independently shown to be attacker-triggerable.

Collapsed note on non-security changes: The IppCreateForwardPath diff is a lock-visibility ordering tweak, and the remaining diffs within FseProcessIncomingMessages (frame-pointer introduction, thunk relocation) are incidental register-allocation/relocation noise — the only meaningful security diff is the added bounds/overflow checks flagged in 2.1.


9. Unmatched Functions

  • Removed (unpatched only): None
  • Added (patched only): None
  • Implication: The patch is purely additive-in-place — no new sanitizer function was introduced as a separate symbol; the bounds checks (RtlUIntAdd at 0x1c00d490c, the >0x400 comparisons, the WPP trace calls) were inlined into FseProcessIncomingMessages and gated behind Feature_4058244411__private_IsEnabledDeviceUsage(). Because the fix is feature-flag gated rather than unconditional, whether the vulnerable path is actually reachable on a given build depends on the feature's runtime state.

10. Confidence & Caveats

  • Confidence: high
  • Rationale:
  • The arithmetic and data flow are unambiguous: edi = *(arg2+0x38) flows directly into the copy length at 0x1c01d14cc/0x1c01d14d6 with no comparison against 0x400, into a fixed buffer at +0x88 that ends at +0x488.
  • The patch is a textbook fix idiom: checked RtlUIntAdd plus explicit > 0x400 bounds checks inserted immediately before the copies, with dedicated telemetry (WPP 0x2c0x31) firing only on the newly-added rejection paths.
  • The header-path length check (lea eax,[r14-0x21]; cmp eax,0x3df) demonstrates the developers' intended [0x21,0x400] invariant, which the loop path violates — confirming the bug is a missing check rather than intended behavior.
  • Assumptions made:
  • The exact network protocol/offload that drives the FSE receive fast-path (and thus how an attacker sets arg1+0x30, arg1+0x20, arg2+0x38) is inferred from the code layout, not fully proven end-to-end.
  • The pool type of the flow-context object is assumed non-paged (typical for network fast-path objects) but not directly confirmed.
  • The default runtime state of Feature_4058244411 on shipping builds is unknown; the patched binary may still expose the old path if the feature is off.
  • To verify before a PoC:
  • Confirm the exact FSE receive path and the traffic shape that yields a flow-context with +0x488 == 0 and reaches FseProcessIncomingMessages with arg2+0x38 > 0x400.
  • Confirm the flow-context object layout (+0x88 buffer size 0x400, +0x488 count) and its pool allocation type/location by breaking at entry and dumping the object.
  • Reproduce the bugcheck on the unpatched binary with the breakpoints in 7.1 and confirm the patched binary takes the reject path (result=0, WPP 0x2e/0x2f/0x31) for the same input — and determine whether the Feature_4058244411 gate is enabled by default.