ksecpkg.sys — WIL feature-staged context-buffer allocator refactor
KB5082200
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 (0x1C0009194 → 0x1C0009F94), Pku2uFreeString (0x1C000B684 → 0x1C000C504), WDigestQueryContextAttributes (0x1C0025090 → 0x1C0027240), 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'=0x436C7353for the SSL session-key path,'PrCb'=0x43627250for context buffers) orKSecAllocateContextBuffer, and freed withExFreePoolWithTag. - Feature disabled (default): control falls through to the original code — the same
_SECPKG_KERNEL_FUNCTIONScallback 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_FUNCTIONSstructures installed by LSA, not an attacker-controlled.datadispatch table. - Feature-gated with the old path retained. The change is a staged rollout; the default
elsebranch 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, 0FFFDhinPku2uBuildFullServiceNameis identical in both builds).
3. Pseudocode Diff
SpQuerySessionKey (0x1C0009194 → 0x1C0009F94), 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 (0x1C000B684 → 0x1C000C504), 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 (0x1C0025090 → 0x1C0027240)
// 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 SpQuerySessionKey — 0x1C0009194
; ---- ?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 SpQuerySessionKey — 0x1C0009F94
; ---- ?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 Pku2uFreeString — 0x1C000B684
; ---- 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 Pku2uFreeString — 0x1C000C504
; ---- 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:
- Obtain an SSPI credential handle via
AcquireCredentialsHandlefor one of the affected packages (SSL/TLS/Schannel context queried bySpQuerySessionKey, or NTLM / Kerberos / PKU2U / WDigest / TS for the QueryContextAttributes handlers). - Establish a security context with
InitializeSecurityContext(client) orAcceptSecurityContext(server). - Query a context attribute with
QueryContextAttributes/QueryContextAttributesEx, e.g.SECPKG_ATTR_SESSION_KEY(exercisesSpQuerySessionKey) orSECPKG_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 @ 0x1C002C9D0 → Pku2uQueryContextAttributes @ 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 @ 0x1C001F510 → TSQueryContextAttributes @ 0x1C002EE10 |
0.39 | Cross-package diff pairing; same feature-gated allocator wrapper. |
| 3 | Pku2uQueryContextAttributes @ 0x1C0025AD0 → KerbQueryContextAttributes @ 0x1C0022E40 |
0.43 | Cross-package diff pairing; same wrapper. |
| 4 | KerbQueryContextAttributes @ 0x1C0020D50 → NtLmQueryContextAttributes @ 0x1C0021510 |
0.50 | Cross-package diff pairing; same wrapper. |
| 5 | Pku2uBuildFullServiceName @ 0x1C000B2F0 → Pku2uDuplicateString @ 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 @ 0x1C000B3D4 → Pku2uBuildFullServiceName @ 0x1C000C120 |
0.53 | Cross-name diff pairing. Length check preserved; only the allocator was wrapped. |
| 7 | Pku2uFreeString @ 0x1C000B684 → Pku2uFreeString @ 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 @ 0x1C0009194 → SpQuerySessionKey @ 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 @ 0x1C0025090 → WDigestQueryContextAttributes @ 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
- SSPI call chain: The
Sp*QueryContextAttributeshandlers andSpQuerySessionKeyare reachable from user mode viaQueryContextAttributesdispatched throughksecdd.sys. This is the normal package surface and does not by itself indicate a defect. - Feature staging:
Feature_4099632442__private_IsEnabledDeviceUsageis 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. - Package labels: Package identities above are taken from the exported/mangled symbol names present in the disassembly (
SslLsaKernelFunctions,Pku2uKernelFunctions,g_LsaKernelFunctions, and the*QueryContextAttributesnames).