1. Overview

  • Unpatched Binary: afd_unpatched.sys
  • Patched Binary: afd_patched.sys
  • Overall Similarity: 0.9845
  • Diff Statistics: 1182 matched, 22 changed, 1160 identical, 0 unmatched (in both directions).

Verdict: The patch does not deliver a reachable security fix. It stages a new device-identity check in the AFD transport-create and transport-cache paths, but every new check is guarded by a Windows feature-staging flag (EvaluateCurrentState on g_Feature_2881162555_60797108...) with the exact pre-patch code path retained in the feature-disabled branch. Reaching a device mismatch also requires the ability to redirect or insert into the transport device stack (filter-driver attach or shadowing a \Device\ transport name), which is an administrative operation, not a primitive available to an unprivileged Winsock caller. No memory-corruption, type-confusion, or privilege-escalation impact is demonstrable from the binaries.

2. Vulnerability Summary

  • Severity: None (informational)
  • Vulnerability Class: Feature-staged defense-in-depth device-identity check (no delivered fix)
  • Affected Functions: WskTdiCreateCO, WskTdiCreateAO, AfdTdiCreateAO, and the transport-cache helpers AfdGetTransportInfo, AfdQueryProviderInfo, AfdFreeTransportInfo

Nature of the change: In the unpatched build, the transport-create path opens the transport device (via IoCreateFile), references the returned FILE_OBJECT (ObReferenceObjectByHandle), obtains the device object with IoGetRelatedDeviceObject, stores it, and proceeds to issue the control request. There is no comparison of the opened device against any cached expected device.

In the patched build the same functions add, immediately after IoGetRelatedDeviceObject, a check gated by a feature-staging flag: EvaluateCurrentState(g_Feature_2881162555_60797108_FeatureDescriptorDetails). When the feature evaluates to false the code jumps directly to the unchanged control-request path (identical to unpatched). When the feature evaluates to true it calls IoGetDeviceAttachmentBaseRef on the opened device and compares the attachment-base against a device pointer cached earlier at offset 0x28; on mismatch it sets STATUS_INVALID_DEVICE_STATE (0xC0000184) and tears the connection down.

Because the pre-patch behavior is preserved intact behind the disabled-feature branch, this is a staged rollout of a defense-in-depth check, not a delivered remediation. The IoGetDeviceAttachmentBaseRef API is not new; it is already present in the unpatched binary at five call sites. The patch adds new call sites, all behind the same feature flag.

What the check actually guards: The comparison verifies that the base of the just-opened transport device stack matches a device reference cached earlier by AfdGetTransportInfo/AfdQueryProviderInfo (offset 0x28 of the cache entry). This detects a transport device stack that changed identity between caching and open. Producing such a mismatch requires attaching a filter driver to, or redirecting the namespace name of, the transport device stack — both administrative operations. An unprivileged process performing a normal Winsock connect/bind opens a fixed transport device determined by the protocol catalog and cannot steer AFD to an unexpected device.

Attacker-Reachable Entry Point & Data Flow: 1. User-mode application issues a Winsock command (WSAConnect, connect, bind). 2. This translates to NtDeviceIoControlFile on \Device\Afd. 3. The IOCTL is dispatched through the AFD dispatch path to AfdTdiCreateAO. 4. AfdTdiCreateAO invokes WskTdiCreateCO or WskTdiCreateAO, which calls IoCreateFile to open the transport device handle. 5. IoGetRelatedDeviceObject is called on the resulting FileObject. 6. The device object is stored and the control request proceeds. The opened device is the fixed transport device for the requested protocol; the caller does not control which device this is.

3. Pseudocode Diff

The relevant change is in WskTdiCreateCO (relocated from 0x1C00480F8 in unpatched to 0x1C0048298 in patched). The patch inserts a feature-gated identity check after the device object is retrieved; when the feature is off, control flows to the same request path as before.

// === UNPATCHED WskTdiCreateCO (@ 0x1C00480F8) ===
// After IoCreateFile and ObReferenceObjectByHandle succeed:
DEVICE_OBJECT* dev = IoGetRelatedDeviceObject(FileObject);
arg4[2] = dev;                     // store device, no identity check
Object = arg2;
Status = AfdIssueDeviceControl(FileObject, &Object, 8, nullptr, 0, 1);
// returns SUCCESS if the request succeeds


// === PATCHED WskTdiCreateCO (@ 0x1C0048298) ===
DEVICE_OBJECT* dev = IoGetRelatedDeviceObject(FileObject);
arg4[2] = dev;

if (!EvaluateCurrentState(&g_Feature_2881162555_60797108))
{
    // FEATURE OFF: identical to unpatched, proceed directly
    goto issue_request;
}

// FEATURE ON: identity check
DEVICE_OBJECT* base = IoGetDeviceAttachmentBaseRef(arg4[2]);
if (*(arg1 + 0x28) == base)        // matches cached expected device
{
    ObfDereferenceObject(base);
issue_request:
    Status = AfdIssueDeviceControl(FileObject, &Object, 8, nullptr, 0, 1);
}
else
{
    ObfDereferenceObject(base);
    Status = STATUS_INVALID_DEVICE_STATE;   // 0xC0000184, tear down
}

4. Assembly Analysis

Unpatched WskTdiCreateCO (@ 0x1C00480F8) — device stored, no identity check:

00000001C0048229  call    cs:__imp_IoGetRelatedDeviceObject   ; device from FileObject
00000001C0048235  mov     rcx, [rsi+8]                        ; FileObject
00000001C0048245  mov     r8d, r12d                           ; Size = 8
00000001C0048248  mov     [rsi+10h], rax                      ; store device object
00000001C004824C  mov     [rbp+57h+Object], r14
00000001C0048255  call    AfdIssueDeviceControl               ; issue control request
00000001C004825A  mov     ebx, eax
00000001C004825E  js      short loc_1C0048264                 ; on failure -> cleanup
00000001C0048260  xor     eax, eax                            ; return SUCCESS
00000001C0048262  jmp     short loc_1C004829A

Patched WskTdiCreateCO (@ 0x1C0048298) — feature-gated identity check inserted:

00000001C00483D1  call    cs:__imp_IoGetRelatedDeviceObject
00000001C00483DD  lea     rcx, g_Feature_2881162555_60797108_FeatureDescriptorDetails
00000001C00483E4  mov     [rdi+10h], rax                      ; store device object
00000001C00483E8  call    EvaluateCurrentState               ; feature-staging gate
00000001C00483ED  test    eax, eax
00000001C00483EF  jz      loc_1C004847F                       ; FEATURE OFF -> old path
00000001C00483F5  mov     rcx, [rdi+10h]                      ; DeviceObject
00000001C00483F9  call    cs:__imp_IoGetDeviceAttachmentBaseRef
00000001C0048405  mov     rcx, rax
00000001C0048408  cmp     [rsi+28h], rax                      ; compare cached expected device
00000001C004840C  jz      short loc_1C0048473                 ; match -> proceed
00000001C004840E  call    cs:__imp_ObfDereferenceObject
00000001C004841A  mov     ebx, 0C0000184h                     ; STATUS_INVALID_DEVICE_STATE
; ... teardown ...
00000001C0048473  call    cs:__imp_ObfDereferenceObject       ; match path: deref base
00000001C004847F  mov     rcx, [rdi+8]                        ; loc_1C004847F: old/issue path
00000001C004849B  call    AfdIssueDeviceControl

The jz loc_1C004847F at 0x1C00483EF is the load-bearing detail: when the feature is disabled the code reaches the same AfdIssueDeviceControl call as the unpatched build, with no device comparison. The identical construct appears in AfdTdiCreateAO (patched @ 0x1C003E099: EvaluateCurrentState then jz 0x1C003E0FA skipping the IoGetDeviceAttachmentBaseRef/cmp [r13+28h] check) and in AfdGetTransportInfo (patched @ 0x1C003B4AE: EvaluateCurrentState then jz 0x1C003B4E5, so the cached expected-device field is only written when the feature is on).

5. Trigger Conditions

  1. Precondition (administrative): For the added check to ever fail, the transport device stack that AFD opens must resolve to, or be intercepted by, a device other than the expected transport. Producing that state requires attaching a filter driver to the transport device stack or redirecting a \Device\ transport name in the Object Manager namespace. Both require administrative privilege; an unprivileged user cannot create or shadow \Device\ transport objects.
  2. Socket Creation: A process creates a Winsock endpoint (WSASocket) or opens \Device\Afd.
  3. Action Initiation: The process issues a standard Winsock connect/bind, driving AFD's transport-create path.
  4. Behavior: AFD opens the fixed transport device for the requested protocol. The caller does not choose which device is opened. With the feature on, the new check compares the opened device's attachment base against the cached expected device; with the feature off, behavior is identical to the unpatched build.

6. Exploit Primitive & Development Notes

  • Primitive: None demonstrable from the binaries. The change adds a device-identity comparison in a path where the opened device is fixed by the protocol catalog for an unprivileged caller. No attacker-controlled input steers device selection, and no memory-corruption or type-confusion consequence is shown by either build.
  • Preconditions do the work: The only way to make the opened device differ from the expected one (filter-driver attach or namespace redirection of a transport device) is itself an administrative capability. A caller with that capability already operates at or above the privilege level the check would protect, so the check is defense-in-depth robustness, not a fix for an unprivileged-reachable flaw.
  • Feature staging: Every added check is guarded by EvaluateCurrentState(g_Feature_2881162555_60797108...) with the pre-patch path retained in the feature-disabled branch. This is a staged rollout, not a delivered remediation.

7. Debugger PoC Playbook

For an analyst with a kernel debugger attached, the following breakpoints show the relevant code paths in each build. There is no unprivileged trigger to demonstrate impact; these are for observing the feature-gated control flow.

Breakpoints

bp afd!WskTdiCreateCO+0x131   ; @ 0x1C0048229 (unpatched) / equivalent IoGetRelatedDeviceObject call
bp afd!AfdTdiCreateAO         ; main transport-create handler

What to Inspect

  • Unpatched WskTdiCreateCO at 0x1C0048229: IoGetRelatedDeviceObject returns in rax; it is stored to [rsi+0x10] at 0x1C0048248 and the request proceeds at 0x1C0048255 with no comparison.
  • Patched WskTdiCreateCO at 0x1C00483E8: EvaluateCurrentState result in eax. If zero (jz 0x1C004847F), execution reaches the same AfdIssueDeviceControl call as unpatched. If non-zero, IoGetDeviceAttachmentBaseRef runs and cmp [rsi+0x28], rax at 0x1C0048408 decides accept vs. STATUS_INVALID_DEVICE_STATE.

Expected Observation

For a normal Winsock connect/bind from an unprivileged process, the opened device is the fixed transport device and the comparison (when the feature is on) matches, so behavior is identical to the unpatched build. No fault or corruption is produced.

Struct/Offset Notes

  • [rsi+0x8] / [rdi+0x8]: FileObject.
  • [rsi+0x10] / [rdi+0x10]: stored DeviceObject from IoGetRelatedDeviceObject.
  • [rsi+0x28] (patched only): cached expected base device, written by AfdGetTransportInfo only when the feature is on. The transport-cache entry is enlarged by 8 bytes in the patched build to hold this field.

8. Changed Functions — Full Triage

  • WskTdiCreateCO (unpatched @ 0x1C00480F8, patched @ 0x1C0048298) (Feature-staged): Adds a feature-gated IoGetDeviceAttachmentBaseRef identity check after IoGetRelatedDeviceObject; feature-off branch (jz 0x1C004847F) is the unchanged pre-patch path. Also switches from the AfdIssueDeviceControl at 0x1C0040C1C to the relocated equivalent — a relocation, not a behavior change. (Sim: 0.8598)
  • WskTdiCreateAO (unpatched @ 0x1C0047CD4, patched @ 0x1C0047E24) (Feature-staged): Same feature-gated identity check pattern as WskTdiCreateCO; cache field offsets shifted by 8 bytes for the new cached-device field. (Sim: 0.9461)
  • AfdTdiCreateAO (@ 0x1C003D750) (Feature-staged): Main transport-create handler. Adds the same feature-gated check (EvaluateCurrentState at 0x1C003E0A3, jz 0x1C003E0FA to the old path; IoGetDeviceAttachmentBaseRef + cmp [r13+0x28] + STATUS_INVALID_DEVICE_STATE when on). Cache field offsets shifted for the 8-byte-larger entry. (Sim: 0.9491)
  • AfdGetTransportInfo (@ 0x1C003B044) (Feature-staged): Transport-cache management. Cache entry enlarged by 8 bytes; new field at offset 0x28 receives IoGetDeviceAttachmentBaseRef(IoGetRelatedDeviceObject(fileobj)), written only when the feature is on (EvaluateCurrentState at 0x1C003B4B7, jz 0x1C003B4E5). (Sim: 0.8669)
  • AfdQueryProviderInfo (@ 0x1C003B220) (Feature-staged): Provider/device lookup helper called by AfdGetTransportInfo; gains an output parameter for the base device object, populated only under the feature flag. (Sim: 0.6383)
  • AfdFreeTransportInfo (@ 0x1C003B300) (Feature-staged): Cache-entry free path; adds a feature-gated ObfDereferenceObject of the new cached device pointer (offset 0x28) so the added reference is released. Balances the reference taken by the cache path; only relevant when the feature is on. (Sim: 0.2879)
  • AfdSocketTransferEnd (@ 0x1C0061C30) (Behavioral): Bluetooth RFCOMM IRP handler; reference-management helper functions relocated/renamed, refcount logic reorganized. IRP minor code 0x520F. Not security-relevant. (Sim: 0.6833)
  • AfdSocketTransferBegin (@ 0x1C0061E50) (Behavioral): Mirror of AfdSocketTransferEnd with IRP minor code 0x510F; same relocation/refcount reorganization. Not security-relevant. (Sim: 0.6833)
  • AfdWaitForListen (@ 0x1C005DE80) (Behavioral): Receive handler with reordered branches; cancel routine relocated; a feature-staging check added in the not-connected path. Not security-relevant. (Sim: 0.9086)
  • AfdSanAcceptCore (@ 0x1C007E0B8) (Feature-staged, not security-relevant): SAN receive-completion size handling. Unpatched requires the minimum 12-byte size only when the feature flag g_Feature_30880057_59779870 is on (cmp [r13+0x10], 0Ch; jb skip gated by EvaluateCurrentState); patched makes cmp ebx, 0Ch; jb skip unconditional. This finalizes a feature-staged size gate on the SAN (Winsock Direct) accept path; no demonstrable impact for the sub-12-byte case and this path is not the transport-create change. (Sim: 0.8636)
  • AfdTLSendMessage (@ 0x1C0039544) (Behavioral): Feature-staging conditional removed (assignment made unconditional); helper references relocated. Not security-relevant. (Sim: 0.9414)
  • AfdSend (@ 0x1C0050950) (Behavioral): Receive IRP handler with reorganized status-code error paths. Not security-relevant. (Sim: 0.7593)
  • sub_1C0052029 (Behavioral): Large function with structural reorganization and variable renames. Not security-relevant. (Sim: 0.7574)
  • AfdTLSendMsgComplete (@ 0x1C0062070) (Cosmetic): Feature-staging conditional removed from an IRP-completion test. (Sim: 0.9522)
  • AfdTLDgramSendToComplete (@ 0x1C0077030) (Cosmetic): Same feature-staging conditional removal. (Sim: 0.9539)
  • AfdSanConnectHandler (@ 0x1C007F630) (Cosmetic): Large function, address shifts and renames. (Sim: 0.9549)
  • AfdTLVcSendDgram (@ 0x1C0062790) (Cosmetic): Feature-staging conditional removed for an assignment. (Sim: 0.9579)
  • AfdTLVcSendDgramComplete (@ 0x1C00629A0) (Cosmetic): Same feature-staging conditional removal. (Sim: 0.9594)
  • AfdGetInformation (@ 0x1C00389B0) (Cosmetic): IOCTL dispatch function, variable renames and address shifts. (Sim: 0.9746)
  • AfdSendDatagram (@ 0x1C00765A0) (Cosmetic): IRP dispatch handler, variable renames and a feature-staging conditional removal. (Sim: 0.9814)
  • AfdUnload (@ 0x1C0040330) (Cosmetic): Driver unload; adds a feature-staging teardown call and relocated references. (Sim: 0.9839)
  • AfdBind (@ 0x1C0037A80) (Cosmetic): IOCTL dispatch function, address shifts and variable renames. (Sim: 0.9848)

9. Unmatched Functions

There are no added or removed functions in this binary patch (0 unmatched in both directions). All changes are inside existing functions.

10. Confidence & Caveats

  • Confidence: High that the titled change is feature-staged and not a delivered fix. Every new device-identity site is guarded by EvaluateCurrentState(g_Feature_2881162555_60797108...) with the pre-patch path retained in the feature-disabled branch in WskTdiCreateCO (jz 0x1C004847F), AfdTdiCreateAO (jz 0x1C003E0FA), and AfdGetTransportInfo (jz 0x1C003B4E5).
  • Why not High: The prior framing (kernel type confusion → arbitrary read/write → local privilege escalation) is not supported by either build. It relied on preconditions — shadowing a \Device\ transport name or attaching a filter driver — that require administrative privilege and are not reachable by an unprivileged Winsock caller, and on a memory-corruption consequence that neither build demonstrates.
  • What the change is: A staged, defense-in-depth device-identity check plus the supporting transport-cache field (offset 0x28) and its reference cleanup, together with unrelated feature-staging finalizations, IRP-handler reorganizations, and relocations/renames across the other changed functions.