WinXP 中的登录密码过滤器
一篇关于如何在 WinXP 上构建登录密码过滤器的文章。
引言
在本文中,我将演示如何通过创建密码过滤器来增强 Windows 登录屏幕的安全性。这允许组织对密码复杂度有更严格的要求。虽然也可以创建额外的功能,例如用于设置最小复杂度要求的 GUI 界面,但我主要关注的是使过滤器正常工作所需的最基本内容。
背景
要了解这些信息的原始来源,您可以查阅 MSDN Library | Security | Security (General) | Management | Using Management | Using Password Filters。此外,VS.NET 2003(学生版)的以下目录中还安装了示例程序:Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\samples\Security\NetProviders。这两处都提供了让项目工作的必要信息,但我发现它们单独都不够,需要合并使用。
Using the Code
首先,我们将创建一个 DLL,它导出操作系统将调用以激活的特定函数。此外,我们将在注册表中插入条目,让操作系统知道我们正在使用哪个密码过滤器。最后,我们必须使用“本地安全策略”MMC 管理单元来激活密码过滤器。那么,让我们先从创建 DLL 开始。
构建 DLL
- 您需要做的第一件事是创建一个新的 Visual C++ 项目:选择 Win32 文件夹,单击 Win32 Project,然后输入项目名称。
- 在下一个屏幕中,您需要选择 Application Settings 并选择 DLL project。如果您想导出符号,这是可选的。如果您以前从未做过,它可以生成存根函数,帮助您理解 DLL 的实际作用。
- 现在具体来说,至少需要导出三个函数作为 最低 要求。这是通过创建一个 .def 文件来实现的,密码过滤器的最低导出函数集为:
- LIBRARY
LoginFilter
EXPORTS
NPGetCaps
NPLogonNotify
NPPasswordChangeNotify
- LIBRARY
- 既然编译器知道需要导出什么,现在就可以查看函数签名了。编译需要包含以下头文件:
#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 )
这些函数本身什么也不做,没有实现。由于此目的在于获取用户的密码以检查其是否符合特定标准,因此我们需要从传入这些函数的参数中获取密码。在此特定示例中,用户名和密码 不会 与任何模式进行匹配。此示例仅展示了信息存储的位置。
// LoginFilter.cpp : Defines the entry point for the DLL application. // #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; // credential manager break; case WNNC_SPEC_VERSION: // We are using version 5.1 of the spec. dwRes = WNNC_SPEC_VERSION51; break; case WNNC_DRIVER_VERSION: dwRes = 1; // This driver is version 1. break; case WNNC_START: dwRes = 1; // We are already "started" break; default: dwRes = 0; // We don't support anything else 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]; //Be careful of the TEMPLATE escape sequences, //in this case I used = %lS to force UNICODE //otherwise it would have to have been defined. char *FormateInfo = "StationName=%lS DomainName" " = %lS UserName=%lS Password=%lS\r\n"; // // If the primary authenticator is not MSV1_0, return success. // Why? Because this is the only auth info structure that we // understand and we don't want to interact with other types. // if ( lstrcmpiW (MSV1_0_AUTH_TYPE, lpAuthentInfoType) ) { //Any sort of file IO can take place here but mostly just to //let the user know that we are not //intrested in this data stucter type. SetLastError(NO_ERROR); return NO_ERROR; } // // Do something with the authentication information // This is the data structure we really need! // The information is stored as UNICODE strings. //pAuthInfo->LogonDomainName.Buffer //pAuthInfo->Password.Buffer //pAuthInfo->UserName.Buffer 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); // Let's utilize the logon script capability to display // our logon information // // The Caller MUST free this memory *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 ) { //Same information about parameters are found NPLogonNotify 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; // // Seek to the end of the file // SetFilePointer(hFile, 0, NULL, FILE_END); WriteFile( hFile, String, lstrlen(String)*sizeof(TCHAR), &dwBytesWritten, NULL ); CloseHandle(hFile); return TRUE; }
- 现在函数有了非常基本的实现,就可以构建 DLL 了。
- (题外话)构建完成后,DLL 本身通常无法运行,需要登录屏幕才能实际运行代码。这对于调试来说很麻烦,因为某些致命错误可能会导致您无法登录自己的工作站。如果您不确定所写的代码,请尝试在普通可执行文件中进行测试,以避免在登录时崩溃。
- 您还应该将 DLL 放置在 C:\Windows\System32 文件夹中。这符合 Microsoft 的文档。
- 如果您想让 DLL 完全自给自足,还可以向您的
EXPORTS
添加另外两个函数签名。这将在下一步中进行解释。可以实现 STDAPIDllRegisterServer(void)
和 STDAPIDllUnregisterServer(void)
来添加必要的注册表项。
添加到注册表
好的,现在 DLL 已经 তৈরি 好了,是时候将注册表项添加到正确的位置了。MSDN 提供的文档在这里是零散的。
- 在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ 中,向 Notification Packages 追加 一行,值为您的 DLL 名称(不含扩展名)。例如:PasswordFilter.dll 变成 PasswordFilter。不要 覆盖当前存在的条目!!!!条目都在单独的行上,类型为
REG_MULTI_SZ
。 - 在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\NetworkProvider 中,向 ProviderOrder 追加 您的 DLL 名称(不含扩展名)。这些条目以逗号分隔。此条目的类型为
REG_SZ
。 - 现在,在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ 下创建一个新项。例如:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LoginFilter。
- 在此新项下,添加一个名为 NetworkProvider 的子项。应如下所示:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\ Services\LoginFilter\NetworkProvider。
- 在此子项中,添加以下注册表名称和类型:
类 REG_DWORD
2 名称 REG_SZ
Login Filter ProviderPath REG_EXPAND_SZ
%SystemRoot%\system32\LoginFilter.dll - 哇,真够麻烦的!看起来需要输入很多内容,这就是
DllRegisterServer
和DllUnregisterServer
的用武之地。为这些函数添加实现可以将有关 DLL 的所有必要信息打包到 DLL 中。使用注册表函数,您可以自动操作注册表。实现这两个函数后,您可以使用 Regsvr32.exe 来注册密码过滤器。
启动密码过滤器
- 假设编译好的 DLL 位于 C:\WINDOWS\SYSTEM32 目录中,并且所有注册表项都已正确输入,那么唯一剩下的就是启用激活它的服务。
- 转到 开始 -> 控制面板 -> 性能和维护(类别视图) -> 管理工具 -> 本地安全策略。
- 在根目录中,选择 安全设置 | 账户策略 | 密码策略。双击 密码必须符合复杂度要求。
- 启用 此设置。
- 注销,密码过滤器策略将生效。
停止密码过滤器
- 转到 开始 -> 控制面板 -> 性能和维护(类别视图) -> 管理工具 -> 本地安全策略。
- 在根目录中,选择 安全设置 | 账户策略 | 密码策略。双击 密码必须符合复杂度要求。
- 禁用 此设置。
- 重启.
- 删除 C:\WINDOWS\SYSTEM32 中的 DLL。密码过滤器可能仍然处于活动状态,我认为它可能被缓存了。
关注点
我发现的一个陷阱是 wsprintf
具有 TEMPLATE 转义字符。如果定义了 UNICODE,它会假定传入的参数是 UNICODE;如果未定义,它会假定为 ANSI。此外,wsprintf
具有强制模式的转义字符,所以请注意您想要的模式。由于数据结构(pAuthInfo->UserName.Buffer
)中的缓冲区是基于 UNICODE 的,如果未定义 UNICODE,普通的字符串函数将无法正常工作。普通字符串函数会假定 UNICODE 字符为空字符串。务必使用 UNICODE 字符串函数来解决此问题。尽管我删除了大部分实际过滤密码的代码,但这样做主要是为了关注接口而不是过滤器的细节。
既然您正在以这种方式处理安全性,那么您应该遵循任何已知的最佳实践来保护密码安全。甚至可以考虑使用混淆技术来防止攻击者过多地了解您的算法。
我怎么强调都不为过,这可能会被滥用。 一旦被滥用,它就会变成一个密码窃取器。
在测试过程中,我注意到从“本地安全策略”MMC 管理单元禁用密码过滤器还不够。即使在重新启动后,它仍然保持活动状态。在禁用该设置并重新启动后,我删除了过滤器 DLL,以防止登录过滤器之后处于活动状态。我还不确定为什么会这样;如果有人知道原因,我很想知道。
历史
这是第一个版本。这不过是一个模板程序。稍后将对自注册、定义设置和过滤进行更多工作。