afd.sys — Out-of-bounds write in AfdSanAcceptCore SAN transport-address copy (CWE-787) fixed
KB5075999
1. Overview
- Unpatched Binary:
afd_unpatched.sys - Patched Binary:
afd_patched.sys - Overall Similarity: 0.9888
- Diff Statistics: 1073 matched functions, 10 changed functions, 1063 identical functions, and 0 unmatched functions in either direction.
- Verdict: The patch adds bounds clamping to the SAN (System Area Network) accept path in
AfdSanAcceptCore, where the remote peer's transport-address length is used as a copy size without an upper-bound check against the destination buffer capacity. The clamps address a real out-of-bounds write, but they are gated behind a Windows feature flag (EvaluateCurrentStateong_Feature_232206649_59779869); when the flag is disabled the patched code follows the same unbounded path as the unpatched build. The path is only reachable through SAN sockets (endpoint type0x1AFD), a specialized direct-access networking path, so severity is assessed as Medium rather than High.
2. Vulnerability Summary
- Severity: Medium
- Vulnerability Class: Out-of-bounds write (CWE-787)
- Affected Function:
AfdSanAcceptCore
Root Cause:
AfdSanAcceptCore handles SAN socket accept operations. When a connection is accepted, the function copies the remote peer's transport-address data (captured at connect time) into the accepting socket's address buffer, which is described by an MDL and reached through MmMapLockedPages. The copy size is taken directly from a 16-bit length field in the connection data (*(word)(ConnData+0x14), plus a small constant of 8, 2, or 4 depending on the address format). In the unpatched build, the destination capacity (*(dword)(AcceptInfo+0x10), AddressBufferLength) is only tested for being non-zero (cmp [rbx+0x10], edi; jbe); it is never compared against the copy size. A peer whose transport-address length exceeds the accepting socket's address-buffer capacity therefore causes a write past the end of the destination buffer.
The patched build adds, before each copy, a minimum-capacity requirement (>= 0xc) and a clamp of the copy size to the destination capacity (min(copy_size, capacity)). Each of these new checks is guarded by EvaluateCurrentState(&g_Feature_232206649_59779869_FeatureDescriptorDetails); when the feature evaluates to disabled, the copies run without the clamp exactly as in the unpatched build.
Attacker-Reachable Entry Point & Call Chain:
Reachable locally from a process that creates an AFD SAN socket and issues an accept IOCTL.
1. NtDeviceIoControlFile (User-mode API targeting \Device\Afd)
2. AfdDispatchDeviceControl (Kernel dispatch)
3. AfdAccept / AfdServiceSuperAccept (IOCTL_AFD_ACCEPT / IOCTL_AFD_SUPER_ACCEPT)
4. AfdServiceWaitForListen
5. AfdSanAcceptCore (address copy)
Reachability is constrained to SAN endpoints (type 0x1AFD); creating a functional SAN socket requires an active SAN provider and is not part of the ordinary TCP/UDP accept path.
3. Decompilation Diff
Real decompiled excerpts from both builds. v15 is the accept address descriptor (*(endpoint+0xB8)): v15[2]=AddressBuffer offset (+0x8), v15[4]=AddressBufferLength capacity (+0x10), v15[6]=AddressOffset (+0x18). v36/v59 is the stored remote transport address; *(word)(v36+0x14) is the peer-supplied length.
// --- UNPATCHED AfdSanAcceptCore ---
if ( v15[4] != 0 ) // ONLY checks capacity != 0
{
v21 = *(unsigned __int16 *)(v36 + 20); // peer address length (+0x14)
LODWORD(Size) = v21 + 8; // copy size, no upper bound
v22 = *(_QWORD *)(a1 + 8); // MDL
if ( *(_BYTE *)(v8 + 172) != 0 ) // TDI-format flag
{
v23 = v21 + 2;
...
memmove(&v26[v15[6] + v15[2]], (const void *)(v36 + 22), v23); // OOB write
}
else
{
...
memmove(&v28[v15[6] + v15[2]], (const void *)(v36 + 16), (unsigned int)Size); // OOB write
}
...
}
// --- PATCHED AfdSanAcceptCore ---
if ( v15[4] == 0
|| EvaluateCurrentState(&g_Feature_232206649_59779869_...) && v15[4] < 0xCu ) // NEW min capacity 12 (feature-gated)
{
goto LABEL_59;
}
LODWORD(Irql) = v59[10];
v22 = (_DWORD)Irql + 8;
if ( *(_BYTE *)(v7 + 172) != 0 )
{
v23 = (_WORD)Irql + 2;
if ( EvaluateCurrentState(&g_Feature_232206649_59779869_...) // NEW clamp (feature-gated)
&& (unsigned __int64)(unsigned int)v15[4] - 2 < v23 )
{
v23 = *((_WORD *)v15 + 8) - 2; // clamp to capacity - 2
}
...
memmove(&v25[v15[2]], v59 + 11, v23);
}
else
{
if ( EvaluateCurrentState(&g_Feature_232206649_59779869_...) && v15[4] < v22 ) // NEW clamp (feature-gated)
v22 = v15[4]; // clamp to capacity
...
memmove(&v32[v15[2] + v15[6]], v59 + 8, v33);
}
When EvaluateCurrentState returns 0, none of the v15[4] comparisons run and the copies use the unclamped sizes, matching the unpatched behavior.
4. Assembly Analysis
Instructions below are copied from the disassembly of AfdSanAcceptCore @ 0x1C007508C in each build.
Unpatched (missing upper-bound check and unclamped copies):
; Capacity check (only != 0):
0x1c0075305 cmp [rbx+0x10], edi ; edi=0; AddressBufferLength
0x1c0075308 jbe 0x1c0075485 ; skip copies only if capacity == 0
; Load peer length and compute copy size, no bounds:
0x1c007530e movzx r12d, word [r9+0x14] ; r12 = peer transport addr len
0x1c0075313 lea eax, [r12+0x8] ; size = len + 8
0x1c0075318 mov [rsp+Size], eax
; Primary copy (non-TDI path):
0x1c00753d2 mov r8d, [rsp+Size] ; r8 = len + 8 (unclamped)
0x1c00753df lea rdx, [r12+0x10] ; src = ConnData+0x10
0x1c00753e4 mov eax, [rbx+0x18] ; AddressOffset
0x1c00753e7 mov ecx, [rbx+0x8] ; AddressBuffer
0x1c00753ea add rax, r9
0x1c00753ed add rcx, rax ; dest = AddressBuffer + AddressOffset + MDL VA
0x1c00753f0 call memmove
; Primary copy (TDI path, size = len + 2):
0x1c007539c call memmove
; Secondary copies (also unclamped):
0x1c0075432 movzx r8d, word [r12+0x18] ; secondary len
0x1c007543e add r8, r13 ; + 2 (TDI secondary)
0x1c0075449 call memmove
0x1c007546b movzx eax, word [r12+0x18]
0x1c0075471 add eax, 0x4 ; + 4 (non-TDI secondary)
0x1c0075480 call memmove
Patched (feature-gated minimum-capacity and clamp-to-capacity):
0x1c0075317 cmp [rbx+0x10], edi ; same capacity != 0 check
0x1c007531a jbe 0x1c0075610
0x1c0075320 lea rcx, g_Feature_232206649_59779869_FeatureDescriptorDetails
0x1c0075327 call EvaluateCurrentState ; feature gate
0x1c007532c test eax, eax
0x1c007532e jz 0x1c007533a ; feature disabled -> skip min-size check
0x1c0075330 cmp dword [rbx+0x10], 0xc ; NEW: require capacity >= 12
0x1c0075334 jb 0x1c0075610
; ...
0x1c0075433 cmp [rbx+0x10], r14d ; NEW: clamp size to capacity
0x1c0075437 cmovb r14d, [rbx+0x10] ; r14 = min(size, capacity)
5. Trigger Conditions
- Obtain a handle: Create an AFD SAN socket (endpoint type
0x1AFD) or open\Device\Afdand drive it to the SAN endpoint state. - Bind and Listen: Issue an
AfdBindIOCTL with a SAN transport address. The accepting socket's address buffer is set up with a specific capacity (AddressBufferLengthatAcceptInfo+0x10). - Issue Accept: Post an accept on the listening SAN socket via
IOCTL_AFD_ACCEPTorIOCTL_AFD_SUPER_ACCEPTthroughNtDeviceIoControlFile. - Connect with an oversized address: Have a peer connect with a transport address whose length field (
*(word)(ConnData+0x14)or the secondary length at+0x18) plus the fixed offset (8, 2, or 4) exceeds the accepting socket'sAddressBufferLength. - Observable Effect:
AfdSanAcceptCorewrites past the end of the mapped destination address buffer. Depending on layout this results in corruption of adjacent memory in the mapped region or a fault when the write reaches unmapped pages.
6. Exploit Primitive & Notes
- Primitive: An out-of-bounds write whose size is derived from a peer-controlled 16-bit transport-address length (up to
0xFFFFplus a small constant), against the SAN accept address buffer reached throughMmMapLockedPages. The write extends past the intended destination within the mapped address region. - Reachability: Only through the SAN accept path (endpoint type
0x1AFD), which requires an active SAN provider. This is not the ordinary TCP/UDP accept path and is not reachable by a default unprivileged process without SAN support present. - Delivered fix status: The patched clamps are gated behind
EvaluateCurrentState(g_Feature_232206649_59779869). When the feature is disabled the patched binary executes the same unbounded copies as the unpatched binary, so this is a staged (feature-flagged) rollout of the bounds check rather than an unconditionally active fix. - Detection: Driver Verifier Special Pool placed after the destination buffer will fault on the overwrite at the copy instruction, making the boundary violation directly observable.
No working privilege-escalation chain is demonstrated from these binaries; any claim of controlled kernel-object corruption, information disclosure, or code execution would require artifacts not present in this diff.
7. Debugger Verification Playbook
Attach a kernel debugger to the unpatched build to confirm the boundary condition.
- Breakpoints:
text bp afd_unpatched!AfdSanAcceptCore+0x282 ; 0x1c007530e, peer length load bp afd_unpatched!AfdSanAcceptCore+0x364 ; 0x1c00753f0, primary memmove - What to inspect:
- At
0x1c007530e:wo(@r9+0x14)is the peer transport-address length;dwo(@rbx+0x10)is the destinationAddressBufferLength. Iflength + 8 > capacity, the copy will overflow. - At
0x1c00753f0:r8dis the copy size (length + 8),rcxis the destination,rdxis the source (ConnData+0x10).r8dstrictly greater than*(rbx+0x10)confirms the out-of-bounds write. - Key instructions/offsets:
0x1c0075305/0x1c0075308: the only capacity gate (> 0, no upper bound).0x1c0075313: unclamped size computation (len + 8).0x1c00753f0,0x1c007539c,0x1c0075449,0x1c0075480: the four copies.- Patched equivalents:
0x1c0075330(cmp [rbx+0x10], 0xc) and0x1c0075433/0x1c0075437(cmp+cmovbclamp), each reached only when the feature flag is enabled.
8. Changed Functions — Full Triage
AfdSanAcceptCore(Sim: 0.8109, Security Relevant): Adds minimum-capacity (>= 0xc) and clamp-to-capacity checks before eachmemmovethat copies SAN transport addresses. All new checks are gated onEvaluateCurrentState(g_Feature_232206649_59779869); with the flag disabled the unbounded copies of the unpatched build remain.AfdSanConnectHandler(Sim: 0.9407, Behavioral): Retains the connection-data copy (sub_1c000c900) unchanged; only register allocations differ. The copy is bounded by its own allocation, so no overflow.AfdSocketTransferBegin(Sim: 0.8896, Behavioral): Removed feature-gating around socket-state validation; state checks now applied unconditionally. Defense-in-depth, not an exploitable fix.AfdSocketTransferEnd(Sim: 0.8896, Behavioral): Same pattern asAfdSocketTransferBegin; state checks now unconditional.AfdBind(Sim: 0.9682, Behavioral): Feature-flag stabilization; unbind-prevention calls during bind are now always made.AfdGetInformation(Sim: 0.9768, Cosmetic): Register reallocation and stack-offset changes. No logic change.EvaluateCurrentState(Sim: 0.1602, Cosmetic): Logic unchanged; offsets shifted by global data relocation.EvaluateCurrentStateFromRegistry(Sim: 0.255, Cosmetic): Logic unchanged; offset shifts.QueryFeatureOverride(Sim: 0.1061, Cosmetic): Logic unchanged; offset shifts.AfdCopyMdlChainToBufferAvoidMapping$filt$0(Sim: 0.5905, Cosmetic): Exception filter stack-layout adjustment; no behavioral change.
9. Unmatched Functions
No functions were added or removed in the patched binary.
10. Confidence & Caveats
- Confidence: High that the pattern is real and the direction is correct. The unpatched build copies peer-controlled transport-address lengths into the accept address buffer with only a
capacity != 0gate, and the patched build adds minimum-capacity and clamp-to-capacity checks at the same copies. - Feature-gated: The delivered clamps are conditional on
EvaluateCurrentState(g_Feature_232206649_59779869). In this build the fix is a staged rollout; the unbounded path is retained for the disabled state. - Reachability: SAN sockets are a specialized direct-access networking path (endpoint type
0x1AFD) requiring a SAN provider. This constrains practical reachability and is the main reason severity is Medium rather than High. - Not demonstrated: No exploit primitive beyond the out-of-bounds write itself is provable from these binaries; escalation to privilege gain is not established here.