What SMB2 actually needs from a security package
pktmon — built into Windows, nothing to install — captured this straight from both VMs during an SMB2 mount attempt against a share protected by a custom security package:
pktmon filter add -p 445 pktmon start --etw -f C:\temp\capture.etl <reproduce the mount> pktmon stop pktmon pcapng C:\temp\capture.etl -o capture.pcapng pktmon filter remove
capture.pcapng opened in Wireshark: Negotiate Protocol succeeds, the first Session Setup round returns the normal STATUS_MORE_PROCESSING_REQUIRED, then a later round comes back Error: STATUS_INVALID_PARAMETER (0xc000000d) — followed immediately by RST and a fresh connection attempt.STATUS_MORE_PROCESSING_REQUIRED is completely normal for a multi-round NegoEx exchange. STATUS_INVALID_PARAMETER tearing down the session is not — and it isn't a network-layer or credential problem. It's the security package answering one specific query wrong.
Every round of session setup, SMB2's NegoEx fast path calls QueryContextAttributes (kernel-mode: SpGetExtendedInformation/EIDQueryAttributes in the package's own dispatch) several times over, not just once. Confirmed live, in order of how often they show up in a trace:
SECPKG_ATTR_SIZES,SECPKG_ATTR_NAMES,SECPKG_ATTR_PACKAGE_INFO,SECPKG_ATTR_NEGOTIATION_INFO,SECPKG_ATTR_USER_FLAGS,SECPKG_ATTR_LOGOFF_TIME— asked every round, essentially bookkeeping.SECPKG_ATTR_SESSION_KEY— asked repeatedly per round; this is what SMB2 signing ultimately derives from.SECPKG_ATTR_NEGO_KEYS— the server side only, used bynegoexts.dllto verify NegoEx's own MIC. The client never asks for it: it derives its MIC from the outer session key instead.negoexts!WSTRetrieveSessionKeystolerates exactly one failure here —STATUS_NOT_FOUND, meaning "fall back to the outer key" — anything else is a hard failure surfaced to the client.SECPKG_ATTR_FLAGSandSECPKG_ATTR_IS_LOOPBACK— see below.
A package that doesn't recognize one of these has to decide what to answer. Getting that decision wrong is exactly what tore down the session above.
The default case for an unrecognized ContextAttribute was:
default:
Status = SEC_E_INVALID_PARAMETER; // 0x8009035D
Reasonable on its face — it's what most SSPI code does for an attribute it doesn't implement. But negoexts.dll's own tolerance for a missing answer (the STATUS_NOT_FOUND exception for nego keys, above) is its own logic, not a property of the kernel fast path underneath it. ksecdd.sys/srv2.sys do not extend the same tolerance: whatever SEC_E_INVALID_PARAMETER the package returns internally comes back out, unchanged, as STATUS_INVALID_PARAMETER on the wire — and that fails the entire session, not just the one query.
In other words: a query a package considers cosmetic can still be load-bearing, because the layer enforcing that isn't the one that looks forgiving.
This was the actual query that produced the capture above: SECPKG_ATTR_IS_LOOPBACK, id 37 (0x25) — a modern attribute, added to sspi.h after this project's local, vendored copy of ntsecpkg.h was last updated, so it simply wasn't defined and fell into the default: case. The live trace pinpoints it exactly:
KspQueryAttributes(173) : Enter ContextId = ... attribute 0x25, then EIDQueryAttributes(472) : ContextAttribute=0x00000025 SEC_E_INVALID_PARAMETER — 0x25 is 37 decimal, SECPKG_ATTR_IS_LOOPBACK. The very next query, SECPKG_ATTR_NAMES (attribute 0x1), leaves with Status = 0x00000000 — a normal query succeeding right next to the one that wasn't.It's a yes/no question — "is this authentication happening against localhost?" — semantically about as harmless as a query can be. The fast path disagreed and killed the session over it anyway. Fix:
case SECPKG_ATTR_IS_LOOPBACK:
// Real struct layout isn't publicly documented; writing just the
// leading BOOLEAN is safe regardless of the caller's actual buffer size.
*((PBOOLEAN) pBuffer) = FALSE;
Status = STATUS_SUCCESS;
break;
Always FALSE — this package only ever authenticates real network peers, never a loopback connection. Once this case was added, the same capture went on to a clean session setup; the mount itself then failed for the mundane reason of the target share not existing, which is a different problem entirely and a good sign the security layer was no longer the one at fault.
A separate, unrelated bug turned up while chasing SMB2 failures in the same investigation: a kernel crash in ksecpkg!NegoExtsDeleteKernelContext, caused by something further upstream in the package-lookup chain, not by a missing attribute answer. Covered with its own capture in Live-loading a kernel security package (KSP) without a reboot.