LsaGetInterface: the interface registry inside lsass.exe
PKU2U (pku2u.dll) gets part of its logic from a function that lsass.exe itself exports: LsaGetInterface. It is one of four exports of lsass.exe, none of them documented. Two of them form a small registry of function tables that the DLLs loaded in LSASS use to call each other without importing each other. This page lists what Windows registers there, what each table contains, and how to use it from your own package. The header, the import library and a sample package are on GitHub.
NTSTATUS NTAPI LsaGetInterface(ULONG Id, PVOID* Interface); NTSTATUS NTAPI LsaRegisterInterface(ULONG Id, PVOID Interface); NTSTATUS NTAPI LsaRegisterExtension(ULONG Type, PVOID Table); NTSTATUS NTAPI LsaImpersonateKsecCaller(PHANDLE Token);
LsaRegisterInterfaceadds an entry to a linked list,gLsapIfList, with the id and a pointer to the table. If the id is already taken it returnsSTATUS_ALREADY_REGISTERED. No function removes an entry, so the table has to live as long as LSASS.LsaGetInterfacewalks that list and returns the table, orSTATUS_NOT_FOUND.LsaRegisterExtensionis something else. It is how lsasrv hands lsass.exe its own dispatch tables (reference monitor, SSPI, lookups), since lsass.exe itself is only a thin shell around lsasrv.LsaImpersonateKsecCallerreturns the token of the thread that ksecdd is serving, for packages called from kernel mode.
| Id | Registered by | Table | Used by |
|---|---|---|---|
| 0 | lsass.exe | gLsapRmFunctions: LsapCallRm, commands to the kernel reference monitor | lsasrv, for logon sessions |
| 1 | lsass.exe | gLsapCallbackFunctions: client callback, ksecdd IOCTL, System pid | lsasrv |
| 1001 | lsasrv.dll | LsapLsasrvIfTable, 18 entries: memory protection, LsaICallPackage, auth data to token, credential keys, gMSA password change… | netlogon, dpapisrv |
| 1002 | dpapisrv.dll | g_pDPAPIIfTable, 9 entries: DPAPICryptProtectData and the like, logon and credential key notifications | lsasrv (LsaICryptProtectData, the credential manager) |
| 1003 | lsasrv.dll | LsapIdProvExtIfTable, the identity-provider extension, 3 entries | pku2u |
lsass.exe registers ids 0 and 1 at startup. Ids 1001 and 1002 are not in the code at all. They come from the registry:
HKLM\SYSTEM\CurrentControlSet\Control\LsaExtensionConfig\Interfaces\1001
Extension = lsasrv.dll Name = LsaLsasrvInterface
HKLM\SYSTEM\CurrentControlSet\Control\LsaExtensionConfig\Interfaces\1002
Extension = dpapisrv.dll Name = LsaDpapiInterface
lsass loads the DLL named there and asks its QueryLsaInterface export for the table. DPAPI and lsasrv serve each other this way. dpapisrv!InitializeLsaExtension asks for 1001 to get lsasrv's helpers (LsapGetCredentialKey, LsapDecryptDPAPIMasterKey…), and once the extensions are loaded lsasrv asks for 1002 to serve LsaICryptProtectData. PKU2U never touches DPAPI.
lsasrv registers id 1003 at run time (LsapInitIdProvExtension), and only when the product type is not NtProductLanManNt. On a domain controller it does not exist, and PKU2U refuses every credential.
Looking for the callers of the two functions is not enough: a DLL can resolve LsaRegisterInterface with GetProcAddress. So the sample package reads the list directly. It finds gLsapIfList from the code of LsaGetInterface, walks it, and then asks LsaGetInterface for every id from 0 to 0xFFFF to cross-check. Output on Windows 11 24H2 (26100.1742), with the sample's own test id at the end:
registry: gLsapIfList at lsass.exe+0xd740 id 0 (0x0): table lsass.exe+0xd000, owner - id 1 (0x1): table lsass.exe+0xd008, owner - id 1003 (0x3eb): table lsasrv.dll+0x18dd08, owner - id 1001 (0x3e9): table lsasrv.dll+0x18d150, owner lsasrv.dll+0xd3e30 id 1002 (0x3ea): table dpapisrv.dll+0x47000, owner dpapisrv.dll+0x230e0 id 1297304576 (0x4d534c00): table LsaIfDump-c282e32f.dll+0x25000, owner - scan 0-0xFFFF: 0 1 1001 1002 1003
Nothing else. The "owner" column shows which ids came from LsaExtensionConfig. The last line of the walk is also a warning: that entry was registered by a previous load of the sample, and it still points into that copy of the DLL.
typedef struct _LSA_IDPROV_EXT_INTERFACE {
// the certificate an identity provider associates with a credential of a logon session
NTSTATUS (NTAPI* GetIdProvCertFromCred)(PLUID LogonId, GUID* ProviderId,
PVOID Credential, ULONG CredentialSize, PCCERT_CONTEXT* Certificate);
// what the peer's certificate means locally (Role 0: the peer is the user)
NTSTATUS (NTAPI* AcceptPeerCertificate)(GUID* ProviderId, int Role,
PCCERT_CONTEXT PeerCertificate, PVOID* LogonInformation);
// WS2022 and later
BOOL (NTAPI* ShouldBuildClientPac)(GUID* ProviderId);
} LSA_IDPROV_EXT_INTERFACE;
lsasrv is partly written in C++, so the public symbols give these signatures exactly, for example long LsapAcceptPeerCertificate(struct _GUID *, int, struct _CERT_CONTEXT const *, void **). Only the parameter names are not Microsoft's.
Each call carries the GUID of the identity provider the certificate belongs to, the one certpoleng found (see the PKU2U page). lsasrv handles the two built-in certificate providers itself and passes any other GUID to the identity provider it has loaded for it:
| Provider | AcceptPeerCertificate, role 0 | ShouldBuildClientPac |
|---|---|---|
{6D0F37E4-…-6B8343FE4953} Local Cert Issuer | S4U logon of the local account the certificate names, type 0x2A | FALSE |
{E6284137-…-51C9FF2ACEF0} Container Cert Issuer | same, type 0x2B | TRUE |
A TTD recording of LSASS during a PKU2U logon (local provider, client and server on the same machine) shows three calls:
- Server,
Pku2uCreatePac:AcceptPeerCertificate(local, 0, client certificate). lsasrv runsLsapGetS4ULogonUserToken, and the result becomes the PAC of the ticket. This is where the remote user becomes a local account. - Client,
Pku2uHandlePeer2PeerAsReply:AcceptPeerCertificate(local, 1, server certificate). For a built-in provider this returns success and nothing else. - Client,
Pku2uBuildApRequest:ShouldBuildClientPac(local)returns FALSE.
Only code running inside lsass.exe can call these functions: an authentication package, a security package or an LSA extension. You need an import library for lsass.exe, which the SDK does not provide. The GitHub repository builds one from a .def file, for x64 and ARM64:
LIBRARY lsass.exe
EXPORTS
LsaGetInterface
LsaImpersonateKsecCaller
LsaRegisterExtension
LsaRegisterInterface
#include "LsaInterface.h" // after windows.h, ntsecapi.h, ntsecpkg.h, wincrypt.h
PLSA_IDPROV_EXT_INTERFACE idprov;
if (NT_SUCCESS(LsaGetInterface(LSA_INTERFACE_IDPROV_EXT, (PVOID*)&idprov)))
{
BOOLEAN pac = idprov->ShouldBuildClientPac(&LSA_IDPROV_CONTAINER_CERT_ISSUER);
}
LsaRegisterInterface also works with your own ids. It is a simple way for two packages of the same product, say an authentication package and a security package, to share a table without exporting anything. Pick an id far from Microsoft's, and remember that it cannot be unregistered.
The repository contains the header (all five tables, with Microsoft's function names), lsass.def, the LsaIfDump sample package that produced the output above, and a script that loads a package into the running LSASS with AddSecurityPackage. On a test machine only, with LSA protection off: LSASS refuses an unsigned DLL otherwise.