ksecdd.sys — NULL-pointer dereference & missing ProbeForRead in SSPI dispatch and IOCTL paths (CWE-476, CWE-822) fixed
KB5094128
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | ksecdd_unpatched.sys |
| Patched binary | ksecdd_patched.sys |
| Overall similarity | 0.7384 (73.8%) |
| Matched functions | 371 |
| Changed functions | 234 |
| Identical functions | 137 |
| Unmatched (unpatched → patched) | 0 / 0 |
Verdict: The patch delivers input-validation hardening in the kernel-mode SSPI provider. Two genuine, verifiable classes of change are present:
-
NULL-pointer / dispatch hardening in the SSPI message and context APIs (
EncryptMessage,DecryptMessage,QuerySecurityContextToken,ImpersonateSecurityContext, and the internal dispatch helpers). After the existing unsigned upper-bound check on the package index, the patched code adds anlfencespeculation barrier, reloads the dispatch-table base, and NULL-checks both the resolved package entry and the target function pointer before the CFG-guarded indirect call. For the four message/context entry points it additionally adds NULL checks on the caller-supplied context handle (and message buffer) that were absent in the unpatched build, closing a kernel NULL-pointer dereference (local BSOD) reachable when a NULL context handle is passed. -
Missing
ProbeForReadon user-supplied IOCTL input buffers in the fast-I/O device-control handlers (KsecIoctlFreeVm,KsecIoctlAllocVm,KsecIoctlFreePool,KsecIoctlAllocPool,KsecIoctlCopyPool,KsecIoctlDupLsaHandle,KsecIoctlInsertProtectedProcessAddress,KsecValidateAddress, and related handlers reached throughKsecFastIoDeviceControl). The unpatched build contains zeroProbeForReadcalls; the patched build adds them before every dereference of the caller-supplied input buffer.
Two claims that a first pass might reach — that the patch removes an IOCTL 0x398000 "kernel process handle leak" and adds object validation to an IOCTL 0x390058 "arbitrary VM free" in KsecDispatch — do not hold against the binaries: both IOCTL handlers are functionally identical in the two builds. Those items are recorded below as No security-relevant change.
2. Vulnerability Summary
Finding 1 — Kernel NULL Dereference / dispatch hardening in SSPI context APIs (LOW)
| Attribute | Value |
|---|---|
| Severity | Low (local denial of service) |
| Vulnerability class | CWE-476 (NULL Pointer Dereference); dispatch-index speculation barrier (lfence) |
| Affected functions | EncryptMessage (SSPI SealMessage), DecryptMessage (SSPI UnsealMessage), QuerySecurityContextToken, ImpersonateSecurityContext, DeleteUserModeContext (internal), ProcessSecurityContextUnMarshalResponse (internal) |
| Primitive | Kernel BSOD (local DoS) when a NULL context handle is passed to a message/context API |
Root cause: In the unpatched build, EncryptMessage, DecryptMessage, QuerySecurityContextToken, and ImpersonateSecurityContext read the package index from *phContext and use it to index a per-package dispatch table, without first checking that phContext is non-NULL. If phContext == NULL, the mov that loads *phContext faults in kernel mode. The subsequent dispatch does an unsigned upper-bound check on the index and then loads a package-entry pointer and a function pointer without NULL-checking either; the indirect call is made through the Control-Flow-Guard dispatch thunk (__guard_dispatch_icall_fptr).
The patch:
- adds
if (phContext == NULL ...) return SEC_E_INVALID_HANDLE(and, forEncryptMessage/DecryptMessage, a NULL check on the message-buffer argument); - inserts an
lfenceafter the unsigned bounds check (a Spectre-v1 / bounds-check-bypass barrier on the attacker-influenced index); - reloads the dispatch-table base pointer after the bounds check; and
- adds
test reg,reg / je erroron both the resolved package entry and the loaded function pointer before the indirect call.
For QuerySecurityContextToken, ImpersonateSecurityContext, and DeleteUserModeContext, the inline dispatch was moved into shared validating helpers (KernelPackageCallValidatorWorkers::CallValidator<…>::InvokeById, at 0x180029C10 for the offset-0x40 slot and 0x180029960 for the offset-0x08 slot) that contain the same lfence + entry-NULL + function-pointer-NULL logic.
Note on scope: the unpatched DeleteUserModeContext already NULL-checks its context-handle argument and the function pointer; for that function the added protection is the lfence, the table reload, and the entry-pointer NULL check. ProcessSecurityContextUnMarshalResponse is an internal async-response unmarshaller whose dispatch index comes from previously unmarshalled data rather than a raw caller handle; it received the same dispatch hardening.
Entry point & data flow (NULL-deref variant):
- A caller invokes a kernel SSP message/context routine (
EncryptMessage/DecryptMessage/QuerySecurityContextToken/ImpersonateSecurityContext) with a NULLPCtxtHandle. - The unpatched routine executes
mov reg, [phContext]to read the package index — withphContext == NULLthis faults in kernel mode → bugcheck (local DoS). - The patched routine returns
SEC_E_INVALID_HANDLE(0x80090301) instead.
Finding 2 — IOCTL 0x398000 handler (No security-relevant change)
| Attribute | Value |
|---|---|
| Severity | None (no change between builds) |
| Affected function | KsecDispatch (sub_1c001e8c0 unpatched / 0x180026dc0 patched) |
The IOCTL 0x398000 handler is present and functionally identical in both builds. In both, it opens a handle to PsGetCurrentProcess() (the caller's own process) with ObOpenObjectByPointer(process, OBJ_KERNEL_HANDLE, NULL, 0x478, NULL, KernelMode, &state[1]). The handle is stored into the kernel LSA-state structure (v19+1 / v25+1), not returned to user mode; the user output buffer receives only the DWORD KsecSystemProcessId. 0x478 is a set of process access rights (PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION), not token rights, and the object type passed to ObOpenObjectByPointer is NULL. There is no handle leak and no removal of this handler. The only diff in this branch is a refactor (InitSecurityInterfaceW() → CreateClient(...)). No security-relevant change.
Finding 3 — IOCTL 0x390058 handler (No security-relevant change)
| Attribute | Value |
|---|---|
| Severity | None (no change between builds) |
| Affected function | KsecDispatch (sub_1c001e8c0 unpatched / 0x180026dc0 patched) |
The IOCTL 0x390058 handler is functionally identical in both builds. In both, it requires RequestorMode == UserMode, a non-NULL requestor process, and a non-NULL buffer; validates the target address against the driver's own tracked-allocation list via KsecRemoveValidVm(RequestorProcess, address) (which returns NULL — and the request fails — for any address ksecdd did not previously allocate for that process); and only then calls ZwFreeVirtualMemory. The IOCTL is METHOD_BUFFERED (0x390058 & 3 == 0), so the input is read from the kernel-copied system buffer and needs no ProbeForRead. No ProbeForRead or ObReferenceObjectByHandle was added to this branch in the patched build. No security-relevant change. (The ProbeForRead/ObReferenceObjectByHandle additions are in the separate fast-I/O IOCTL handlers — see Finding 6.)
Finding 4 — SspiAcceptSecurityContextAsync: added output-buffer-count bound check (LOW)
| Attribute | Value |
|---|---|
| Severity | Low (defensive bound check) |
| Vulnerability class | CWE-1284 (improper validation of a specified quantity) |
| Affected function | SspiAcceptSecurityContextAsync (0x1c0020250 → 0x1800240f0) |
Root cause / change: The unpatched SspiAcceptSecurityContextAsync delegates directly to KsecProcessSecurityContext. The patched version was restructured to marshal the async request inline and adds if (pOutput->cBuffers > 0x12) return 0x8009035D (SEC_E_INVALID_TOKEN), plus NULL checks on phContext/phCredential/phNewContext and a MapKernelContextHandle call on phContext. pOutput is the output SecBufferDesc supplied by the local caller, not network-controlled attacker data; there is no demonstrable overflow in either build. This is a defensive upper bound on a caller-provided buffer count, not a fix for a reachable overflow primitive.
Finding 5 — Missing NULL validation in ImportSecurityContextW (LOW)
| Attribute | Value |
|---|---|
| Severity | Low (local denial of service) |
| Vulnerability class | CWE-476 (NULL Pointer Dereference) |
| Affected function | ImportSecurityContextW (0x1c0023b50 → 0x180021ac0) |
Root cause / change: The unpatched ImportSecurityContextW dereferences the output context handle phContext (phContext->dwUpper = …; phContext->dwLower = …) without a NULL check, and passes pPackedContext to the package handler. The patch adds if (pPackedContext != NULL && phContext != NULL) { … } else return STATUS_INVALID_PARAMETER (0xC000000D). Both builds already NULL-check the package function pointer (if (v10 != NULL)), so no function-pointer check was added. Impact is a local NULL-pointer dereference (BSOD) if a caller passes a NULL phContext.
Finding 6 — Missing ProbeForRead on user IOCTL input buffers in fast-I/O device control (MEDIUM)
| Attribute | Value |
|---|---|
| Severity | Medium (untrusted user-pointer dereference in kernel) |
| Vulnerability class | CWE-822 (Untrusted Pointer Dereference) / missing ProbeForRead; CWE-367 (double-fetch / TOCTOU) for KsecIoctlHandleFunctionReturn and KsecIoctlClientCallback |
| Affected functions | KsecIoctlHandleFunctionReturn, KsecIoctlClientCallback, KsecIoctlAllocVm, KsecIoctlFreeVm, KsecIoctlCopyVm, KsecIoctlAllocPool, KsecIoctlFreePool, KsecIoctlCopyPool, KsecIoctlDupLsaHandle, KsecIoctlInsertProtectedProcessAddress, KsecIoctlRemoveProtectedProcessAddress, and the VM helper KsecCopyVirtualMemory, all reached via KsecFastIoDeviceControl (0x1c0002910) |
| Primitive | Kernel dereference of an unvalidated caller-supplied pointer (invalid-address fault / read from an address the caller did not prove is user-mode); double-fetch of a user buffer for two handlers |
Root cause / change: The fast-I/O device-control path (KsecFastIoDeviceControl) passes the caller-supplied input buffer pointer straight to the per-IOCTL handlers. In the unpatched build these handlers dereference that pointer directly with no ProbeForRead — the entire unpatched binary contains zero ProbeForRead calls, versus 24 in the patched binary. The fix was applied individually across the whole KsecIoctl* handler family, not to a single handler. Two forms are present:
- Probe-then-use (
KsecIoctlFreeVm0x1C000E91C→0x18000E168,KsecIoctlAllocVm0x1C0001700→0x180005B98,KsecIoctlFreePool0x1C000E890→0x18000E084,KsecIoctlAllocPool0x1C0005A00→0x18000D8E8,KsecIoctlCopyPool0x1C00057C8→0x18000DE2C,KsecIoctlCopyVm0x1C0001AA4→0x1800032F0,KsecIoctlDupLsaHandle0x1C0001C1C→0x180005FD8,KsecIoctlInsertProtectedProcessAddress0x1C0001694→0x18000E360,KsecIoctlRemoveProtectedProcessAddress0x1C0001650→0x180006230,KsecCopyVirtualMemory0x1C0003F74→0x180005AC0): aProbeForRead(inputBuffer, <len>, <align>)is added before the dereference. Some of these previously used only a weak start-of-range test (e.g.if (*a1 >= MmUserProbeAddress)) that did not validate the whole buffer. - Probe-and-capture / double-fetch fix (
KsecIoctlHandleFunctionReturn0x1C0006898→0x180006728,KsecIoctlClientCallback0x1C000E778→0x18000DB54): the unpatched handler passed the raw user pointer into the downstream callee.KsecIoctlHandleFunctionReturnonly did a force-fault touch of the range end (if (a1+size > MmUserProbeAddress || overflow) *(BYTE*)MmUserProbeAddress = 0;) and then calledCallInProgressCompleted(a1, a1, size)on the user pointer. The patch addsProbeForRead(Src, size, 1), allocates a pool buffer,RtlCopyFromUser(Pool, Src, size), and operates on the kernel copy — removing a double-fetch/TOCTOU window as well as the missing probe.
The handle values read from these buffers are, in both builds, separately validated with ObReferenceObjectByHandle(..., PsProcessType, ...) and addresses with KsecRemoveValidVm/KsecInsertValidVm, which limits impact; the added probing closes the window in which a caller could supply a non-user-mode or invalid input-buffer address (or mutate it after a check) and have the driver dereference it in kernel mode.
Note: KsecUpdateUserTokenSessionId (?KsecUpdateUserTokenSessionId@@YAJPEAXK@Z) is a new handler present only in the patched build and is part of the same probing wave; it has no unpatched counterpart to diff against.
Entry point & data flow:
- A caller reaches the ksecdd fast-I/O device-control interface (
KsecFastIoDeviceControl) with one of the CNG kernel IOCTL codes and an input-buffer pointer. - Unpatched: the handler (e.g.
KsecIoctlFreeVm) reads*inputBufferwith noProbeForRead;KsecIoctlHandleFunctionReturnforwards the raw user pointer toCallInProgressCompleted. - Patched: the handler calls
ProbeForReadfirst (and, forKsecIoctlHandleFunctionReturn/KsecIoctlClientCallback, captures the buffer into kernel memory) before use.
3. Pseudocode Diff
EncryptMessage (SSPI SealMessage) — representative of the message/context dispatch fix
// UNPATCHED EncryptMessage @ 0x1C00262D0
uint64_t EncryptMessage(int64_t* phContext, int32_t arg2, int64_t pMessage, int32_t arg4)
{
KsecddLsaStateRef ref; // constructed, IsValid() checked
_KSECDDLSASTATE* state = ref; // operator _KSECDDLSASTATE*
int64_t table = state->[0x1c8]; // dispatch table base
if (!table) return 0xC0000001;
uint64_t index = *phContext; // <-- no NULL check on phContext
if (index >= state->[0x1d0]) // unsigned upper-bound only
return 0x80090301;
void* entry = table[index]; // no NULL check
return (entry->[0x30])(phContext[1], arg2, pMessage, arg4); // CFG-guarded call, no fn-ptr NULL check
}
// PATCHED EncryptMessage @ 0x180029A70
uint64_t EncryptMessage(int64_t* phContext, int32_t arg2, int64_t pMessage, int32_t arg4)
{
if (phContext == NULL) return 0x80090301; // NEW
if (pMessage == NULL) return 0x80090301; // NEW
uint64_t index = *phContext;
void* pkgarg = phContext[1];
KsecddLsaStateRef ref; _KSECDDLSASTATE* state = ref;
if (!state->[0x1c8]) return 0xC0000001;
if (index >= state->[0x1d0]) return 0x80090301;
_lfence(); // NEW: speculation barrier
void* entry = state->[0x1c8][index]; // NEW: table base reloaded post-check
if (entry == NULL) return 0x80090301; // NEW
void* fn = entry->[0x30];
if (fn == NULL) return 0x80090302; // NEW
return fn(pkgarg, arg2, pMessage, arg4); // CFG-guarded call
}
SspiAcceptSecurityContextAsync — new output-buffer-count check
// PATCHED (added):
if (pOutput->cBuffers > 0x12) // cmp dword [r15+0x4], 0x12 / ja error
return 0x8009035D; // SEC_E_INVALID_TOKEN
// UNPATCHED: no such check; delegates directly to KsecProcessSecurityContext
ImportSecurityContextW — new NULL checks
// PATCHED (added):
if (pPackedContext == NULL || phContext == NULL) {
// cleanup
return 0xC000000D; // STATUS_INVALID_PARAMETER
}
// UNPATCHED: phContext dereferenced (phContext->dwUpper = ...) without a NULL check
KsecIoctlFreeVm — new ProbeForRead (Finding 6)
// UNPATCHED KsecIoctlFreeVm @ 0x1C000E91C
if (a1 == NULL || a2 != 16) return 0xC000000D;
void* h = *a1; // <-- deref of caller buffer, no ProbeForRead
status = ObReferenceObjectByHandle(h, 0, PsProcessType, 0, &Object, NULL);
...
// PATCHED KsecIoctlFreeVm @ 0x18000E168
if (a1 == NULL || a2 != 16) return 0xC000000D;
ProbeForRead(a1, 0x10, 8); // NEW
Handle = *a1;
status = ObReferenceObjectByHandle(Handle[0], 0, PsProcessType, 0, &Object, NULL);
...
KsecIoctlHandleFunctionReturn — double-fetch / TOCTOU fix (Finding 6)
// UNPATCHED @ 0x1C0006898
if ((unsigned)a2 < 0x10) return 0xC0000023;
if (a1 == 0) return 0xC000000D;
...
if (a1 + size > MmUserProbeAddress || a1 + size < a1) // force-fault touch of range end only
*(BYTE*)MmUserProbeAddress = 0;
v5 = CallInProgressCompleted(a1, a1, size); // <-- raw user pointer forwarded
// PATCHED @ 0x180006728
if ((unsigned)Size < 0x10) return 0xC0000023;
if (Src == NULL) return 0xC000000D;
...
ProbeForRead(Src, size, 1); // NEW
Pool = ExAllocatePool2(256, size, ...);
if (Pool) {
RtlCopyFromUser(Pool, Src, size); // NEW: capture to kernel copy
v7 = CallInProgressCompleted(Pool, Pool, size); // operates on kernel copy
...
}
4. Assembly Analysis
EncryptMessage — Unpatched vs Patched
Unpatched (ksecdd_unpatched.sys @ 0x1C00262D0):
00000001C0026311 mov r8, [rax+1C8h] ; r8 = dispatch table base
00000001C0026318 test r8, r8
00000001C002631B jz loc_1C0026370 ; table NULL -> 0xC0000001
00000001C002631D mov rdx, [rbx] ; rdx = *phContext (rbx=phContext) — NO NULL check
00000001C0026320 mov ecx, [rax+1D0h] ; ecx = entry count
00000001C0026326 cmp rdx, rcx
00000001C0026329 jnb short loc_1C0026377 ; unsigned upper-bound only; no lfence
00000001C002632B mov rax, [r8+rdx*8] ; entry = table[index] — NO NULL check
00000001C002633B mov rax, [rax+30h] ; fn = entry->[0x30] — NO NULL check
00000001C002633F call cs:__guard_dispatch_icall_fptr ; CFG-guarded indirect call
Patched (ksecdd_patched.sys @ 0x180029A70):
0000000180029A80 test rcx, rcx ; NULL check phContext (arg1)
0000000180029A83 jnz short loc_180029A93
0000000180029A85 mov eax, 80090301h ; SEC_E_INVALID_HANDLE
0000000180029A93 test rbx, rbx ; NULL check pMessage (arg3; rbx=r8)
0000000180029A96 jz short loc_180029A85
0000000180029AD8 mov eax, [rax+1D0h] ; entry count
0000000180029ADE cmp rbp, rax ; rbp = *phContext (index)
0000000180029AE1 jnb short loc_180029B45
0000000180029AE3 lfence ; NEW: speculation barrier
0000000180029AE6 mov rax, [rcx+1C8h] ; NEW: reload table base after check
0000000180029AED mov rcx, [rax+rbp*8] ; entry = table[index]
0000000180029AF1 test rcx, rcx ; NEW: NULL check entry
0000000180029AF4 jz short loc_180029B45
0000000180029AF6 mov rax, [rcx+30h] ; fn = entry->[0x30]
0000000180029AFA test rax, rax ; NEW: NULL check fn ptr
0000000180029AFD jz short loc_180029B30
0000000180029B0A call _guard_dispatch_icall ; CFG-guarded indirect call
DecryptMessage — Unpatched vs Patched
Unpatched (@ 0x1C0026210):
00000001C0026252 mov r8, [rax+1C8h] ; table base
00000001C0026259 test r8, r8
00000001C002625C jz short loc_1C00262B2
00000001C002625E mov rdx, [rbx] ; *phContext — NO NULL check on rbx
00000001C0026261 mov ecx, [rax+1D0h] ; count
00000001C0026267 cmp rdx, rcx
00000001C002626A jnb short loc_1C00262B9 ; upper bound only
00000001C002626C mov rax, [r8+rdx*8] ; entry — NO NULL check
00000001C002627D mov rax, [rax+38h] ; fn ptr (offset 0x38) — NO NULL check
00000001C0026281 call cs:__guard_dispatch_icall_fptr
Patched (@ 0x180029D00):
0000000180029D11 test rcx, rcx ; NULL check phContext
0000000180029D14 jz loc_180029DBC ; -> SEC_E_INVALID_HANDLE
0000000180029D1A test rdx, rdx ; NULL check message arg
0000000180029D1D jz loc_180029DBC
0000000180029D6E lfence ; NEW
0000000180029D71 mov rax, [rcx+1C8h] ; NEW: reload table base
0000000180029D78 mov rcx, [rax+rbp*8] ; entry
0000000180029D7C test rcx, rcx ; NEW: NULL check entry
0000000180029D7F jz short loc_180029DDF
0000000180029D81 mov rax, [rcx+38h] ; fn ptr (offset 0x38)
0000000180029D85 test rax, rax ; NEW: NULL check fn ptr
0000000180029D88 jz short loc_180029DD1
0000000180029D96 call _guard_dispatch_icall
QuerySecurityContextToken — Unpatched
Unpatched (@ 0x1C0026010):
00000001C002604C mov rdx, [rax+1C8h] ; table base
00000001C0026053 test rdx, rdx
00000001C0026056 jz loc_1C002656A
00000001C002605C mov rcx, [rbx] ; *phContext — NO NULL check on rbx
00000001C002605F mov eax, [rax+1D0h] ; count
00000001C0026065 cmp rcx, rax
00000001C0026068 jnb short loc_1C00260D8 ; upper bound only
00000001C002606A mov rax, [rdx+rcx*8] ; entry — NO NULL check
00000001C002607A mov rax, [rax+40h] ; fn ptr (offset 0x40) — NO NULL check
00000001C002607E call cs:__guard_dispatch_icall_fptr
; on success, NtDuplicateObject duplicates the returned token handle at 0x1C00260B2
Patched (@ 0x180029390): adds a NULL check on phContext and delegates the dispatch to KernelPackageCallValidatorWorkers::CallValidator<…>::InvokeById at 0x180029C10, which performs the bounds check, lfence, table reload, entry-NULL and function-pointer-NULL checks:
; ---- InvokeById (0x180029C10), offset-0x40 slot ----
0000000180029C60 mov ecx, [rax+1D0h] ; count
0000000180029C66 cmp rbx, rcx
0000000180029C69 jnb short loc_180029CE7
0000000180029C6B lfence ; speculation barrier
0000000180029C6E mov rcx, [rax+1C8h] ; reload table base
0000000180029C75 mov rax, [rcx+rbx*8] ; entry
0000000180029C79 test rax, rax ; NULL check entry
0000000180029C7C jz short loc_180029CE7
0000000180029C7E mov rax, [rax+40h] ; fn ptr (offset 0x40)
0000000180029C82 test rax, rax ; NULL check fn ptr
0000000180029C85 jz short loc_180029CB9
0000000180029C90 call _guard_dispatch_icall
KsecIoctlFreeVm — added ProbeForRead (Finding 6)
Unpatched (@ 0x1C000E91C) reads the caller buffer with no probe (leads straight to ObReferenceObjectByHandle at 0x1C000E963).
Patched (@ 0x18000E168):
000000018000E184 mov edx, 10h ; Length
000000018000E189 cmp r8d, edx
000000018000E18C jnz loc_18000E313
000000018000E192 lea r8d, [rdx-8] ; Alignment = 8
000000018000E196 call cs:__imp_ProbeForRead ; NEW
000000018000E1A2 movups xmm0, xmmword ptr [rbx]; deref of caller buffer AFTER probe
000000018000E1A5 movdqu xmmword ptr [rsp+58h+Handle], xmm0
Function-pointer offset table (SSPI dispatch)
| Function | Fn-ptr offset | Notes |
|---|---|---|
EncryptMessage |
+0x30 |
SealMessage dispatch |
DecryptMessage |
+0x38 |
UnsealMessage dispatch |
QuerySecurityContextToken |
+0x40 |
via InvokeById 0x180029C10 (patched) |
ImpersonateSecurityContext |
+0x40 |
via InvokeById 0x180029C10 (patched) |
DeleteUserModeContext |
+0x08 |
via InvokeById 0x180029960 (patched) |
ProcessSecurityContextUnMarshalResponse |
+0x10 |
internal unmarshal dispatch |
5. Trigger Conditions
Trigger A: NULL Context Handle → BSOD (local DoS) — Finding 1
- Invoke a kernel SSP message/context routine (
EncryptMessage/DecryptMessage/QuerySecurityContextToken/ImpersonateSecurityContext) with a NULLPCtxtHandle. - In the unpatched build the routine executes
mov reg, [phContext]withphContext == NULL, faulting in kernel mode. - Result: kernel bugcheck (local DoS). The patched build returns
SEC_E_INVALID_HANDLE(0x80090301).
Trigger B: NULL output handle → BSOD (local DoS) — Finding 5
- Call
ImportSecurityContextWwith a NULLphContext(output handle). - Unpatched:
phContext->dwUpper = …dereferences NULL → bugcheck. - Patched: returns
STATUS_INVALID_PARAMETER(0xC000000D).
Trigger C: Unvalidated input-buffer pointer — Finding 6
- Reach the ksecdd fast-I/O device-control interface with one of the CNG kernel IOCTL codes and an input-buffer pointer that is not a valid user-mode range.
- Unpatched: the handler dereferences the pointer with no
ProbeForRead. - Patched:
ProbeForReadrejects a non-user-mode / invalid buffer address before the dereference.
Findings 2 and 3 have no trigger: the IOCTL 0x398000 and 0x390058 handlers are unchanged between builds and are not affected by this patch.
6. Impact Assessment
Finding 1 — NULL dereference / dispatch hardening
The concrete, demonstrable primitive is a local kernel NULL-pointer dereference (BSOD) when a NULL context handle is passed. The dispatch index is subject to an unsigned upper-bound check in both builds, so an out-of-bounds table read is not reachable through the index alone; the added lfence is a speculative-execution (bounds-check-bypass) barrier, and the added entry/function-pointer NULL checks are defense-in-depth on the CFG-guarded call site. No out-of-bounds write, information leak, or code-execution primitive is demonstrable from these changes.
Finding 4 — output-buffer-count check
A defensive upper bound (cBuffers ≤ 0x12) on a caller-supplied output SecBufferDesc. The value is not attacker-controlled network data, and no overflow is demonstrable in the unpatched build.
Finding 5 — ImportSecurityContextW NULL checks
Local NULL-pointer dereference (BSOD) if a caller passes a NULL packed-context or output-handle argument.
Finding 6 — missing ProbeForRead
Dereference of an unvalidated caller-supplied pointer in kernel mode. Because the handle read from the buffer is separately validated (ObReferenceObjectByHandle with PsProcessType) and addresses are checked against ksecdd's own allocation list (KsecRemoveValidVm), the practical impact is an invalid-address fault or a bounded (16-byte) read from a caller-named address; the added ProbeForRead closes the missing user-mode-range check.
7. Debugger Notes
Finding 1 — EncryptMessage NULL dereference
bp ksecdd!EncryptMessage
At entry, rcx = phContext (arg1) and r8 = pMessage (arg3). In the unpatched build the faulting instruction is mov rdx, [rbx] at 0x1C002631D (where rbx = phContext). Struct offsets used by the dispatch:
| Offset | Field | Notes |
|---|---|---|
+0x00 (context handle) |
package index | *phContext, used as the dispatch-table index |
+0x1C8 (LSA state) |
dispatch-table base | reloaded post-check in the patched build |
+0x1D0 (LSA state) |
entry count | unsigned upper bound (cmp / jnb) |
+0x30 (entry) |
EncryptMessage handler |
|
+0x38 (entry) |
DecryptMessage handler |
|
+0x40 (entry) |
QuerySecurityContextToken / ImpersonateSecurityContext handler |
|
+0x08 (entry) |
DeleteUserModeContext handler |
|
+0x10 (entry) |
ProcessSecurityContextUnMarshalResponse handler |
Finding 6 — ProbeForRead in KsecIoctlFreeVm
bp ksecdd!KsecIoctlFreeVm
At entry, rcx = a1 (caller input buffer). In the patched build (0x18000E168), ProbeForRead(a1, 0x10, 8) runs at 0x18000E196 before movups xmm0, [rbx] at 0x18000E1A2; the unpatched build (0x1C000E91C) dereferences the buffer with no probe.
SspiAcceptSecurityContextAsync — output-count check
bp ksecdd!SspiAcceptSecurityContextAsync
The patched check is cmp dword ptr [r15+4], 12h at 0x1800241E4, ja 0x1800242BC. SecBufferDesc.cBuffers is at offset +0x4 (ulVersion at +0x0, pBuffers at +0x8).
8. Changed Functions — Full Triage
Security-Relevant Changes
| Function | Change Type | Notes |
|---|---|---|
EncryptMessage (SealMessage) |
Security | Added NULL checks on phContext/message arg, lfence, table reload, entry-NULL and fn-ptr NULL checks (offset 0x30). |
DecryptMessage (UnsealMessage) |
Security | Same pattern; fn-ptr offset 0x38. |
QuerySecurityContextToken |
Security | Added phContext NULL check; dispatch moved to validating InvokeById 0x180029C10 (offset 0x40). |
ImpersonateSecurityContext |
Security | Added phContext NULL check (returns 0xC000000D); dispatch moved to InvokeById 0x180029C10; then PsImpersonateClient. |
DeleteUserModeContext (sub_1c001e190) |
Security | Already had arg1 and fn-ptr NULL checks; gained lfence, table reload, entry-NULL check via InvokeById 0x180029960 (offset 0x08). |
ProcessSecurityContextUnMarshalResponse (sub_1c001fac0) |
Security | Internal async-response unmarshaller; same dispatch hardening; fn-ptr offset 0x10. |
SspiAcceptSecurityContextAsync |
Security | Added cBuffers > 0x12 check and NULL checks on context/credential handles; restructured. |
ImportSecurityContextW |
Security | Added NULL checks on pPackedContext/phContext. |
KsecIoctlHandleFunctionReturn, KsecIoctlClientCallback |
Security | Raw user pointer replaced by ProbeForRead + RtlCopyFromUser into a kernel copy (double-fetch / TOCTOU fix). |
KsecIoctlAllocVm, KsecIoctlFreeVm, KsecIoctlCopyVm, KsecIoctlAllocPool, KsecIoctlFreePool, KsecIoctlCopyPool, KsecIoctlDupLsaHandle, KsecIoctlInsertProtectedProcessAddress, KsecIoctlRemoveProtectedProcessAddress, KsecCopyVirtualMemory |
Security | Added ProbeForRead on caller input buffer before dereference (0 → 24 ProbeForRead calls binary-wide). |
KsecUpdateUserTokenSessionId |
New handler | Present only in the patched build; part of the same probing wave (no unpatched counterpart). |
No Security-Relevant Change
| Function | Change Type | Notes |
|---|---|---|
KsecDispatch (sub_1c001e8c0 → 0x180026dc0) |
Refactor | IOCTL 0x398000 and 0x390058 handlers functionally identical between builds (see Findings 2 and 3). Diff is refactoring (InitSecurityInterfaceW → CreateClient, extension-dispatch signature, IoctlIpcGetQueuedFunctionCalls). |
AcceptSecurityContext |
Cosmetic | Thin wrapper updated to call the restructured worker. |
KsecProcessSecurityContext (sub_1c001e260) |
Behavioral | Token-duplication / process-token-query paths refactored; helper-call renumbering. Not a security fix. |
The remaining changed functions are register-allocation, compiler-optimization, and inlining differences from the recompilation, plus WPP/ETW tracing churn; none are security-relevant.
9. Unmatched Functions
No functions were added or removed (unmatched_unpatched: 0, unmatched_patched: 0). The patched build introduces the shared dispatch-validation helpers KernelPackageCallValidatorWorkers::CallValidator<…>::InvokeById (0x180029C10, 0x180029960), which replace the inline dispatch-table indexing inside already-matched functions rather than appearing as standalone unmatched entries.
10. Confidence & Caveats
- Finding 1 (NULL deref / dispatch hardening): High confidence in the mechanism. The added NULL checks,
lfence, table reload, and entry/fn-ptr NULL checks are all present in the patched disassembly and absent in the unpatched. Impact is a local NULL-deref DoS; no OOB or code-execution primitive is demonstrable. - Finding 2 (IOCTL 0x398000) and Finding 3 (IOCTL 0x390058): High confidence that there is no security-relevant change — both handlers are functionally identical in the two builds.
- Finding 4 (output-count check): The
cBuffers > 0x12check is genuinely added, but the input is a caller-supplied output descriptor and no overflow is demonstrable; treated as a defensive bound. - Finding 5 (ImportSecurityContextW): The NULL checks are genuinely added; the function-pointer NULL check exists in both builds.
- Finding 6 (ProbeForRead): High confidence the probes were added (0 → 24 binary-wide). Practical impact is bounded by the existing
ObReferenceObjectByHandle/KsecRemoveValidVmvalidation; precise reachability of the fast-I/O interface by an unprivileged caller depends on the device ACL and the protected-process gating inKsecFastIoDeviceControl.