作者:Loong716@Amulab
0x00 前言
Mimikatz是法國安全研究員Benjamin Delpy開發的一款安全工具。ST測試人員對mimikatz印象最深的肯定就是抓取Windows憑證,但作者對它的描述是“a tool I’ve made to learn C and make somes experiments with Windows security.”,其實它的功能不僅僅是抓取憑證,還包含了很多Windows安全相關的技術和知識
這裡借用@daiker師傅的思維導圖,mimikatz的子產品大緻可分為幾個部分:

是以文章也會大緻分為windows 通路控制模型,windows 憑據以及加解密,windows AD 安全,windows 程序以及服務,mimikatz 其他子產品五個小系列。之前自己一直想分析mimikatz的相關功能,主要是出于以下原因:
- mimikatz中有許多功能利用了Windows的一些機制和特性,以changentlm為例,其利用MS-SAMR協定修改使用者的密碼,我們再根據MS-SAMR或RPC進行知識延伸,肯定也有不少收獲
- mimikatz中涉及大量記憶體的操作,其中運用的記憶體Patch技術也被經常應用于一些安全機制的繞過(如繞過AMSI、Credential Guard等),于是自己想在分析過程中通過windbg學到一些調試的技巧
- mimikatz在實戰中被殺的很厲害,了解相應原理可以自己實作相應功能
- 學習/練習C語言 ????
mimikatz中與Windows通路控制模型相關的有privilege、token、sid三個子產品,其分别對應特權、通路令牌、安全辨別符三個知識,本文主要分析token子產品,并簡要介紹Windows通路控制模型
由于mimikatz代碼邏輯較為複雜,涉及大量回調,是以文中代碼都是經過簡化的。文章可能也會有一些技術上或者邏輯上的錯誤,還請師傅們指正
0x01 通路控制模型簡介
Windows通路控制模型有兩個基本組成部分:
- 通路令牌(Access Token):包含有關登入使用者的資訊
- 安全描述符(Security Descriptor):包含用于保護安全對象的安全資訊
1. 通路令牌(Access Token)
通路令牌(Access Token)被用來描述一個程序或線程的安全上下文,使用者每次登入成功後,系統會為其建立通路令牌,該使用者的所有程序也将擁有此通路令牌的副本
當線程與安全對象進行互動或嘗試執行需要特權的系統任務時,系統使用通路令牌來辨別使用者。使用windbg檢視程序的token,其包含資訊如下圖所示:
2. 安全描述符(Security Descriptor)
安全描述符(Security Descriptor)包含與安全對象有關的安全資訊,這些資訊規定了哪些使用者/組可以對這個對象執行哪些操作,安全描述符主要由以下部分構成:
- 所有者的SID
- 組SID
- 自主通路控制清單(DACL),規定哪些使用者/組可以對這個對象執行哪些操作
- 系統通路控制清單(SACL),規定哪些使用者/組的哪些操作将被記錄到安全審計日志中
在windbg中檢視一個安全對象的安全描述符,可以清晰的看到安全描述符的組成:
可以看到該安全描述符的DACL中有三條ACE,ACE的類型都是
ACCESS_ALLOWED_ACE_TYPE
,
Mask
是權限掩碼,用來指定對應的權限。以第一條ACE為例,其表示允許SID為S-1-5-32-544的對象能夠對該安全對象做0x001fffff對應的操作
3. 權限檢查的過程
當某個線程嘗試通路一個安全對象時,系統根據安全對象的ACE對照線程的通路令牌來判斷該線程是否能夠對該安全對象進行通路。通常,系統使用請求通路的線程的主通路令牌。但是,如果線程正在模拟其他使用者,則系統會使用線程的模拟令牌
此時将在該安全對象的DACL中按順序檢查ACE,直到發生以下事件:
- 某一條拒絕類型的ACE顯式拒絕令牌中某個受信者的所有通路權限
- 一條或多條允許類型的ACE允許令牌中列出的受信者的所有通路權限
- 檢查完所有ACE但沒有一個權限顯式允許,那麼系統會隐式拒絕該通路
我們以微軟文檔中的圖檔為例,描述一下整個過程:
- 線程A請求通路安全對象,系統讀取ACE1,發現拒絕Andrew使用者的所有通路權限,而線程A的通路令牌是Andrew,是以拒絕通路,并不再檢查ACE2、ACE3
- 線程A請求通路,系統按順序讀取ACE,ACE1不适用,讀取到ACE2發現适用,再讀取到ACE3也适用,是以最終該使用者擁有對該安全對象的讀、寫、執行權限
0x02 Mimikatz的Token子產品
Mimikatz的token子產品共有5個功能:
- token::whoami:列出目前程序/線程的token資訊
- token::list:列出目前系統中存在的token
- token::elevate:竊取其他使用者的token
- token::run:利用某使用者權限運作指定程式
- token::revert:恢複為原來的token
1. token::whoami
該功能用于列出目前程序/線程的token資訊
隻有一個可選參數
/full
,當指定該參數時會列印出目前token的組資訊和特權資訊:
該功能的原理大緻如下:
- 通過
擷取目前程序/線程的句柄OpenProcess()
- 調用
擷取token的各種資訊并輸出GetTokenInformation()
其核心為調用
GetTokenInformation()
來擷取token的各種資訊,我們先來看這個API定義
BOOL GetTokenInformation(
HANDLE TokenHandle,
TOKEN_INFORMATION_CLASS TokenInformationClass,
LPVOID TokenInformation,
DWORD TokenInformationLength,
PDWORD ReturnLength
);
其中第二個參數是一個
TOKEN_INFORMATION_CLASS
枚舉類型,我們可以通過指定它的值來擷取token指定的資訊
typedef enum _TOKEN_INFORMATION_CLASS {
TokenUser,
TokenGroups,
TokenPrivileges,
TokenOwner,
TokenPrimaryGroup,
TokenDefaultDacl,
TokenSource,
...
} TOKEN_INFORMATION_CLASS, *PTOKEN_INFORMATION_CLASS;
例如擷取token的SessionID并輸出,可以使用以下代碼:
if (!GetTokenInformation(hToken, TokenSessionId, &sessionId, sizeof(TokenSessionId), &dwSize))
{
wprintf(L"[!] GetTokenInformation error: %u\n", GetLastError());
}
wprintf(L"\t%-21s: %u\n", L"Session ID", sessionId);
2. token::list
該功能是擷取目前系統中所有的token,注意使用前需要先擷取
SeDebugPrivilege
,否則列出的token不全
該功能原理大緻如下:
-
擷取系統程序資訊(如程序PID等)NtQuerySystemInformation()
- 循環周遊所有程序的PID,使用
功能中的方法對指定token資訊進行輸出token::whoami
NtQuerySystemInformation()
用來檢索指定的系統資訊:
__kernel_entry NTSTATUS NtQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
其第一個參數是一個
SYSTEM_INFORMATION_CLASS
枚舉類型,我們同樣可以指定不同參數來擷取不同的系統資訊
以擷取系統程序名和PID為例,代碼如下:
PSYSTEM_PROCESS_INFORMATION pProcessInfo = NULL;
DWORD flag = TRUE;
pProcessInfo = (PSYSTEM_PROCESS_INFORMATION)malloc(dwSize);
ntReturn = NtQuerySystemInformation(SystemProcessInformation, pProcessInfo, dwSize, &dwSize);
while (ntReturn == STATUS_INFO_LENGTH_MISMATCH) {
free(pProcessInfo);
pProcessInfo = (PSYSTEM_PROCESS_INFORMATION)malloc(dwSize);
ntReturn = NtQuerySystemInformation(SystemProcessInformation, pProcessInfo, dwSize, &dwSize);
}
while (flag)
{
if (pProcessInfo->NextEntryOffset == 0)
flag = FALSE;
wprintf(L"%-15d", (DWORD)pProcessInfo->UniqueProcessId);
wprintf(L"%-50s", (wchar_t*)pProcessInfo->ImageName.Buffer);
pProcessInfo = (PSYSTEM_PROCESS_INFORMATION)((BYTE*)pProcessInfo + pProcessInfo->NextEntryOffset);
}
PS:按照該思路,理論上利用
CreateToolhelp32Snapshot()
+
Process32First()
周遊程序PID也可以實作該功能
3. token::elevate
該子產品用于竊取指定使用者的token,共有7個可選參數,這些參數主要用來指定要竊取的token,如果不指定參數則預設竊取
NT AUTHORITY\SYSTEM
的token
- /id:指定目标token的TokenID
- /domainadmin:竊取域管的token
- /enterpriseadmin:竊取企業管理者的token
- /admin:竊取本地管理者的token
- /localservice:竊取Local Service權限的token
- /networkservice:竊取Network Service權限的token
- /system:竊取SYSTEM權限的token
假設我們現在在目标機器上發現的域管權限的token
我們可以指定目标TokenID,或者使用
/domainadmin
來竊取域管的token,執行成功後可以看到目前線程已經擁有域管的模拟令牌:
然後我們就可以在目前mimikatz上下文中使用域管身份執行操作了,如DCSync
該功能大緻過程如下:
-
OpenProcess()
-
打開與程序相關的token句柄OpenProcessToken()
- 使用
使用目标程序token建立一個新的模拟tokenDuplicateTokenEx()
-
設定目前線程的token為上一步建立的新的模拟tokenSetThreadToken()
由于竊取token是Access Token利用的重點,該過程放在本文後面分析
4. token::run
該功能是使用指定的token來運作程式,也可以使用
token::elevate
中的幾個參數來指定運作程式的token,除此之外還有一個參數:
- /process:指定要運作的程式,預設值為whoami.exe
其原理前三步與
token::elevate
大緻相同,差別在于使用
DuplicateTokenEx()
竊取token後,該功能使用
CreateProcessAsUser()
來使用新的primary token建立一個程序
BOOL CreateProcessAsUserA(
HANDLE hToken,
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
建立程序後,利用匿名管道做程序間通信,将新建立程序的标準輸出寫入到匿名管道的write端,從管道read端讀取資料進行回顯(在webshell等非互動場景下很有用)
if (CreatePipe(&hStdoutR, &hStdoutW, &saAttr, 0))
{
SetHandleInformation(hStdoutR, HANDLE_FLAG_INHERIT, 0);
si.cb = sizeof(STARTUPINFO);
si.hStdOutput = hStdoutW;
si.hStdError = si.hStdOutput;
si.dwFlags |= STARTF_USESTDHANDLES;
if (CreateProcessWithTokenW(hDupToken, LOGON_WITH_PROFILE, NULL, cmd, CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, NULL, NULL, &si, &pi))
{
CloseHandle(si.hStdOutput);
si.hStdOutput = si.hStdError = NULL;
while (ReadFile(hStdoutR, resultBuf, sizeof(resultBuf), &dwRead, NULL) && dwRead)
{
for (i = 0; i < dwRead; i++)
wprintf(L"%c", resultBuf[i]);
}
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
else wprintf(L"CreateProcessWithTokenW error 0x%08X\n", GetLastError());
}
else wprintf(L"CreatePipe error! 0x%08X\n", GetLastError());
5. token::revert
該子產品用來清除線程的模拟令牌:
原理很簡單,直接使用
SetThreadToken(NULL, NULL)
即可将目前線程的token清除
0x03 令牌竊取
在ST測試中,竊取token是administrator -> system的常見手法之一,還經常被用于降級等使用者切換操作
1. 原理
竊取token主要涉及以下幾個API:
- OpenProcess
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);
該函數打開指定PID的程序的句柄,需要注意的是第一個參數dwDesiredAccess,主要會用到的是以下三個權限
- PROCESS_ALL_ACCESS
- PROCESS_QUERY_INFORMATION (0x0400)
- PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
我在編寫竊取Token的代碼時,發現對部分程序(如smss.exe、csrss.exe等)調用OpenProcess會出現拒絕通路的情況,查閱網上資料後發現這些程序存在保護,需要使用
PROCESS_QUERY_LIMITED_INFORMATION
權限打開句柄,詳情請參考這篇文章
- OpenProcessToken
BOOL OpenProcessToken(
HANDLE ProcessHandle,
DWORD DesiredAccess,
PHANDLE TokenHandle
);
該函數打開與程序相關聯的令牌的句柄,其中第二個參數DesiredAccess同樣用來指定令牌的通路權限,需要以下幾個:
- TOKEN_DUPLICATE:複制令牌需要的權限
- TOKEN_QUERY:查詢令牌需要的權限
如果要調用
DuplicateTokenEx
需要指定TOKEN_DUPLICATE,如果調用
ImpersonatedLoggedOnUser
則需要指定TOKEN_DUPLICATE和TOKEN_QUERY
- DuplicateTokenEx
BOOL DuplicateTokenEx(
HANDLE hExistingToken,
DWORD dwDesiredAccess,
LPSECURITY_ATTRIBUTES lpTokenAttributes,
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
TOKEN_TYPE TokenType,
PHANDLE phNewToken
);
DuplicateTokenEx
用來複制現有的令牌來生成一張新令牌,該函數可以選擇生成主令牌還是模拟令牌
- hExistingToken:指定現有的令牌句柄,可以使用
獲得OpenProcessToken
- dwDesiredAccess:用來指定令牌通路權限,需要指定以下幾個來支援後面調用
:CreateProcessWithToken
- TOKEN_DUPLICATE:需要複制通路令牌
- TOKEN_QUERY:需要查詢通路令牌
- TOKEN_ASSIGN_PRIMARY:将令牌附加到主程序的權限
- TOKEN_ADJUST_DEFAULT:需要更改通路令牌的預設所有者、主要組或 DACL
- TOKEN_ADJUST_SESSIONID:需要調整通路令牌的會話 ID,需要 SE_TCB_NAME 權限
- lpTokenAttributes:指向SECURITY_ATTRIBUTES結構的指針,該 結構指定新令牌的安全描述符并确定子程序是否可以繼承該令牌
- ImpersonationLevel:指定令牌的模拟級别,當進行複制令牌時,主令牌被複制為模拟令牌是始終被允許的,而模拟令牌複制為主令牌則需要模拟級别 >= Impersonate
- TokenType:指定新令牌的類型,是主令牌(Primary Token)還是模拟令牌(Impersonate Token)
- phNewToken:傳回令牌句柄的位址
複制完一張新令牌後,我們就可以利用這張新令牌來運作我們指定的程序了
- CreateProcessWithTokenW
BOOL CreateProcessWithTokenW(
HANDLE hToken,
DWORD dwLogonFlags,
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
該函數建立一個新程序及其主線程,新程序在指定令牌的安全上下文中運作。我們直接指定前面複制出來的新令牌,使用該令牌建立我們指定的程序即可
2. 利用
根據mimikatz的token子產品的原理,簡單實作了一個demo,也有許多token相關的工具如incognito等
當擷取管理者權限後,我們可以列出系統中程序對應的token:
然後竊取指定程序的token來運作我們的程式,如直接運作上線馬
如果想要拿回程式輸出的話,則可以通過管道等程序間通信的方法來回顯輸出
如果拿到一台機器有域管的程序,那麼我們可以直接竊取域管程序的token來進行DCSync *GJ*
0x04 參考連結
https://docs.microsoft.com/
https://github.com/gentilkiwi/mimikatz/