http.sys — Double-fetch / TOCTOU on user-supplied trailer source pointer (CWE-367) in UlGenerateTrailers fixed
KB5077181
1. Overview
- Unpatched Binary:
http_unpatched.sys - Patched Binary:
http_patched.sys - Similarity Score: 0.9918
- Diff Statistics: 3910 matched, 6 changed, 3904 identical, 0 unmatched.
- Verdict: The patch resolves a high-severity Time-of-Check Time-of-Use (TOCTOU / CWE-367) double-fetch vulnerability in the HTTP Server API kernel driver (
http.sys), where concurrent user-mode threads could race to trick the kernel into copying arbitrary kernel memory into an attacker-readable output buffer.
2. Vulnerability Summary
- Severity: High
- Vulnerability Class: Double-fetch / TOCTOU (CWE-367)
- Primary Vulnerable Function:
UlGenerateTrailers (sub_1400755C0) - Related KIR-gated user-mode pointer changes:
UxQuicConnectionCopyBindings (sub_1400E1AB4),UxQuicConnectionGetPeerIssuerListClient (sub_1400E28A8),UxpQuicConnectionCopyCertBlob (sub_1400E9D8C)
Root Cause & Exploitability:
UlGenerateTrailers builds the trailer section of an outbound HTTP response from a user-supplied descriptor. The descriptor at a2 holds a count at offset +0x8 and, at offset +0x10, a pointer to an array of 0x18-byte entries. Each entry carries two length fields (offsets +0x0 and +0x2) and two data pointers (offsets +0x8 and +0x10) that point into user-mode memory. The driver validates the entry array once with UlProbeForRead, then validates each embedded data pointer with UlProbeAnsiString before copying its bytes with memmove.
In the unpatched build, each embedded data pointer is read from user memory twice: once when it is passed to UlProbeAnsiString, and again when it is passed to memmove. Between those two reads a second user-mode thread can overwrite the pointer field, swapping the probed user-mode address for a kernel address. The probe passes on the original value while the copy dereferences the swapped value, so memmove reads from an attacker-chosen kernel address into the response trailer buffer.
The patched build captures each embedded data pointer once into register r12 and, when the KIR feature flag UxKirHttpReadingTrailersFromUm is enabled, uses that captured value for both the probe and the copy, closing the race window. When the flag is disabled the old re-read path is retained, so the fix is gated on that Known-Issue-Rollback flag (set from a Feature-staging query in UxStartEnvironmentModule).
Attacker-Reachable Entry Point & Data Flow:
1. A local process using the HTTP Server API submits a response-send request carrying trailer descriptors.
2. The request is dispatched through UlSendEntityBodyIoctl (sub_140098b80) / UlSendEntityBodyFastIo (sub_14004b3e0).
3. Processing flows UlFastSendHttpResponse (sub_1400978b8) -> UlCaptureHttpResponse (sub_14009ab5c) -> UlpPrepareHttpResponse (sub_14009b744).
4. UlGenerateTrailers (sub_1400755C0) is called to copy the user-supplied trailer entries. The double-fetch occurs here.
3. Pseudocode Diff
Below is a conceptual look at the vulnerable code pattern versus the patched code pattern in UlGenerateTrailers (sub_1400755C0).
// === UNPATCHED LOGIC (Vulnerable to TOCTOU) ===
void UlGenerateTrailers(..., USER_ENTRY *EntryArray, int Count, char *OutBuf) {
UlProbeForRead(EntryArray, 24 * Count, 8, mode); // whole array validated once
for (int i = 0; i < Count; i++) {
USER_ENTRY *entry = &EntryArray[i];
USHORT len = entry->length;
// TIME OF CHECK: probe reads entry->data_ptr from user memory
UlProbeAnsiString(entry->data_ptr, len, mode);
// --- RACE WINDOW: attacker overwrites entry->data_ptr with a kernel address ---
// TIME OF USE: memmove re-reads entry->data_ptr from user memory
memmove(OutBuf, entry->data_ptr, len);
}
}
// === PATCHED LOGIC (Single-Capture, KIR-gated) ===
void UlGenerateTrailers(..., USER_ENTRY *EntryArray, int Count, char *OutBuf) {
UlProbeForRead(EntryArray, 24 * Count, 8, mode);
for (int i = 0; i < Count; i++) {
USER_ENTRY *entry = &EntryArray[i];
USHORT len = entry->length;
// SINGLE FETCH: capture the pointer once (register r12)
void *captured_ptr = entry->data_ptr;
UlProbeAnsiString(captured_ptr, len, mode);
// Source used for the copy depends on the Known-Issue-Rollback flag:
void *src = UxKirHttpReadingTrailersFromUm ? captured_ptr // fixed: reuse captured value
: entry->data_ptr; // rollback: re-read (old behavior)
memmove(OutBuf, src, len);
}
}
4. Assembly Analysis
The double-fetch is visible in the unpatched assembly of UlGenerateTrailers (sub_1400755C0). The function body is split, with the entry-processing loop placed in a cold chunk beginning at 0x14015f402. The entry array base is evaluated, and the pointers at offsets +0x8 and +0x10 are fetched twice. sub_1400640f0 is UlProbeAnsiString; sub_14014de80 is memmove.
Unpatched UlGenerateTrailers (sub_1400755C0) — double-fetch
; === Entry processing loop setup ===
0x14015f49a: movzx eax, ax ; loop index
0x14015f4a1: mov rax, qword [rsp+0x58] ; rax = array base (user memory)
0x14015f4a6: lea rax, [rax+rcx*8] ; rax = current entry (base + index*0x18)
0x14015f4af: movzx ecx, word [rax] ; ecx = entry->length (offset 0x0)
; === DOUBLE-FETCH #1: entry+0x8 pointer ===
0x14015f4cb: mov rcx, qword [rax+0x8] ; FIRST READ of ptr for probe
0x14015f4cf: call UlProbeAnsiString ; UlProbeAnsiString(ptr, length, mode)
...
0x14015f504: mov rdx, qword [rsp+0x50] ; reload entry base
0x14015f509: mov rdx, qword [rdx+0x8] ; SECOND READ of ptr (TOCTOU WINDOW)
0x14015f50d: mov rcx, r13 ; rcx = destination trailer buffer
0x14015f510: call memmove ; memmove(out_buf, attacker_swapped_ptr, len)
; === DOUBLE-FETCH #2: entry+0x10 pointer ===
0x14015f546: mov rcx, qword [rax+0x10] ; FIRST READ of secondary ptr
0x14015f54a: call UlProbeAnsiString ; UlProbeAnsiString
...
0x14015f56b: mov rdx, qword [rsp+0x50] ; reload entry base
0x14015f570: mov rdx, qword [rdx+0x10] ; SECOND READ of secondary ptr
0x14015f574: mov rcx, r13 ; rcx = destination trailer buffer
0x14015f577: call memmove ; memmove(out_buf, attacker_swapped_ptr, len)
Patched UlGenerateTrailers (sub_14009DE00) — single-capture
The patched version reads each pointer into r12 once; when UxKirHttpReadingTrailersFromUm is set it feeds that captured register to the copy instead of re-reading user memory.
0x14009df66: mov r12, qword [rax+8] ; CAPTURE pointer once into r12
0x14009df77: mov rcx, r12 ; probe uses the captured r12
0x14009df7a: cmp cs:UxKirHttpReadingTrailersFromUm, 0
0x14009df81: jz 0x14009dfa5 ; flag==0 -> rollback (re-read) path
0x14009df83: call UlProbeAnsiString ; UlProbeAnsiString(captured_ptr)
...
0x14009dfa0: mov rdx, r12 ; FIX: reuse captured r12 as copy source
0x14009dfa3: jmp 0x14009dfc6
; --- rollback path (flag==0): re-reads entry+8 from user memory ---
0x14009dfc2: mov rdx, qword [r13+8] ; source re-read (old behavior)
0x14009dfc6: mov rbx, r14
0x14009dfcf: call memmove ; memmove(out_buf, source, len)
sub_14014de80 (unpatched) and sub_14014e300 (patched) are the same memmove routine at a shifted address; the copy helper itself is unchanged between builds.
5. Trigger Conditions
- Obtain a handle to the HTTP Server API via Win32 (
HttpCreateHttpHandle,HttpCreateRequestQueue) or native (NtDeviceIoControlFiledirectly targeting\Device\Http\*). - Allocate an input buffer for the IOCTL containing an array header (count at offset
+0x8, must be1to0x2710) and a pointer to the array at offset+0x10. - Populate the array with
Nentries. Each entry is 0x18 bytes:[uint16 length1][uint16 length2][ptr data1][ptr data2]. Ensurelengthmatches your buffer sizes. - Initialize
data1to point to a legitimate user-mode buffer. - Spawn a racing thread that continuously executes
InterlockedExchange64on thedata1field, alternating between the legitimate user-mode pointer and a target kernel address (e.g.,0xFFFFF805'C0000000). - Submit the IOCTL from the main thread in a tight loop.
- Inspect the IOCTL output buffer after each call. If the race is won, the output buffer will contain the raw bytes of the target kernel address instead of your user-mode buffer data.
6. Exploit Primitive & Development Notes
- Primitive: Arbitrary Kernel Memory Read (Information Disclosure).
- Limitations: Because it relies on winning a race condition, you cannot reliably read large contiguous blocks in a single request. However, by requesting small lengths (e.g., 8 bytes) and looping, you can slowly exfiltrate memory.
- Usage: This primitive is primarily an info-leak. It can be used to:
- Defeat KASLR by reading known kernel structures to find base addresses.
- Scan for authentication tokens in kernel pool memory.
- Locate the
EPROCESSofSystem.exeto prepare for a subsequent token-stealing exploit. - Mitigations: Standard kernel protections like SMEP, SMAP, and CFG do not prevent this vulnerability, as the kernel is legally performing a read on behalf of an IRP. SMAP actually helps the attacker by ensuring the probe correctly passes user-mode pointers before the swap.
7. Debugger PoC Playbook
To validate and analyze this bug on the unpatched binary, attach WinDbg in kernel mode and use the following commands.
Breakpoints
Set targeted breakpoints to monitor the probe and the copy phases:
bp http+0x15f4cb "printf \"[PROBE] Entry Ptr: %p\\n\", rax; .if (poi(rax+8) > 0xFFFF800000000000) { echo WARNING: Kernel Addr in user struct! }; gc"
bp http+0x15f509 "printf \"[COPY ] Re-read Ptr: %p\\n\", rdx; gc"
bp http+0x15f510 ".if (rdx > 0xFFFF800000000000) { echo [VICTORY] Kernel Address detected in memmove source! } .else { gc }"
What to Inspect
- At
UlGenerateTrailerscold chunk (0x14015f4cb): Inspectrax(the user-mode entry structure). Look at[rax+8]— this is the pointer being submitted toUlProbeAnsiString. - At
0x14015f509: Inspectrdx. Ifrdxdiffers from what you observed at0x14015f4cb, the race succeeded. Ifrdxis greater than0xFFFF800000000000, a kernel address was swapped in. - At the
memmoveroutine (http+0x14de80):rcxis the destination trailer buffer (r13),rdxis the source,r8is the length.
Key Instructions/Offsets
0x14015f4cb: First fetch of the user pointer.0x14015f4cf:UlProbeAnsiStringcall.0x14015f509: THE BUG. Second fetch of the user pointer.0x14015f510: The dangerousmemmoveexecution.
Trigger Setup
Drive an HTTP response send through the HTTP Server API (or NtDeviceIoControlFile against \Device\Http\*) with a response descriptor whose trailer field points to an array of at least one 0x18-byte entry, each entry carrying non-zero length fields and user-mode data pointers.
Expected Observation
If the race is lost (the probe catches the kernel address), UlProbeAnsiString raises a STATUS_ACCESS_VIOLATION which http.sys catches internally and converts to a request error. The system remains stable.
If the race is won, the breakpoint at 0x14015f510 halts. rdx holds a kernel address; stepping over the call memmove executes the copy, and dumping rcx afterward reveals the leaked kernel memory.
8. Changed Functions — Full Triage
UlGenerateTrailers (sub_1400755C0)(Sim: 0.684): Vulnerable. Copies user-supplied HTTP trailer entries, performing a source-pointer double-fetch onentry+0x8andentry+0x10(probe withUlProbeAnsiString, then re-read formemmove). Patched by capturing each pointer once intor12, used for the copy whenUxKirHttpReadingTrailersFromUmis enabled.UxQuicConnectionCopyBindings (sub_1400E1AB4)(Sim: 0.858): User-mode destination-pointer handling. Copies ALPN/binding data from an in-kernel QUIC connection object (*(a1+1712)) into a caller output buffer under a push lock (a1+0x28). The source is kernel data read once, so this is not a source double-fetch. The unpatched code truncates the destination pointera3+8to 32 bits ((unsigned int)(a3+8)); the patched code preserves the full 64-bit destination pointer whenUxKirWritingAndReadingUmis set, and only truncates in the rollback path.UxQuicConnectionGetPeerIssuerListClient (sub_1400E28A8)(Sim: 0.898): Destination-base capture. Copies the peer certificate issuer list from an in-kernel QUIC object (*(a1+1680)) into the caller buffer under a shared push lock. Source pointers are kernel data. The patched code, gated onUxKirWritingAndReadingUmCertList, captures the output basea3+16once instead of re-reading*(a3)(which was just written into the caller-visible buffer) when placing the per-entry header pointers.UxpQuicConnectionCopyCertBlob (sub_1400E9D8C)(Sim: 0.864): User-mode destination-pointer handling. Copies a certificate blob from an in-kernel structure into the caller buffer. The unpatcheda2 != 0branch truncates the destinationa3+8to 32 bits. The patched build moves this branch into the helperUxpQuicConnectionCopyCertBlob32 (sub_1400ea280), which preserves the full 64-bit destination whenUxKirWritingAndReadingUmis set.UxStartEnvironmentModule (sub_1400B6AF0)(Sim: 0.942): Initialization / feature staging. Sets the three Known-Issue-Rollback flagsUxKirHttpReadingTrailersFromUm,UxKirWritingAndReadingUm, andUxKirWritingAndReadingUmCertListfrom Windows Feature-staging queries (Feature_2280337722,Feature_1255102777,Feature_1282877755). These flags gate the corrected paths above. Not directly reachable by an attacker.sub_14015F5C7(Sim: 0.969): This address falls inside theUlGenerateTrailerscold chunk, not a separate function. The register-allocation difference (rdivsrbx) is a byproduct of the recompile with no independent behavioral change.
9. Unmatched Functions
- Removed: None.
- Added: None.
(Note:
sub_14014de80in the unpatched build andsub_14014e300in the patched build are the samememmoveroutine at a relocated address; the copy helper is unchanged. The behavioral fix lives in the callers, not in a new wrapper.UxpQuicConnectionCopyCertBlob32 (sub_1400ea280)already exists in both builds asUxpQuicConnectionCopyCertBlob32.)
10. Confidence & Caveats
- Confidence Level: High. The double-fetch assembly pattern is a textbook CWE-367 vulnerability. The fix directly maps to standard mitigation techniques for TOCTOU bugs.
- Assumptions Made:
- The attacker has normal, unprivileged access to the HTTP Server API to submit a response with trailer descriptors that reach
UlGenerateTrailers. - The
UxQuicConnection*functions are query paths that copy data out of in-kernel QUIC connection objects into a caller buffer; their changes concern 64-bit user-mode destination-pointer handling rather than a source double-fetch, so they are lower severity than the trailer path. - Verification Required: A PoC writer must reconstruct the trailer descriptor layout (count at
+0x8, entry-array pointer at+0x10, 0x18-byte entries with data pointers at+0x8/+0x10) that reaches theUlGenerateTrailersloop, and must confirmUxKirHttpReadingTrailersFromUmis enabled on the patched build when validating the fix, since the single-capture path is gated on that flag.