http.sys — Integer overflow in per-request array capacity (CWE-190) causing kernel pool overflow in UlpParseNextRequest fixed
KB5094127
1. Overview
| Item | Value |
|---|---|
| Unpatched binary | http_unpatched.sys |
| Patched binary | http_patched.sys |
| Overall similarity | 0.9896 |
| Matched functions | 3205 |
| Changed functions | 6 |
| Identical functions | 3199 |
| Unmatched (unpatched → patched) | 0 / 0 |
Verdict: A tiny but critical patch that introduces a 16-bit overflow check around a per-connection HTTP request tracking array's capacity field, closing a remotely reachable kernel pool overflow in http.sys.
Of the 6 changed functions, only one (UlpParseNextRequest (sub_1c000e4a0) → UlpParseNextRequest (sub_1c000e550)) is security-relevant. The rest are the same functions relocated to new addresses (register/offset shifts) or heuristic mispairings, plus harmless configuration plumbing (a new MaxHeadersCount registry parameter). The actual fix is concentrated in the request-processing loop: the patched build calls the standard safe-integer library routine RtlUShortAdd (sub_1c001c6e4) to perform capacity + 5 with overflow detection, and this checked path is selected by the KIR feature flag UxKirRefBufferOverflowCheck (data_1c0078d80). When that flag is off, the patched build still runs the original unchecked add cx, 5 path, so the fix is a KIR-gated safe path rather than an unconditional replacement.
2. Vulnerability Summary
Critical — Integer Overflow (CWE-190) → Kernel Pool Buffer Overflow (CWE-122)
Affected function: UlpParseNextRequest (sub_1c000e4a0) (unpatched), UlpParseNextRequest (sub_1c000e550) (patched) — the HTTP request processing main loop.
Root cause: The function maintains, per connection, a dynamic array of HTTP request/range objects. The array tracks three small fields on the connection object:
+0x630—WORDcapacity+0x632—WORDcount+0x638—PVOIDbuffer pointer (intoNonPagedPoolNx)
Whenever count >= capacity, the code grows the array: it allocates a fresh pool buffer of size (capacity * 8) + 0x28, copies the old entries, frees the old buffer, and then writes the new capacity with a 16-bit addition of 5:
*(conn + 0x630) = capacity + 5; // 16-bit add, no overflow check
capacity is a WORD. Once it reaches 0xFFFB..0xFFFF, adding 5 wraps to 0x0000..0x0004. On the next growth event, the allocation size is computed from that tiny wrapped capacity (e.g. 1*8 + 0x28 = 0x30 bytes), but the count keeps climbing from its previous value (~0xFFFC+). The very next entry write
mov qword [rax + rcx*8], rsi ; rcx = count (large), rax = tiny buffer
then walks 8-byte pointers far past the end of the new pool allocation — a large, attacker-driven, kernel pool overflow.
Why exploitable: The index (count) and stride (8 bytes) are completely controlled by the attacker, who can pace the growth of the array simply by sending more HTTP requests/ranges on a long-lived connection. The buffer lives in NonPagedPoolNx, where adjacent allocations frequently contain other connection objects, headers, or pool metadata.
What the patch does: The patched loop calls the standard RTL safe-add library routine RtlUShortAdd (sub_1c001c6e4), which performs capacity + 5 in 32-bit arithmetic, compares the truncated 16-bit result back against the input, and detects the wrap with cmp ax, cx; jb. On wrap it clamps to 0xFFFF and returns STATUS_INTEGER_OVERFLOW (0xC0000095); the caller bails on a negative status, so an overflowed capacity never reaches ExAllocatePoolWithTagPriority. This checked path is selected at run time by the KIR feature flag UxKirRefBufferOverflowCheck (data_1c0078d80) (set from a WIL feature-staging query). When the flag is disabled, the patched build falls back to the original unchecked add cx, 5 path (0x1c000ea02), which is byte-for-byte the vulnerable code. The unpatched build has only that unchecked path and no call to RtlUShortAdd.
Attacker-reachable entry point: Any HTTP listener that goes through http.sys (IIS, HTTP API v2, WinRM, Print Spooler's HTTP API, etc.).
Call chain:
1. Remote client opens a TCP connection to a target HTTP endpoint.
2. TDI/WSK receive hands the bytes to http.sys.
3. http.sys IRP dispatch routes the request into the request-processing main loop.
4. Callers UlpAuthenticateRequestCompletion (sub_1c01176a0) → UlResumeParsing (sub_1c000a940) → UlpHandleRequest (sub_1c000e1a0) → UlBeginOpaqueMode (sub_1c002dc84) → UlpParseNextRequest (sub_1c000e4a0) (vulnerable loop).
5. Inside the loop, ExAllocatePoolWithTagPriority is called with the undersized wrapped capacity.
6. The next entry write at mov qword [rax+rcx*8], rsi (0x1c000e8d7) corrupts adjacent NonPagedPoolNx memory.
3. Pseudocode Diff
// === UNPATCHED: UlpParseNextRequest (sub_1c000e4a0) ===========================================
void process_request(conn_t *conn, request_t *req) {
uint16_t count = *(uint16_t *)(conn + 0x632);
uint16_t capacity = *(uint16_t *)(conn + 0x630);
if (count >= capacity) {
size_t new_size = (size_t)capacity * 8 + 0x28; // uses CURRENT capacity
void *new_buf = ExAllocatePoolWithTagPriority(..., new_size, ...);
if (!new_buf) return;
memcpy(new_buf, conn->buf, capacity * 8);
ExFreePoolWithTag(conn->buf);
conn->buf = new_buf;
// *** BUG: 16-bit add with NO overflow check ***
*(uint16_t *)(conn + 0x630) = capacity + 5; // wraps past 0xFFFF!
}
// index by the *monotonically increasing* count
conn->buf[count] = req; // OOB WRITE if capacity wrapped
*(uint16_t *)(conn + 0x632) = count + 1;
}
// === PATCHED: UlpParseNextRequest (sub_1c000e550) =============================================
// RtlUShortAdd is a standard RTL safe-integer library routine, not a bespoke http.sys helper.
NTSTATUS RtlUShortAdd(uint16_t cur, uint16_t addend, uint16_t *out) { // sub_1c001c6e4
uint32_t sum = (uint32_t)cur + addend; // 32-bit add
if ((uint16_t)sum < cur) { // cmp ax,cx ; jb
*out = 0xFFFF; // clamp
return STATUS_INTEGER_OVERFLOW; // 0xC0000095
}
*out = (uint16_t)sum;
return STATUS_SUCCESS;
}
void process_request(conn_t *conn, request_t *req) {
uint16_t count = *(uint16_t *)(conn + 0x632);
uint16_t capacity = *(uint16_t *)(conn + 0x630);
if (count >= capacity) {
if (UxKirRefBufferOverflowCheck) { // NEW KIR-gated safe path
uint16_t new_cap;
if (RtlUShortAdd(capacity, 5, &new_cap) < 0) // NEW overflow-checked add
goto fail_request; // NEW bail-out on wrap
size_t new_size = (size_t)new_cap * 8; // uses VALIDATED capacity
void *new_buf = ExAllocatePoolWithTagPriority(..., new_size, ...);
if (!new_buf) goto fail_request;
memcpy(new_buf, conn->buf, count * 8);
if (capacity > 1) ExFreePoolWithTag(conn->buf);
*(uint16_t *)(conn + 0x630) = new_cap; // safe, validated
conn->buf = new_buf;
} else { // KIR flag off: original unchecked path
size_t new_size = (size_t)capacity * 8 + 0x28;
void *new_buf = ExAllocatePoolWithTagPriority(..., new_size, ...);
if (new_buf) {
memcpy(new_buf, conn->buf, count * 8);
if (capacity > 1) ExFreePoolWithTag(conn->buf);
*(uint16_t *)(conn + 0x630) = capacity + 5; // still wraps 16-bit!
conn->buf = new_buf;
}
}
}
conn->buf[count] = req;
*(uint16_t *)(conn + 0x632) = count + 1;
}
Key takeaways:
- The dangerous line in the unpatched version is the unguarded capacity + 5.
- The checked path never trusts the addition result until RtlUShortAdd (sub_1c001c6e4) returns success, and it uses the validated value for allocation size, stored capacity, and entry write.
- The patched build keeps the original unchecked add cx, 5 path as the fallback taken when UxKirRefBufferOverflowCheck (data_1c0078d80) is disabled, so the fix depends on that KIR flag being enabled.
4. Assembly Analysis
UNPATCHED — vulnerable allocation block
; --- read the two 16-bit fields ---
0x1c000e82f | movzx eax, word [rdi+0x632] ; eax = count
0x1c000e836 | movzx ecx, word [rdi+0x630] ; ecx = capacity
0x1c000e83d | cmp ax, cx
0x1c000e840 | jb 0x1c000e8cd ; if count < capacity, skip growth
; (this is the ONLY size check)
; --- compute new allocation size using CURRENT capacity ---
0x1c000e846 | lea rdx, [rcx*8 + 0x28] ; rdx = capacity*8 + 0x28 <-- trusted blindly
0x1c000e85c | call qword [rel 0x1c0086c50] ; ExAllocatePoolWithTagPriority
0x1c000e86b | test rax, rax
0x1c000e86e | je 0x1c000e8e9 ; alloc failed -> bail
; --- (copy old data via memmove, free old buffer) ---
; ... (omitted for clarity) ...
; --- BUG: capacity update is a 16-bit add with no overflow detection ---
0x1c000e8ad | movzx ecx, word [rdi+0x630] ; reload capacity
0x1c000e8bb | add cx, 5 ; 16-bit add; wraps if cx >= 0xFFFB
0x1c000e8bf | mov [rdi+0x630], cx ; store wrapped capacity, no check
; --- entry write: count is the index, capacity is no longer related ---
0x1c000e8cd | movzx ecx, ax ; ecx = count (was set above, still large)
0x1c000e8d0 | mov rax, qword [rdi+0x638] ; rax = current buffer pointer
0x1c000e8d7 | mov qword [rax+rcx*8], rsi ; *** OOB WRITE ***
; rsi = entry pointer, rcx*8 = index
0x1c000e8db | inc word [rdi+0x632] ; count++
Annotations:
- 0x1c000e83d–0x1c000e840: only comparison in the path. It tells the function whether to grow; it does not validate the growth arithmetic.
- 0x1c000e846: lea rdx, [rcx*8 + 0x28] trusts the wrapped capacity.
- 0x1c000e8d7: the corrupting write. Once capacity has wrapped and a fresh small allocation has been made, rcx (count) is on the order of 0xFFFC, so rcx*8 ≈ 0x7FFE0 bytes past the new buffer.
PATCHED — the safe path and the RTL safe-add routine RtlUShortAdd (sub_1c001c6e4)
In UlpParseNextRequest, the grow decision is followed by a KIR-flag test that selects the checked path:
0x1c000e8ec | movzx ecx, word [rdi+0x630] ; capacity (usAugend)
0x1c000e8f3 | cmp [rdi+0x632], cx ; count < capacity? -> skip grow
0x1c000e8fa | jb 0x1c000ea14
0x1c000e900 | cmp UxKirRefBufferOverflowCheck, r14b ; KIR flag == 1 ?
0x1c000e907 | jz 0x1c000e994 ; flag off -> OLD unchecked path
0x1c000e90d | lea r8, [rbp+pusResult] ; checked path
0x1c000e911 | call RtlUShortAdd
0x1c000e916 | test eax, eax
0x1c000e918 | js 0x1c000ea36 ; overflow -> bail
0x1c000e91e | movzx r15d, [rbp+pusResult] ; validated new capacity
0x1c000e926 | mov edx, r15d
0x1c000e92e | shl rdx, 3 ; NumberOfBytes = new_cap*8
0x1c000e938 | call ExAllocatePoolWithTagPriority
...
0x1c000e98a | mov [rdi+0x630], r15w ; store validated capacity
The RTL safe-add routine itself:
0x1c001c6e4 | lea eax, [rcx+0x5] ; 32-bit: capacity + 5
0x1c001c6e7 | cmp ax, cx ; did the low 16 bits go backwards?
0x1c001c6ea | jb 0x1c001c6f1 ; yes -> overflow path
0x1c001c6ec | movzx edx, ax ; no -> use safe value
0x1c001c6ef | jmp 0x1c001c6f6
0x1c001c6f1 | mov edx, 0xffff ; clamp path: edx = 0xFFFF
0x1c001c6f6 | sbb eax, eax ; eax = 0xFFFFFFFF if CF (overflow)
0x1c001c6f8 | mov word [r8], dx ; store validated capacity
0x1c001c6fc | and eax, 0xc0000095 ; mask in STATUS_INTEGER_OVERFLOW
0x1c001c701 | retn
Annotations:
- lea eax, [rcx+5] performs the add in 32 bits, so wrap is detectable.
- cmp ax, cx; jb is the classic "smaller after add" overflow test for unsigned 16-bit.
- sbb eax, eax turns CF into 0/-1, then and produces either 0 (success) or 0xC0000095 (overflow).
- The caller gates the allocation on a non-negative return value, so the bad size is never passed to ExAllocatePoolWithTagPriority.
- The fallback at 0x1c000e994 (taken when UxKirRefBufferOverflowCheck is off) is the original code: lea rdx, [rcx*8+0x28] allocation, then add cx, 5 / mov [rdi+0x630], cx at 0x1c000ea02 with no overflow check.
5. Trigger Conditions
To reach and fire the bug on the unpatched driver:
- Reach the target. Identify any HTTP service on the host backed by
http.sys(default port 80, 443, 5985/WinRM, 41792/Print HTTP, or anyhttp.sysURL reservation). Open a single persistent TCP connection. - Drive the array to grow. Each request (or, in some paths, each Range or chunked segment) adds an entry. The array starts small and grows by 5 capacity slots per fill.
- Reach the wrap threshold. Capacity must climb from its initial value to
~0xFFFB. With growth of 5 slots per fill, this requires on the order of ~13,107 grow events, i.e. roughly0xFFFBtotal request/range entries sent on the same connection. - Cross the boundary. Once the post-add value wraps (
capacity+5overflows past0xFFFF), the next request that triggers growth will allocate a buffer sized by the wrapped value (~0x30bytes typical) whilecountis still ~0xFFFC. - Write past the end. Any single additional request causes
mov qword [rax+rcx*8], rsito write 8 bytes at offsetcount*8from the start of the new tiny allocation — well outside the buffer. - Observable effect. Expect a kernel-mode bugcheck once the corrupted pool is next examined. Likely stop codes:
KERNEL_DATA_INPAGE_ERROR(0x7A) orPAGE_FAULT_IN_NONPAGED_AREA(0x50) — faulting virtual address in the high0xFFFF_FFFF_xxxx_xxxxrange derived fromcount*8against an undersized NonPagedPool region, orBAD_POOL_HEADER(0x19) /POOL_CORRUPTIONif pool metadata adjacent to the small allocation is what gets clobbered.
Notes on pacing:
- Use HTTP pipelining or chunked POST to push many entries on a single connection without round-trips; this minimizes wall-clock time to reach 65k entries.
- HTTP Range requests are especially attractive if the per-segment path feeds into the same array (Range: bytes=0-1,2-3,4-5,... produces many entries per request).
- Some endpoints rate-limit; spreading the attack across multiple sockets to the same http.sys instance does not help, because the array is per-connection.
6. Exploit Primitive & Development Notes
Primitive: Controlled kernel pool overflow with a fixed 8-byte stride. The attacker:
- chooses count (and therefore the offset count*8 from the buffer start),
- supplies the 8-byte pointer value being written (the request/range object pointer, which the attacker indirectly controls via earlier content),
allowing disciplined writes past the undersized NonPagedPoolNx allocation.
Turning it into a full exploit:
- Heap grooming. Reclaim the slot adjacent to the undersized allocation with a controllable victim object. Good candidates in
NonPagedPoolNxinclude: HALPRCB-adjacent structures (avoid),Token/Processobjects (size mismatched — usually no),Pipeinstances,Eventobjects,Semaphoreobjects, or other connection-scoped structures (yes).- Best targets are objects with a function pointer in the first 8 bytes after the pool header.
- Forge the corruption. Overflow into the victim object's vtable/function-pointer/
LIST_ENTRY, replacing it with a pointer to attacker-controlled data. - Defeat KASLR.
http.sysleaks no kernel pointers by default. Either: - Use an info-leak primitive in another driver (e.g., Win32k, gdi) to obtain the kernel base, or
- Spray
NonPagedPoolNxwith objects whose address can be inferred via deterministic allocation patterns. - Code execution. Once a controlled function pointer is hit, redirect to a ROP/JOP chain located in
nt!Ke*gadgets, or — if SMEP/SMAP allow data-only attacks — corrupt a_TOKENpointer for privilege escalation without ever executing userland shellcode.
Mitigations to consider:
- kASLR: must be defeated for code-execution flows; the data-only token-swap variant sidesteps it.
- SMEP / SMAP: blocks ret-into-userland flows; the kernel-ROP variant bypasses them.
- CFG (kernel CFG, CET): constrains indirect call targets. Pick corruption targets outside CFG-protected call sites (e.g., LIST_ENTRY unlinking for arbitrary-write primitives, I/O completion callbacks).
- HVCI / VBS: turns many ROP/JOP chains into hypervisor faults. Data-only token swaps remain viable.
- Pool header integrity (Win10+): randomizes and validates POOL_HEADER; pure metadata corruption is harder. Prefer corrupting object fields, not pool headers.
Realistic outcome: high-impact LPE to SYSTEM from a low-privileged local context, and remote kernel crash / remote LPE when combined with a separate info leak.
7. Debugger PoC Playbook
Assume WinDbg/KD attached to the unpatched http.sys with symbols resolved via the local PDB or via the offsets below.
Breakpoints
bp http_unpatched+0xE4A0 ; entry of UlpParseNextRequest (sub_1c000e4a0) — confirms request reached the loop
bp http_unpatched+0xE82F ; capacity/count read — observe the 16-bit fields live
bp http_unpatched+0xE846 ; size computation: lea rdx,[rcx*8+0x28]
bp http_unpatched+0xE85C ; ExAllocatePoolWithTagPriority call — inspect rdx (size)
bp http_unpatched+0xE8CD ; index prep — observe ecx (=count) right before OOB write
bp http_unpatched+0xE8D7 ; *** the OOB write itself ***
bp nt!ExAllocatePoolWithTagPriority
If http.sys is loaded at a base other than 0x1c0000000, substitute the equivalent RVAs: +0x1A0, +0x82F, +0x846, +0x85C, +0x8CD, +0x8D7 from the driver base.
What to inspect at each breakpoint
- At
+0xE82F:rdi= connection object. Dump fields:text dp rdi+0x630 L1 ; bits [0..15] = capacity, bits [16..31] = count dq rdi+0x638 L1 ; current buffer pointerConfirmcountis climbing andcapacityis stepping by 5. - At
+0xE846:rcx= capacity at the time of growth.rdx=(capacity*8)+0x28. Watch for the wrap: whenrcx≥0xFFFB, the next hit of this breakpoint will haverdxnear0x30(tiny) — that is the smoking-gun allocation. - At
+0xE85C:rdxis the requested size. Confirm it is implausibly small relative to the previous allocation. - At
+0xE8D7: rax= new (tiny) buffer pointer.ecx= count — this is the multiplier.- Compute the target virtual address in WinDbg:
? rax + rcx*8. Anything more than a few hundred bytes pastraxconfirms OOB. - Set a watchpoint on the byte being written:
ba w8 <addr>.
Trigger setup (from user mode)
- Open one persistent TCP socket to the target HTTP port.
- Build an HTTP request that creates many array entries. Easiest path with
http.sys:http POST / HTTP/1.1 Host: <target> Transfer-Encoding: chunked Range: bytes=0-1,2-3,4-5,6-7,8-9,...
Each comma-separated range creates one entry. Alternatively, pipeline many requests on the same socket without waiting for responses.
3. Loop until ~65,535 entries have been added. The function at +0xE4A0 should be hit on each request; the capacity field at [rdi+0x630] should climb in steps of 5 until it wraps.
4. Once the wrap happens, send one more request to fire the OOB write at +0xE8D7.
Expected observation
- Right before the bugcheck, the
ba w8watchpoint on the corrupted address fires, withrsi(request pointer) as the written value. - The bugcheck should occur shortly after, with a faulting instruction either in
nt!ExpPool...routines, innt!KiPageFault, or inside an adjacent corrupted object's method. - Stop codes most likely:
0x50 PAGE_FAULT_IN_NONPAGED_AREA,0x19 BAD_POOL_HEADER, or0xC2 BAD_POOL_CALLER. - Inspect the faulting address: it will be of the form
tiny_buffer + count*8, far past the actual allocation.
Struct / offset notes
Connection object layout (relevant fields):
+0x630 WORD capacity ; 16-bit, +5 on grow
+0x632 WORD count ; 16-bit, +1 per entry
+0x634 ? (padding)
+0x638 PVOID buffer ; NonPagedPoolNx array of pointers
each entry: 8 bytes (pointer to request/range object)
RTL safe-add routine (emitted only in patched build):
RtlUShortAdd (sub_1c001c6e4) (cx=cur_capacity, dx=addend(5), r8=&out_new_capacity)
returns: 0 on success, 0xC0000095 on overflow
8. Changed Functions — Full Triage
-
UlpParseNextRequest (sub_1c000e4a0)→UlpParseNextRequest (sub_1c000e550)(similarity 0.9142, security-relevant): The vulnerable request-processing loop. The patched build adds a KIR-gated safe grow path: acmp UxKirRefBufferOverflowCheck (data_1c0078d80)test selects between a checked path that calls the RTL safe-add routineRtlUShortAdd (sub_1c001c6e4)(bails withSTATUS_INTEGER_OVERFLOWon wrap and allocates from the validated capacity) and the original uncheckedadd cx, 5path preserved for KIR rollback.UlpCheckTrailerLengthis unchanged in behavior; it is a separate function that merely moved fromsub_1c001b7b4tosub_1c001c70cand is not invoked by the grow logic. -
UxDuoProcessCompleteCatalog (sub_1c00b77c0)→UxDuoProcessCompleteCatalog (sub_1c00b8950)(similarity 0.9663, behavioral): HTTP request body/chunk processing state machine. Changes are register renaming and offset shifts from0x1c0076xxxto0x1c0077xxx(data-section relocation) plus minor control-flow restructuring. No security impact. -
UxReadHttp11ParserSettings (sub_1c00d8940)→UxReadHttp11ParserSettings (sub_1c00d9b80)(similarity 0.911, behavioral): Configuration initialization. Reads registry parameters (MaxRequestBytes,MaxFieldLength, …) and adds a newMaxHeadersCountparameter with feature-flagUxKirMaxHeadersCountLimit (data_1c0078d81). Not directly security-relevant, but it gives an interesting attenuation knob: ifMaxHeadersCountdefaults to0x32with a max of0xFFFF, an attacker pushing the array past the configured cap may trigger an earlier rejection on patched builds. Worth noting for trigger development on patched systems. -
UxStartEnvironmentModule (sub_1c015e480)↔wil_details_FeatureReporting_ReportUsageToServiceDirect (sub_1c001bce0)(similarity 0.2783, cosmetic/heuristic): Algorithmic mismatch — paired by call-sequence heuristics, not a true diff. Treat as independent functions. -
UlpCheckTrailerLength (sub_1c001b7b4)↔WPP_SF_qddd (sub_1c0097654)(similarity 0.1667, heuristic mispairing): Two unrelated functions paired by call-reference matching.UlpCheckTrailerLengthitself is the same function relocated tosub_1c001c70cin the patched build;WPP_SF_qdddis a WPP/ETW tracing wrapper (sub_1c0096654→sub_1c0097654). Not a semantic diff. -
WPP_SF_qddd (sub_1c0096654)↔RtlUShortAdd (sub_1c001c6e4)(similarity 0.0082, heuristic mispairing): Unpatched-sideWPP_SF_qdddis a WPP/ETW tracing wrapper; patched-sideRtlUShortAddis the standard RTL safe-add library routine that the fixed loop calls. They are different functions paired erroneously.RtlUShortAdd's role in the fix is covered under theUlpParseNextRequest (sub_1c000e4a0)finding.
Collapsed note on non-security changes: the only meaningful diffs across all six functions are (a) the KIR-gated overflow-checked grow path added to UlpParseNextRequest (sub_1c000e4a0), which calls the RTL library routine RtlUShortAdd (sub_1c001c6e4), and (b) the new MaxHeadersCount registry parameter in UxReadHttp11ParserSettings. Everything else is data-section relocation, register allocation, or diff-tool mispairing.
9. Unmatched Functions
None. Both removed and added lists are empty. The fix was implemented in the patched request-processing loop, plus the RTL safe-add routine RtlUShortAdd (sub_1c001c6e4) which is emitted only in the patched build and was paired to an unrelated unpatched function by call-reference heuristics rather than listed as added.
Implication: there is no removed sanitizer and no removed check. The patch is additive: a new overflow-checked grow path guarded by the KIR flag UxKirRefBufferOverflowCheck (data_1c0078d80). The unpatched binary has no overflow check on this capacity + 5 add. Note that the patched binary still contains the original unchecked path, reachable when the KIR flag is disabled.
10. Confidence & Caveats
Confidence: high.
Rationale:
- The vulnerable arithmetic is unambiguous in disassembly: a 16-bit add followed immediately by a 32-bit-safe allocation size computation, with no intervening comparison.
- The patch introduces a textbook overflow-detection idiom (lea; cmp; jb) that exactly targets this add.
- The OOB write at mov qword [rax+rcx*8], rsi uses the same register that just tracked count, making the data flow self-evident.
Assumptions made:
- That the per-connection array can in fact reach ~65,535 entries on a real system. The patch ships a default MaxHeadersCount of 0x32 and a max of 0xFFFF, which suggests Microsoft believes the array can legitimately grow to 0xFFFF in some configurations. The exact attacker-controlled input that drives growth (Range headers vs. pipelined requests vs. chunked segments) was inferred from context; verify by tracing which caller increments the count field in your lab.
- That the array lives in NonPagedPoolNx. This is consistent with the ExAllocatePoolWithTagPriority call and the lack of IRQL checks elsewhere in the function, but confirm via pool tagging (!pool, !poolfind) at runtime.
- That the caller chain UlpAuthenticateRequestCompletion (sub_1c01176a0) → UlResumeParsing (sub_1c000a940) → UlpHandleRequest (sub_1c000e1a0) → UlBeginOpaqueMode (sub_1c002dc84) → UlpParseNextRequest (sub_1c000e4a0) is reachable from remote HTTP traffic. The chain is taken from cross-references; verify each step with breakpoints in a live debugger before claiming remote reachability for certain.
What a researcher should verify before writing a PoC:
1. Confirm the connection object layout by dumping [rdi+0x630], [rdi+0x632], [rdi+0x638] on a fresh connection and correlating with request count.
2. Identify the exact request shape that adds one entry per request — test Range, chunked, and pipelined variants.
3. Measure real-world throughput to 65k entries; expect that some intermediaries (proxies, TLS terminators) limit range/pipeline depth and may force the use of many independent keep-alive requests.
4. Confirm pool location and neighbor objects: capture a pool walk immediately after the undersized allocation to identify the realistic victim object for exploitation.
5. Validate bugcheck conditions and pool-corruption patterns in a VM with Driver Verifier enabled (to catch the overflow earlier and more cleanly).