1. Overview

Item Value
Unpatched binary kdnic_unpatched.sys
Patched binary kdnic_patched.sys
Overall similarity 0.4943
Functions matched 58
Functions changed 43
Functions identical 15
Unmatched (either direction) 0

Diff statistics - The large change set (43/58 functions changed) and low overall similarity (~49%) reflect a broad architectural rewrite, not a targeted security fix. - The patch moves kdnic.sys from the legacy NDIS timer-callback datapath (packets processed inside NdisAllocateTimerObject callbacks) to the newer NdisRegisterPoll poll model, and adds WDF class binding (WdfVersionBind) and ETW TraceLogging instrumentation.

One-sentence verdict: No security-relevant change was delivered. The patch is a datapath refactor (legacy NDIS timer callbacks → NdisRegisterPoll poll handlers, plus WDF binding and ETW tracing); the stack cookies that appear in the rewritten receive/send handlers are a compiler artifact of their new, larger stack frames (which now hold local ETW descriptor arrays), not a fix for any demonstrable stack-corruption bug.


2. Vulnerability Summary

No exploitable vulnerability was identified in either build, and no security-relevant behavioral difference was found between them. The two functions examined below were the strongest candidates in the report; both are re-classified as no security-relevant change after verification against the binaries.

Finding 1 — No security-relevant change

Field Value
Function (unpatched) RxReceiveIndicateDpc @ 0x1C0002460
Function (patched) HandleReceive @ 0x1400024F8
Class Datapath refactor (timer callback → NDIS poll handler); stack cookie added as compiler artifact
Severity None

What actually changed. In the unpatched build RxReceiveIndicateDpc runs as an NDIS timer callback (registered via NdisAllocateTimerObject, timer object stored at adapter+0x130 in NICAllocAdapter). It flushes the received-packet SList at adapter_ext+0x40, classifies each frame by its destination MAC, and indicates the resulting list with NdisMIndicateReceiveNetBufferLists. In the patched build the same work is done by HandleReceive, a poll callback invoked through NdisPoll after the driver registers with NdisRegisterPoll. The poll callback returns the built list to NDIS via its output parameter ([rsi+0x10]) and honours the poll budget (cmp r15d, [rsi]; jnb at 0x1400026CF) instead of indicating inline.

Why this is not a security fix.

  1. The "missing stack cookie" is a compiler artifact, not a vulnerability. The unpatched function has a 0x40-byte frame with no __security_cookie because it contains no overflowable stack buffer. The 6 bytes it copies (mov eax, [r8+0x14] / movzx eax, word [r8+0x18] at 0x1C000251A/0x1C0002522, into [rsp+0x68]/[rsp+0x6C]) are the destination-MAC field of a driver-managed descriptor, copied into a fixed 6-byte slot that is passed by pointer to NICGetFrameTypeFromDestination. It is a fixed-size read, not a variable-length copy, and cannot overflow. The patched HandleReceive gets a cookie because its 0xD0-byte frame now holds local ETW TraceLogging descriptor arrays passed to _tlgWriteTransfer_EtwWriteTransfer; the compiler protects those, not any newly discovered bug.

  2. The linked-list integrity checks exist in BOTH builds. The unpatched function already fails fast on back-pointer mismatch: cmp [rax+8], rdx; jnz 0x1C0002658mov ecx, 3; int 29h (RtlFailFast(FAST_FAIL_CORRUPT_LIST_ENTRY)). The patched build has the identical check (... jnz 0x14000271Bmov ecx, 3; int 29h). The patch does not add list integrity validation.

  3. The count limit is the poll API contract, not a bounds fix. The r15d < [rsi] guard limits how many packets a single poll returns because NdisPoll supplies a per-poll budget; the remainder is handled on the next poll. The unpatched loop is already bounded (it drains a finite flushed SList). This is not an unbounded-loop fix.

Registration (both builds, unpatched addresses): NICAllocAdapter @ 0x1C000755C allocates the 0x2D0-byte adapter context (0x1C000758E) and registers the timer callback at 0x1C0007892 (lea rax, RxReceiveIndicateDpc; object stored at adapter+0x130).

Finding 2 — No security-relevant change

Field Value
Function (unpatched) TXSendCompleteDpc @ 0x1C00027D0
Function (patched) HandleSend @ 0x1400027F0
Class Datapath refactor (send-complete timer callback → NDIS poll handler)
Severity None

What actually changed. TXSendCompleteDpc is the transmit-completion handler (not a receive-return handler): it flushes the send-complete SList at adapter_ext+0x20 (0x1C00027EA), and for each completed transmit control block either records a send failure (inc [rbx+0x2B0]; sets NBL status 0xC000009A = STATUS_INSUFFICIENT_RESOURCES at 0x1C00028A9) or updates the send statistics (+0x248/+0x250/+0x258 counts and +0x278/+0x280/+0x288 byte totals), then calls TCBFree and finally TXTransmitQueuedSends. The status byte it reads ([rax+0x1C]) and the frame type ([rdx+0x28]) are fields the driver's own send path wrote, not attacker-supplied network data. In the patched build HandleSend performs the same completion work as an NdisPoll callback, bounded by the poll budget (cmp r12d, [r14]; jnb) and using a new in-flight list at adapter+0x158.

Why this is not a security fix. Same reasoning as Finding 1: the unpatched function has a 0x30-byte frame with no stack buffer and therefore no cookie; it already fails fast on list corruption (mov ecx, 3; int 29h at 0x1C0002902); and the patched cookie/count-limit/refcount changes follow from the poll-model rewrite and ETW instrumentation, not from a demonstrable corruption bug. The registration is NdisAllocateTimerObject with the timer object stored at adapter+0xC0 (0x1C0007840 in NICAllocAdapter).


3. Pseudocode Diff

RxReceiveIndicateDpc (unpatched) → HandleReceive (patched)

// === UNPATCHED RxReceiveIndicateDpc @ 0x1C0002460 (no cookie; fixed 6-byte read) ===
char RxReceiveIndicateDpc(ctx *a1, ctx *a2) {
    ext = *(void**)(a2 + 0x30);              // adapter extension
    if (!ext) { RXScheduleTheReceiveIndication(a2); return; }
    ...
    SLIST_ENTRY *p = ExpInterlockedFlushSList(ext + 0x40);  // received packets
    if (p) {
        LIST_ENTRY sentinel; InitializeListHead(&sentinel); // stack-local list
        int   dst6;                          // [rsp+0x68] 6-byte dest-MAC slot
        short dst6_hi;                        // [rsp+0x6C]
        do {
            sg = p->ScatterGather;            // [p+0x28]
            dst6    = *(int*)(sg + 0x14);     // fixed 4-byte read
            dst6_hi = *(short*)(sg + 0x18);   // fixed 2-byte read
            switch (NICGetFrameTypeFromDestination(&dst6)) { ... stats ... }
            if (*(void**)(sentinel.Blink + 8) != &sentinel) __fastfail(3);  // present here
            InsertTailList(&sentinel, &p->link);
            p = next;
        } while (next);
        ... drain sentinel, NdisMIndicateReceiveNetBufferLists(..., 0x401) ...
    }
    RXScheduleTheReceiveIndication(a2);
    // no cookie because there is no overflowable stack buffer
}

// === PATCHED HandleReceive @ 0x1400024F8 (poll callback; cookie from ETW locals) ===
char HandleReceive(ctx *a1, NDIS_POLL_RECEIVE *poll) {
    cookie = __security_cookie ^ &frame;     // frame now holds ETW descriptor arrays
    ext = *(void**)(a1 + 0x30);
    ...
    while (built < poll->max /* [rsi] */) {  // NDIS poll budget
        ...
        if (*(void**)(entry.Blink + 8) != &sentinel) __fastfail(3);  // SAME check
        ...
    }
    poll->list = built; poll->count = ...;   // return to NDIS instead of inline indicate
    if (dword_140008000 > 5) EtwWriteTransfer(...);   // added tracing
    __security_check_cookie(cookie ^ &frame);
}

The differences are: timer callback → poll callback, inline indicate → list returned via poll output parameter, added ETW tracing, and a stack cookie protecting the new ETW local descriptors. The 6-byte destination-MAC read and the __fastfail(3) list-integrity check are the same in both builds.

TXSendCompleteDpc (unpatched) → HandleSend (patched)

// === UNPATCHED TXSendCompleteDpc @ 0x1C00027D0 (transmit completion) ===
void TXSendCompleteDpc(ctx *a1, ctx *a2) {
    ext = *(void**)(a2 + 0x30);
    SLIST_ENTRY *c = ExpInterlockedFlushSList(ext + 0x20);   // completed sends
    // build stack-local list, then for each completed TCB:
    if (*(int*)(tcb + 0x1C) != 0) {            // driver-set completion flag
        ++*(_QWORD*)(a2 + 0x2B0);              // send-error count
        *(int*)(nbl + 0x8C) = 0xC000009A;      // STATUS_INSUFFICIENT_RESOURCES
    } else { ... update send stats at +0x248/+0x250/+0x258, +0x278/+0x280/+0x288 ... }
    TCBFree(a2, tcb);
    // __fastfail(3) on list corruption (present here too)
    TXTransmitQueuedSends(a2);
    // no cookie; no stack buffer
}

// === PATCHED HandleSend @ 0x1400027F0 (poll callback) ===
char HandleSend(ctx *a1, NDIS_POLL_TRANSMIT *poll) {
    cookie = __security_cookie ^ &frame;
    ... same completion work, bounded by poll->max ([r14]), using in-flight list at +0x158 ...
    __security_check_cookie(cookie ^ &frame);
}

4. Assembly Analysis

RxReceiveIndicateDpc @ 0x1C0002460 — Unpatched

; ---- prologue: 0x40 frame, NO cookie (no overflowable buffer to protect) ----
0x1C0002460  mov     [rsp+8], rbx
0x1C0002465  mov     [rsp+18h], rbp
0x1C000246A  push    rsi
0x1C000246B  push    rdi
0x1C000246C  push    r14
0x1C000246E  sub     rsp, 40h
0x1C0002472  mov     rdi, [rdx+30h]          ; adapter extension
0x1C0002476  mov     rbx, rdx
0x1C0002479  test    rdi, rdi
0x1C000247C  jz      0x1C000263D
; ---- refill free descriptors, poll break-in ----
0x1C000248C  call    RCBAllocate
0x1C00024CC  lea     rcx, [rdi+40h]
0x1C00024D0  call    ExpInterlockedFlushSList ; received packet SList
0x1C00024DC  mov     rdx, rax
0x1C00024E2  jz      0x1C000263D
; ---- stack-local list sentinel at [rsp+0x30] ----
0x1C00024E8  lea     rax, [rsp+30h]
0x1C00024ED  mov     [rsp+38h], rax
0x1C00024F7  mov     [rsp+30h], rax
; ---- fixed 6-byte destination-MAC read for classification ----
0x1C0002515  lea     rcx, [rsp+68h]
0x1C000251A  mov     eax, [r8+14h]           ; 4 bytes (descriptor field)
0x1C000251E  mov     [rsp+68h], eax
0x1C0002522  movzx   eax, word ptr [r8+18h]  ; 2 bytes
0x1C0002527  mov     [rsp+6Ch], ax
0x1C000252C  call    NICGetFrameTypeFromDestination
; ---- InsertTailList with integrity check (PRESENT in unpatched) ----
0x1C0002575  mov     rax, [rsp+30h]
0x1C0002583  cmp     [rax+8], rdx
0x1C0002587  jnz     0x1C0002658             ; -> RtlFailFast(3)
; ---- indicate, cleanup; NO cookie check ----
0x1C0002631  call    NdisMIndicateReceiveNetBufferLists
0x1C0002640  call    RXScheduleTheReceiveIndication
0x1C0002657  retn
; ---- list-corruption fail-fast landing pad ----
0x1C0002658  mov     ecx, 3
0x1C000265D  int     29h                     ; RtlFailFast(FAST_FAIL_CORRUPT_LIST_ENTRY)

HandleReceive @ 0x1400024F8 — Patched

; ---- prologue: 0xD0 frame WITH cookie (frame holds ETW descriptor locals) ----
0x140002508  lea     rbp, [rsp-27h]
0x14000250D  sub     rsp, 0D0h
0x140002514  mov     rax, cs:__security_cookie
0x14000251B  xor     rax, rsp
0x14000251E  mov     [rbp+17h], rax          ; store cookie
; ---- same fixed 6-byte read ----
0x140002600  mov     eax, [r9+14h]
0x140002604  mov     [rbp-0Bh], eax
0x140002607  movzx   eax, word ptr [r9+18h]
0x14000260C  mov     [rbp-0Fh], ax
0x140002610  call    NICGetFrameTypeFromDestination
; ---- poll budget limit ----
0x1400026CF  cmp     r15d, [rsi]
0x1400026D2  jnb     0x140002722
; ---- SAME list integrity fail-fast ----
0x1400026D4  cmp     [rcx+8], rdi
0x1400026D8  jnz     0x14000271B
0x14000271B  mov     ecx, 3
0x140002720  int     29h                     ; identical to unpatched
; ---- ETW tracing (added) ----
0x1400025A1  call    _tlgWriteTransfer_EtwWriteTransfer
0x1400027BD  call    _tlgWriteTransfer_EtwWriteTransfer
; ---- cookie check (from the larger ETW frame) ----
0x1400027C2  mov     rcx, [rbp+17h]
0x1400027C6  xor     rcx, rsp
0x1400027C9  call    __security_check_cookie

The prologue/epilogue cookie is the only "protection" difference, and it tracks the new ETW local storage in the 0xD0-byte frame. The receive-path integrity check (int 29h, code 3) is byte-for-byte the same behaviour in both builds.

TXSendCompleteDpc @ 0x1C00027D0 — Unpatched

0x1C00027D0  push    rbx
0x1C00027D2  sub     rsp, 30h                ; no cookie, no stack buffer
0x1C00027D6  mov     rcx, [rdx+30h]          ; adapter extension
0x1C00027E6  add     rcx, 20h
0x1C00027EA  call    ExpInterlockedFlushSList ; send-complete SList
...
0x1C0002894  mov     eax, [rax+1Ch]          ; driver-set completion flag
0x1C0002898  inc     qword ptr [rbx+2B0h]    ; send-error count (failure branch)
0x1C00028A9  mov     dword ptr [rcx+8Ch], 0C000009Ah ; STATUS_INSUFFICIENT_RESOURCES
...
0x1C00028F3  call    TCBFree
0x1C0002902  mov     ecx, 3
0x1C0002907  int     29h                     ; same list-corruption fail-fast
0x1C000290C  call    TXTransmitQueuedSends
0x1C0002916  retn

The patched HandleSend @ 0x1400027F0 uses a 0xA0-byte frame with a cookie at [rbp+0x17], a poll-budget limit (cmp r12d, [r14]; jnb), and a new in-flight list at adapter+0x158/+0x168 — all consequences of the poll-model rewrite and ETW additions, not a corruption fix.


5. Trigger Conditions

Not applicable. No exploitable condition was demonstrated in either build. The receive/send handlers process fixed-size descriptor fields and finite flushed SLists, guarded in both builds by the same RtlFailFast(FAST_FAIL_CORRUPT_LIST_ENTRY) list-integrity checks. The datapath is reached in the ordinary course of KDNET operation (the timer callbacks, or in the patched build the NdisPoll callbacks, fire to drain hardware receive/transmit queues); there is no attacker-controllable length or pointer that reaches an unguarded write.


6. Exploit Primitive & Development Notes

Not applicable. No corruption primitive exists. The candidate functions have no variable-length stack copy, no attacker-controlled size or pointer feeding an unchecked write, and the same list-integrity fail-fasts in both builds. The absence of a __security_cookie in the unpatched receive/send handlers reflects that the compiler found no overflowable stack buffer to protect, not a bypassed mitigation.


7. Debugger PoC Playbook

Not applicable. There is no vulnerability to reproduce. For reference, the datapath entry points are:

  • Unpatched receive: RxReceiveIndicateDpc @ 0x1C0002460 (timer callback, object at adapter+0x130).
  • Unpatched send-complete: TXSendCompleteDpc @ 0x1C00027D0 (timer callback, object at adapter+0xC0).
  • Patched receive/send: HandleReceive @ 0x1400024F8 / HandleSend @ 0x1400027F0, driven by NdisPoll @ 0x1400017E0 after NdisRegisterPoll (in MPInitializeEx).

Verified struct offsets (unpatched adapter context, 0x2D0 bytes):

adapter_ctx (unpatched)
  +0x28    NdisMiniportAdapterHandle
  +0x30    adapter extension pointer
  +0xC0    NdisTimerObject -> TXSendCompleteDpc
  +0x130   NdisTimerObject -> RxReceiveIndicateDpc
  +0x138   NdisTimerObject -> NICAsyncResetOrPauseDpc
  +0x230   rx unicast count      +0x260 rx unicast bytes
  +0x238   rx multicast count    +0x268 rx multicast bytes
  +0x240   rx broadcast count    +0x270 rx broadcast bytes
  +0x248   tx unicast count      +0x278 tx unicast bytes
  +0x250   tx multicast count    +0x280 tx multicast bytes
  +0x258   tx broadcast count    +0x288 tx broadcast bytes
  +0x2B0   tx send-error count

adapter_ext = adapter_ctx->extension  (adapter_ctx+0x30)
  +0x20    send-complete SList head    (TXSendCompleteDpc)
  +0x40    received-packet SList head  (RxReceiveIndicateDpc)

received-packet descriptor (SList entry)
  +0x00    next (SList link)
  +0x10    LIST_ENTRY link
  +0x20    NET_BUFFER_LIST *
  +0x28    scatter/gather descriptor  (holds destination MAC at +0x14/+0x18)

8. Changed Functions — Full Triage

Datapath refactor (timer callbacks → NDIS poll model)

  • RxReceiveIndicateDpc @ 0x1C0002460HandleReceive @ 0x1400024F8 (sim 0.5842) — not security-relevant. Receive handler moved from timer callback to NdisPoll callback. Adds a stack cookie (compiler artifact of the ETW-descriptor locals in the larger frame), a poll-budget count limit, and ETW tracing. The 6-byte destination-MAC classification read and the int 29h (code 3) list-integrity check are present and identical in both builds.
  • TXSendCompleteDpc @ 0x1C00027D0HandleSend @ 0x1400027F0 (sim 0.4224) — not security-relevant. Transmit-completion handler moved to NdisPoll callback. Adds cookie, poll-budget limit, and a new in-flight list at adapter+0x158. Reads driver-written TCB fields, not attacker network data. Same list-integrity fail-fast in both builds.
  • TXTransmitQueuedSends @ 0x1C00029200x1400010C0 (sim 0.6514) — not security-relevant. Pairs entries from two interlocked lists (+0x60/+0x70 and +0x80/+0x90 in unpatched; shifted in patched) and pushes scatter/gather descriptors. Adds ETW telemetry and cookie; no stack buffer and no attacker-controlled size.

Behavioral only (structural refactor around poll model)

  • MPSendNetBufferLists @ 0x1C00021B0 (sim 0.666) — Adapter field offsets shift (+0xD0 → +0xC0 outstanding count, +0xC8 → +0xB8 queue count). Per-NET_BUFFER send work extracted into TXQueueNetBufferForSend (0x140001290). Error status now computed rather than branch-selected.
  • MPInitializeEx @ 0x1C00072C0 (sim 0.2251) — Adds NdisRegisterPoll with two poll handlers at adapter+0x120; reads adapter parameters from new offsets due to the larger (0x308-byte) context; linearized error handling.
  • NICAllocAdapter @ 0x1C000755C (sim 0.7555) — Adapter allocation grows 0x2D0 → 0x308. Timer/SList field offsets shift; the poll registration replaces some of the timer wiring.
  • MPQueryInformation @ 0x1C0001B98 (OID query, sim 0.2646) — Rewritten to a table-driven OID dispatch; statistics offsets shift. The unpatched build carries a __security_cookie and the patched build (0x140001EE0) drops it. This is not a regression: the only stack buffer is the fixed vendor string "Microsoft" written with constant data and copied out to the caller, and every output copy is bounded by the same caller-length check in both builds (cmp ebx, [rsi+0x30]; ja → error 0xC0010016, else memmove of size ebx). With no variable-length attacker write into the stack buffer, the compiler's stack-protection heuristic legitimately omits the cookie in the patched codegen.
  • TCBFree @ 0x1C0002BB0 (sim 0.6869) — The DispatchLevel argument passed to the NBL-release/complete path changes from TRUE to FALSE; list-head offsets shift 0x60/0x70 → 0x58/0x68. Behavioral correctness change (IRQL claim on completion), not a memory-safety fix.
  • TXFlushSendQueue @ 0x1C0002668 (sim 0.3159) — Drains an additional in-flight list (+0x158/+0x168) in the patched build, with the same back-pointer int 29h fail-fast used elsewhere.
  • MPSetInformation @ 0x1C0002010 (OID set, sim 0.8594) — Field offsets shift only. The OID 0x1010E mask check (test ecx, 0xFFFFFFD0) is present and identical in both builds.
  • DriverEntry @ 0x1C0009080 (sim 0.2233) — Patched adds WDF class binding (WdfVersionBind, WdfLdrQueryInterface) and a DriverUnload hook, and still calls NdisMRegisterMiniportDriver. Diagnostic DbgPrintEx on failure paths.

Also verified as no security change

  • NICSetMulticastList — Multicast-list length validation (must be a multiple of 6, error 0xC0010014; capped at 0xC0 = 32 MACs, error 0xC0010009; bounded memset/memmove) is present and identical in both builds.
  • HWGetDestinationAddress @ 0x1C0002D40MdlSpanIterateBuffers @ 0x140002EA0 — The unpatched specialized routine walks the MDL chain with a literal 14-byte (0x0E) cap (mov eax, 0Eh; sub eax, edi; cmovb eax, ecx) but only consumes the first 6 bytes (destination MAC). The patched build re-implements this as a generic, parameter-driven MDL iterator: the caller TXQueueNetBufferForSend (0x140001290) pre-checks the packet length (cmp dword [rdx+18h], 6; jb → error 0xC0010015) and sets the read count to 6 (mov qword [r11-20h], 6), and each span is clamped to the requested count (cmp rcx, rbx; cmovb), fail-fasting via int 29h (code 0x1C) if the chain ends early. Both builds read exactly the bounded destination MAC; the cap became a runtime parameter (6) instead of a literal (14). Not a security change.
  • NICReadRegParameters — Reads admin-controlled NDIS configuration values; no attacker-controlled buffer copy.

Cosmetic / register-allocation only

The remaining changed functions exhibit only register renaming and basic-block reordering induced by the larger adapter structure and the new poll handlers — no semantic difference.


9. Unmatched Functions

unmatched_unpatched and unmatched_patched are both 0: every function matches a counterpart across the rewrite (some renamed/split, e.g. RxReceiveIndicateDpcHandleReceive, TXSendCompleteDpcHandleSend). The patched build additionally links WDF/TraceLogging support routines, consistent with a framework-and-instrumentation refactor rather than a targeted security patch. No defensive check was removed in either direction.


10. Confidence & Caveats

Confidence: High that this is not a security-relevant change.

What is established from both builds: - The unpatched receive/send handlers lack __security_cookie because they contain no overflowable stack buffer; the patched poll handlers gain a cookie because their larger frames now hold local ETW descriptor arrays. This is a compiler artifact of the refactor, not a fixed vulnerability. - The linked-list integrity fail-fasts (RtlFailFast(FAST_FAIL_CORRUPT_LIST_ENTRY), int 29h with code 3) are present and identical in both builds. - The poll-budget count limits in the patched handlers are the NdisPoll API contract, not a bounds fix; the unpatched loops are already bounded by finite flushed SLists. - The classic input-validation sites — NICSetMulticastList (multiple-of-6 and 0xC0 cap), MPSetInformation (OID 0x1010E mask), HWGetDestinationAddress (14-byte MDL cap) — are byte-for-byte equivalent across builds. - MPQueryInformation carries a __security_cookie in the unpatched build; the patched rewrite drops it, which is a stack-protection codegen change (all writes to its constant "Microsoft" stack buffer are copied out under the same 0xC0010016 caller-length bounds check in both builds), not a security-relevant behavioral difference.

Overall, the change set is an architecture migration from the legacy NDIS timer-callback datapath to NdisRegisterPoll, plus WDF class binding and ETW TraceLogging. No security-relevant behavioral difference was found, and no exploitable primitive exists in either build.