1. Overview

  • Unpatched Binary: afd_unpatched.sys
  • Patched Binary: afd_patched.sys
  • Overall Similarity Score: 0.6712 (67.12%)
  • Diff Statistics:
  • Matched Functions: 1170
  • Identical Functions: 254
  • Changed Functions: 916
  • Unmatched (Unpatched/Patched): 0 / 0
  • Verdict: The dominant security-relevant change is a driver-wide rollout of reference-count integrity checks. Plain interlocked reference-count increments were replaced with checked 64-bit interlocked increments that immediately bugcheck (__fastfail(0xE), FAST_FAIL_INVALID_REFERENCE_COUNT) when the resulting count is not greater than 1, i.e. when code takes a new reference on an object whose count was already 0. This is a defense-in-depth mitigation against reference-count-based use-after-free. The unpatched build has this check at 2 sites; the patched build has it at 227. No out-of-bounds read/write bug was found in the buffer-handling paths: the AFD accept/listen data path bounds checks are logically equivalent between the two builds.

2. Vulnerability Summary

Finding 1: AfdServiceWaitForListen — buffer validation is unchanged; only the reference-count integrity check was added

  • Severity: N/A (no buffer bug); the only behavioral change is defense-in-depth hardening (see Finding 2)
  • Vulnerability Class: None demonstrable from the diff (buffer validation unchanged)
  • Affected Function: AfdServiceWaitForListen @ 0x1C00197C0 (patched @ 0x140043C80)
  • What actually changed: The bounds-check block was refactored from a goto-based sequence into one compound conditional, but the conditions are logically identical in both builds. When endpoint flag bit 0x100 is clear, both builds check only the primary size field (*(a2+0xa0) <= Parameters.Create.Options) and do not check the secondary field; when bit 0x100 is set, both builds additionally check *(a2+0xa0)+6 and, if the secondary maximum is non-zero, *(a2+0xa8)+6. Both builds read the copy source from the same indirect pointer *(a2+0x98) with length *(*(a2+0x98)+4)+2. The struct field offsets shifted (the endpoint context grew), which accounts for most of the byte-level diff. The one behavioral addition is the reference-count integrity fastfail described in Finding 2, which this function also received.
  • Note: An earlier hypothesis of an out-of-bounds write via a skipped secondary-buffer check is not supported by the binaries. The "unvalidated secondary buffer" branch exists identically in the patched build, and the copy already uses the validated *(a2+0x98) pointer in both builds.

Finding 2: Reference-count integrity fastfail added to interlocked increments (use-after-free hardening)

  • Severity: Medium (defense-in-depth hardening; no specific exploitable root cause is visible in the diff)
  • Vulnerability Class: Use-after-free / reference-count integrity (CWE-416)
  • Affected Functions (examples): AfdServiceWaitForListen @ 0x1C00197C0, AfdRestartBufferReceiveWithUserIrp @ 0x1C0005A10, AfdTLTPacketsSend @ 0x1C003EFE8, and ~225 further sites across the driver
  • Root Cause / Mechanism: In the unpatched build, taking a new reference on an AFD object is a plain 32-bit interlocked increment (lock inc dword ptr [obj+off]) with no validation of the result. If a reference is taken on an object whose reference count has already reached 0 (a use-after-free / reference-count-underflow condition), the unpatched code silently revives the dangling object and continues. The patched build performs the increment as a 64-bit interlocked add and then checks the result: if the new count is not greater than 1, it executes __fastfail(0xE) (int 29h with ecx = 0xE, FAST_FAIL_INVALID_REFERENCE_COUNT), halting the machine before the corrupted object can be used.
  • Entry Point & Data Flow:
  • An AFD object (endpoint / connection / receive-repost context) reaches reference count 0 through a teardown path.
  • A concurrent path (pending I/O completion, receive repost, or accept/listen servicing) takes a fresh reference on that object.
  • Unpatched: the increment succeeds silently and the object is used after free.
  • Patched: the checked increment observes a resulting count of 1 or less and bugchecks via __fastfail(0xE).
  • Honest scope: This is a systematic mitigation applied broadly (227 sites), not a single targeted logic fix. The diff shows the guard being added, but it does not by itself prove a reachable, attacker-controllable underflow. Treat this as reference-count hardening rather than a demonstrated exploitable primitive.

3. Pseudocode Diff

Bounds check in AfdServiceWaitForListen — unchanged logic (refactor only)

// === UNPATCHED (0x1C00197C0) ===
if ( (*(_DWORD *)(FsContext + 8) & 0x100) == 0 )          // flag bit 0x100 clear
{
    if ( *(_DWORD *)(a2 + 0xA0) <= CurrentStackLocation->Parameters.Create.Options )
        goto MAP_AND_COPY;                                // primary only; secondary not checked
    goto ERROR;                                           // -1073741789 (0xC0000023)
}
if ( *(_DWORD *)(a2 + 0xA0) + 6 > CurrentStackLocation->Parameters.Create.Options )
    goto ERROR;
LowPart = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart;   // secondary maximum
if ( LowPart != 0 && *(_DWORD *)(a2 + 0xA8) + 6 > LowPart )
    goto ERROR;
MAP_AND_COPY:
    v15 = AfdMapMdlChain(Irp->MdlAddress);
    // ... copy uses *(a2+0x98) as source in both builds ...
    memmove(dst, (const void *)(*(_QWORD *)(a2 + 0x98) + 6), *(_WORD *)(*(_QWORD *)(a2 + 0x98) + 4) + 2);

// === PATCHED (0x140043C80) ===
// Same three conditions, expressed as one compound predicate (logically identical):
if ( ((flags & 0x100) != 0 || *(a2 + 0xA0) <= max_primary)
  && ((flags & 0x100) == 0 || *(a2 + 0xA0) + 6 <= max_primary)
  && ((flags & 0x100) == 0 || (v9 = max_secondary) == 0 || *(a2 + 0xA8) + 6 <= v9) )
{
    v10 = AfdMapMdlChain(Irp->MdlAddress);
    // ... identical copy: source *(a2+0x98)+6, length *(*(a2+0x98)+4)+2 ...
}
else
    return STATUS_INVALID_PARAMETER;   // 0xC0000023

Reference-count integrity check in AfdRestartBufferReceiveWithUserIrp

// === UNPATCHED (0x1C0005A10) ===
*(_DWORD *)(a3 + 4) |= 0x10000000u;
_InterlockedIncrement((volatile signed __int32 *)(a3 + 0x30));   // 32-bit, result ignored
AfdLRListAddItem((PSLIST_ENTRY)(a3 + 0xC0));

// === PATCHED (0x14000C0B0) ===
*(_DWORD *)(a3 + 4) |= 0x10000000u;
if ( _InterlockedIncrement64((volatile signed __int64 *)(a3 + 0x30)) <= 1 )
    __fastfail(0xEu);                                            // FAST_FAIL_INVALID_REFERENCE_COUNT
AfdLRListAddItem((PSLIST_ENTRY)(a3 + 0xC0));

The same increment-then-check transformation appears in AfdServiceWaitForListen (field +0x40, was +0x38) and twice in AfdTLTPacketsSend (fields +0x30 and +0x40, the latter was +0x38). The field-offset shifts reflect struct-layout growth, not a logic change.


4. Assembly Analysis

AfdRestartBufferReceiveWithUserIrp — the reference-count fastfail

// === UNPATCHED AfdRestartBufferReceiveWithUserIrp @ 0x1C0005A10 ===
00000001C002290E  mov     [rdi+4], r8d
00000001C0022912  lock inc dword ptr [rdi+30h]      ; refcount += 1 (32-bit, unchecked)
00000001C0022916  lea     rcx, [rdi+0C0h]           ; ListEntry
00000001C002291D  lea     rdx, AfdLRRepostReceive
00000001C0022924  call    AfdLRListAddItem

// === PATCHED AfdRestartBufferReceiveWithUserIrp @ 0x14000C0B0 ===
000000014000C249  mov     rax, rsi                  ; rsi = 1
000000014000C24C  mov     [rbx+4], edx
000000014000C24F  lock xadd [rbx+30h], rax          ; rax = old refcount; refcount += 1 (64-bit)
000000014000C255  add     rax, rsi                  ; rax = new refcount
000000014000C258  cmp     rax, rsi                  ; new count vs 1
000000014000C25B  jg      short loc_14000C264       ; new > 1 -> continue
000000014000C25D  mov     ecx, 0Eh                  ; FAST_FAIL_INVALID_REFERENCE_COUNT
000000014000C262  int     29h                       ; __fastfail(0xE)
000000014000C264  lea     rcx, [rbx+0C0h]           ; ListEntry
000000014000C26B  lea     rdx, AfdLRRepostReceive
000000014000C272  call    AfdLRListAddItem

AfdServiceWaitForListen — same transformation

// === UNPATCHED AfdServiceWaitForListen @ 0x1C00197C0 ===
00000001C002D7B4  lock inc dword ptr [r14+38h]      ; refcount += 1 (32-bit, unchecked)
00000001C002D7B9  lea     rcx, [r14+80h]
00000001C002D7C0  mov     rdx, [rcx+8]

// === PATCHED AfdServiceWaitForListen @ 0x140043C80 ===
0000000140043F69  mov     rax, r12                  ; r12 = 1
0000000140043F6C  lock xadd [r14+40h], rax          ; rax = old refcount; refcount += 1 (64-bit)
0000000140043F72  add     rax, r12                  ; rax = new refcount
0000000140043F75  cmp     rax, r12                  ; new count vs 1
0000000140043F78  jg      short loc_140043F81       ; new > 1 -> continue
0000000140043F7A  mov     ecx, 0Eh                  ; FAST_FAIL_INVALID_REFERENCE_COUNT
0000000140043F7F  int     29h                       ; __fastfail(0xE)
0000000140043F81  lea     rcx, [r14+90h]

AfdTLTPacketsSend — same transformation plus allocation API update

// === UNPATCHED AfdTLTPacketsSend @ 0x1C003EFE8 ===
00000001C003F09B  lock inc dword ptr [r12+38h]      ; refcount += 1 (32-bit, unchecked)
00000001C003F0A6  lock inc dword ptr [rax+30h]      ; refcount += 1 (32-bit, unchecked)
00000001C003F116  call    cs:__imp_ExAllocatePoolWithQuotaTag ; ntoskrnl (size 0x30, tag 'AfdF')

// === PATCHED AfdTLTPacketsSend @ 0x140053CE8 ===
0000000140053DC6  lock xadd [r13+40h], rax          ; checked increment -> __fastfail(0xE) on <= 1
0000000140053DD7  int     29h
0000000140053DDE  lock xadd [rcx+30h], rax          ; checked increment
0000000140053DEC  mov     ecx, 0Eh
0000000140053DF1  int     29h
0000000140053E6F  call    cs:__imp_ExAllocatePool2  ; ntoskrnl (size 0x30/48, tag 'AfdF')

The allocation change in AfdTLTPacketsSend is ExAllocatePoolWithQuotaTag(NonPagedPool|Quota, 0x30, 'AfdF') becoming ExAllocatePool2(0x61, 48, 'AfdF'). The size (0x30 = 48) and pool tag (0x46646641 = 'AfdF') are identical; this is the standard modernization to the ExAllocatePool2 API, not a size correction.


5. Trigger Conditions

Reaching the reference-count fastfail

The fastfail fires only if an AFD object's reference count is already at or below 0 at the moment a new reference is taken. From user mode this requires driving the driver into a reference-count underflow, which is not demonstrated by the diff. A plausible race to probe: 1. Create a TCP socket via WSASocket(AF_INET, SOCK_STREAM, 0) and connect/bind it. 2. Issue an overlapped WSARecv (mapped to AFD_RECV) so a receive-repost context is live. 3. From a second thread, call closesocket() while the receive is pending, racing endpoint teardown against AfdRestartBufferReceiveWithUserIrp / AfdTLTPacketsSend reference operations. 4. On a patched machine, if the race drives the count to 0 before a reference is re-taken, the checked increment executes __fastfail(0xE) and the machine bugchecks (0x139 KERNEL_SECURITY_CHECK_FAILURE with fastfail code 0xE). On an unpatched machine the same race would silently continue with a revived (dangling) object.

Note: reproducing this deterministically is not established here; the guard is defense-in-depth.

AFD accept/listen data path (AfdServiceWaitForListen)

No buffer-validation bug was found in this path. The accept/listen data delivery reached via NtDeviceIoControlFileAfdWaitForListenAfdServiceWaitForListen performs the same bounds checks and the same MDL-mapped copy in both builds.


6. Exploit Primitive & Development Notes

  • Primitive: None is provided by the diff. The reference-count fastfail is a mitigation: it converts a would-be use-after-free (reference taken on a freed object) into an immediate, non-exploitable bugcheck. It does not by itself expose an out-of-bounds write or a controllable free.
  • What a researcher would still need to confirm:
  • Whether any AFD reference count can actually be driven to 0 while a concurrent path still takes a reference (the underflow precondition). The diff shows the guard, not a reachable trigger.
  • Which specific object types (endpoint, connection, receive-repost context) are reachable with attacker-controlled timing.
  • Mitigation nature: The __fastfail(0xE) / FAST_FAIL_INVALID_REFERENCE_COUNT pattern is a runtime integrity check. It stops exploitation of a reference-count underflow at the point of detection; it does not remove a root-cause logic defect, because none is visible in this diff.

7. Debugger PoC Playbook

Attach a kernel debugger (WinDbg/KD) to the target machine.

Observing the reference-count check

  • Breakpoints: text bp afd+0xC24F ; patched AfdRestartBufferReceiveWithUserIrp checked increment (0x14000C24F) bp afd+0xC262 ; the int 29h fastfail site (0x14000C262) (On the unpatched build the corresponding increment is at AfdRestartBufferReceiveWithUserIrp + 0x22912, lock inc dword ptr [rdi+30h], with no following check.)
  • What to Inspect:
  • At the lock xadd [rbx+30h], rax site: read [rbx+0x30] before the increment. A value of 0 (or negative) means the next instruction sequence will take rax/new-count <= 1 and route to int 29h.
  • At the fastfail: ecx = 0xE confirms FAST_FAIL_INVALID_REFERENCE_COUNT. The resulting bugcheck is 0x139 (KERNEL_SECURITY_CHECK_FAILURE) with subcode 0xE.
  • Trigger Setup:
  • Run a tight loop of overlapped WSARecv immediately followed by closesocket() from separate threads across many sockets to maximize the teardown-vs-repost race window.
  • Expected Observation:
  • On patched: an immediate __fastfail-driven bugcheck (0x139, subcode 0xE) if and only if the race drives a reference count to 0 before a new reference is taken.
  • On unpatched: no fastfail; the reference count is silently incremented from 0, leaving a revived object that may be used after free later.

Comparing the accept/listen data path

  • Breakpoints: text bp afd!AfdServiceWaitForListen ; entry, both builds
  • What to Inspect:
  • Confirm the bounds checks and the memmove source (*(a2+0x98)+6) and length (*(*(a2+0x98)+4)+2) are the same in both builds. This path is unchanged apart from the reference-count fastfail and struct-offset shifts.

8. Changed Functions — Full Triage

(Note: Many functions changed only by register reassignment, struct-offset shifts from context-structure growth, or the reference-count fastfail rollout. Those are grouped accordingly.)

Reference-count integrity hardening (security-relevant)

  • AfdServiceWaitForListen @ 0x1C00197C0 (Sim: 0.9073): Bounds-check block refactored to an equivalent compound conditional (no logic change); reference-count increment at field +0x38 became a checked 64-bit increment at +0x40 with __fastfail(0xE). No OOB write present.
  • AfdRestartBufferReceiveWithUserIrp @ 0x1C0005A10 (Sim: 0.8368): Reference-count increment at [obj+0x30] changed from lock inc (32-bit) to lock xadd (64-bit) with a __fastfail(0xE) guard when the new count is <= 1.
  • AfdTLTPacketsSend @ 0x1C003EFE8 (Sim: 0.827): Two reference-count increments (+0x30, +0x38+0x40) hardened with __fastfail(0xE); allocation modernized from ExAllocatePoolWithQuotaTag(0x30, 'AfdF') to ExAllocatePool2(0x61, 48, 'AfdF') — same size (0x30) and tag ('AfdF').
  • WskKnrCompletePending @ 0x1C0013068 (Sim: 0.9054, Behavioral): Reference-count decrement handling restructured (fields consolidated); the driver-wide fastfail pattern is consistent with the rest of the patch. No independent exploitable change identified.

Other changed functions (behavioral / cosmetic)

  • AfdRioCreateRqPair @ 0x1C0048484 (Sim: 0.9023): Registered-I/O buffer registration (MDL allocate + probe/lock + map). Uses AfdRioCheckRqBufferSize @ 0x1C0047030 to validate count*0x48+0x10 == total_size with count <= 0x38E38E2 (integer-overflow guard). Error paths were consolidated; no new bug identified.
  • AfdFastTransmitFile @ 0x1C003D7B4 (Sim: 0.9225, Behavioral): Fast-path file send using FsRtlCopyRead / FsRtlMdlRead. Sums three sizes and uses an int-sized allocation length on the buffered path; the large-transfer path uses MDL-based I/O. No security-relevant change confirmed in the diff.
  • AfdRioCheckRqBufferSize @ 0x1C0047030 (Sim: 0.9524, Cosmetic): Integer-overflow guard (arg1 * 0x48 + 0x10 == arg2, arg1 <= 0x38E38E2). Equivalent logic in both builds.
  • AFDETW_TRACERECVDATAGRAM @ 0x1C000AC10 (Sim: 0.9077, Cosmetic): ETW trace logging; register/data-reference changes only.
  • WskTdiCreateAO @ 0x1C00751B0 (Sim: 0.9526, Behavioral): Device file creation via IoCreateFileEx with an ECP list; allocation wrapper changed. No security-relevant change identified.
  • AfdCommonDelAddressHandler @ 0x1C00668D0 (Sim: 0.9561, Cosmetic): List management; register renaming, equivalent logic.

9. Unmatched Functions

There were no unmatched functions in either build. All modifications were in-place, consistent with an iterative patch (a broad reference-count-integrity hardening pass plus API modernization) rather than an architectural refactor.


10. Confidence & Caveats

  • Confidence Level: High for the description of the change (reference-count increments hardened with __fastfail(0xE) at multiple sites in both the disassembly and decompilation). Low for any claim of a concrete exploitable primitive, because the diff shows only the guard being added, not a reachable underflow.
  • Corrections to earlier hypotheses:
  • The reported out-of-bounds write in AfdServiceWaitForListen is not supported by the binaries: the bounds checks are logically identical between builds and the copy source is the same validated pointer in both.
  • The reported "non-atomic decrement / double-free" mechanism is not present. Both builds use atomic operations. The real change is a checked increment with __fastfail(0xE), not a decrement guard, and the fastfail code is 0xE, not 0xD.
  • The AfdTLTPacketsSend allocation change does not fix an undersized allocation; size and tag are unchanged.
  • Verification Required:
  • To upgrade Finding 2 beyond hardening, demonstrate a reachable reference-count underflow for a specific AFD object under attacker-influenced timing.