1. Overview

  • Unpatched Binary: afd_unpatched.sys
  • Patched Binary: afd_patched.sys
  • Overall Similarity Score: 0.979
  • Diff Statistics: 1182 matched functions, 18 changed functions, 1164 identical functions, and 0 unmatched functions in either direction.
  • Verdict: The patch delivers two genuine hardening changes. The headline change is in the SuperAccept address-completion routine AfdRestartSuperAcceptGetAddress: the unpatched build gates the correct copy-length clamp behind a runtime feature flag, and when that flag is off it omits the 10-byte reservation needed for the in-place buffer shift, allowing a bounded out-of-bounds write into the mapped transport-address buffer (CWE-787). The patched build removes the gate and always clamps. A second genuine change removes a feature-flag gate that could skip device-stack validation during accept/connect (CWE-843). Two further changes — an added, feature-gated callback-serialization counter at endpoint offset +0xf8, and a reference-count ordering tweak in AfdAddConnectedReference — are feature-staged (the old behavior is retained in an else-branch) and are not demonstrable vulnerabilities in the unpatched build.

2. Vulnerability Summary

Finding 1: Out-of-Bounds Write in SuperAccept Address Copy

  • Severity: High
  • Vulnerability Class: Out-of-bounds Write (CWE-787)
  • Affected Function: AfdRestartSuperAcceptGetAddress (unpatched @ 0x1C006D7F0, patched @ 0x1C006D730) — the AcceptEx/SuperAccept IRP completion routine.
  • Root Cause: The routine maps the transport-address buffer and shifts its contents forward by 10 (0xa) bytes with memmove(dst = ptr+0xa, src = ptr, size). The copy length must therefore be clamped to leave 10 bytes of headroom at the destination. In the unpatched build the correct clamp (size - 0xa) is gated behind EvaluateCurrentState(g_Feature_298020153_59190810_FeatureDescriptorDetails). When that flag evaluates false, the code instead clamps only to MdlByteCount - offset, which does not reserve the 10 bytes consumed by the +0xa destination shift. As a result the memmove destination can extend up to 10 bytes past the end of the mapped buffer, writing transport-supplied bytes into adjacent memory.
  • Scope / Correction of impact: The overwrite is bounded to at most 10 bytes past the destination end. The memmove source stays within the buffer (src through src + size where size <= MdlByteCount - offset), so there is no out-of-bounds read and no kernel-memory information disclosure. The store mov [r15+rbp-2], di that follows is present identically in both builds and merely records the computed length; it is not part of the defect and not a separate corruption primitive.
  • Attacker-Reachable Entry Point & Data Flow:
  • A local process issues an AcceptEx / AFD_SUPER_ACCEPT operation on a listening socket over \Device\Afd.
  • The IRP is dispatched to the TCP/IP transport, which completes it with the accepted connection's address data.
  • Completion invokes AfdRestartSuperAcceptGetAddress, which maps the address MDL and reads the length field at [mapped + offset + 8].
  • With the feature flag off, the length is clamped only against MdlByteCount - offset, and memmove shifts the data forward by 10 bytes, writing up to 10 bytes past the buffer end.

Finding 2: Feature-Staged Callback Serialization Counter (endpoint +0xf8)

  • Severity: Low
  • Vulnerability Class: Race Condition (CWE-362) — not demonstrated
  • Affected Functions: AfdTLListenComplete (unpatched @ 0x1C005FC10, patched @ 0x1C0060E90), AfdSetInformation, AfdQueryCompartmentId, AfdTLPauseUnPause, AfdGetAddress, AfdGetInformation.
  • What actually changed: The patched build introduces a counter at endpoint offset +0xf8 that is atomically incremented before, and decremented after, TDI callback invocations, and adds a while (*(rbx+0xf8) != 0) { pause; } spin-wait in AfdTLListenComplete before its callback dispatch. The unpatched build contains no reference to offset +0xf8 in these paths at all (the endpoint structure grew by 8 bytes; e.g. the close-refcount field moved from [rbx+0x158] to [rbx+0x160]).
  • Why this is not rated as a delivered fix: Every +0xf8 increment/decrement and the spin-wait are gated behind EvaluateCurrentState(g_Feature_3088522553_60513072_FeatureDescriptorDetails), and each gate keeps the original counter-less path in the else-branch (jz skips the counter when the flag is off). This is a staged rollout of a serialization mechanism, not an unconditionally delivered change. The callback dispatch is otherwise byte-for-byte equivalent between builds, and no use-after-free or double-free is demonstrable in the unpatched code, so the underlying race is not established.

Finding 3: Type Confusion via Bypassed Device-Stack Validation

  • Severity: High
  • Vulnerability Class: Type Confusion (CWE-843)
  • Affected Functions: AfdCreateConnection (unpatched @ 0x1C0055BF0, patched @ 0x1C0056280), AfdCreateConnection_Old (@ 0x1C0007DE4 in both).
  • Root Cause: During accept/connect the driver compares the device-stack base of the endpoint's connection object against that of the supplied handle (IoGetDeviceAttachmentBaseRef on each, then cmp), returning STATUS_INVALID_PARAMETER (0xC000000D) on mismatch. In the unpatched build the entire comparison block is wrapped in if (EvaluateCurrentState(g_Feature_1552108857_59735561_FeatureDescriptorDetails)); when the flag is false the check is skipped and execution falls through to AfdIssueDeviceControl without validating that the handle belongs to the same device stack. An attacker can then associate a handle from a different device stack with the AFD endpoint, causing subsequent I/O to interpret the wrong object type.
  • Direction: Correct. The patched build removes the gate entirely and performs the IoGetDeviceAttachmentBaseRef comparison unconditionally (the only preceding branch is the handle != NULL null-check), so the bypass path no longer exists.
  • Attacker-Reachable Entry Point & Data Flow:
  • NtDeviceIoControlFile targets \Device\Afd with an accept/connect request supplying an attacker-chosen handle.
  • In the unpatched build the feature flag is off, so the device-stack comparison is skipped.
  • The mismatched handle is attached to the AFD endpoint via AfdIssueDeviceControl.

Finding 4: Reference-Count Ordering in AfdAddConnectedReference

  • Severity: Low
  • Vulnerability Class: Race Condition (CWE-362) — not demonstrated
  • Affected Function: AfdAddConnectedReference (unpatched @ 0x1C0058F28, patched @ 0x1C0059A48).
  • What actually changed: The unpatched build performs lock inc [rbx+0x30] (the reference count) after KeReleaseInStackQueuedSpinLock. The patched build can perform the same lock inc before the release — but only when EvaluateCurrentState(g_Feature_2476938553_60405306_FeatureDescriptorDetails) is true; when the flag is false it retains the original order (increment after release). The increment is an atomic lock inc in both builds and in both branches, so the count is never torn regardless of lock position. This is a feature-staged ordering adjustment with the old path retained; no use-after-free is demonstrable.

3. Pseudocode Diff

Finding 1: AfdRestartSuperAcceptGetAddress (High OOB write)

// === UNPATCHED ===
// ptr = MappedSystemVa + offset;  size field = *(ctx + 0x18)
uint16_t len = *(uint16_t*)(ptr + 8) + 2;      // transport-supplied length + 2

if (EvaluateCurrentState(&g_Feature_298020153_59190810)) {
    // FLAG ON: correct clamp, reserves 0xa for the +0xa destination shift
    uint16_t cap = (uint16_t)(*(uint16_t*)(ctx + 0x18) - 0xa);
    if (*(uint16_t*)(ctx + 0x18) < 0xa) cap = 0;
    len = (len <= cap) ? len : cap;
} else {
    // FLAG OFF: clamps only to (MdlByteCount - offset); NO 0xa reservation
    uint32_t avail = irp->MdlAddress->ByteCount - offset;
    if (avail < len) len = (uint16_t)(irp->MdlAddress->ByteCount - offset);
}

memmove(ptr, ptr + 0xa, len);   // dst = ptr + 0xa can end up to 10 bytes past buffer
*(uint16_t*)(sizefield + ptr - 2) = len;   // identical in both builds

// === PATCHED ===
// No feature flag; always the reserving clamp
uint16_t cap = (uint16_t)(*(uint16_t*)(ctx + 0x18) - 0xa);
if (*(uint16_t*)(ctx + 0x18) < 0xa) cap = 0;
uint16_t len = *(uint16_t*)(ptr + 8) + 2;
len = (cap <= len) ? cap : len;             // min(size - 0xa, len)
memmove(ptr, ptr + 0xa, len);
*(uint16_t*)(sizefield + ptr - 2) = len;

Finding 3: AfdCreateConnection (High Type Confusion)

// === UNPATCHED ===
if (EvaluateCurrentState(&g_Feature_1552108857_59735561)) {
    ObReferenceObjectByHandle(handle, ...);
    DEVICE_OBJECT* a = IoGetDeviceAttachmentBaseRef(connectionDevice);
    DEVICE_OBJECT* b = IoGetDeviceAttachmentBaseRef(IoGetRelatedDeviceObject(handleObj));
    if (a != b) return 0xC000000D;   // STATUS_INVALID_PARAMETER
}
// flag off: comparison skipped entirely -> falls through to attach
AfdIssueDeviceControl(...);

// === PATCHED ===
// No feature flag; comparison always runs when a handle is supplied
if (handle != NULL) {
    ObReferenceObjectByHandle(handle, ...);
    DEVICE_OBJECT* a = IoGetDeviceAttachmentBaseRef(connectionDevice);
    DEVICE_OBJECT* b = IoGetDeviceAttachmentBaseRef(IoGetRelatedDeviceObject(handleObj));
    if (a != b) return 0xC000000D;
}
AfdIssueDeviceControl(...);

Finding 4: AfdAddConnectedReference (Low, feature-staged ordering)

// === UNPATCHED ===
bts [rbx+4], 0xc;                       // set state bit under lock
KeReleaseInStackQueuedSpinLock(&h);     // lock released
lock inc [rbx+0x30];                     // atomic increment after release

// === PATCHED ===
bts [rbx+4], 0xc;
if (EvaluateCurrentState(&g_Feature_2476938553_60405306)) {
    lock inc [rbx+0x30];                 // atomic increment before release
    KeReleaseInStackQueuedSpinLock(&h);
} else {
    KeReleaseInStackQueuedSpinLock(&h);  // old order retained
    lock inc [rbx+0x30];
}

4. Assembly Analysis

Finding 1: AfdRestartSuperAcceptGetAddress copy-length clamp

Unpatched build (@ 0x1C006D7F0), the feature-gated clamp and copy:

00000001C006D8CE  cmp     [rdi+0ACh], r12b   ; endpoint TLV/address flag (r12b=0); must be set
00000001C006D8D5  jz      loc_1C006D96B
00000001C006D8F9  mov     ebp, [rsi+8]        ; offset into the mapped buffer
00000001C006D8FC  lea     rcx, g_Feature_298020153_59190810_FeatureDescriptorDetails
00000001C006D903  mov     r15d, [rsi+18h]     ; buffer size field
00000001C006D907  add     rbp, rax            ; ptr = MappedSystemVa + offset
00000001C006D90A  movzx   edi, word ptr [rbp+8] ; transport-supplied length
00000001C006D90E  add     di, 2               ; len = length + 2
00000001C006D912  call    EvaluateCurrentState
00000001C006D917  test    eax, eax
00000001C006D919  jz      short loc_1C006D937 ; flag OFF -> weak clamp
; --- SAFE PATH (flag on): reserves 0xa ---
00000001C006D91B  movzx   eax, word ptr [rsi+18h]
00000001C006D91F  cmp     ax, 0Ah
00000001C006D923  lea     ecx, [rax-0Ah]      ; cap = size - 0xa
00000001C006D926  cmovb   cx, r12w            ; if size < 0xa, cap = 0
00000001C006D92B  cmp     cx, di
00000001C006D92E  cmovnb  cx, di              ; len = min(cap, len)
00000001C006D932  movzx   edi, cx
00000001C006D935  jmp     short loc_1C006D955
; --- WEAK PATH (flag off): no 0xa reservation ---
00000001C006D937  mov     rax, [rbx+8]        ; MDL
00000001C006D93B  mov     r8d, [rsi+8]        ; offset
00000001C006D93F  mov     edx, [rax+28h]      ; MDL ByteCount
00000001C006D942  mov     ecx, edx
00000001C006D944  sub     ecx, r8d            ; avail = ByteCount - offset (0xa NOT reserved)
00000001C006D947  movzx   eax, di
00000001C006D94A  cmp     ecx, eax
00000001C006D94C  jnb     short loc_1C006D955
00000001C006D94E  movzx   edi, dx
00000001C006D951  sub     di, r8w             ; len = ByteCount - offset
; --- COPY: destination is shifted +0xa ---
00000001C006D955  movzx   r8d, di             ; Size
00000001C006D959  lea     rdx, [rbp+0Ah]      ; dst = ptr + 0xa
00000001C006D95D  mov     rcx, rbp            ; src = ptr
00000001C006D960  call    memmove             ; up to 10 bytes past buffer end when flag off
00000001C006D965  mov     [r15+rbp-2], di     ; store computed length (identical in both builds)

Patched build (@ 0x1C006D730), the same region with the flag removed and only the reserving clamp:

00000001C006D832  mov     edi, [r15+18h]      ; size field
00000001C006D836  mov     esi, [r15+8]        ; offset
00000001C006D83A  add     rsi, rax            ; ptr = MappedSystemVa + offset
00000001C006D83D  lea     eax, [rdi-0Ah]      ; cap = size - 0xa
00000001C006D840  movzx   ecx, word ptr [rsi+8]
00000001C006D844  lea     rdx, [rsi+0Ah]      ; dst = ptr + 0xa
00000001C006D848  add     cx, 2               ; len = length + 2
00000001C006D84C  cmp     di, 0Ah
00000001C006D850  cmovb   ax, r12w            ; if size < 0xa, cap = 0
00000001C006D855  cmp     ax, cx
00000001C006D858  cmovnb  ax, cx              ; len = min(cap, len)
00000001C006D85C  mov     rcx, rsi            ; src = ptr
00000001C006D85F  movzx   ebx, ax
00000001C006D862  mov     r8d, ebx            ; Size
00000001C006D865  call    memmove
00000001C006D86A  mov     [rdi+rsi-2], bx

There is no EvaluateCurrentState call and no ByteCount - offset fallback in the patched routine.

Finding 2: AfdTLListenComplete spin-wait (feature-gated)

Patched build (@ 0x1C0060E90):

00000001C0060F0F  lea     rcx, g_Feature_3088522553_60513072_FeatureDescriptorDetails
00000001C0060F2E  call    EvaluateCurrentState
00000001C0060F33  test    eax, eax
00000001C0060F35  jz      short loc_1C0060F47   ; flag OFF -> skip wait (old behavior)
00000001C0060F39  pause                          ; loop body
00000001C0060F3B  mov     rax, [rbx+0F8h]        ; read counter
00000001C0060F42  test    rax, rax
00000001C0060F45  jnz     short loc_1C0060F39    ; spin while counter != 0
00000001C0060F47  ...     call __guard_dispatch_icall_fptr  ; callback dispatch (same in both builds)

The unpatched AfdTLListenComplete (@ 0x1C005FC10) has no +0xf8 read and no EvaluateCurrentState; it dispatches the callback directly. Every +0xf8 increment/decrement in the other listed functions is guarded by the same flag with a jz that skips the counter when it is off.

Finding 3: AfdCreateConnection device-stack comparison

Unpatched build (@ 0x1C0055BF0), gate that can skip the check:

00000001C0055F67  lea     rcx, g_Feature_1552108857_59735561_FeatureDescriptorDetails
00000001C0055F6E  call    EvaluateCurrentState
00000001C0055F73  test    eax, eax
00000001C0055F75  jz      loc_1C0056076          ; flag OFF -> jump PAST the whole comparison
00000001C0055FCC  call    IoGetDeviceAttachmentBaseRef
00000001C0055FED  call    IoGetDeviceAttachmentBaseRef
00000001C0055FFC  cmp     rsi, rax
00000001C0055FFF  jz      loc_1C005604C          ; match -> proceed
00000001C0056042  mov     eax, 0C000000Dh        ; mismatch -> STATUS_INVALID_PARAMETER

Patched build (@ 0x1C0056280) performs the comparison unconditionally (only a handle != NULL null-check precedes it):

00000001C005662E  call    IoGetDeviceAttachmentBaseRef
00000001C005664F  call    IoGetDeviceAttachmentBaseRef
00000001C005665E  cmp     rdi, rax
00000001C0056661  jz      loc_1C00566AE          ; match
00000001C00566A4  mov     eax, 0C000000Dh        ; mismatch -> STATUS_INVALID_PARAMETER

Finding 4: AfdAddConnectedReference increment ordering

Unpatched (@ 0x1C0058F28): KeReleaseInStackQueuedSpinLock at 0x1C0058F62, then lock inc dword ptr [rbx+30h] at 0x1C0058F6E. Patched (@ 0x1C0059A48): EvaluateCurrentState(g_Feature_2476938553_60405306) at 0x1C0059A9B; when true, lock inc [rbx+30h] at 0x1C0059AA9 precedes the release at 0x1C0059AAD; when false, the release at 0x1C0059ABB precedes lock inc [rbx+30h] at 0x1C0059AC7. The increment is atomic in every path.


5. Trigger Conditions

To exercise the Finding 1 out-of-bounds write in AfdRestartSuperAcceptGetAddress on the unpatched build: 1. Open a listening socket and issue an AcceptEx / AFD_SUPER_ACCEPT operation on \Device\Afd, establishing the SuperAccept completion path. 2. Complete a connection so the transport returns address data; the completion routine runs with IoStatus.Information == 0x1a (checked at cmp dword ptr [rdx+38h], 1Ah) and the endpoint's address flag at [rdi+0xac] set. 3. The address buffer's length field at [mapped + offset + 8] must be large enough that, after the +0xa destination shift, the clamp to MdlByteCount - offset still allows the copy to reach past the buffer end (overshoot up to 10 bytes). 4. EvaluateCurrentState(g_Feature_298020153_59190810_FeatureDescriptorDetails) must return false, routing execution to the weak clamp at 0x1C006D937.


6. Exploit Primitive & Development Notes

  • Provided Primitive: A bounded out-of-bounds write of up to 10 bytes of transport-supplied address data immediately past the end of the mapped transport-address buffer in kernel pool. There is no out-of-bounds read: the memmove source range stays within the buffer.
  • Constraints: The overshoot is capped at 10 bytes; the written bytes are the shifted tail of the address structure rather than fully arbitrary content; and the path is only reachable when the feature flag is off. Turning a 10-byte adjacent-pool overwrite into a broader primitive would depend on pool layout and the contents of the neighboring allocation, none of which is established here.

7. Debugger PoC Playbook

Attach a local kernel debugger to the unpatched target.

Breakpoints

bp afd_unpatched!AfdRestartSuperAcceptGetAddress
bp afd_unpatched+0x6d937 ".printf \"[!] weak clamp path\\n\"; r di"
bp afd_unpatched+0x6d960 ".printf \"[!] memmove size=%x src=%p dst=%p\\n\", @r8d, @rcx, @rdx"

(Offsets are relative to the image base; 0x1C006D937 and 0x1C006D960 are the file addresses.)

What to Inspect

  • At 0x1C006D912 (call EvaluateCurrentState): the return value in eax. eax == 0 selects the weak clamp path.
  • At 0x1C006D937: di holds the transport-supplied length + 2 before the weak clamp.
  • At 0x1C006D960 (memmove): r8d is the copy size, rcx the source (ptr), rdx the destination (ptr + 0xa). If rdx + r8d exceeds MappedSystemVa + MdlByteCount, the write is out of bounds.

Expected Observation

When the weak clamp is taken with a large length, rdx + r8d extends up to 10 bytes past MappedSystemVa + MdlByteCount. With special pool enabled on the affected allocation this surfaces as a pool-corruption or page-fault bugcheck at or after the memmove call.


8. Changed Functions — Full Triage

The security-relevant behavioral changes are documented above. The remaining changed functions are structural reorganization and offset shifts:

  • AfdSetInformation (unpatched sub_1C005B800, 0.603 similarity): Operation-code dispatch restructured from cascaded compares into a bounded jump-table (cmp eax,0Eh / ja default) — a refactor with no unbounded index, so no out-of-bounds write. Also carries the feature-gated +0xf8 counter (Finding 2).
  • AfdCreateConnection (unpatched sub_1C0055BF0, 0.9425 similarity): Removed the feature-flag gate that could skip device-stack validation (Finding 3).
  • AfdCreateConnection_Old (sub_1C0007DE4, 0.9637 similarity): Same gate removal as AfdCreateConnection (Finding 3).
  • AfdAddConnectedReference (unpatched sub_1C0058F28, 0.468 similarity): Feature-gated reference-count increment ordering (Finding 4).
  • AfdQueryCompartmentId (unpatched sub_1C0036D94, 0.912 similarity): Feature-gated +0xf8 counter around the TDI query callback (Finding 2). The memset zero-fill size is a fixed 0x50 in both builds; there is no buffer-size change.
  • AfdTLPauseUnPause (unpatched sub_1C0061A70, 0.8586 similarity): Feature-gated +0xf8 counter around the TDI callback (Finding 2).
  • AfdConnect (unpatched sub_1C005A4F0, 0.9643 similarity): Register reallocation from recompilation; no confirmed security-relevant change.
  • Cosmetic & Restructuring Changes:
  • AfdCleanupCore (unpatched sub_1C00571F0): Adjusted struct offsets (0x158 -> 0x160) reflecting the 8-byte endpoint growth, and re-addressed callees.
  • AfdDoDatagramConnect (unpatched sub_1C0061F80): Adjusted struct offsets, re-addressed callees, added an EvaluateCurrentState conditional.
  • AfdGetInformation (unpatched sub_1C0038900 -> patched sub_1C00389B0) and AfdGetAddress (sub_1C0036320, in place): IOCTL info/address query handler reorganization; also carry the feature-gated +0xf8 counter (Finding 2).
  • AfdSetEventHandler (unpatched sub_1C00410A0 -> patched sub_1C0041120) and AfdCloseRouteChangeNotifyHandles (unpatched sub_1C004194C -> patched sub_1C00419CC): shifted in memory layout.
  • AfdGetAddressFileReference (unpatched sub_1C006DDF4 -> patched sub_1C006DCF4) and AfdGetDeliveryStatus (unpatched sub_1C0072140 -> patched sub_1C0072040): reordered in binary layout.
  • AfdTLConnectEventHandler (sub_1C00549B0): Register and trace-data reference adjustments.

9. Unmatched Functions

No functions were exclusively added or removed; all changes were in-place modifications affecting the 18 matched-but-changed functions.


10. Confidence & Caveats

  • Confidence Level: High for the direction and mechanism of all four findings, which are read directly from both disassemblies.
  • Reachability of Finding 1: Whether unpatched systems are actually exposed depends on the runtime default of g_Feature_298020153_59190810. The correct clamp is present in the unpatched build but gated; the patched build removes the gate and hardwires it. The value written is transport-supplied address data, and the overshoot is bounded to 10 bytes.
  • Findings 2 and 4 are feature-staged: the new behavior is gated behind a WIL feature flag with the original behavior retained in the else-branch, and no use-after-free or double-free is demonstrable in the unpatched build. They are reported as Low pending evidence of a concrete race.
  • Manual Verification: Confirming Finding 1 in practice requires attaching a kernel debugger and identifying the AcceptEx/SuperAccept sequence that drives the completion routine with an address-length field large enough to overshoot after the +0xa shift.