Introduction
In this article, I will demonstrate how to increase the security at the Windows login screen by creating a password filter. This allows an organization to have more strict requirements about minimum password complexity. Although someone could also create additional functionality, such as a GUI interface to set up specifics of what the minimum complexity should be, I will mostly focus on the very basics needed to get the filter working.
Background
To get an idea of where this information can be found in its purest form, you can look in MSDN Library | Security| Security (General) | Management | Using Management | Using Password Filters. Also, sample programs are installed in the following directory of VS.NET 2003 (student version): Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\samples\Security\NetProviders. Both of these have information that is needed to get a project to work, but I have seen that neither of these are sufficient information by themselves, and need to be merged.
Using the Code
To begin with, we are going to make a DLL that exports certain functions that the operating system will call to activate. Also, we will insert entries into the registry that will let the OS know what password filter we are using. And finally, we will have to to activate the password filter using the Local Security Policies MMC plug-in. So, let's begin with creating the DLL first.
Building the DLL
- The first thing you will need to do is to create a new Visual C++ project: select Win32 folder, click Win32 Project, and enter a project name.
- In the next screen, you will need to select Application Settings and select DLL project. If you want to export symbols, that is optional. It can make stub functions if you have never done this, it can help you understand what DLLs really do.
- Now specifically, there are at least three functions that are a minimum set of functions that must be exported. This is done by making a .def file and the minimum set of exported functions for the password filter:
- LIBRARY
LoginFilter
EXPORTS
NPGetCaps
NPLogonNotify
NPPasswordChangeNotify
- Now that the compiler knows what needs to be exported, it's time to look at the function signatures. The following header files need to be included for compilation:
#include <Npapi.h>
#include <Ntsecapi.h>
WORD WINAPI NPGetCaps ( DWORD nIndex )
DWORD WINAPI NPLogonNotify ( PLUID lpLogonId, LPCWSTR lpAuthentInfoType, LPVOID lpAuthentInfo, LPCWSTR lpPreviousAuthentInfoType, LPVOID lpPreviousAuthentInfo, LPWSTR lpStationName, LPVOID StationHandle, LPWSTR *lpLogonScript )
DWORD WINAPI NPPasswordChangeNotify ( LPCWSTR lpAuthentInfoType, LPVOID lpAuthentInfo, LPCWSTR lpPreviousAuthentInfoType, LPVOID lpPreviousAuthentInfo, LPWSTR lpStationName, LPVOID StationHandle, DWORD dwChangeInfo )
These by themselves do nothing without any implementation. Since the goal of this is to obtain the user's password to see if it meets a certain standard, we need to get the password from the parameters that are coming into these functions. In this particular example, the user name and password are not checked with any pattern. This example just shows where the information is stored.
#include "stdafx.h"
#include <Npapi.h>
#include <Ntsecapi.h>
#define MSV1_0_AUTH_TYPE L"MSV1_0:Interactive"
#define KERBEROS_TYPE L"Kerberos:Interactive"
#define LOGFILE TEXT("C:\\Login.txt")
BOOL WriteLogFile(LPTSTR String);
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call, LPVOID lpReserved )
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls((HMODULE)hModule);
}
return TRUE;
}
DWORD WINAPI NPGetCaps( DWORD nIndex )
{
DWORD dwRes;
switch (nIndex)
{
case WNNC_NET_TYPE:
dwRes = WNNC_CRED_MANAGER;
break;
case WNNC_SPEC_VERSION:
dwRes = WNNC_SPEC_VERSION51;
break;
case WNNC_DRIVER_VERSION:
dwRes = 1;
break;
case WNNC_START:
dwRes = 1;
break;
default:
dwRes = 0;
break;
}
return dwRes;
}
DWORD WINAPI NPLogonNotify ( PLUID lpLogonId,
LPCWSTR lpAuthentInfoType, LPVOID lpAuthentInfo,
LPCWSTR lpPreviousAuthentInfoType,
LPVOID lpPreviousAuthentInfo,
LPWSTR lpStationName, LPVOID StationHandle,
LPWSTR *lpLogonScript )
{
PMSV1_0_INTERACTIVE_LOGON pAuthInfo;
TCHAR szBuf[1024];
char *FormateInfo = "StationName=%lS DomainName"
" = %lS UserName=%lS Password=%lS\r\n";
if ( lstrcmpiW (MSV1_0_AUTH_TYPE, lpAuthentInfoType) )
{
SetLastError(NO_ERROR);
return NO_ERROR;
}
pAuthInfo = (PMSV1_0_INTERACTIVE_LOGON) lpAuthentInfo;
if(pAuthInfo->LogonDomainName.Length>0)
{
if(pAuthInfo->Password.Length>0)
{
if(pAuthInfo->UserName.Length>0)
{
wsprintf(szBuf, FormateInfo, lpStationName,
pAuthInfo->LogonDomainName.Buffer,
pAuthInfo->UserName.Buffer,
pAuthInfo->Password.Buffer);
MessageBox(NULL, szBuf,"Login Info",MB_OK);
WriteLogFile(szBuf);
}
else
MessageBox(NULL,"No Username","",MB_OK);
}
else
MessageBox(NULL,"No Password","",MB_OK);
}
else
MessageBox(NULL,"No domain Name","",MB_OK);
*lpLogonScript = (LPWSTR)LocalAlloc(LPTR,1024);
wsprintf(*lpLogonScript,L"notepad %s",LOGFILE);
return NO_ERROR;
}
DWORD WINAPI NPPasswordChangeNotify ( LPCWSTR lpAuthentInfoType,
LPVOID lpAuthentInfo, LPCWSTR lpPreviousAuthentInfoType,
LPVOID lpPreviousAuthentInfo, LPWSTR lpStationName,
LPVOID StationHandle, DWORD dwChangeInfo )
{
return NO_ERROR;
}
BOOL WriteLogFile(LPTSTR String)
{
HANDLE hFile;
DWORD dwBytesWritten;
hFile = CreateFile( LOGFILE, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN, NULL );
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
SetFilePointer(hFile, 0, NULL, FILE_END);
WriteFile( hFile, String, lstrlen(String)*sizeof(TCHAR),
&dwBytesWritten, NULL );
CloseHandle(hFile);
return TRUE;
}
- Now that the function has some very primitive implementation, the DLL can be built.
- (Side note) Once built, a DLL typically can't run by itself, it will need the login screen to actually run the code. This is problematic for debugging since certain fatal errors may prevent you from logging on to your own station. If you are unsure about the code you write, try testing it out in a regular executable to keep from crashing at login.
- You should also place the DLL in the C:\Windows\System32 folder. This is in accordance with the Microsoft documentation.
- You can also add two more function signatures to your
EXPORTS
if you want to make the DLL completely contained. This will be explained in the next step as well. STDAPI DllRegisterServer(void)
and STDAPI DllUnregisterServer(void)
can be implemented to add the necessary registry entries.
Adding to the Registry
OK, now that the DLL is made, it is time to add the registry entries in the right spots. This is where the documentation provided in MSDN was fragmented.
- In the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\, append a line to Notification Packages with the name of your DLL minus the extension. So, for example: PasswordFilter.dll becomes PasswordFilter. Do not overwrite the entries currently there!!!! The entries are all on separate lines and are of the type
REG_MULTI_SZ
.
- In HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\NetworkProvider, append the name of your DLL minus the extension in ProviderOrder. These entries are separated by a comma. This entry is of the type
REG_SZ
.
- Now, create a new key in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\. So, it looks like this: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LoginFilter.
- In this new key, add a subkey called NetworkProvider. It should look like this: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\ Services\LoginFilter\NetworkProvider.
- In this subkey, add the following registry names and types:
Class | REG_DWORD | 2 |
Name | REG_SZ | Login Filter |
ProviderPath | REG_EXPAND_SZ | %SystemRoot%\system32\LoginFilter.dll |
- Wow, what a mess! It seems like a lot to be typed in, so this is where
DllRegisterServer
and DllUnregisterServer
come in. Adding implementation to these can package all the information that is needed about your DLL in the DLL. Using registry functions, you can manipulate the registry automatically. Once you have implemented those two functions, you would use Regsvr32.exe to register the password filter.
Starting the Password Filter
- Assuming that the compiled DLL is in the C:\WINDOWS\SYSTEM32 directory and all the registry entries have been entered correctly, the only thing left to do is to turn on the service that activates it.
- Go to Start -> Control Panel -> Performance and Maintenance (Category View) -> Administrator Tools -> Local Security Policies.
- From the root, Security Settings | Account Polices | Password Polices. Double click on the Password must meet complexity requirements.
- Enable this setting.
- Log out, and the password filter policy will take effect.
Stopping the Password Filter
- Go to Start -> Control Panel -> Performance and Maintenance (Category View)-> Administrator Tools ->Local Security Policies.
- From the root, Security Settings | Account Polices| Password Polices. Double click on the Password must meet complexity requirements.
- Disable this setting.
- Reboot.
- Delete DLL out of C:\WINDOWS\SYSTEM32. The password filter may still be active after all this, I believe it may be cached.
Points of Interest
One landmine I discovered is that wsprintf
has TEMPLATE escape characters. If UNICODE is defined, it assumes that the incoming parameter is UNICODE; if it is not, it assumes it to be ANSI. Also, wsprintf
has escape characters to force the modes, so just keep in mind what mode you want. Since the buffers in the data structure (pAuthInfo->UserName.Buffer
) are UNICODE based, if UNICODE is not defined, ordinary string functions will not work. Normal string functions will presume UNICODE characters as being empty strings. Be sure to use UNICODE string functions to solve this. Although I took out most of the code that actually filters the password, this was mainly done to focus on the interface and not the details of the filter.
Since you are working with the security in this way, you should use any best practice known to keep passwords secure. Even consider using obfuscation techniques to prevent attackers from learning too much about your algorithms.
I can not emphasis enough how this could be misused. Once misused, this becomes a password grabber.
While testing, I noticed that disabling the password filter from the Local Security Polices MMC plug-in wasn't enough. It still remains active even after reboot. I have deleted the filter DLL after the setting was disabled, and rebooted to keep the login filter from being active afterwards. Not sure why this happens yet; if anyone finds out why I, am curious to know.
History
This is the first release. This is no more then a template program. More work will be done on self registration, defining settings, and filtering, later.