65.9K
CodeProject 正在变化。 阅读更多。
Home

WinXP 中的登录密码过滤器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (14投票s)

2005年11月2日

CPOL

6分钟阅读

viewsIcon

109913

downloadIcon

2600

一篇关于如何在 WinXP 上构建登录密码过滤器的文章。

Sample Image

引言

在本文中,我将演示如何通过创建密码过滤器来增强 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

  1. 您需要做的第一件事是创建一个新的 Visual C++ 项目:选择 Win32 文件夹,单击 Win32 Project,然后输入项目名称。
  2. 在下一个屏幕中,您需要选择 Application Settings 并选择 DLL project。如果您想导出符号,这是可选的。如果您以前从未做过,它可以生成存根函数,帮助您理解 DLL 的实际作用。
  3. 现在具体来说,至少需要导出三个函数作为 最低 要求。这是通过创建一个 .def 文件来实现的,密码过滤器的最低导出函数集为:
    • LIBRARY LoginFilter
    • EXPORTS
    • NPGetCaps
    • NPLogonNotify
    • NPPasswordChangeNotify
  4. 既然编译器知道需要导出什么,现在就可以查看函数签名了。编译需要包含以下头文件:
    • #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;
    }
  5. 现在函数有了非常基本的实现,就可以构建 DLL 了。
    • (题外话)构建完成后,DLL 本身通常无法运行,需要登录屏幕才能实际运行代码。这对于调试来说很麻烦,因为某些致命错误可能会导致您无法登录自己的工作站。如果您不确定所写的代码,请尝试在普通可执行文件中进行测试,以避免在登录时崩溃。
    • 您还应该将 DLL 放置在 C:\Windows\System32 文件夹中。这符合 Microsoft 的文档。
    • 如果您想让 DLL 完全自给自足,还可以向您的 EXPORTS 添加另外两个函数签名。这将在下一步中进行解释。可以实现 STDAPI DllRegisterServer(void) 和 STDAPI DllUnregisterServer(void) 来添加必要的注册表项。

添加到注册表

好的,现在 DLL 已经 তৈরি 好了,是时候将注册表项添加到正确的位置了。MSDN 提供的文档在这里是零散的。

  1. HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ 中,向 Notification Packages 追加 一行,值为您的 DLL 名称(不含扩展名)。例如:PasswordFilter.dll 变成 PasswordFilter不要 覆盖当前存在的条目!!!!条目都在单独的行上,类型为 REG_MULTI_SZ
  2. HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\NetworkProvider 中,向 ProviderOrder 追加 您的 DLL 名称(不含扩展名)。这些条目以逗号分隔。此条目的类型为 REG_SZ
  3. 现在,在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ 下创建一个新项。例如:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LoginFilter
  4. 在此新项下,添加一个名为 NetworkProvider 的子项。应如下所示:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\ Services\LoginFilter\NetworkProvider
  5. 在此子项中,添加以下注册表名称和类型:
    REG_DWORD 2
    名称 REG_SZ Login Filter
    ProviderPath REG_EXPAND_SZ %SystemRoot%\system32\LoginFilter.dll
  6. 哇,真够麻烦的!看起来需要输入很多内容,这就是 DllRegisterServerDllUnregisterServer 的用武之地。为这些函数添加实现可以将有关 DLL 的所有必要信息打包到 DLL 中。使用注册表函数,您可以自动操作注册表。实现这两个函数后,您可以使用 Regsvr32.exe 来注册密码过滤器。

启动密码过滤器

  1. 假设编译好的 DLL 位于 C:\WINDOWS\SYSTEM32 目录中,并且所有注册表项都已正确输入,那么唯一剩下的就是启用激活它的服务。
  2. 转到 开始 -> 控制面板 -> 性能和维护(类别视图) -> 管理工具 -> 本地安全策略
  3. 在根目录中,选择 安全设置 | 账户策略 | 密码策略。双击 密码必须符合复杂度要求
  4. 启用 此设置。
  5. 注销,密码过滤器策略将生效。

停止密码过滤器

  1. 转到 开始 -> 控制面板 -> 性能和维护(类别视图) -> 管理工具 -> 本地安全策略
  2. 在根目录中,选择 安全设置 | 账户策略 | 密码策略。双击 密码必须符合复杂度要求
  3. 禁用 此设置。
  4. 重启.
  5. 删除 C:\WINDOWS\SYSTEM32 中的 DLL。密码过滤器可能仍然处于活动状态,我认为它可能被缓存了。

关注点

我发现的一个陷阱是 wsprintf 具有 TEMPLATE 转义字符。如果定义了 UNICODE,它会假定传入的参数是 UNICODE;如果未定义,它会假定为 ANSI。此外,wsprintf 具有强制模式的转义字符,所以请注意您想要的模式。由于数据结构(pAuthInfo->UserName.Buffer)中的缓冲区是基于 UNICODE 的,如果未定义 UNICODE,普通的字符串函数将无法正常工作。普通字符串函数会假定 UNICODE 字符为空字符串。务必使用 UNICODE 字符串函数来解决此问题。尽管我删除了大部分实际过滤密码的代码,但这样做主要是为了关注接口而不是过滤器的细节。

既然您正在以这种方式处理安全性,那么您应该遵循任何已知的最佳实践来保护密码安全。甚至可以考虑使用混淆技术来防止攻击者过多地了解您的算法。

我怎么强调都不为过,这可能会被滥用。 一旦被滥用,它就会变成一个密码窃取器。

在测试过程中,我注意到从“本地安全策略”MMC 管理单元禁用密码过滤器还不够。即使在重新启动后,它仍然保持活动状态。在禁用该设置并重新启动后,我删除了过滤器 DLL,以防止登录过滤器之后处于活动状态。我还不确定为什么会这样;如果有人知道原因,我很想知道。

历史

这是第一个版本。这不过是一个模板程序。稍后将对自注册、定义设置和过滤进行更多工作。

© . All rights reserved.