afd.sys — feature-staged device-identity check, unpatched path retained behind a feature flag
KB5087538
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 helpersAfdGetTransportInfo,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
- 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. - Socket Creation: A process creates a Winsock endpoint (
WSASocket) or opens\Device\Afd. - Action Initiation: The process issues a standard Winsock
connect/bind, driving AFD's transport-create path. - 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
WskTdiCreateCOat0x1C0048229:IoGetRelatedDeviceObjectreturns inrax; it is stored to[rsi+0x10]at0x1C0048248and the request proceeds at0x1C0048255with no comparison. - Patched
WskTdiCreateCOat0x1C00483E8:EvaluateCurrentStateresult ineax. If zero (jz 0x1C004847F), execution reaches the sameAfdIssueDeviceControlcall as unpatched. If non-zero,IoGetDeviceAttachmentBaseRefruns andcmp [rsi+0x28], raxat0x1C0048408decides 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]: storedDeviceObjectfromIoGetRelatedDeviceObject.[rsi+0x28](patched only): cached expected base device, written byAfdGetTransportInfoonly 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-gatedIoGetDeviceAttachmentBaseRefidentity check afterIoGetRelatedDeviceObject; feature-off branch (jz 0x1C004847F) is the unchanged pre-patch path. Also switches from theAfdIssueDeviceControlat0x1C0040C1Cto 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 asWskTdiCreateCO; 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 (EvaluateCurrentStateat0x1C003E0A3,jz 0x1C003E0FAto the old path;IoGetDeviceAttachmentBaseRef+cmp [r13+0x28]+STATUS_INVALID_DEVICE_STATEwhen 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 offset0x28receivesIoGetDeviceAttachmentBaseRef(IoGetRelatedDeviceObject(fileobj)), written only when the feature is on (EvaluateCurrentStateat0x1C003B4B7,jz 0x1C003B4E5). (Sim: 0.8669)AfdQueryProviderInfo(@ 0x1C003B220) (Feature-staged): Provider/device lookup helper called byAfdGetTransportInfo; 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-gatedObfDereferenceObjectof the new cached device pointer (offset0x28) 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 code0x520F. Not security-relevant. (Sim: 0.6833)AfdSocketTransferBegin(@ 0x1C0061E50) (Behavioral): Mirror ofAfdSocketTransferEndwith IRP minor code0x510F; 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 flagg_Feature_30880057_59779870is on (cmp [r13+0x10], 0Ch; jb skipgated byEvaluateCurrentState); patched makescmp ebx, 0Ch; jb skipunconditional. 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 inWskTdiCreateCO(jz 0x1C004847F),AfdTdiCreateAO(jz 0x1C003E0FA), andAfdGetTransportInfo(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.