1. Overview

  • Unpatched Binary: http_unpatched.sys
  • Patched Binary: http_patched.sys
  • Overall Similarity Score: 98.06%
  • Diff Statistics: 4,342 matched functions, 133 changed functions, 4,209 identical functions, and 0 unmatched functions in either direction.
  • Verdict: This diff contains no security-relevant behavior change. The most substantive change is in the global performance-counter copy routine UlpPcwCopyGlobalCounters (sub_140149B64): the patched build zeroes the entire 0x28-byte output structure inside the function and starts every accumulator from zero, whereas the unpatched build clears only the first 8 bytes inside the function and reads the remaining output fields before writing them. This is a defense-in-depth hardening: the function's single caller UlpPcwCollectInstances (sub_14008CFCC) already fully zeroes the 40-byte buffer before the call in both builds (a memset of the leading 32 bytes plus an explicit = 0 of the trailing 8-byte field), so the unpatched reads return zero rather than stack residue and the produced counter totals are identical in both builds. The remaining changed functions are Known-Issue-Rollback (KIR) feature-flag consolidations: branches guarded by per-feature UxKir* globals are collapsed to a single path once the corresponding fix is made permanent. These are staging cleanups with no security-relevant behavior change.

2. Vulnerability Summary

Finding 1: Performance Counter Copy — In-Function Zeroing Widened (No Security-Relevant Change)

  • Severity: Informational
  • Vulnerability Class: None (defense-in-depth hardening; the buffer is already fully initialized by the caller)
  • Affected Function: UlpPcwCopyGlobalCounters (sub_140149B64)
  • What Changed: The unpatched routine zeroes only the first 8 bytes of a 40-byte (0x28) statistics output buffer (*(_QWORD *)a2 = 0). It then seeds four 32-bit accumulators from the buffer's own fields at offsets +0x08, +0x0C, +0x10, and +0x14, and accumulates 64-bit values into the fields at +0x18 and +0x20 with += (that last path gated by UxKirHttpTlsHandshakePerfCounter). The patched version zeroes the whole 0x28-byte structure up front (a 16-byte store at +0x00, a 16-byte store at +0x10, and an 8-byte store at +0x20) and starts every accumulator from zero.
  • Why It Is Not Exploitable: The function has exactly one caller, UlpPcwCollectInstances (sub_14008CFCC), in both builds. That caller declares the buffer as _QWORD v24[4] (32 bytes) immediately followed by __int64 v25 (8 bytes), a contiguous 40-byte region, and initializes all of it before the call: memset(v24, 0, sizeof(v24)) clears offsets +0x00..+0x1F and v25 = 0 clears offsets +0x20..+0x27. This is identical in the patched caller (v22[4] + v23). Consequently the unpatched function's reads of +0x08/+0x0C/+0x10/+0x14 and its += into +0x18/+0x20 operate on zero, not on stack residue, so no uninitialized data is ever folded into the result and the returned counter totals are identical in both builds. The patch simply moves the zeroing responsibility from the caller into the callee (defense in depth), producing no observable behavior difference.
  • Entry Point: Performance-counter enumeration (PerfMon, typeperf, or the PCW query path) for the HTTP Server API global counter set reaches this routine via UlpPcwCollectInstances for counter type 0, but the returned data is the same in both builds.

Finding 2: SSL Error-Header Validation — Feature-Flag Consolidation (No Security-Relevant Change)

  • Severity: Informational
  • Affected Function: UxSslValidateErrorHeaders (sub_14008AEB8)
  • Summary: This function validates the header list of an SSL/TLS error response. For each header it looks up the name in a hash table, rejects the :status pseudo-header, and scans the name and value bytes for disallowed control characters. In the unpatched build the per-header body is duplicated into two identical branches gated by the KIR global UxKirHttpBugFix25Q1; both branches perform the same name-pointer NULL check (if ( v8 == nullptr || <name-len> == 0 ) break;) before scanning the name. The patched build removes the rolled-back branch and keeps the single unified path. The NULL/zero-length check is present in both builds, and the one length-guarded read of the name's first byte (<name-len> != 0 && **name == ':') is byte-for-byte identical in both builds. No pointer-validation was added or removed and no NULL dereference is fixed here.

Finding 3: Header Hash-Table Insert — Struct Growth & KIR Flag (No Security-Relevant Change)

  • Severity: Informational
  • Affected Function: HkAddPairToTable (sub_140032640)
  • Summary: This routine allocates a node for a name/value pair and copies both strings after a fixed header. The patched build enlarges that fixed header from 0x48 to 0x50 bytes (an extra 8-byte field; the hash-chain node moves from offset +0x20 to +0x28) and grows the allocation to match in lockstep (name_len + value_len + 0x48name_len + value_len + 0x50). In both builds the copied data fits the allocation exactly, and the wrap-around checks on every size addition are functionally equivalent (the unpatched build combines them into one compound if; the patched build splits them into sequential early-returns). The change is gated by the new KIR flag UxKirHkPerformanceImprovements, which also swaps the name-hash insertion helper from HkAddPairToNameHashOld (sub_14014AEEC) to HkAddPairToNameHash (sub_14006EE90). There is no undersized allocation and no pool overflow in either build.

3. Pseudocode Diff

UlpPcwCopyGlobalCounters (sub_140149B64) (Statistics Aggregation)

// === CALLER UlpPcwCollectInstances (both builds, identical) ===
// _QWORD v24[4];  __int64 v25;      // contiguous 40-byte (0x28) output buffer
v25 = 0;                             // clears bytes +0x20..+0x27
memset(v24, 0, sizeof(v24));         // clears bytes +0x00..+0x1F  => whole buffer zero
UlpPcwCopyGlobalCounters(a1, v24);   // called with a fully zeroed buffer

// === UNPATCHED callee ===
*(_QWORD *)a2 = 0;              // zeroes only bytes 0..7 in-function
...
v4  = *(_DWORD *)(a2 + 8);      // reads a field the caller already zeroed
v6  = *(_DWORD *)(a2 + 12);     // already zero
v8  = *(_DWORD *)(a2 + 16);     // already zero
v10 = *(_DWORD *)(a2 + 20);     // already zero
...
*(_QWORD *)(a2 + 24) += ...;    // += onto a caller-zeroed field
*(_QWORD *)(a2 + 32) += ...;    // += onto a caller-zeroed field

// === PATCHED callee ===
*(_OWORD *)a2 = 0;              // zero bytes 0..15
*(_OWORD *)(a2 + 16) = 0;       // zero bytes 16..31
*(_QWORD *)(a2 + 32) = 0;       // zero bytes 32..39  (full 0x28 struct)
...
v4 = v5 = v6 = v7 = v8 = v10 = 0;   // every accumulator starts at zero in-function

UxSslValidateErrorHeaders (sub_14008AEB8) (SSL Error-Header Validation)

// === UNPATCHED ===
if ( (_WORD)v5 != 0 && **(_BYTE **)(v6 + 8) == 58 )   // length-guarded read of name[0]
    break;
...
if ( UxKirHttpBugFix25Q1 != 0 ) {          // rolled-back duplicate branch
    if ( v8 == nullptr || <name-len> == 0 ) break;    // NULL check present
    // scan value chars, scan name chars
} else {
    if ( v8 == nullptr || <name-len> == 0 ) break;    // identical NULL check
    // scan value chars, scan name chars
}

// === PATCHED ===
if ( *(_WORD *)v5 != 0 && **(_BYTE **)(v5 + 8) == 58 ) // identical length-guarded read
    break;
if ( v6 == nullptr || <name-len> == 0 ) break;         // same NULL check, single path
// scan value chars, scan name chars

HkAddPairToTable (sub_140032640) (Header Pair Insert)

// === UNPATCHED ===
// wrap checks combined in one compound if(...)
v16 = v13 + 72;                 // header 0x48 + name_len + value_len
memset(v17, 0, v16);
memmove(v18 + 72, name, name_len);           // data at 0x48
// hash-chain node lives at v18 + 32

// === PATCHED ===
// same wrap checks as sequential early-returns
v16 = v15 + 80;                 // header 0x50 + name_len + value_len
memset(v17, 0, v16);
memmove(v18 + 80, name, name_len);           // data at 0x50
// hash-chain node lives at v18 + 40
// UxKirHkPerformanceImprovements selects HkAddPairToNameHash vs ...Old

4. Assembly Analysis

UlpPcwCopyGlobalCounters (sub_140149B64)

; Unpatched @ 0x140149B64: mov [rdx], rax (rax=0) clears only bytes +0x00..+0x07;
; then mov edi,[rdx+8] / esi,[rdx+0Ch] / ebp,[rdx+10h] / r14d,[rdx+14h] load fields
; the caller already zeroed, and add [rdx+18h]/[rdx+20h] (gated by
; UxKirHttpTlsHandshakePerfCounter) accumulate onto caller-zeroed fields.

; Patched (UlpPcwCopyGlobalCounters @ 0x1401478FC):
; xorps xmm0,xmm0 ; movups [rdx],xmm0 ; movups [rdx+10h],xmm0 ; mov [rdx+20h],rax(=0)
; zero the full 0x28 struct, then xor ebx/edi/esi/ebp/r14d/r15d init all accumulators.
; Caller UlpPcwCollectInstances zeroes the whole 40-byte buffer before the call in both
; builds, so the unpatched loads read zero and both builds return identical totals.

UxSslValidateErrorHeaders (sub_14008AEB8)

; The name-pointer NULL/zero-length guard exists on every per-header path in
; both builds. Patched build header: UxSslValidateErrorHeaders @ 0x140088AE8.
; Only structural change: the UxKirHttpBugFix25Q1 duplicate branch is dropped.

5. Trigger Conditions

Performance Counter Copy (Finding 1)

  1. On the local host, open performance-counter enumeration (PerfMon) or run typeperf against the HTTP Server API global counter set while HTTP traffic is being processed.
  2. Query the global counters (for example \HTTP Service\Total Requests).
  3. Observable Effect: None between builds. The collection callback UlpPcwCollectInstances fully zeroes the 40-byte buffer before calling UlpPcwCopyGlobalCounters in both builds, so the returned totals are identical. The only difference is where the zeroing happens (caller-only in the unpatched build; caller and callee in the patched build).

6. Exploit Primitive & Development Notes

  • Finding 1 Primitive: None. There is no information exposure. The single caller UlpPcwCollectInstances fully zeroes the 40-byte buffer before the call in both builds, so the unpatched function's reads of the not-yet-written output fields return zero rather than stack residue. The patch is a defense-in-depth hardening that moves the zeroing into the callee; the returned counter totals are identical in both builds.
  • Findings 2 and 3 are not exploitable: they are KIR feature-flag consolidations with no attacker-controlled security consequence.

7. Debugger PoC Playbook

For an analyst with a kernel debugger (WinDbg/KD) attached to http_unpatched.sys:

Targeting the Statistics Copy Routine

  • Breakpoints: text bp http!UlpPcwCopyGlobalCounters ; sub_140149B64
  • What to inspect:
  • rdx (arg2) points at the 0x28-byte output buffer supplied by UlpPcwCollectInstances (sub_14008CFCC).
  • Dump 0x28 bytes at entry: db rdx L28. In both builds the caller has already zeroed the full 40 bytes (memset of the leading 32 bytes plus = 0 of the trailing 8-byte field), so all 0x28 bytes read as zero on entry.
  • The KIR global UxKirHttpTlsHandshakePerfCounter controls whether the +0x18 and +0x20 fields are also accumulated.
  • Expected Observation: The values loaded from +0x08/+0x0C/+0x10/+0x14 and the += into +0x18/+0x20 operate on caller-zeroed memory, so no stale bytes reach the result. The returned counter totals are identical in the unpatched and patched builds; only the location of the zeroing differs.

8. Changed Functions — Full Triage

  • UlpPcwCopyGlobalCounters (sub_140149B64) (Hardening, No Security-Relevant Change): In-function zeroing widened from 8 bytes to the full 0x28-byte output struct and all accumulators now start from zero. The sole caller UlpPcwCollectInstances already fully zeroes the 40-byte buffer in both builds, so no uninitialized data was ever read and the returned totals are identical. Defense-in-depth only; no information exposure.
  • UxSslValidateErrorHeaders (sub_14008AEB8) (Feature Staging): The UxKirHttpBugFix25Q1 duplicate branch is removed and the single unified path retained. The name-pointer NULL/zero-length check is present in both builds; no security-relevant change.
  • HkAddPairToTable (sub_140032640) (Feature Staging): Fixed header grown 0x48→0x50 with the allocation growing in lockstep; wrap checks equivalent; UxKirHkPerformanceImprovements selects HkAddPairToNameHash vs HkAddPairToNameHashOld. No undersized allocation.
  • UlConnectionCleanupWorker (sub_1400583B0) (Behavioral): KIR-gated branches consolidated; a conditional tracing call is replaced with a hardcoded null argument, disabling that log path.
  • UlSetPreSendState (sub_140085920) / UlSetSendCompleteState (sub_14005C6A0) (Behavioral): Send-response state and connection-counter handling simplified; duplicate KIR-gated paths removed.
  • UlpIsFastForwardingAllowed (sub_14011ED58) (Behavioral): State/condition logic restructured to a goto-based flow; a helper call added on one path.
  • UlCopyChannelBindConfigFromPropertyInfo (sub_1400DB58C) (Behavioral): Channel-binding/credential property copy refactored; a KIR-gated branch consolidated; allocation-size bounds checks reorganized without changing the effective limit.
  • wil_details_GetCurrentFeatureEnabledState (sub_1400C26DC) / wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState (sub_1400C2578) (Library): Windows Implementation Library feature-state cache helpers. Bitmask/state computation reorganized; not application security logic.
  • UcpCreateClientConfiguration (sub_140162D0C) (Behavioral): Object-attribute flag 0x200000 application changed from a KIR-gated conditional to an unconditional set with an inverse !(arg1 & 0x100) guard.
  • UlpHttpIdealBacklogFastForwarderNotification (sub_140056804) (Behavioral): Dispatch/IRQL path selection consolidated; work-item helper targets updated; silo attach/detach cleanup refactored.

9. Unmatched Functions

There are no unmatched added or removed functions between the unpatched and patched binaries. The patch modifies existing execution paths only.


10. Confidence & Caveats

  • Confidence Level: High for the Finding 1 classification: the unpatched routine clears only 8 of 40 bytes in-function, but its single caller UlpPcwCollectInstances fully zeroes the 40-byte buffer before every call in both builds (a memset of the leading 32 bytes plus = 0 of the trailing 8-byte field), so the not-yet-written reads return zero and the returned totals are identical. The change is defense-in-depth hardening, not a security fix. High for the Finding 2/3 classifications: the null check and the exact-fit allocation are visible in both builds, so those changes are feature-flag staging rather than security fixes.
  • Feature-flag context: The UxKir* globals are per-feature Known-Issue-Rollback switches. Each guards one specific change (for example UxKirHttpBugFix25Q1, UxKirHttpTlsHandshakePerfCounter, UxKirHkPerformanceImprovements), and the patch removes individual rolled-back branches as their fixes become permanent. They are not a single global gate.
  • Manual Verification: Drive identical HTTP load against both builds and compare the global counter values returned via the PCW path; they are identical, confirming the change has no observable effect.