网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
下图简要概述了如何使用基于虚拟化的安全性来隔离 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。
1. 基础知识
1.1 自定义安全包
自定义安全包 API 支持组合开发自定义安全支持提供程序(SSP),后者为客户端/服务器应用程序提供非交互身份验证服务和安全消息交换,以及开发自定义身份验证包,为执行交互式身份验证的应用程序提供服务。这些服务在单个包中合并时称为安全支持提供程序/身份验证包(SSP/AP)。
SSP/AP 中部署的安全包与 LSA 完全集成。使用可用于自定义安全包的 LSA 支持函数,开发人员可以实现高级安全功能,例如令牌创建、 补充凭据支持和直通身份验证。
如果我们自定义安全支持提供程序/身份验证包(SSP/AP),并将其注册到系统,当用户重新进行交互式身份验证时,系统就会同通过我们自定义的 SSP/AP 传递明文凭据,这意味着我们可以提取到明文凭据并将其保存下来。这样便可以绕过 Credential Guard 的保护机制。
SSP/AP 安全包,为了同时执行身份验证包(AP)和安全支持提供程序(SSP),可以作为操作系统的一部分以及作为用户应用程序的一部分执行。这两种执行模式分别称为 LSA 模式和用户模式。这里我们需要的是 LSA 模式。
下面简单介绍一下关于 LSA 模式的初始化。
1.2 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 | 将为经过身份验证的安全主体存储的凭据传递给安全包。 |
1.3 由 SSP/AP 实现的函数
以下函数由我们自定义的安全支持提供程序/身份验证包(SSP/AP)实现,本地安全机构(LSA)通过使用 SSP/AP 的 SpLsaModeInitialize 函数提供的 SECPKG_FUNCTION_TABLE
结构来访问这些函数。
SpInitialize
SpInitialize 函数由本地安全机构(LSA)调用一次,用于执行任何与初始化相关的处理,并提供一个函数指针列表,其中包含安全包调用的 LSA 函数的指针。
函数声明如下:
NTSTATUS Spinitializefn(
[in] ULONG_PTR PackageId,
[in] PSECPKG_PARAMETERS Parameters,
[in] PLSA_SECPKG_FUNCTION_TABLE FunctionTable
);
参数如下:
- • [in] PackageId:LSA 分配给每个安全包的唯一标识符。该值在重新启动系统之前有效。
- • [in] Parameters:指向包含主域和计算机状态信息的
SECPKG_PARAMETERS
结构的指针。 - • [in] FunctionTable:指向可以安全包调用的 LSA 函数的指针列表。
SpShutDown
SpShutDown 函数在卸载安全支持提供程序/身份验证包 (SSP/AP) 之前,由本地安全机构(LSA)调用,用于在卸载 SSP/AP 之前执行所需的任何清理,以便释放资源。
函数声明如下:
NTSTATUS SpShutDown(void);
这个函数没有参数。
SpGetInfo
SpGetInfo 函数提供有关安全包的一般信息,例如其名称和功能描述。客户端调用安全支持提供程序接口(SSPI)的 QuerySecurityPackageInfo 函数时,将调用 SpGetInfo 函数。
函数声明如下:
NTSTATUS Spgetinfofn(
[out] PSecPkgInfo PackageInfo
);
参数如下:
- • [out] PackageInfo:指向由本地安全机构(LSA)分配的 SecPkgInfo 结构的指针,必须由包填充。
SpAcceptCredentials
SpAcceptCredentials 函数由本地安全机构(LSA)调用,以将为经过身份验证的安全主体存储的任何凭据传递给安全包。为 LSA 存储的每组凭据调用一次此函数。
函数声明如下:
NTSTATUS Spacceptcredentialsfn(
[in] SECURITY_LOGON_TYPE LogonType,
[in] PUNICODE_STRING AccountName,
[in] PSECPKG_PRIMARY_CRED PrimaryCredentials,
[in] PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials
);
参数如下:
- • [in] LogonType:指示登录类型的
SECURITY_LOGON_TYPE
值。 - • [in] AccountName:指向存储登录帐户名称的
UNICODE_STRING
结构的指针。 - • [in] PrimaryCredentials:指向包含登录凭据的
SECPKG_PRIMARY_CRED
结构的指针。 - • [in] SupplementalCredentials:指向包含特定于包的补充凭据的
ECPKG_SUPPLEMENTAL_CRED
结构的指针。
2. 编程实现
通过 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",
};


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618636735)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
(img-ywIDlHXv-1715658828047)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618636735)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**