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:

  1. 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 an lfence speculation 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.

  2. Missing ProbeForRead on 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 through KsecFastIoDeviceControl). The unpatched build contains zero ProbeForRead calls; 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:

  1. adds if (phContext == NULL ...) return SEC_E_INVALID_HANDLE (and, for EncryptMessage/DecryptMessage, a NULL check on the message-buffer argument);
  2. inserts an lfence after the unsigned bounds check (a Spectre-v1 / bounds-check-bypass barrier on the attacker-influenced index);
  3. reloads the dispatch-table base pointer after the bounds check; and
  4. adds test reg,reg / je error on 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):

  1. A caller invokes a kernel SSP message/context routine (EncryptMessage/DecryptMessage/QuerySecurityContextToken/ImpersonateSecurityContext) with a NULL PCtxtHandle.
  2. The unpatched routine executes mov reg, [phContext] to read the package index — with phContext == NULL this faults in kernel mode → bugcheck (local DoS).
  3. 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 (0x1c00202500x1800240f0)

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 (0x1c0023b500x180021ac0)

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 (KsecIoctlFreeVm 0x1C000E91C0x18000E168, KsecIoctlAllocVm 0x1C00017000x180005B98, KsecIoctlFreePool 0x1C000E8900x18000E084, KsecIoctlAllocPool 0x1C0005A000x18000D8E8, KsecIoctlCopyPool 0x1C00057C80x18000DE2C, KsecIoctlCopyVm 0x1C0001AA40x1800032F0, KsecIoctlDupLsaHandle 0x1C0001C1C0x180005FD8, KsecIoctlInsertProtectedProcessAddress 0x1C00016940x18000E360, KsecIoctlRemoveProtectedProcessAddress 0x1C00016500x180006230, KsecCopyVirtualMemory 0x1C0003F740x180005AC0): a ProbeForRead(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 (KsecIoctlHandleFunctionReturn 0x1C00068980x180006728, KsecIoctlClientCallback 0x1C000E7780x18000DB54): the unpatched handler passed the raw user pointer into the downstream callee. KsecIoctlHandleFunctionReturn only did a force-fault touch of the range end (if (a1+size > MmUserProbeAddress || overflow) *(BYTE*)MmUserProbeAddress = 0;) and then called CallInProgressCompleted(a1, a1, size) on the user pointer. The patch adds ProbeForRead(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:

  1. 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.
  2. Unpatched: the handler (e.g. KsecIoctlFreeVm) reads *inputBuffer with no ProbeForRead; KsecIoctlHandleFunctionReturn forwards the raw user pointer to CallInProgressCompleted.
  3. Patched: the handler calls ProbeForRead first (and, for KsecIoctlHandleFunctionReturn/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

  1. Invoke a kernel SSP message/context routine (EncryptMessage/DecryptMessage/QuerySecurityContextToken/ImpersonateSecurityContext) with a NULL PCtxtHandle.
  2. In the unpatched build the routine executes mov reg, [phContext] with phContext == NULL, faulting in kernel mode.
  3. Result: kernel bugcheck (local DoS). The patched build returns SEC_E_INVALID_HANDLE (0x80090301).

Trigger B: NULL output handle → BSOD (local DoS) — Finding 5

  1. Call ImportSecurityContextW with a NULL phContext (output handle).
  2. Unpatched: phContext->dwUpper = … dereferences NULL → bugcheck.
  3. Patched: returns STATUS_INVALID_PARAMETER (0xC000000D).

Trigger C: Unvalidated input-buffer pointer — Finding 6

  1. 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.
  2. Unpatched: the handler dereferences the pointer with no ProbeForRead.
  3. Patched: ProbeForRead rejects 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_1c001e8c00x180026dc0) Refactor IOCTL 0x398000 and 0x390058 handlers functionally identical between builds (see Findings 2 and 3). Diff is refactoring (InitSecurityInterfaceWCreateClient, 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 > 0x12 check 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/KsecRemoveValidVm validation; precise reachability of the fast-I/O interface by an unprivileged caller depends on the device ACL and the protected-process gating in KsecFastIoDeviceControl.