kdnic.sys — NDIS poll-model + WDF refactor
KB5078752
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.
-
The "missing stack cookie" is a compiler artifact, not a vulnerability. The unpatched function has a 0x40-byte frame with no
__security_cookiebecause it contains no overflowable stack buffer. The 6 bytes it copies (mov eax, [r8+0x14]/movzx eax, word [r8+0x18]at0x1C000251A/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 toNICGetFrameTypeFromDestination. It is a fixed-size read, not a variable-length copy, and cannot overflow. The patchedHandleReceivegets 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. -
The linked-list integrity checks exist in BOTH builds. The unpatched function already fails fast on back-pointer mismatch:
cmp [rax+8], rdx; jnz 0x1C0002658→mov ecx, 3; int 29h(RtlFailFast(FAST_FAIL_CORRUPT_LIST_ENTRY)). The patched build has the identical check (... jnz 0x14000271B→mov ecx, 3; int 29h). The patch does not add list integrity validation. -
The count limit is the poll API contract, not a bounds fix. The
r15d < [rsi]guard limits how many packets a single poll returns becauseNdisPollsupplies 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 atadapter+0x130). - Unpatched send-complete:
TXSendCompleteDpc@0x1C00027D0(timer callback, object atadapter+0xC0). - Patched receive/send:
HandleReceive@0x1400024F8/HandleSend@0x1400027F0, driven byNdisPoll@0x1400017E0afterNdisRegisterPoll(inMPInitializeEx).
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@0x1C0002460→HandleReceive@0x1400024F8(sim 0.5842) — not security-relevant. Receive handler moved from timer callback toNdisPollcallback. 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 theint 29h(code 3) list-integrity check are present and identical in both builds.TXSendCompleteDpc@0x1C00027D0→HandleSend@0x1400027F0(sim 0.4224) — not security-relevant. Transmit-completion handler moved toNdisPollcallback. Adds cookie, poll-budget limit, and a new in-flight list atadapter+0x158. Reads driver-written TCB fields, not attacker network data. Same list-integrity fail-fast in both builds.TXTransmitQueuedSends@0x1C0002920→0x1400010C0(sim 0.6514) — not security-relevant. Pairs entries from two interlocked lists (+0x60/+0x70and+0x80/+0x90in 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 → +0xC0outstanding count,+0xC8 → +0xB8queue count). Per-NET_BUFFER send work extracted intoTXQueueNetBufferForSend(0x140001290). Error status now computed rather than branch-selected.MPInitializeEx@0x1C00072C0(sim 0.2251) — AddsNdisRegisterPollwith two poll handlers atadapter+0x120; reads adapter parameters from new offsets due to the larger (0x308-byte) context; linearized error handling.NICAllocAdapter@0x1C000755C(sim 0.7555) — Adapter allocation grows0x2D0 → 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_cookieand 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→ error0xC0010016, elsememmoveof sizeebx). 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) — TheDispatchLevelargument passed to the NBL-release/complete path changes fromTRUEtoFALSE; list-head offsets shift0x60/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-pointerint 29hfail-fast used elsewhere.MPSetInformation@0x1C0002010(OID set, sim 0.8594) — Field offsets shift only. The OID0x1010Emask check (test ecx, 0xFFFFFFD0) is present and identical in both builds.DriverEntry@0x1C0009080(sim 0.2233) — Patched adds WDF class binding (WdfVersionBind,WdfLdrQueryInterface) and aDriverUnloadhook, and still callsNdisMRegisterMiniportDriver. DiagnosticDbgPrintExon failure paths.
Also verified as no security change
NICSetMulticastList— Multicast-list length validation (must be a multiple of 6, error0xC0010014; capped at0xC0= 32 MACs, error0xC0010009; boundedmemset/memmove) is present and identical in both builds.HWGetDestinationAddress@0x1C0002D40→MdlSpanIterateBuffers@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 callerTXQueueNetBufferForSend(0x140001290) pre-checks the packet length (cmp dword [rdx+18h], 6; jb→ error0xC0010015) 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 viaint 29h(code0x1C) 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. RxReceiveIndicateDpc → HandleReceive, TXSendCompleteDpc → HandleSend). 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.