Implementing an LSA proxy authentication package

LSA authentication packages are a part of the core security ecosystem in Windows NT. LSA APs are provided with credentials by logon applications, such as Winlogon, authenticate these credentials and provide the logon application with a logon session handle if authentication was successful. Two authentication packages provided with Windows are of special interest. MSV1_0 authenticates user credentials against the local SAM database or against a domain controller through the legacy logon protocols. Kerberos.dll uses the Kerberos protocol with modern domain controllers (or third party KDCs, etc.) for establishing logon sessions.

Applications that want to provide value-added logon functionality wish to become involved in the logon process. Winlogon provides the GINA facility before Windows Vista and the new Credential Providers in Windows Vista and later to allow involvement in the end-user logon experience.

While customizing the logon process is done at the logon application side with GINAs or CPs, sometimes customization is required at the authentication side, in the Local Security Authority subsystem. For instance, a fingerprint reader may customize the logon UI to allow fingerprint scanning as a means of authentication. The customized logon application will call LsaLogonUser with the fingerprint scanner’s custom LSA authentication package as the desired authenticator. The fingerprint LSA AP will scan a fingerprint database it maintains to perform the authentication process. That would be an independent LSA authenticaion package.

There are other scenarios in which the regular MSV1_0/Kerberos based logon authentication process is desired, but special pre or post-processing is required. In Windows XP, the problematic GINA architecture makes replacing the GINA cumbersome, as multiple third-party applications may attempt to install conflicting GINA replacements. Indeed, Microsoft documentation has been modified retroactively to recommend GINA hooks and stubs in lieu of outright replacement of the MSGINA implementation previously suggested.

With the ability to instrument the logon application at the GINA side limited and version dependent (a totally different approach is required for Vista’s CPs), the alternative of instrumentation at the LSA side warrants exploration. We can implement an LSA proxy authentication package. Such a package would appear to the LSA server as an ordinary package, but would be implemented by delegating to original APs like MSV1_0 and Kerberos, providing it with monitoring and instrumentation capabilities.

LSA authentication packages are loaded by LSASS at system startup by enumerating them from the registry value HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Security Packages. Both LSA authentication packages (APs) and security providers (SSPs) are registered at that location. For the purposes of this post, we’ll ignore pure SSPs and focus only on APs. We will register our proxy authentication package instead of MSV1_0 (and possibly Kerberos) at this location, and leave loading of the original APs to our proxy AP implementation.

Let us examine the system’s core APs, MSV1_0 and Kerberos:

C:\WINDOWS\system32>dumpbin /exports msv1_0.dll
Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file msv1_0.dll
File Type: DLL
Section contains the following exports for msv1_0.dll
00000000 characteristics
48025C84 time date stamp Sun Apr 13 22:18:28 2008
0.00 version
1 ordinal base
32 number of functions
17 number of names
ordinal hint RVA name
5 0 0000175C LsaApCallPackage = _LsaApCallPackage@28
6 1 00014BC8 LsaApCallPackagePassthrough = _LsaApCallPackagePassthr
ough@28
7 2 00014A59 LsaApCallPackageUntrusted = _LsaApCallPackageUntrusted
@28
8 3 0000BDBB LsaApInitializePackage = _LsaApInitializePackage@20
9 4 0000F7FE LsaApLogonTerminated = _LsaApLogonTerminated@4
10 5 00007498 LsaApLogonUserEx2 = _LsaApLogonUserEx2@64
11 6 0001A7E5 Msv1_0ExportSubAuthenticationRoutine = _Msv1_0ExportSu
bAuthenticationRoutine@40
12 7 0001A7C6 Msv1_0SubAuthenticationPresent = _Msv1_0SubAuthenticat
ionPresent@4
13 8 00016E63 MsvGetLogonAttemptCount = _MsvGetLogonAttemptCount@0
2 9 0001B704 MsvIsLocalhostAliases = ?MsvIsLocalhostAliases@@YGHPAU
_UNICODE_STRING@@@Z (int __stdcall MsvIsLocalhostAliases(struct _UNICODE_STRING
*))
14 A 00016E59 MsvSamLogoff = _MsvSamLogoff@12
15 B 0000A7BA MsvSamValidate = _MsvSamValidate@52
16 C 00016E6E MsvValidateTarget = _MsvValidateTarget@12
1 D 0000E415 SpInitialize = _SpInitialize@12
32 E 00006BC2 SpInstanceInit = _SpInstanceInit@12
3 F 0000F08F SpLsaModeInitialize = ?SpLsaModeInitialize@@YGJKPAKPAP
AU_SECPKG_FUNCTION_TABLE@@0@Z (long __stdcall SpLsaModeInitialize(unsigned long,
unsigned long *,struct _SECPKG_FUNCTION_TABLE * *,unsigned long *))
4 10 00006AE0 SpUserModeInitialize = ?SpUserModeInitialize@@YGJKPAKP
APAU_SECPKG_USER_FUNCTION_TABLE@@0@Z (long __stdcall SpUserModeInitialize(unsign
ed long,unsigned long *,struct _SECPKG_USER_FUNCTION_TABLE * *,unsigned long *))
Summary
2000 .data
2000 .reloc
2000 .rsrc
1D000 .text
C:\WINDOWS\system32>dumpbin /exports kerberos.dll
Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file kerberos.dll
File Type: DLL
Section contains the following exports for Kerberos.dll
00000000 characteristics
48025C4A time date stamp Sun Apr 13 22:17:30 2008
0.00 version
1 ordinal base
32 number of functions
10 number of names
ordinal hint RVA name
5 0 00026F8A KerbCreateTokenFromTicket = _KerbCreateTokenFromTicket
@44
2 1 00025773 KerbDomainChangeCallback = ?KerbDomainChangeCallback@@
YGXW4_POLICY_NOTIFICATION_INFORMATION_CLASS@@@Z (void __stdcall KerbDomainChange
Callback(enum _POLICY_NOTIFICATION_INFORMATION_CLASS))
6 2 000018B0 KerbFree = _KerbFree@4
7 3 00020A71 KerbIsInitialized = _KerbIsInitialized@0
8 4 00020A7C KerbKdcCallBack = _KerbKdcCallBack@0
9 5 0000380B KerbMakeKdcCall = _KerbMakeKdcCall@32
1 6 00013CAD SpInitialize = _SpInitialize@12
32 7 0000EDDF SpInstanceInit = _SpInstanceInit@12
3 8 000151DE SpLsaModeInitialize = ?SpLsaModeInitialize@@YGJKPAKPAP
AU_SECPKG_FUNCTION_TABLE@@0@Z (long __stdcall SpLsaModeInitialize(unsigned long,
unsigned long *,struct _SECPKG_FUNCTION_TABLE * *,unsigned long *))
4 9 0000ED1E SpUserModeInitialize = ?SpUserModeInitialize@@YGJKPAKP
APAU_SECPKG_USER_FUNCTION_TABLE@@0@Z (long __stdcall SpUserModeInitialize(unsign
ed long,unsigned long *,struct _SECPKG_USER_FUNCTION_TABLE * *,unsigned long *))
Summary
2000 .data
3000 .reloc
3000 .rsrc
43000 .text

Both DLLs export the SpInitialize function used for SSP initialization and for AP initialization export SpLsaModeInitialize and SpUserModeInitialize, called in LSASS context and in logon application context by secur32.dll, respectively.

As can be seen in MSV1_0’s export table, it provides a subauthentication package facility for those interested in becoming involved in the logon process. Clearly, this is the facility Microsoft intended third-parties to use, rather than creating proxy authentication packages. Unfortunately, subauthentication packages only provide a subset of authentication package functionality, primarily failing to provide access to the interactive logon (as opposed to the network logon) process. You should carefully consider your requirements. If they can be met with a subauthentication package, it is best to opt for that approach as it is far more likely to remain intact in Windows 7 and beyond. That said, the proxy authentication package model is given some legitimacy by this diagram, illustrating delegation to MSV1_0 by a hypothetical third-party custom authentication package.

It is interesting to note that MSV1_0 chooses to export the various LSA AP functions, such as LsaApLogonUserEx2, etc., via its DLL export table, while Kerberos opts not to export them, though quick examination of Kerberos.dll’s public symbols clearly illustrates the presence of the AP functions. So how does the LSA server know how to find and call the various AP functions? They are provided in a function dispatch table filled by the AP at initialization time, when SpLsaModeInitialize or SpUserModeInitialize are invoked (depending on the context of the AP’s use).

Keeping in mind that MSV1_0 was around before Kerberos was added to Windows NT in the Windows 2000 release and therefore may have remaining legacy cruft, reviewing the disassembly of the LSA server DLL, LSASRV, reveals that LSA first calls the Initialize functions to let the AP fill the function dispatch table, but then promptly examines the AP’s export table with GetProcAddress in the aptly named lsasrv!BindOldPackage function. This function appears to be invoked for the AP even when the function dispatch table has been filled. However, empirical evidence appears to suggest that the function dispatch table takes precedence over the contents of the export table. Indeed, Kerberos opts to do away with the export table entirely.

It appears that the export-based model was replaced with the dispatch table based model at some point during the lifetime of Windows NT. The export model has an obvious disadvantage of allowing a single DLL to implement only a single authentication package, a restriction will come back to later. The dispatch table model, on the other hand, allows the initialization function to return multiple dispatch tables, for multiple authentication packages, from a single DLL. This may yet prove useful for a proxy package.

In order to explore the feasibility of creating a proxy LSA authentication package, I wrote a test DLL, dubbed “LsaPrxAp”. This DLL provides the SpInitialize, SpLsaModeInitialize and SpUserModeInitialize exports. When invoked, it loads msv1_0 if it is not yet loaded. The DLL’s Initialize functions call the original msv1_0’s Initialize functions and then hook the authentication package by replacing function pointers with pointers to implementations of the LSA AP functions residing in LSAPrxAp. The original function pointers are saved in a global data structure so that LSAPrxAp’s stubs can invoke them while having value-added processing during the logon sequence. Since the name of the authentication package is the one provided in the SpLsaModeInitialize function by the AP, rather than the DLL’s name, it is easy to proxy the MSV1_0 module and become involved in the logon process. I did not bother with supporting Kerberos in my test, but hypothetically the proxy AP can call the Initialize functions for both MSV1_0 and Kerberos and return two dispatch tables as the parameter model for it allows.

Proxying a known authentication package of a known revision is one thing, but a more generic approach warrants special consideration. In both XP and Vista, the APs provide the latest LsaApLogonUserEx2 function. However, it is preceeded by LsaApLogonUserEx and LsaApLogonUser. The documentation for AP developers seems to indicate it is still legitimate to only support one of the older variants. Therefore, the proxy should take note that NULL function pointers in the dispatch table provided by the original APs are possible and should react accordingly.

As I investigated the behavior of the LSA server with respect to the export-based model for locating LSA AP functions vs. the function dispatch table model, I considered the problem of implementing a generic proxy if the dispatch table model was not preferred over the export table. The proxy AP would have to load the original AP and only have an LsaApLogonUserEx2 function if the original AP has. If the original AP has no LsaApLogonUser function exported, neither should the proxy AP. This presents special complication since compiling a different proxy AP for every possible set of exports is not feasible. I reiterate that the problem is theoretical since the function dispatch table model has no such fallacy.

In order to deal with this issue, I devised an approach where the proxy AP would initially export all possible LSA AP functions. At runtime, as the actual exports of the original AP is detected, functions that are missing from the original AP can be unexported from the proxy AP. I discuss the technique of unexporting functions through name obfuscation in the previous post.

Successfully compiling an LSA AP DLL required numerous acts of header juggling. For reference purposes, I provide the sequence I used:

#include <ntstatus.h>
#define WIN32_NO_STATUS
#include <Windows.h>
#include <NTSecAPI.h>
#define SECURITY_WIN32
#include <SSPI.h>

#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)
#endif

// LsaApCallPackageUntrusted is partially missing from NTSecPkg.h
typedef NTSTATUS
(NTAPI LSA_AP_CALL_PACKAGE_UNTRUSTED)(
	__in PLSA_CLIENT_REQUEST ClientRequest,
	__in PVOID ProtocolSubmitBuffer,
	__in PVOID ClientBufferBase,
	__in ULONG SubmitBufferLength,
	__out PVOID* ProtocolReturnBuffer,
	__out PULONG ReturnBufferLength,
	__out PNTSTATUS ProtocolStatus
);

The WIN32_NO_STATUS definition is required to prevent Windows.h from defining the NTSTATUS values. Letting ntstatus.h define them is required because Windows.h only defines a subset of the required values. SSPI.h requires choosing between Win32, Kernel-mode and Macintosh Classic (heh…) usage, resulting in the SECURITY_WIN32 preprocessor definition. The NTSecPkg.h header included with the Windows SDK I’m using makes the embarrassing error of missing the prototype for LsaApCallPackageUntrusted, while curiously attempting to provide the function pointer type for it.

It is wise to avoid loading the original AP DLLs during DllMain, due to potential loader lock issues. The first functions invoked after an AP DLL has been mapped to a process are those that fill the function dispatch table; SpInitialize for the SSP case, SpLsaModeInitialize for the LSASS-side AP case and SpUserModeInitialize for the secur32.dll logon application (Winlogon) side case. All three functions ought to load the original AP if required and perform any required hooking of the dispatch tables, pointing to your own proxy functions.

The initialization functions should also extract the function pointers for the original AP implementations and preserve them in global data. This can later be used to forward calls to the original implementation. Consider the implementation of LsaApCallPackage in my LsaPrxAp test DLL:

NTSTATUS NTAPI LsaApCallPackage(
	__in PLSA_CLIENT_REQUEST ClientRequest,
	__in PVOID ProtocolSubmitBuffer,
	__in PVOID ClientBufferBase,
	__in ULONG SubmitBufferLength,
	__out PVOID* ProtocolReturnBuffer,
	__out PULONG ReturnBufferLength,
	__out PNTSTATUS ProtocolStatus
)
{
	OutputDebugString(TEXT("lsaaprxap LsaApCallPackage\n"));

	return g_LsaPrxApFunctionTable.pLsaApCallPackage(
		ClientRequest, ProtocolSubmitBuffer, ClientBufferBase,
		SubmitBufferLength, ProtocolReturnBuffer, ReturnBufferLength,
		ProtocolStatus);
}

Obviously a real-world proxy AP would perform required processing before or after forwarding to the original function pointer preserved in global data, as well.

Hopefully this primer on implementing an LSA proxy AP will prove to be a welcome addition to Microsoft’s sparse documentation on LSA authentication packages.

Unexporting a function from a DLL at runtime by name obfuscation

Usually, the set of functions exported by a Windows DLL is considered to be immutable. When load-time binding is used, functions appearing in the loading module’s import table are resolved against the export table of the loaded DLL. If any imported functions are missing, the loader aborts the module load attempt and unmaps the modules.

Library clients which require a more flexible reaction to the presence or absence of specific exports opt for a more dynamic binding approach, either late binding via GetProcAddress directly or through the Visual C++ delay loading facility, which allows for setting callbacks for missing modules or exports.

Recently, as I was experimenting with implementing an LSA proxy authentication package (more on that in a followup post) I considered the issue of properly implementing a proxy DLL for a DLL whose set of exports is only partially known to the proxy. Modern LSA APs have a callback function that provides a function dispatch table and do no rely on the module’s export table, but for pedagogic purposes let us consider an hypothetical situation in which the export table is the only lookup apparatus in use by the LSA.

In the case of an LSA authentication package, the package may provide an implementation of either LsaApLogonUser, LsaApLogonUserEx or LsaApLogonUserEx2. An original LSA AP can export one or more of these functions. If we want to create a generic LSA AP proxy DLL, we may wish to have a specific export from our module, loaded in lieu of the original AP, only if the original module also had it. This presents difficulty since we may not be able to predict the specific set of exports during proxy compile time.

If we recognize that DLL exports are in fact a poor man’s native code reflection mechanism, we can adopt a non-traditional approach of dynamic modification of our module’s set of exported functions. Our proxy DLL shall initially (at compile time) export all functions that could potentially be exported by the original DLL, e.g. by a module definition (.DEF) file like this:

LIBRARY "lsaprxap"
EXPORTS
LsaApInitializePackage
LsaApLogonUser
LsaApLogonUserEx
LsaApLogonUserEx2

LsaApCallPackage
LsaApCallPackagePassthrough
LsaApLogonTerminated
LsaApCallPackageUntrusted
SpInitialize
SpInstanceInit
SpLsaModeInitialize
SpUserModeInitialize

Notice how we export all three functions, even though some may not appear in the original DLL. LSA uses late binding via GetProcAddress to decide which function in the AP to call. If we export LsaApLogonUserEx2 and our original DLL does not have that function, we’ll have nothing to do when our proxy function is called (no original to forward to after our own processing). There is no telling what will happen if we return STATUS_NOT_IMPLEMENTED. Besides, LSA AP’s are only an illustration, and in other cases the export might not even have the option to return a failure exit code. Therefore, the behavior we desire is that GetProcAddress for LsaApLogonUserEx2 will fail and return NULL if the original DLL for which we are acting as a proxy does not export LsaApLogonUserEx2 itself.

The names and addresses of exported functions from a DLL appear in the PE image’s export directory. By accessing the export directory, looking up an exported function of interest and removing it we can alter the behavior of GetProcAddress for the exported function at runtime, after the module has been loaded. Note that this alteration is only useful for GetProcAddress invocations from that time forward, and callers that invoked GetProcAddress earlier and cached the result or callers that used load-time binding against our export table already obtained a function pointer to the exported function. Therefore, this technique is only useful in limited circumstances.

The export directory points to a block of null-terminated string pointers, indexed by export ordinal. In order to outright remove an export from the middle of the export table, we’d have to copy the export table aside, remove the desired functions and point the PE header to the new export table. This is a feasible, but cumbersome approach. Instead I opted for an alternative technique – obfuscating the name of the export to prevent GetProcAddress callers from resolving it by its well-known name. The function is still exported, but its name is unknown to other callers. This is probably sufficient for the vast majority of cases. As for the obfuscation itself, in this illustration we’ll merely increment the character value of the first letter in the export:

// Set by the linker to the base address of the module.
EXTERN_C IMAGE_DOS_HEADER __ImageBase;

void UnexportFunction(LPSTR ExportName)
{
	IMAGE_DOS_HEADER* dosHeader = &__ImageBase;
	assert(dosHeader->e_magic == IMAGE_DOS_SIGNATURE);
	IMAGE_NT_HEADERS* ntHeaders = reinterpret_cast<IMAGE_NT_HEADERS*>(
		reinterpret_cast<BYTE*>(dosHeader) + dosHeader->e_lfanew);
	assert(ntHeaders->Signature == 0x00004550);
	IMAGE_OPTIONAL_HEADER* optionalHeader = &ntHeaders->OptionalHeader;
	IMAGE_DATA_DIRECTORY* exportDataDirectory =
		&optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
	IMAGE_EXPORT_DIRECTORY* exportDirectory = reinterpret_cast<IMAGE_EXPORT_DIRECTORY*>(
		reinterpret_cast<BYTE*>(dosHeader) + exportDataDirectory->VirtualAddress);

	ULONG* addressOfNames = reinterpret_cast<ULONG*>(
		reinterpret_cast<BYTE*>(dosHeader) + exportDirectory->AddressOfNames);
	for (DWORD i = 0; i < exportDirectory->NumberOfFunctions; i++)
	{
		LPSTR exportFunctionName = reinterpret_cast<LPSTR>(
			reinterpret_cast<BYTE*>(dosHeader) + addressOfNames[i]);
		if (strcmp(exportFunctionName, ExportName) == 0)
		{
			DWORD oldProtect = 0;
			BOOL rc = VirtualProtect(
				exportFunctionName,
				strlen(exportFunctionName),
				PAGE_READWRITE,
				&oldProtect);
			if (rc == FALSE)
			{
				OutputDebugString(TEXT("VirtualProtect failed.\n"));
			}
			exportFunctionName[0]++;
			break;
		}
	}
}

The sample UnexportFunction function iterates over the current module’s export table until the function of interest is encountered. Since the export table is mapped as read-only memory, VirtualProtect must be used to allow for its modification. The string containing the name of the exported function is modified in place as a trivial obfuscation. This is sufficient to result in the “unexporting” of the symbol:

FARPROC before = GetProcAddress((HMODULE)&__ImageBase, "LsaApLogonUser");
UnexportFunction("LsaApLogonUser");
FARPROC after = GetProcAddress((HMODULE)&__ImageBase, "LsaApLogonUser");
assert(before != after);
assert(after == NULL);

With this tool of export entry removal at our disposal, we can devise an architecture in which the proxy DLL contains all exports that are feasible to be present in the original DLL at runtime. Through “reflection” of the original DLL, the proxy shall determine which redundant exports it wishes to hide, resulting it runtime behavior consistent with that of the original DLL (again, assuming only late binding with GetProcAddress is used).

What if we are unable to construct a superset of all possible exports from the original DLL? Perhaps we wish to be future-proof as new, unknown exports are added to the original DLL. For that a variety of solutions may be considered. In particular, copying the original DLL’s export table to our own and inserting hooks as needed comes to mind. Perhaps a topic for a future post…