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

  1. An attacker initiates an HTTP Server API operation from user mode (e.g., HttpSendHttpResponse with response trailers), triggering an NtDeviceIoControlFile syscall to \Device\Http\ReqQueue.
  2. The kernel IOCTL dispatcher routes the request to HTTP.sys response processing functions (UlCaptureHttpResponse (sub_1C00DDA70) / sub_1c00ddff7).
  3. The response preparation path (UlpPrepareHttpResponse (sub_1C00DE140)) prepares the output buffer and calls the vulnerable function UlGenerateTrailers (sub_1C000CF94).
  4. 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

  1. KIR Flag State: The target system must resolve the KIR feature flag UxKirHttpReadingTrailersFromUm to 0, enabling the vulnerable Path A. This flag is set in UxStartEnvironmentModule from Feature_2347446586__private_IsEnabledDeviceUsage; a value of 0 corresponds to the feature being rolled back / disabled.
  2. API Setup: The attacker must use the HTTP Server API (Win32 HttpSendHttpResponse / HttpSendResponseEntityBody with response trailers) to trigger the vulnerable IOCTL path via NtDeviceIoControlFile on \Device\Http\ReqQueue.
  3. 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).
  4. 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 +0x8 or +0x10).
  5. 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).
  6. 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 (Bugcheck 0x50).

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 JZ branch to 0x1c000d125. 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 rcx for comparison. Alternatively, inspect the Src stack 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 rdx value to the rcx value from the previous breakpoint. If the PoC racing thread is active, you will eventually catch this where rdx contains 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

  1. In user mode, initialize the HTTP Server API (HttpInitialize).
  2. Create a request queue and register a URL.
  3. Construct a response trailer array (each entry has name/value length fields and value pointers). Store the array in globally accessible user-memory.
  4. Initialize a secondary thread that enters an infinite loop, using InterlockedExchange64 (or similar) to flip the trailer value pointer (offset +0x8 of the entry) between a valid string pointer and a target kernel address (e.g., 0xFFFFF90000000000).
  5. Call HttpSendHttpResponse in a loop from the primary thread.
  6. The kernel debugger will hit the breakpoints. Allow the loop to continue (g) until the breakpoint at 0x1c000d14d catches 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 the UxKirHttpReadingTrailersFromUm-gated dual code path, forcing a single-fetch capture pattern for both primary and secondary trailer data pointers before passing them to UlProbeAnsiString and memmove.
  • UxStartEnvironmentModule (sub_1C01767B0) (Similarity: 0.9516 | Change: Behavioral) This function initializes the driver's KIR (Known Issue Rollback) feature flags by calling wil_InitializeFeatureStaging followed by a series of Feature_*__private_IsEnabledDeviceUsage probes. The patched build removes the Feature_2347446586__private_IsEnabledDeviceUsage call and its setnz cs:UxKirHttpReadingTrailersFromUm store (dropping the KIR flag set from 7 to 6). This deletes the rollback gate for the trailer double-fetch, consistent with the finalized UlGenerateTrailers fix. 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 inside UlGenerateTrailers (maps 0x80000000/0x80000005 status codes to 0xC000000D). The patched counterpart is at 0x1C0028BD7; 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 UxKirHttpReadingTrailersFromUm on their specific target build (set in UxStartEnvironmentModule from Feature_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 (flag 0). Additionally, optimizing the racing thread to consistently hit the narrow instruction window will require empirical tuning on the target hardware.