http.sys — Unsafe direct user-mode reads in legacy send/capture paths hardened with safe-capture primitives (CWE-822) fixed
KB5079473
1. Overview
- Unpatched Binary:
http_unpatched.sys - Patched Binary:
http_patched.sys - Overall Similarity Score: 0.9267
- Diff Statistics:
- Matched Functions: 3905
- Identical Functions: 3706
- Changed Functions: 199
- Unmatched Functions: 0 (Added: 0, Removed: 0)
- Verdict: The patch is a user-mode-access (UMA) hardening rollout across the legacy ("Old") HTTP response-send and channel-binding-capture paths. Where the unpatched build dereferences user-supplied pointers directly (each direct read already guarded by
ProbeForRead/UlProbeAnsiStringon the string data, and the array as a whole probed once on the 64-bit path), the patched build adds an alternative code path that reads each field once through the safe-capture intrinsics (RtlReadUShortFromUser/RtlReadULongFromUser/RtlReadULong64FromUser,RtlCopyFromUser/RtlCopyVolatileMemory), adds explicit alignment validation (UxProbeAlignment/ abase & 7check), and uses a combined probe-and-copy helper (UxTryProbeAndCopyAnsiStringFromMode). The new path is selected by default through a new Known-Issue-Rollback (KIR) feature flag,UxKirNewUma(present 280 times in the patched build, absent in the unpatched build). Because this is a KIR, the old direct-dereference code is retained in the patched binary and runs whenever the flag is disabled. The safe-capture helpers (RtlRead*FromUser,RtlCopy*,UxProbeAlignment,ReadPtr32FromMode) do not appear at all in the unpatched build. This is defense-in-depth hardening of user-mode reads; no exploitable primitive (out-of-bounds write, unbounded copy, or observable information leak) is demonstrable in the binaries, because the string data is probed and the copy lengths are bounds-checked in both builds.
2. Vulnerability Summary
Finding 1: Direct/Double User-Mode Read of Trailer Descriptors — UlGenerateTrailers (sub_14009DE00)
- Severity: Low (local; defense-in-depth hardening)
- Vulnerability Class: Unsafe direct user-mode pointer dereference, including a double read of a length field (CWE-822, with a CWE-367 double-fetch aspect)
- Affected Function:
UlGenerateTrailers(unpatchedsub_14009DE00@ 0x14009DE00; patched @ 0x14009EEA0) - Root Cause:
UlGenerateTrailerswalks an array of trailer name/value descriptors that the caller of the HTTP Server API supplies in the response object (array base at*(a2 + 16), count at*(a2 + 8), rejected at or above0x2710). In the unpatched build each descriptor length is read by dereferencing the pointer directly: the name length is loaded intov18(v18 = *v17) and then loaded a second time intov33(v33 = *v17), and the value length is likewise loaded intov23/v24fromv17[1]; the name/value string pointers are read directly from*(entry + 8)/*(entry + 16). The name-length reads are only two adjacent loads of the same field, so the double-fetch window is extremely narrow. The string data itself is probed withUlProbeAnsiStringbefore it is copied, and the copy into the kernel destination is bounded byif (name_len > remaining) fail(returning0xC0000096/-1073741670) in both builds, so this path does not overflow the kernel buffer. On the 64-bit caller path the whole descriptor array is probed once (UlProbeForRead(base, 24 * count, 8, mode)); on the 32-bit / already-captured caller path the array is read directly, because for that caller the descriptor is a kernel-side capture (the same direct read is present in both the unpatched build and the new patched path). The patched build adds, underUxKirNewUma, an alternative path that reads each field once throughRtlReadUShortFromUser/RtlReadULong64FromUser, validates the array-base alignment ((base & 7) != 0rejected withSTATUS_DATATYPE_MISALIGNMENT,0xC000000D), and copies the string withUxTryProbeAndCopyAnsiStringFromMode. The pre-existing direct-read path (with its own internalUxKirHttpReadingTrailersFromUmsub-gate,UlProbeAnsiStringOld+memmove) is kept as theelsebranch and runs whenUxKirNewUmais off. - Entry Point: A local process calls the HTTP Server API to send a response with trailers (for example
HttpSendHttpResponse/HttpSendResponseEntityBodywith a trailer array). - Data Flow (Call Chain):
- Caller of the HTTP Server API sends a response with trailers.
http.syshandles the send IRP.UlpSendResponseOrEntityBodyFastIo(sub_14004A5B0) / the legacy send path drive the fast-send flow.UlFastSendHttpResponseOld(sub_1400CC828) callsUlGenerateTrailers(sub_14009DE00) to serialize the trailers, where the descriptors are read.
Finding 2: Transfer-Encoding Header Read Before Comparison — UlFastSendHttpResponseOld (sub_1400CC828)
- Severity: Low (local; defense-in-depth hardening)
- Vulnerability Class: Direct user-mode read before string comparison (CWE-822)
- Affected Function:
UlFastSendHttpResponseOld(unpatchedsub_1400CC828@ 0x1400CC828; patched @ 0x1400CEC3C) - Root Cause: This is the legacy fast response-send routine. When it inspects the caller-supplied
Transfer-Encodingheader value to decide whether the response is chunked, the unpatched build runs_strnicmp(user_ptr, "chunked", 7)directly against the user-mode string pointer after an ANSI-string probe (UlProbeAnsiString(*(v50 + 104), 7, mode)). The patched build, underUxKirNewUma, first copies the 7 bytes into a local kernel buffer (Str1) withRtlCopyFromUser/RtlCopyVolatileMemory(afterUlProbeAlignmentAnsiStringOld) and compares the local copy; the old direct-compare path is retained as theelsebranch. The case-insensitive"chunked"comparison itself exists in both builds. The MDL construction in this function (ByteCount,Size,ByteOffset,IoBuildPartialMdl) and its length bounds checkif (len > remaining) breakare unchanged between the two builds; this function makes no MDL size-calculation change. - Entry Point: Local HTTP Server API caller sends a response through the fast-send path.
- Data Flow (Call Chain):
- Caller sends a response.
http.syssend IRP dispatch.UlpSendResponseOrEntityBodyFastIo(sub_14004A5B0).UlFastSendHttpResponseOld(sub_1400CC828) reads the Transfer-Encoding header value.
Finding 3: Direct User-Mode Read of Channel-Binding Config Array — UlCaptureChannelBindConfigOld (sub_140121DD0)
- Severity: Low (local; defense-in-depth hardening)
- Vulnerability Class: Direct user-mode pointer dereference (CWE-822)
- Affected Function:
UlCaptureChannelBindConfigOld(unpatchedsub_140121DD0@ 0x140121DD0; patched @ 0x14012E9C4). Note: in the patched binary the address 0x140121DD0 is occupied by an unrelated ETW template helper (McTemplateK0pq_EtwWriteTransfer) because of layout shift; the matching function is anchored by symbol, at 0x14012E9C4. - Root Cause: This routine captures a channel-binding configuration passed by the caller: an array of up to
0x40service-name/channel-binding entry pointers (count rejected above0x40with-1073741811in both builds). In the unpatched build the header fields and each entry are read by dereferencing pointers directly (*a2,a2[1],a2[2]/a2[3]on the 32-bit path,*((_QWORD*)a2 + 1)/a2[4]on the 64-bit path, then the per-entry pointers). The patched build, underUxKirNewUma, reads each scalar once throughRtlReadULongFromUser/RtlReadULong64FromUser(orRtlCopyVolatileMemory), validates alignment withUxProbeAlignment, and marshals 32-bit entry pointers throughReadPtr32FromMode(sub_1400C9060). The 32-bit-versus-64-bit caller handling (IoIs32bitProcess, 4-byte versus 8-byte pointer stride,4*countversus8*countprobe length) is already present in both builds; the patch does not add WOW64 stride selection, it makes the per-field reads route through the safe-capture intrinsics. - Entry Point: Local caller of the HTTP Server API supplies a channel-binding configuration through the corresponding IOCTL.
- Data Flow (Call Chain):
- Caller submits a channel-binding config IOCTL.
http.sysIOCTL dispatch.UlCaptureChannelBindConfigOld(sub_140121DD0) captures and copies the entry array.
3. Pseudocode Diff
UlGenerateTrailers (sub_14009DE00 → sub_14009EEA0)
// === UNPATCHED UlGenerateTrailers (sub_14009DE00) ===
// 64-bit path probes the whole array once; the 32-bit / captured path reads directly.
if ( IoIs32bitProcess(Irp) != 0 )
v15 = v28; // caller descriptor already kernel-captured
else
UlProbeForRead(v28, 24 * v13, 8, a3); // probe whole 24*count array
while ( v16 < count ) {
v17 = (unsigned __int16 *)(v15 + 24LL * v16);
v18 = *v17; // name length, read #1 (used for copy + bounds)
v33 = *v17; // name length, read #2 (reported to UlSetParsedHeader)
v19 = *(_QWORD *)(entry + 8); // name pointer, direct read
UlProbeAnsiString(v19, (uint16)v18, a3); // string data IS probed
if ( (unsigned int)v18 > v12 ) { v11 = -1073741670; break; } // bounds the write
memmove(v7, v20, v18); // copy user string into kernel buffer
v7 += v18; v12 -= v18;
// value length read twice (v23/v24 from v17[1]); value copied the same way
v11 = UlSetParsedHeader(a6, v30, v21, v22, v24);
}
// === PATCHED UlGenerateTrailers (sub_14009EEA0) ===
// New KIR path selected by default; the else branch below still runs the old code.
if ( UxKirNewUma != 0 ) {
if ( IoIs32bitProcess(a1) == 0 && (base & 7) != 0 ) { // alignment check (non-32bit)
v10 = 0xC000000D; goto out; // STATUS_DATATYPE_MISALIGNMENT
}
for ( i = 0; i < count; ++i ) {
len = fromUser ? RtlReadUShortFromUser(base + 24LL*i) : *(WORD *)(base + 24LL*i);
if ( len != 0 ) {
ptr = fromUser ? RtlReadULong64FromUser(entry + 8) : *(void **)(entry + 8);
if ( len > remaining ) { v10 = -1073741670; goto out; } // bounds the write
v10 = UxTryProbeAndCopyAnsiStringFromMode(dst, ptr); // probe + copy in one
if ( v10 < 0 ) goto out;
dst += len; remaining -= len;
// value read via RtlReadUShortFromUser / RtlReadULong64FromUser, then copied
}
}
} else {
// RETAINED unpatched path: UlProbeForRead on 64-bit, direct *v43 reads,
// UlProbeAnsiStringOld + memmove, internal UxKirHttpReadingTrailersFromUm sub-gate.
}
4. Assembly / Code Analysis
UlGenerateTrailers name/value copy (unpatched vs patched)
- Unpatched
sub_14009DE00: the per-entry name length is read twice (v18 = *v17, thenv33 = *v17), the name pointer directly (*(entry+8)), the name/value are validated withUlProbeAnsiString(sub_1400640F0), the write is bounded byif (len > remaining) break(0xC0000096), and the bytes are moved withmemmove(sub_14014E300). The 64-bit path probes the whole array withUlProbeForRead; the 32-bit / already-captured path reads it directly. This direct-read loop is itself already gated by an internalUxKirHttpReadingTrailersFromUmflag in the unpatched build. - Patched
sub_14009EEA0: underUxKirNewUmathe same fields are read throughRtlReadUShortFromUser(sub_1401BD11C, which itself performs aProbeForRead(2, 1)then a single word read) andRtlReadULong64FromUser(sub_1401BD0A4), the array base is alignment-checked on the non-32bit path, and the string copy is performed byUxTryProbeAndCopyAnsiStringFromMode(sub_1400E5D34), whose negative return aborts the loop. WhenUxKirNewUmais off, theelsebranch reproduces the unpatched loop (UlProbeAnsiStringOld+memmove, with theUxKirHttpReadingTrailersFromUmsub-gate).UlSetParsedHeader(sub_14005BCC0) is still called per entry.
sub_14014E300 and sub_14015E3C0 are both memmove; the length check that bounds the destination write is present in both builds, so the copy into the kernel buffer does not overflow. The change is on the read side (per-field safe captures, alignment, probe-and-copy), and it is defense-in-depth rather than a fix for a demonstrable out-of-bounds condition: the string data is probed and the copy is length-bounded in both builds.
5. Trigger Conditions
To reach the changed read path in UlGenerateTrailers (sub_14009DE00):
- From a local process, call the HTTP Server API to send an HTTP response.
- Attach a trailer array to the send (a set of name/value descriptors in the response object).
- Supply descriptor entries so the per-entry length/pointer fields are read.
- Observable difference: On the unpatched build (and on the patched build with
UxKirNewUmadisabled) each length field is read directly and the name length is read twice; on the patched build with the flag enabled each field is read once throughRtlReadUShortFromUser/RtlReadULong64FromUser, a misaligned array base is rejected withSTATUS_DATATYPE_MISALIGNMENT, and the string is copied viaUxTryProbeAndCopyAnsiStringFromMode. Because the string data is probed and the copy is length-bounded in both builds, no out-of-bounds read or write of attacker-influenced size is reachable through this path.
6. Exploit Primitive & Development Notes
- Provided Primitive: None demonstrable in the binaries. The change removes a very narrow double-read of a length field and routes direct user-mode dereferences through the safe-capture intrinsics. In both builds the string pointers are probed (
UlProbeAnsiString/UlProbeAnsiStringOld) and the copy into the kernel buffer is bounded (if (len > remaining) break), so this path yields no out-of-bounds write and no attacker-sized copy. The double-read of the name length is two adjacent loads of the same field, so the time-of-check/time-of-use window is not meaningfully exploitable. - Constraints:
- The destination copy is bounded in both builds, so this path does not give a kernel-pool write overflow.
- The trigger requires a local caller of the HTTP Server API; it is reached through the response-send path, not by remote network bytes.
- The unpatched behavior is retained in the patched binary and runs whenever
UxKirNewUmais disabled, so this is a rollback-gated hardening change, not the removal of an exploitable code path. - Nature of the change: Defense-in-depth user-mode-read hardening (single safe captures, per-field probing, alignment validation), consistent across the three legacy routines and gated by one Known-Issue-Rollback flag.
7. Debugger Playbook
Connect a kernel debugger to the target environment to observe the read-path difference.
Breakpoints:
- bp http!UlGenerateTrailers (sub_14009DE00 @ 0x14009DE00)
- Why: Entry of the trailer serializer. arg1 (rcx) is the IRP, arg4 (r9) is the destination kernel buffer, arg5 is the remaining-size counter, arg2 holds the trailer descriptor object (*(a2+16) array base, *(a2+8) count).
- bp http!IoIs32bitProcess return inside UlGenerateTrailers
- Why: Confirms which caller path is taken; the 64-bit path probes the whole array, the 32-bit / captured path reads it directly in both builds.
- bp http!memmove (sub_14014E300)
- Why: Break on each copy to observe the source pointer and the length that were read from the descriptor and confirm the length is bounds-checked before the copy.
What to Inspect:
- At UlGenerateTrailers entry: descriptor array base and count (*(arg2+16), *(arg2+8)), destination buffer (arg4) and remaining size (arg5).
- At the copy: the per-entry length read from the descriptor, and that the source pointer was probed before the copy.
Trigger Setup (local):
- Use a small HTTP Server API program: HttpInitialize, HttpCreateHttpHandle, receive a request, then HttpSendHttpResponse / HttpSendResponseEntityBody with a trailer array.
Expected Observation:
- On the unpatched build (or patched with UxKirNewUma disabled) the name length is loaded twice and the fields are read directly. On the patched build with the flag enabled the fields are read once through the safe-capture intrinsics and a misaligned array base is rejected with STATUS_DATATYPE_MISALIGNMENT.
8. Changed Functions — Full Triage
UlGenerateTrailers(sub_14009DE00 → sub_14009EEA0) (Similarity: 0.4798):- Security-relevant hardening (Low). Adds a
UxKirNewUma-gated path that reads trailer-descriptor fields once viaRtlReadUShortFromUser/RtlReadULong64FromUser, adds array-base alignment validation, and copies strings withUxTryProbeAndCopyAnsiStringFromMode. The unpatched direct-read path (double name-length read,UlProbeAnsiStringOld+memmove) is retained as theelsebranch. Copy bounds and string probing are unchanged. UlFastSendHttpResponseOld(sub_1400CC828 → sub_1400CEC3C) (Similarity: 0.9482):- Security-relevant hardening (Low). Under
UxKirNewUma, copies the caller'sTransfer-Encodingheader into a local buffer viaRtlCopyFromUser/RtlCopyVolatileMemorybefore the case-insensitive"chunked"comparison; the old direct compare is retained. MDLByteCount/Sizecomputation and its length bounds check are unchanged between builds. UlCaptureChannelBindConfigOld(sub_140121DD0 → sub_14012E9C4) (Similarity: 0.3853):- Security-relevant hardening (Low). Reads the channel-binding config header and each service-name entry through
RtlReadULongFromUser/RtlReadULong64FromUser/RtlCopyVolatileMemory, validates alignment withUxProbeAlignment, and marshals 32-bit entry pointers viaReadPtr32FromMode. The 32-bit/64-bit stride handling and the0x40count cap already exist in both builds. UlpSendResponseOrEntityBodyFastIo(sub_14004A5B0 → sub_140068890) (Similarity: 0.9834):- Behavioral. Adds
UxKirNewUma-gated branch points consistent with the wider user-mode-access rollout; existing count/status/trailer limit checks are preserved. No standalone vulnerability fixed here. UxSslGetCertInfo(sub_1400751F0 → sub_14007DF20) (Similarity: 0.3794):- Behavioral. SSL certificate-info retrieval routine restructured as part of the same build-wide change; its data copies remain
memmove(sub_14014E300/ the relocatedsub_14015E3C0, bothmemmove). No memcpy-to-bounds-checked substitution occurs here. UxpSslCopyTransportIndicationsToHandshakeBuffer(sub_14005F380 → sub_14005E880) (Similarity: 0.5883):- Cosmetic/behavioral. SSL handshake transport-indication buffer copy; variable-allocation churn with the buffer-management (
memmove) structure preserved.
9. Unmatched Functions
- Added: None.
- Removed: None.
Note: The patch updates existing helper and dispatch routines and introduces the UxKirNewUma feature flag rather than adding new exported functions. Function addresses shift between the two builds (for example UlGenerateTrailers moves from 0x14009DE00 to 0x14009EEA0, and 0x140121DD0 is reused by an unrelated ETW helper in the patched binary), so matching is anchored by symbol name.
10. Confidence & Caveats
- Confidence Level: High on the mechanism; the diff consistently shows direct user-mode dereferences supplemented by a new safe-capture path (
RtlReadUShortFromUser/RtlReadULongFromUser/RtlReadULong64FromUser,RtlCopyFromUser/RtlCopyVolatileMemory), added alignment checks (UxProbeAlignment/base & 7), and probe-and-copy (UxTryProbeAndCopyAnsiStringFromMode), all gated by the newUxKirNewUmaflag and absent from the unpatched build. - Nature and Severity: This is defense-in-depth user-mode-read hardening, not the fix for a demonstrable exploitable bug. In both builds the trailer/header string data is probed and the copy lengths are bounds-checked, so no out-of-bounds write or attacker-sized copy is reachable, and the double-read of the name length is two adjacent loads with a negligible race window. Because the change is a Known-Issue-Rollback, the unpatched direct-read code is retained in the patched binary and runs whenever the flag is disabled. Severity is therefore Low and local.
- Scope Notes:
- The affected routines are on the response-send and channel-binding-capture paths; the input is supplied by the local user-mode caller of the HTTP Server API, so any exposure is local, not remote.
- The write-side length checks (
if (len > remaining) break), the string-data probes, and the WOW64 32-bit/64-bit stride selection (IoIs32bitProcess) are present in both builds; the patch does not introduce them.