1. Overview

Field Value
Unpatched binary ksecpkg_unpatched.sys
Patched binary ksecpkg_patched.sys
Overall similarity 0.9462
Matched functions 472
Changed functions 9
Identical functions 463
Unmatched (unpatched) 0
Unmatched (patched) 0

Verdict: The patch wraps the security-context buffer allocation and free sites in several SSP handlers with a WIL feature-staging gate (Feature_4099632442__private_IsEnabledDeviceUsage). When the feature is enabled, buffers are allocated/freed via ExAllocatePoolWithTag / KSecAllocateContextBuffer (with explicit pool tags) and ExFreePoolWithTag. When the feature is disabled — the default — the code takes the original path unchanged: an indirect call through the package's _SECPKG_KERNEL_FUNCTIONS callback table via the Control Flow Guard dispatch thunk (__guard_dispatch_icall_fptr). Because the old path is retained as the default else branch and the new path is gated behind a staged feature flag, this is a staged allocator refactor / defense-in-depth change, not a delivered fix for a reachable vulnerability. No bounds check, size validation, or reachability condition changed.


2. Vulnerability Summary

Finding 1: Context-buffer allocator routing placed behind a feature gate — No security-relevant change

Attribute Detail
Severity No security-relevant change
Vulnerability class N/A (allocator routing refactor / defense-in-depth)
Affected functions SpQuerySessionKey (0x1C00091940x1C0009F94), Pku2uFreeString (0x1C000B6840x1C000C504), WDigestQueryContextAttributes (0x1C00250900x1C0027240), and six additional QueryContextAttributes / string helpers across the SSL/TLS, PKU2U, NTLM, Kerberos, and TS packages
Entry point Sp*QueryContextAttributes handlers, reachable from user mode via QueryContextAttributes / QueryContextAttributesEx

What actually changed

In the unpatched ksecpkg.sys, security-context buffer allocations and frees (session key, package info, credential strings) are performed through the LSA-provided _SECPKG_KERNEL_FUNCTIONS callback table for each package (SslLsaKernelFunctions, g_LsaKernelFunctions, Pku2uKernelFunctions, and the per-package equivalents). The allocate/free member of that table is called indirectly through the Control Flow Guard dispatch thunk __guard_dispatch_icall_fptr. These are legitimate kernel callbacks installed by LSA during package load; they are not an attacker-reachable data table.

The patched version adds, at each of these sites, a call to the WIL feature-staging predicate Feature_4099632442__private_IsEnabledDeviceUsage:

  • Feature enabled: the buffer is allocated with ExAllocatePoolWithTag (with pool tag 'SlSc' = 0x436C7353 for the SSL session-key path, 'PrCb' = 0x43627250 for context buffers) or KSecAllocateContextBuffer, and freed with ExFreePoolWithTag.
  • Feature disabled (default): control falls through to the original code — the same _SECPKG_KERNEL_FUNCTIONS callback invoked through __guard_dispatch_icall_fptr, byte-for-byte equivalent to the unpatched behavior.

Because the pre-patch path is retained verbatim as the default branch and the new path only executes when the staged feature is turned on, the delivered default behavior of the patched binary is identical to the unpatched binary. The gate at address 0x1C0004290 is Feature_4099632442__private_IsEnabledDeviceUsage, a WIL feature-staging predicate whose value is set by feature configuration, not attacker input.

Why this is not a delivered vulnerability fix

  • The unpatched indirect calls are CFG-guarded. Every allocate/free callsite in both builds goes through __guard_dispatch_icall_fptr, so a corrupted function pointer would be rejected by Control Flow Guard. There is no "no null check / no CFG / hijackable table" primitive.
  • The callback targets are legitimate. They are members of the per-package _SECPKG_KERNEL_FUNCTIONS structures installed by LSA, not an attacker-controlled .data dispatch table.
  • Feature-gated with the old path retained. The change is a staged rollout; the default else branch is the unmodified pre-patch code.
  • No bounds/size validation changed. The credential-string helpers retain their existing length checks unchanged (e.g., cmp edx, 0FFFDh in Pku2uBuildFullServiceName is identical in both builds).

3. Pseudocode Diff

SpQuerySessionKey (0x1C00091940x1C0009F94), SSL/TLS package

// UNPATCHED: SpQuerySessionKey (CSslUserContext)
int SpQuerySessionKey(CSslUserContext* ctx, _SecPkgContext_SessionKey* out) {
    if (ctx->key_len_field != 0x10)              // [ctx+0x17C] must be 16
        return 0x80090302;
    out->SessionKeyLength = 0x10;
    // allocate via LSA kernel-function callback, CFG-guarded indirect call
    void* buf = SslLsaKernelFunctions->AllocateContextBuffer(0x10); // __guard_dispatch_icall_fptr
    out->SessionKey = buf;
    if (!buf) return 0x80090300;
    memmove(buf, &ctx->key[0], out->SessionKeyLength);   // src = [ctx+0x180]
    return 0;
}

// PATCHED: same function, feature-gated allocation
int SpQuerySessionKey(CSslUserContext* ctx, _SecPkgContext_SessionKey* out) {
    if (ctx->key_len_field != 0x10)
        return 0x80090302;
    out->SessionKeyLength = 0x10;
    void* buf;
    if (Feature_4099632442__private_IsEnabledDeviceUsage())
        buf = ExAllocatePoolWithTag(SslPoolType, out->SessionKeyLength, 'SlSc');
    else
        buf = SslLsaKernelFunctions->AllocateContextBuffer(out->SessionKeyLength); // unchanged default
    out->SessionKey = buf;
    if (!buf) return 0x80090300;
    memmove(buf, &ctx->key[0], out->SessionKeyLength);
    return 0;
}

Pku2uFreeString (0x1C000B6840x1C000C504), PKU2U package

// UNPATCHED: Pku2uFreeString
void Pku2uFreeString(UNICODE_STRING_like* s) {
    if (!s) return;
    if (s->Buffer)                                     // [s+8]
        Pku2uKernelFunctions.FreeContextBuffer(s->Buffer); // CFG-guarded indirect
    s->Length = s->MaximumLength = 0; s->Buffer = 0;   // 16-byte zero
}

// PATCHED: same function, feature-gated free
void Pku2uFreeString(UNICODE_STRING_like* s) {
    if (!s) return;
    if (s->Buffer) {
        if (Feature_4099632442__private_IsEnabledDeviceUsage())
            ExFreePoolWithTag(s->Buffer, 0);
        else
            Pku2uKernelFunctions.FreeContextBuffer(s->Buffer); // unchanged default
    }
    s->Length = s->MaximumLength = 0; s->Buffer = 0;
}

WDigestQueryContextAttributes allocator path (0x1C00250900x1C0027240)

// UNPATCHED: each attribute-response buffer
buf = g_LsaKernelFunctions->AllocateContextBuffer(size);  // CFG-guarded indirect

// PATCHED: same site, feature-gated
if (Feature_4099632442__private_IsEnabledDeviceUsage())
    buf = KSecAllocateContextBuffer(size);
else
    buf = g_LsaKernelFunctions->AllocateContextBuffer(size);  // unchanged default

Feature predicate Feature_4099632442__private_IsEnabledDeviceUsage (0x1C0004290)

This is a standard WIL (Windows Implementation Library) feature-staging predicate. It exists in both roles' code only as the gate that selects between the new tagged-pool path and the retained legacy path. Its return value is controlled by feature configuration, not by attacker input.


4. Assembly Analysis

Unpatched SpQuerySessionKey0x1C0009194

; ---- ?SpQuerySessionKey@@YAJPEAUCSslUserContext@@PEAU_SecPkgContext_SessionKey@@@Z @ 0x1C0009194 ----
00000001C0009194  mov     [rsp+arg_0], rbx
00000001C0009199  push    rdi
00000001C000919A  sub     rsp, 20h
00000001C000919E  mov     rdi, rcx
00000001C00091A1  mov     rbx, rdx
00000001C00091A4  mov     ecx, 10h
00000001C00091A9  cmp     [rdi+17Ch], ecx
00000001C00091AF  jnz     short loc_1C00091E9
00000001C00091B1  mov     rax, cs:?SslLsaKernelFunctions@@3PEAU_SECPKG_KERNEL_FUNCTIONS@@EA
00000001C00091B8  mov     [rdx], ecx
00000001C00091BA  mov     rax, [rax]
00000001C00091BD  call    cs:__guard_dispatch_icall_fptr   ; CFG-guarded allocate callback
00000001C00091C3  mov     [rbx+8], rax
00000001C00091C7  test    rax, rax
00000001C00091CA  jz      short loc_1C00091E2
00000001C00091CC  mov     r8d, [rbx]
00000001C00091CF  lea     rdx, [rdi+180h]
00000001C00091D6  mov     rcx, rax
00000001C00091D9  call    memmove
00000001C00091DE  xor     eax, eax
00000001C00091E0  jmp     short loc_1C00091EE
00000001C00091E2  mov     eax, 80090300h
00000001C00091E7  jmp     short loc_1C00091EE
00000001C00091E9  mov     eax, 80090302h
00000001C00091EE  mov     rbx, [rsp+28h+arg_0]
00000001C00091F3  add     rsp, 20h
00000001C00091F7  pop     rdi
00000001C00091F8  retn

Patched SpQuerySessionKey0x1C0009F94

; ---- ?SpQuerySessionKey@@YAJPEAUCSslUserContext@@PEAU_SecPkgContext_SessionKey@@@Z @ 0x1C0009F94 ----
00000001C0009F94  mov     [rsp+arg_0], rbx
00000001C0009F99  push    rdi
00000001C0009F9A  sub     rsp, 20h
00000001C0009F9E  cmp     dword ptr [rcx+17Ch], 10h
00000001C0009FA5  mov     rbx, rdx
00000001C0009FA8  mov     rdi, rcx
00000001C0009FAB  jnz     short loc_1C000A01A
00000001C0009FAD  mov     dword ptr [rdx], 10h
00000001C0009FB3  call    Feature_4099632442__private_IsEnabledDeviceUsage   ; NEW gate
00000001C0009FB8  mov     ecx, [rbx]
00000001C0009FBA  xor     r8d, r8d
00000001C0009FBD  test    eax, eax
00000001C0009FBF  setnz   r8b
00000001C0009FC3  test    r8d, r8d
00000001C0009FC6  jz      short loc_1C0009FE4                                ; feature off -> legacy path
00000001C0009FC8  mov     edx, ecx                                          ; NumberOfBytes
00000001C0009FCA  mov     r8d, 436C7353h                                    ; Tag 'SlSc'
00000001C0009FD0  mov     ecx, cs:?SslPoolType@@3W4_POOL_TYPE@@A            ; PoolType
00000001C0009FD6  call    cs:__imp_ExAllocatePoolWithTag                    ; NEW tagged pool path
00000001C0009FDD  nop     dword ptr [rax+rax+00h]
00000001C0009FE2  jmp     short loc_1C0009FF4
00000001C0009FE4  mov     rax, cs:?SslLsaKernelFunctions@@3PEAU_SECPKG_KERNEL_FUNCTIONS@@EA
00000001C0009FEB  mov     rax, [rax]
00000001C0009FEE  call    cs:__guard_dispatch_icall_fptr                    ; retained legacy allocate
00000001C0009FF4  mov     [rbx+8], rax
00000001C0009FF8  test    rax, rax
00000001C0009FFB  jz      short loc_1C000A013
00000001C0009FFD  mov     r8d, [rbx]
00000001C000A000  lea     rdx, [rdi+180h]
00000001C000A007  mov     rcx, rax
00000001C000A00A  call    memmove
00000001C000A00F  xor     eax, eax
00000001C000A011  jmp     short loc_1C000A01F
00000001C000A013  mov     eax, 80090300h
00000001C000A018  jmp     short loc_1C000A01F
00000001C000A01A  mov     eax, 80090302h
00000001C000A01F  mov     rbx, [rsp+28h+arg_0]
00000001C000A024  add     rsp, 20h
00000001C000A028  pop     rdi
00000001C000A029  retn

Unpatched Pku2uFreeString0x1C000B684

; ---- Pku2uFreeString @ 0x1C000B684 ----
00000001C000B684  test    rcx, rcx
00000001C000B687  jz      short locret_1C000B6B2
00000001C000B689  push    rbx
00000001C000B68A  sub     rsp, 20h
00000001C000B68E  mov     rbx, rcx
00000001C000B691  mov     rcx, [rcx+8]
00000001C000B695  test    rcx, rcx
00000001C000B698  jz      short loc_1C000B6AD
00000001C000B69A  mov     rax, qword ptr cs:?Pku2uKernelFunctions@@3U_SECPKG_KERNEL_FUNCTIONS@@A+8
00000001C000B6A1  call    cs:__guard_dispatch_icall_fptr   ; CFG-guarded free callback
00000001C000B6A7  xorps   xmm0, xmm0
00000001C000B6AA  movups  xmmword ptr [rbx], xmm0
00000001C000B6AD  add     rsp, 20h
00000001C000B6B1  pop     rbx
00000001C000B6B2  retn

Patched Pku2uFreeString0x1C000C504

; ---- Pku2uFreeString @ 0x1C000C504 ----
00000001C000C504  test    rcx, rcx
00000001C000C507  jz      short locret_1C000C54D
00000001C000C509  push    rbx
00000001C000C50A  sub     rsp, 20h
00000001C000C50E  cmp     qword ptr [rcx+8], 0
00000001C000C513  mov     rbx, rcx
00000001C000C516  jz      short loc_1C000C548
00000001C000C518  call    Feature_4099632442__private_IsEnabledDeviceUsage   ; NEW gate
00000001C000C51D  mov     rcx, [rbx+8]
00000001C000C521  test    eax, eax
00000001C000C523  jnz     short loc_1C000C534                                ; feature on -> tagged free
00000001C000C525  mov     rax, qword ptr cs:?Pku2uKernelFunctions@@3U_SECPKG_KERNEL_FUNCTIONS@@A+8
00000001C000C52C  call    cs:__guard_dispatch_icall_fptr                     ; retained legacy free
00000001C000C532  jmp     short loc_1C000C542
00000001C000C534  xor     edx, edx                                          ; Tag = 0
00000001C000C536  call    cs:__imp_ExFreePoolWithTag                        ; NEW pool free path
00000001C000C53D  nop     dword ptr [rax+rax+00h]
00000001C000C542  xorps   xmm0, xmm0
00000001C000C545  movups  xmmword ptr [rbx], xmm0
00000001C000C548  add     rsp, 20h
00000001C000C54C  pop     rbx
00000001C000C54D  retn

WDigest WDigestQueryContextAttributes allocator diff

; --- UNPATCHED 0x1C0025090 (each response-buffer allocation) ---
00000001C0025149  mov     rax, cs:?g_LsaKernelFunctions@@3PEAU_SECPKG_KERNEL_FUNCTIONS@@EA
00000001C002515E  call    cs:__guard_dispatch_icall_fptr   ; CFG-guarded allocate callback

; --- PATCHED 0x1C0027240 (same site, feature-gated) ---
00000001C00272FE  call    Feature_4099632442__private_IsEnabledDeviceUsage
00000001C002730A  call    cs:__imp_KSecAllocateContextBuffer               ; feature on
00000001C0027318  mov     rax, cs:?g_LsaKernelFunctions@@3PEAU_SECPKG_KERNEL_FUNCTIONS@@EA
00000001C0027322  call    cs:__guard_dispatch_icall_fptr                   ; retained legacy default

5. Trigger Conditions

The affected code runs on ordinary SSPI flows:

  1. Obtain an SSPI credential handle via AcquireCredentialsHandle for one of the affected packages (SSL/TLS/Schannel context queried by SpQuerySessionKey, or NTLM / Kerberos / PKU2U / WDigest / TS for the QueryContextAttributes handlers).
  2. Establish a security context with InitializeSecurityContext (client) or AcceptSecurityContext (server).
  3. Query a context attribute with QueryContextAttributes / QueryContextAttributesEx, e.g. SECPKG_ATTR_SESSION_KEY (exercises SpQuerySessionKey) or SECPKG_ATTR_PACKAGE_INFO (exercises the QueryContextAttributes allocator path).

Reaching this code does not by itself demonstrate a vulnerability: both builds allocate through a CFG-guarded, LSA-installed callback, and (in the patched build with the feature disabled) take exactly the same path. The size passed to the allocator is the session-key length or package-info structure size, which is not an attacker-controlled primitive here.


6. Exploit Primitive

No exploit primitive is present. The unpatched allocate/free sites call LSA-provided _SECPKG_KERNEL_FUNCTIONS callbacks through the Control Flow Guard dispatch thunk __guard_dispatch_icall_fptr; a corrupted pointer would be rejected by CFG, and the callback table is not an attacker-reachable .data array. There is no out-of-bounds access, use-after-free, allocator/deallocator mismatch, or function-pointer hijack introduced or fixed by this change. The pool tags ('SlSc', 'PrCb') and the switch to ExAllocatePoolWithTag / KSecAllocateContextBuffer are allocation-hygiene refactors that only take effect when the staged feature is enabled.


7. Observation Notes

Target: kernel debugger attached; either build.

Useful anchors for confirming the change on a live system:

; Session-key allocation site (SSL/TLS SpQuerySessionKey)
bp ksecpkg+0x91BD    ; unpatched: call __guard_dispatch_icall_fptr (allocate callback)
bp ksecpkg+0x9FB3    ; patched:   call Feature_4099632442__private_IsEnabledDeviceUsage (the gate)

; PKU2U free (Pku2uFreeString)
bp ksecpkg+0xB6A1    ; unpatched: call __guard_dispatch_icall_fptr (free callback)
bp ksecpkg+0xC518    ; patched:   call Feature_4099632442__private_IsEnabledDeviceUsage (the gate)
Check Unpatched Patched (feature off — default) Patched (feature on)
Allocate path _SECPKG_KERNEL_FUNCTIONS callback via CFG dispatch same as unpatched ExAllocatePoolWithTag / KSecAllocateContextBuffer
Free path _SECPKG_KERNEL_FUNCTIONS callback via CFG dispatch same as unpatched ExFreePoolWithTag
Session-key buffer pool tag callback-dependent callback-dependent 'SlSc' present
Feature predicate at 0x1C0004290 not called called, returns 0 called, returns nonzero

8. Changed Functions — Full Triage

All nine changed functions received the same edit: the context-buffer allocate/free callsite was wrapped in the Feature_4099632442__private_IsEnabledDeviceUsage gate, with the original CFG-guarded _SECPKG_KERNEL_FUNCTIONS callback retained as the default branch. The diff paired several functions across packages (same C++ mangled name in a different package, relocated between builds); the real per-address identities are listed below.

# Unpatched (name @ address) → Patched (name @ address) Similarity Change
1 TSQueryContextAttributes @ 0x1C002C9D0Pku2uQueryContextAttributes @ 0x1C0027E20 0.37 Cross-package diff pairing; both received the feature-gated allocator wrapper. Existing NULL guards on the session-key pointer/length are unchanged.
2 NtLmQueryContextAttributes @ 0x1C001F510TSQueryContextAttributes @ 0x1C002EE10 0.39 Cross-package diff pairing; same feature-gated allocator wrapper.
3 Pku2uQueryContextAttributes @ 0x1C0025AD0KerbQueryContextAttributes @ 0x1C0022E40 0.43 Cross-package diff pairing; same wrapper.
4 KerbQueryContextAttributes @ 0x1C0020D50NtLmQueryContextAttributes @ 0x1C0021510 0.50 Cross-package diff pairing; same wrapper.
5 Pku2uBuildFullServiceName @ 0x1C000B2F0Pku2uDuplicateString @ 0x1C000C238 0.52 Cross-name diff pairing. The combined-length check (cmp edx, 0FFFDh) is identical in both builds; only the allocator was wrapped. No bounds-check change.
6 Pku2uDuplicateString @ 0x1C000B3D4Pku2uBuildFullServiceName @ 0x1C000C120 0.53 Cross-name diff pairing. Length check preserved; only the allocator was wrapped.
7 Pku2uFreeString @ 0x1C000B684Pku2uFreeString @ 0x1C000C504 0.73 Correctly matched. Free callsite wrapped with the feature gate; ExFreePoolWithTag(buffer, 0) added on the enabled path; original Pku2uKernelFunctions free retained as default.
8 SpQuerySessionKey @ 0x1C0009194SpQuerySessionKey @ 0x1C0009F94 0.78 Correctly matched. Session-key allocation wrapped with the feature gate; ExAllocatePoolWithTag(SslPoolType, size, 'SlSc') added on the enabled path; original SslLsaKernelFunctions allocate retained as default. Length gate (==0x10) unchanged.
9 WDigestQueryContextAttributes @ 0x1C0025090WDigestQueryContextAttributes @ 0x1C0027240 0.80 Correctly matched. Attribute-response allocations wrapped with the feature gate; KSecAllocateContextBuffer on the enabled path; original g_LsaKernelFunctions allocate retained as default. A constant store in the SECPKG_ATTR_PACKAGE_INFO path was folded to an immediate — semantically identical.

Cosmetic / Register-Allocation Changes

In function #9 the patched build folds a register-mediated constant store into an immediate store in the SECPKG_ATTR_PACKAGE_INFO path. This is a compiler optimization with no semantic difference.


9. Unmatched Functions

Direction Count Security-Relevant?
Removed (unpatched only) 0 N/A
Added (patched only) 0 N/A

The feature predicate at 0x1C0004290 (Feature_4099632442__private_IsEnabledDeviceUsage) is present in both builds; it is not a newly added function. What is new in the patched build is the set of call sites that invoke it as the allocator gate.


10. Confidence & Caveats

Confidence: High

Rationale: All nine changed functions show the same edit at real, verifiable addresses: a Feature_4099632442__private_IsEnabledDeviceUsage call gating a new tagged-pool allocate/free path, with the pre-patch CFG-guarded _SECPKG_KERNEL_FUNCTIONS callback retained as the default branch. Because the default path is unchanged and the new path is staged behind a feature flag, the delivered default behavior is identical to the unpatched build. No bounds check, size validation, or reachability condition changed.

Notes

  1. SSPI call chain: The Sp*QueryContextAttributes handlers and SpQuerySessionKey are reachable from user mode via QueryContextAttributes dispatched through ksecdd.sys. This is the normal package surface and does not by itself indicate a defect.
  2. Feature staging: Feature_4099632442__private_IsEnabledDeviceUsage is a WIL feature predicate whose value is set by feature configuration, not by attacker input. Whether the tagged-pool path executes on a given system depends on that configuration.
  3. Package labels: Package identities above are taken from the exported/mangled symbol names present in the disassembly (SslLsaKernelFunctions, Pku2uKernelFunctions, g_LsaKernelFunctions, and the *QueryContextAttributes names).