What SMB2 actually needs from a security package

What a failing session setup looks like on the wire

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
Wireshark capture of an SMB2 session showing Session Setup Response Error STATUS_INVALID_PARAMETER followed by a TCP reset and retry
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.

What SMB2 actually asks the package for

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 by negoexts.dll to verify NegoEx's own MIC. The client never asks for it: it derives its MIC from the outer session key instead. negoexts!WSTRetrieveSessionKeys tolerates 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_FLAGS and SECPKG_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.

Why an unanswered query becomes a wire-level error

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.

Case study: SECPKG_ATTR_IS_LOOPBACK

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:

Live ETW trace showing KspQueryAttributes attribute 0x25 failing with SEC_E_INVALID_PARAMETER, code 0x8009035D
Live trace, kernel path: KspQueryAttributes(173) : Enter ContextId = ... attribute 0x25, then EIDQueryAttributes(472) : ContextAttribute=0x00000025 SEC_E_INVALID_PARAMETER0x25 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.

Note — a related kernel-side crash

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.