http.sys — TOCTOU double-fetch leading to kernel memory disclosure (CWE-367, CWE-200) in UlGenerateTrailers fixed
KB5087420
1. Overview
- Unpatched Binary:
http_unpatched.sys - Patched Binary:
http_patched.sys - Overall Similarity Score: 0.9921
- Diff Statistics: 3587 matched functions, 3 changed functions, 3584 identical functions, and 0 unmatched functions in either direction.
- Verdict: The patch eliminates a Time-of-Check to Time-of-Use (TOCTOU) double-fetch vulnerability in HTTP trailer generation (
UlGenerateTrailers) that allows an attacker to swap a validated user-mode pointer for a kernel-mode address, resulting in arbitrary kernel memory disclosure.
2. Vulnerability Summary
- Severity: High
- Vulnerability Class: TOCTOU Race Condition / Double-Fetch (CWE-367) leading to Arbitrary Kernel Memory Disclosure (CWE-200).
- Affected Function:
UlGenerateTrailers (sub_1C000CF94)(trailer array iteration and serialization).
Root Cause
The vulnerable function, UlGenerateTrailers (sub_1C000CF94), serializes an array of HTTP trailers (headers emitted after a chunked response body) into an output buffer. The trailer array resides in user-mode memory. For each 24-byte entry, the function processes two data pointers (at offsets +0x8 and +0x10).
Depending on the Known Issue Rollback (KIR) feature flag UxKirHttpReadingTrailersFromUm, the unpatched binary can take a vulnerable code path (Path A, taken when the flag is 0). In Path A, the function reads the data pointer from the user-mode array and validates it using UlProbeAnsiString to ensure it points to a valid user-mode string. However, immediately before copying the data via memmove, the function re-reads the data pointer directly from the user-mode trailer array instead of using the previously validated pointer.
Because a user-mode thread can concurrently modify the contents of its own memory, an attacker can run a racing thread that continuously flips the data pointer between a valid user-mode address (to pass the UlProbeAnsiString check) and a target kernel-mode address. If the swap occurs in the narrow window between the probe and the memmove, the kernel will copy data from the attacker-supplied kernel address into the HTTP response output buffer, disclosing kernel memory to user mode. The patch captures the pointer once into a stack slot and reuses that validated value for the memmove, and removes the KIR flag branch so only the capture-once path remains.
Attack Surface & Data Flow
- An attacker initiates an HTTP Server API operation from user mode (e.g.,
HttpSendHttpResponsewith response trailers), triggering anNtDeviceIoControlFilesyscall to\Device\Http\ReqQueue. - The kernel IOCTL dispatcher routes the request to HTTP.sys response processing functions (
UlCaptureHttpResponse (sub_1C00DDA70)/sub_1c00ddff7). - The response preparation path (
UlpPrepareHttpResponse (sub_1C00DE140)) prepares the output buffer and calls the vulnerable functionUlGenerateTrailers (sub_1C000CF94). UlGenerateTrailers (sub_1C000CF94)iterates over the user-supplied trailer array, triggering the TOCTOU double-fetch flaw on the trailer data pointers.
3. Pseudocode Diff
The core vulnerability lies in trusting user-mode memory across two consecutive reads. The patch ensures a single fetch (capture-once) pattern.
// --- UNPATCHED LOGIC (UlGenerateTrailers, Path A) ---
void UlGenerateTrailers(struct* trailer_array) {
for (int i = 0; i < trailer_array->count; i++) {
// FIRST FETCH: read pointer for validation, saved to stack (Src)
void* data_ptr = trailer_array->entries[i].data_ptr;
if (UxKirHttpReadingTrailersFromUm == 0) { // Vulnerable Path A
// Validate the originally fetched pointer
UlProbeAnsiString(data_ptr, length, ...);
// SECOND FETCH: re-read pointer directly from user-mode memory
void* malicious_ptr = trailer_array->entries[i].data_ptr; // TOCTOU!
// Copies data using the unvalidated re-read pointer
memmove(output_buf, malicious_ptr, length);
} else { // Path B (flag != 0)
UlProbeAnsiString(data_ptr, length, ...);
memmove(output_buf, data_ptr, length); // reuses captured Src (safe)
}
}
}
// --- PATCHED LOGIC (single path, no KIR flag) ---
void UlGenerateTrailers(struct* trailer_array) {
for (int i = 0; i < trailer_array->count; i++) {
// CAPTURE ONCE: read pointer into local stack slot (Src)
void* data_ptr = trailer_array->entries[i].data_ptr;
// Validate the captured pointer
UlProbeAnsiString(data_ptr, length, ...);
// REUSE captured pointer. No re-read from user memory.
memmove(output_buf, data_ptr, length);
}
}
4. Assembly Analysis
The double-fetch is visible in the unpatched assembly, where the pointer is fetched from the user-mode array (r13) twice on the UxKirHttpReadingTrailersFromUm == 0 path.
Unpatched Vulnerable Sequence (Primary Data Pointer)
; === CRITICAL: First fetch of data pointer (for probe) ===
0x1c000d0cf: mov rcx, [r13+rax*8+8] ; rcx = entry[idx].data_ptr (FIRST FETCH)
0x1c000d0d4: mov [rsp+..Src], rcx ; save captured ptr to stack (Src)
0x1c000d0e5: cmp cs:UxKirHttpReadingTrailersFromUm, 0 ; check KIR feature flag
0x1c000d0ec: jz 0x1c000d125 ; flag==0 => VULNERABLE PATH A
; === VULNERABLE PATH A (flag == 0): double-fetch ===
0x1c000d125: call UlProbeAnsiString ; probe captured ptr (rcx), len
0x1c000d12a: cmp ebx, edi ; length <= remaining?
0x1c000d12c: jbe 0x1c000d13c ; yes => proceed to copy
0x1c000d13c: xor eax, eax
0x1c000d142: mov rbx, r12 ; rbx = length
0x1c000d145: mov r8, r12 ; r8 = length (memmove size)
0x1c000d148: mov r12, [rsp+..var_58] ; r12 = saved loop index*3
; *** VULNERABLE INSTRUCTION ***
0x1c000d14d: mov rdx, [r13+r12*8+8] ; *** SECOND FETCH: re-read ptr from USER ARRAY ***
0x1c000d152: mov rcx, r14 ; rcx = output buffer dest
0x1c000d155: call memmove ; memmove(out_buf, RE-READ ptr, len) -- RACE WINDOW
The Path B branch (flag != 0, fall-through at 0x1c000d0ee) instead reloads the captured pointer with mov rdx, [rsp+..Src] at 0x1c000d111 before memmove at 0x1c000d119, so it is not vulnerable.
Patched Safe Sequence
0x1c0028aa8: mov rax, [r15+rax*8+8] ; capture data ptr ONCE
0x1c0028aad: mov [rsp+..Src], rax ; store captured ptr (Src)
0x1c0028abe: mov rcx, rax ; use captured ptr
0x1c0028ac1: call UlProbeAnsiString ; probe captured ptr
0x1c0028ace: cmp r12d, esi ; bounds check
0x1c0028ae1: xor eax, eax
0x1c0028aed: mov rdx, [rsp+..Src] ; *** REUSE captured ptr (no re-read)
0x1c0028afd: call memmove ; memmove(out, captured ptr, len) -- SAFE
There is no KIR flag branch in the patched function; the capture-once path is unconditional.
Note: A similar double-fetch pattern exists for the secondary data pointer (offset +0x10) at unpatched addresses 0x1c000d1d1 (probe) and 0x1c000d1f4 (re-read), copied at 0x1c000d1ff. The patched counterpart captures once at 0x1c0028b34, probes at 0x1c0028b4f, and copies from the captured value at 0x1c0028b7a.
5. Trigger Conditions
- KIR Flag State: The target system must resolve the KIR feature flag
UxKirHttpReadingTrailersFromUmto0, enabling the vulnerable Path A. This flag is set inUxStartEnvironmentModulefromFeature_2347446586__private_IsEnabledDeviceUsage; a value of0corresponds to the feature being rolled back / disabled. - API Setup: The attacker must use the HTTP Server API (Win32
HttpSendHttpResponse/HttpSendResponseEntityBodywith response trailers) to trigger the vulnerable IOCTL path viaNtDeviceIoControlFileon\Device\Http\ReqQueue. - Payload Layout: The response must carry at least one trailer entry, which maps to the 24-byte internal structure. The trailer count must be between 1 and 9,999 (
0x270F). - The Race Condition: The attacker requires a concurrent racing thread within the same process. This thread must continuously overwrite the data pointer field (the trailer value pointer, at kernel struct offset
+0x8or+0x10). - Race Timing: The racing thread alternates the pointer between the original valid user-mode pointer and a target kernel address. The race must be won in the window between
UlProbeAnsiString(0x1c000d125) and the re-read (0x1c000d14d). - Confirmation: If the race is won, the attacker observes kernel memory leaked into the HTTP response buffer. If the attacker supplies an invalid kernel address, the system crashes with a
PAGE_FAULT_IN_NONPAGED_AREA(Bugcheck0x50).
6. Exploit Primitive & Development Notes
- Primitive: Arbitrary kernel memory disclosure (read).
- Exploit Strategy: While strictly an information disclosure bug, it is highly powerful. By supplying a known kernel virtual address, the attacker can copy kernel pool data, token structures, or session structures into the HTTP response. This can easily be leveraged to defeat KASLR or steal credentials.
- Heap Grooming: Not strictly required for the primitive itself, as the attacker directly specifies the read address.
- Mitigations: Standard kernel mitigations like SMEP, SMAP, and CFG do not prevent information disclosure from kernel reads executed in kernel context. The only barrier is ensuring the target address is paged in and valid to avoid a bugcheck.
- Reliability: The race window is extremely narrow (~6 instructions). The exploit should set thread affinity to a single CPU to force the racing thread and the HTTP.sys processing thread onto the same logical processor, using active spins to maximize the probability of a successful swap.
7. Debugger PoC Playbook
For a researcher with a kernel debugger (KD/WinDbg) attached to the unpatched binary, follow this playbook to confirm and trace the vulnerability:
Breakpoints
Set the following breakpoints to trace execution and validate the race:
bp http!UlGenerateTrailers ; (0x1c000cf94) Entry point. Examine initial arguments.
bp 0x1c000d0e5 ; KIR flag check. Ensure it routes to 0x1c000d125.
bp 0x1c000d125 ; UlProbeAnsiString call. Capture the original pointer (RCX).
bp 0x1c000d14d ; *** VULNERABLE SECOND FETCH. Compare RDX to probed value.
bp 0x1c000d155 ; memmove call. Verify destination (RCX) and source (RDX).
What to Inspect at Each Breakpoint
- At
0x1c000cf94(Function Entry): rdx(arg2): Pointer to the user-mode trailer struct.[rdx+0x8]: Trailer count (must be< 0x2710).[rdx+0x10]: User-mode trailer array pointer.- At
0x1c000d0e5(KIR Flag Check): - Step over and ensure the execution takes the
JZbranch to0x1c000d125. If it falls through, the KIR flag is set and the safe capture-once path runs instead. - At
0x1c000d125(UlProbeAnsiString): rcx: The captured user-mode data pointer being probed.- Copy the value of
rcxfor comparison. Alternatively, inspect theSrcstack slot where it is stored. - At
0x1c000d14d(Vulnerable Re-read): r12: Contains the loop index* 3.- Inspect the instruction execution. Step over this instruction.
- Verification: Compare the new
rdxvalue to thercxvalue from the previous breakpoint. If the PoC racing thread is active, you will eventually catch this whererdxcontains a high address (e.g.,0xFFFF...) instead of a user-mode address. - At
0x1c000d155(memmove): rcx: Output buffer in kernel memory.rdx: Source pointer (the attacker-controlled kernel address if the race was won).r8: Length of the copy.
Trigger Setup
- In user mode, initialize the HTTP Server API (
HttpInitialize). - Create a request queue and register a URL.
- Construct a response trailer array (each entry has name/value length fields and value pointers). Store the array in globally accessible user-memory.
- Initialize a secondary thread that enters an infinite loop, using
InterlockedExchange64(or similar) to flip the trailer value pointer (offset+0x8of the entry) between a valid string pointer and a target kernel address (e.g.,0xFFFFF90000000000). - Call
HttpSendHttpResponsein a loop from the primary thread. - The kernel debugger will hit the breakpoints. Allow the loop to continue (
g) until the breakpoint at0x1c000d14dcatches a modified pointer.
Expected Observation
When the TOCTOU race is won, you will observe at 0x1c000d155 that RDX is a kernel address. If you let the memmove execute and inspect the buffer at RCX, you will see raw kernel memory. If the PoC supplies an invalid address, the system will Bugcheck 0x50 (PAGE_FAULT_IN_NONPAGED_AREA) at the memmove instruction.
8. Changed Functions — Full Triage
UlGenerateTrailers (sub_1C000CF94)(Similarity: 0.7251 | Change: Security Relevant) The primary vulnerable function. The patch eliminates theUxKirHttpReadingTrailersFromUm-gated dual code path, forcing a single-fetch capture pattern for both primary and secondary trailer data pointers before passing them toUlProbeAnsiStringandmemmove.UxStartEnvironmentModule (sub_1C01767B0)(Similarity: 0.9516 | Change: Behavioral) This function initializes the driver's KIR (Known Issue Rollback) feature flags by callingwil_InitializeFeatureStagingfollowed by a series ofFeature_*__private_IsEnabledDeviceUsageprobes. The patched build removes theFeature_2347446586__private_IsEnabledDeviceUsagecall and itssetnz cs:UxKirHttpReadingTrailersFromUmstore (dropping the KIR flag set from 7 to 6). This deletes the rollback gate for the trailer double-fetch, consistent with the finalizedUlGenerateTrailersfix. Remaining differences are recompilation artifacts (WPP trace GUID relocation, instruction reordering). No security-relevant regression.sub_1C000D25B(Similarity: 0.9780 | Change: Cosmetic) A status-normalization tail block insideUlGenerateTrailers(maps0x80000000/0x80000005status codes to0xC000000D). The patched counterpart is at0x1C0028BD7; register/address shifts from recompilation only, no behavioral change.
9. Unmatched Functions
- Removed: None.
- Added: None.
10. Confidence & Caveats
- Confidence: High. The diff provides explicit pseudocode and assembly evidence of a classic TOCTOU double-fetch.
- Assumptions: The analysis assumes that the user-mode memory containing the header array pointers is mutable by a concurrent thread in the same process. This is a standard assumption for Windows user-mode APIs that probe user structures.
- Manual Verification Required: A researcher must confirm the value of the KIR flag
UxKirHttpReadingTrailersFromUmon their specific target build (set inUxStartEnvironmentModulefromFeature_2347446586__private_IsEnabledDeviceUsage) to ensure the vulnerable path is reachable. Because this is a Known Issue Rollback gate, the vulnerable re-read path only executes while the feature is rolled back (flag0). Additionally, optimizing the racing thread to consistently hit the narrow instruction window will require empirical tuning on the target hardware.