從mimikatz學(xué)習(xí)Windows安全之訪問(wèn)控制模型(一)
作者:Loong716@Amulab
0x00 前言
Mimikatz是法國(guó)安全研究員Benjamin Delpy開(kāi)發(fā)的一款安全工具。滲透測(cè)試人員對(duì)mimikatz印象最深的肯定就是抓取Windows憑證,但作者對(duì)它的描述是“a tool I’ve made to learn C and make somes experiments with Windows security.”,其實(shí)它的功能不僅僅是抓取憑證,還包含了很多Windows安全相關(guān)的技術(shù)和知識(shí)
這里借用@daiker師傅的思維導(dǎo)圖,mimikatz的模塊大致可分為幾個(gè)部分:

因此文章也會(huì)大致分為windows 訪問(wèn)控制模型,windows 憑據(jù)以及加解密,windows AD 安全,windows 進(jìn)程以及服務(wù),mimikatz 其他模塊五個(gè)小系列。之前自己一直想分析mimikatz的相關(guān)功能,主要是出于以下原因:
mimikatz中有許多功能利用了Windows的一些機(jī)制和特性,以changentlm為例,其利用MS-SAMR協(xié)議修改用戶的密碼,我們?cè)俑鶕?jù)MS-SAMR或RPC進(jìn)行知識(shí)延伸,肯定也有不少收獲
mimikatz中涉及大量?jī)?nèi)存的操作,其中運(yùn)用的內(nèi)存Patch技術(shù)也被經(jīng)常應(yīng)用于一些安全機(jī)制的繞過(guò)(如繞過(guò)AMSI、Credential Guard等),于是自己想在分析過(guò)程中通過(guò)windbg學(xué)到一些調(diào)試的技巧
mimikatz在實(shí)戰(zhàn)中被殺的很厲害,了解相應(yīng)原理可以自己實(shí)現(xiàn)相應(yīng)功能
學(xué)習(xí)/練習(xí)C語(yǔ)言 ??
mimikatz中與Windows訪問(wèn)控制模型相關(guān)的有privilege、token、sid三個(gè)模塊,其分別對(duì)應(yīng)特權(quán)、訪問(wèn)令牌、安全標(biāo)識(shí)符三個(gè)知識(shí),本文主要分析token模塊,并簡(jiǎn)要介紹Windows訪問(wèn)控制模型
由于mimikatz代碼邏輯較為復(fù)雜,涉及大量回調(diào),因此文中代碼都是經(jīng)過(guò)簡(jiǎn)化的。文章可能也會(huì)有一些技術(shù)上或者邏輯上的錯(cuò)誤,還請(qǐng)師傅們指正
?
0x01 訪問(wèn)控制模型簡(jiǎn)介
Windows訪問(wèn)控制模型有兩個(gè)基本組成部分:
訪問(wèn)令牌(Access Token):包含有關(guān)登錄用戶的信息
安全描述符(Security Descriptor):包含用于保護(hù)安全對(duì)象的安全信息
1. 訪問(wèn)令牌(Access Token)
訪問(wèn)令牌(Access Token)被用來(lái)描述一個(gè)進(jìn)程或線程的安全上下文,用戶每次登錄成功后,系統(tǒng)會(huì)為其創(chuàng)建訪問(wèn)令牌,該用戶的所有進(jìn)程也將擁有此訪問(wèn)令牌的副本
當(dāng)線程與安全對(duì)象進(jìn)行交互或嘗試執(zhí)行需要特權(quán)的系統(tǒng)任務(wù)時(shí),系統(tǒng)使用訪問(wèn)令牌來(lái)標(biāo)識(shí)用戶。使用windbg查看進(jìn)程的token,其包含信息如下圖所示:



2. 安全描述符(Security Descriptor)
安全描述符(Security Descriptor)包含與安全對(duì)象有關(guān)的安全信息,這些信息規(guī)定了哪些用戶/組可以對(duì)這個(gè)對(duì)象執(zhí)行哪些操作,安全描述符主要由以下部分構(gòu)成:
所有者的SID
組SID
自主訪問(wèn)控制列表(DACL),規(guī)定哪些用戶/組可以對(duì)這個(gè)對(duì)象執(zhí)行哪些操作
系統(tǒng)訪問(wèn)控制列表(SACL),規(guī)定哪些用戶/組的哪些操作將被記錄到安全審計(jì)日志中
在windbg中查看一個(gè)安全對(duì)象的安全描述符,可以清晰的看到安全描述符的組成:

可以看到該安全描述符的DACL中有三條ACE,ACE的類型都是ACCESS_ALLOWED_ACE_TYPE
,Mask
是權(quán)限掩碼,用來(lái)指定對(duì)應(yīng)的權(quán)限。以第一條ACE為例,其表示允許SID為S-1-5-32-544的對(duì)象能夠?qū)υ摪踩珜?duì)象做0x001fffff對(duì)應(yīng)的操作
3. 權(quán)限檢查的過(guò)程
當(dāng)某個(gè)線程嘗試訪問(wèn)一個(gè)安全對(duì)象時(shí),系統(tǒng)根據(jù)安全對(duì)象的ACE對(duì)照線程的訪問(wèn)令牌來(lái)判斷該線程是否能夠?qū)υ摪踩珜?duì)象進(jìn)行訪問(wèn)。通常,系統(tǒng)使用請(qǐng)求訪問(wèn)的線程的主訪問(wèn)令牌。但是,如果線程正在模擬其他用戶,則系統(tǒng)會(huì)使用線程的模擬令牌
此時(shí)將在該安全對(duì)象的DACL中按順序檢查ACE,直到發(fā)生以下事件:
某一條拒絕類型的ACE顯式拒絕令牌中某個(gè)受信者的所有訪問(wèn)權(quán)限
一條或多條允許類型的ACE允許令牌中列出的受信者的所有訪問(wèn)權(quán)限
檢查完所有ACE但沒(méi)有一個(gè)權(quán)限顯式允許,那么系統(tǒng)會(huì)隱式拒絕該訪問(wèn)
我們以微軟文檔中的圖片為例,描述一下整個(gè)過(guò)程:

線程A請(qǐng)求訪問(wèn)安全對(duì)象,系統(tǒng)讀取ACE1,發(fā)現(xiàn)拒絕Andrew用戶的所有訪問(wèn)權(quán)限,而線程A的訪問(wèn)令牌是Andrew,因此拒絕訪問(wèn),并不再檢查ACE2、ACE3
線程A請(qǐng)求訪問(wèn),系統(tǒng)按順序讀取ACE,ACE1不適用,讀取到ACE2發(fā)現(xiàn)適用,再讀取到ACE3也適用,因此最終該用戶擁有對(duì)該安全對(duì)象的讀、寫(xiě)、執(zhí)行權(quán)限
?
0x02 Mimikatz的Token模塊
Mimikatz的token模塊共有5個(gè)功能:
token::whoami:列出當(dāng)前進(jìn)程/線程的token信息
token::list:列出當(dāng)前系統(tǒng)中存在的token
token::elevate:竊取其他用戶的token
token::run:利用某用戶權(quán)限運(yùn)行指定程序
token::revert:恢復(fù)為原來(lái)的token
1. token::whoami
該功能用于列出當(dāng)前進(jìn)程/線程的token信息

只有一個(gè)可選參數(shù)/full
,當(dāng)指定該參數(shù)時(shí)會(huì)打印出當(dāng)前token的組信息和特權(quán)信息:

該功能的原理大致如下:
通過(guò)
OpenProcess()
獲取當(dāng)前進(jìn)程/線程的句柄調(diào)用
GetTokenInformation()
獲取token的各種信息并輸出
其核心為調(diào)用GetTokenInformation()
來(lái)獲取token的各種信息,我們先來(lái)看這個(gè)API定義
BOOL GetTokenInformation(
?HANDLE ? ? ? ? ? ? ? ? ?TokenHandle,
?TOKEN_INFORMATION_CLASS TokenInformationClass,
?LPVOID ? ? ? ? ? ? ? ? ?TokenInformation,
?DWORD ? ? ? ? ? ? ? ? ? TokenInformationLength,
?PDWORD ? ? ? ? ? ? ? ? ?ReturnLength
);
其中第二個(gè)參數(shù)是一個(gè)TOKEN_INFORMATION_CLASS
枚舉類型,我們可以通過(guò)指定它的值來(lái)獲取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
該功能是獲取當(dāng)前系統(tǒng)中所有的token,注意使用前需要先獲取SeDebugPrivilege
,否則列出的token不全

該功能原理大致如下:
調(diào)用
NtQuerySystemInformation()
獲取系統(tǒng)進(jìn)程信息(如進(jìn)程PID等)循環(huán)遍歷所有進(jìn)程的PID,使用
token::whoami
功能中的方法對(duì)指定token信息進(jìn)行輸出
NtQuerySystemInformation()
用來(lái)檢索指定的系統(tǒng)信息:
__kernel_entry NTSTATUS NtQuerySystemInformation(
?SYSTEM_INFORMATION_CLASS SystemInformationClass,
?PVOID ? ? ? ? ? ? ? ? ? ?SystemInformation,
?ULONG ? ? ? ? ? ? ? ? ? ?SystemInformationLength,
?PULONG ? ? ? ? ? ? ? ? ? ReturnLength
);
其第一個(gè)參數(shù)是一個(gè)SYSTEM_INFORMATION_CLASS
枚舉類型,我們同樣可以指定不同參數(shù)來(lái)獲取不同的系統(tǒng)信息

以獲取系統(tǒng)進(jìn)程名和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()
遍歷進(jìn)程PID也可以實(shí)現(xiàn)該功能
3. token::elevate
該模塊用于竊取指定用戶的token,共有7個(gè)可選參數(shù),這些參數(shù)主要用來(lái)指定要竊取的token,如果不指定參數(shù)則默認(rèn)竊取NT AUTHORITY\SYSTEM
的token
/id:指定目標(biāo)token的TokenID
/domainadmin:竊取域管的token
/enterpriseadmin:竊取企業(yè)管理員的token
/admin:竊取本地管理員的token
/localservice:竊取Local Service權(quán)限的token
/networkservice:竊取Network Service權(quán)限的token
/system:竊取SYSTEM權(quán)限的token
假設(shè)我們現(xiàn)在在目標(biāo)機(jī)器上發(fā)現(xiàn)的域管權(quán)限的token

我們可以指定目標(biāo)TokenID,或者使用/domainadmin
來(lái)竊取域管的token,執(zhí)行成功后可以看到當(dāng)前線程已經(jīng)擁有域管的模擬令牌:

然后我們就可以在當(dāng)前mimikatz上下文中使用域管身份執(zhí)行操作了,如DCSync

該功能大致過(guò)程如下:
通過(guò)
OpenProcess()
獲取當(dāng)前進(jìn)程/線程的句柄調(diào)用
OpenProcessToken()
打開(kāi)與進(jìn)程相關(guān)的token句柄使用
DuplicateTokenEx()
使用目標(biāo)進(jìn)程token創(chuàng)建一個(gè)新的模擬token調(diào)用
SetThreadToken()
設(shè)置當(dāng)前線程的token為上一步創(chuàng)建的新的模擬token
由于竊取token是Access Token利用的重點(diǎn),該過(guò)程放在本文后面分析
4. token::run
該功能是使用指定的token來(lái)運(yùn)行程序,也可以使用token::elevate
中的幾個(gè)參數(shù)來(lái)指定運(yùn)行程序的token,除此之外還有一個(gè)參數(shù):
/process:指定要運(yùn)行的程序,默認(rèn)值為whoami.exe

其原理前三步與token::elevate
大致相同,區(qū)別在于使用DuplicateTokenEx()
竊取token后,該功能使用CreateProcessAsUser()
來(lái)使用新的primary token創(chuàng)建一個(gè)進(jìn)程
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
);
創(chuàng)建進(jìn)程后,利用匿名管道做進(jìn)程間通信,將新創(chuàng)建進(jìn)程的標(biāo)準(zhǔn)輸出寫(xiě)入到匿名管道的write端,從管道read端讀取數(shù)據(jù)進(jìn)行回顯(在webshell等非交互場(chǎng)景下很有用)
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
該模塊用來(lái)清除線程的模擬令牌:

原理很簡(jiǎn)單,直接使用SetThreadToken(NULL, NULL)
即可將當(dāng)前線程的token清除
?
0x03 令牌竊取
在滲透測(cè)試中,竊取token是administrator -> system的常見(jiàn)手法之一,還經(jīng)常被用于降權(quán)等用戶切換操作
1. 原理
竊取token主要涉及以下幾個(gè)API:
OpenProcess
HANDLE OpenProcess(
?DWORD dwDesiredAccess,
?BOOL ?bInheritHandle,
?DWORD dwProcessId
);
該函數(shù)打開(kāi)指定PID的進(jìn)程的句柄,需要注意的是第一個(gè)參數(shù)dwDesiredAccess,主要會(huì)用到的是以下三個(gè)權(quán)限
PROCESS_ALL_ACCESS
PROCESS_QUERY_INFORMATION (0x0400)
PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
我在編寫(xiě)竊取Token的代碼時(shí),發(fā)現(xiàn)對(duì)部分進(jìn)程(如smss.exe、csrss.exe等)調(diào)用OpenProcess會(huì)出現(xiàn)拒絕訪問(wèn)的情況,查閱網(wǎng)上資料后發(fā)現(xiàn)這些進(jìn)程存在保護(hù),需要使用PROCESS_QUERY_LIMITED_INFORMATION
權(quán)限打開(kāi)句柄,詳情請(qǐng)參考這篇文章
OpenProcessToken
BOOL OpenProcessToken(
?HANDLE ?ProcessHandle,
?DWORD ? DesiredAccess,
?PHANDLE TokenHandle
);
該函數(shù)打開(kāi)與進(jìn)程相關(guān)聯(lián)的令牌的句柄,其中第二個(gè)參數(shù)DesiredAccess同樣用來(lái)指定令牌的訪問(wèn)權(quán)限,需要以下幾個(gè):
TOKEN_DUPLICATE:復(fù)制令牌需要的權(quán)限
TOKEN_QUERY:查詢令牌需要的權(quán)限
如果要調(diào)用DuplicateTokenEx
需要指定TOKEN_DUPLICATE,如果調(diào)用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
用來(lái)復(fù)制現(xiàn)有的令牌來(lái)生成一張新令牌,該函數(shù)可以選擇生成主令牌還是模擬令牌
hExistingToken:指定現(xiàn)有的令牌句柄,可以使用
OpenProcessToken
獲得dwDesiredAccess:用來(lái)指定令牌訪問(wèn)權(quán)限,需要指定以下幾個(gè)來(lái)支持后面調(diào)用
CreateProcessWithToken
:TOKEN_DUPLICATE:需要復(fù)制訪問(wèn)令牌
TOKEN_QUERY:需要查詢?cè)L問(wèn)令牌
TOKEN_ASSIGN_PRIMARY:將令牌附加到主進(jìn)程的權(quán)限
TOKEN_ADJUST_DEFAULT:需要更改訪問(wèn)令牌的默認(rèn)所有者、主要組或 DACL
TOKEN_ADJUST_SESSIONID:需要調(diào)整訪問(wèn)令牌的會(huì)話 ID,需要 SE_TCB_NAME 權(quán)限
lpTokenAttributes:指向SECURITY_ATTRIBUTES結(jié)構(gòu)的指針,該 結(jié)構(gòu)指定新令牌的安全描述符并確定子進(jìn)程是否可以繼承該令牌
ImpersonationLevel:指定令牌的模擬級(jí)別,當(dāng)進(jìn)行復(fù)制令牌時(shí),主令牌被復(fù)制為模擬令牌是始終被允許的,而模擬令牌復(fù)制為主令牌則需要模擬級(jí)別 >= Impersonate
TokenType:指定新令牌的類型,是主令牌(Primary Token)還是模擬令牌(Impersonate Token)
phNewToken:返回令牌句柄的地址
復(fù)制完一張新令牌后,我們就可以利用這張新令牌來(lái)運(yùn)行我們指定的進(jìn)程了
CreateProcessWithTokenW
BOOL CreateProcessWithTokenW(
?HANDLE ? ? ? ? ? ? ? ?hToken,
?DWORD ? ? ? ? ? ? ? ? dwLogonFlags,
?LPCWSTR ? ? ? ? ? ? ? lpApplicationName,
?LPWSTR ? ? ? ? ? ? ? ?lpCommandLine,
?DWORD ? ? ? ? ? ? ? ? dwCreationFlags,
?LPVOID ? ? ? ? ? ? ? ?lpEnvironment,
?LPCWSTR ? ? ? ? ? ? ? lpCurrentDirectory,
?LPSTARTUPINFOW ? ? ? ?lpStartupInfo,
?LPPROCESS_INFORMATION lpProcessInformation
);
該函數(shù)創(chuàng)建一個(gè)新進(jìn)程及其主線程,新進(jìn)程在指定令牌的安全上下文中運(yùn)行。我們直接指定前面復(fù)制出來(lái)的新令牌,使用該令牌創(chuàng)建我們指定的進(jìn)程即可
2. 利用
根據(jù)mimikatz的token模塊的原理,簡(jiǎn)單實(shí)現(xiàn)了一個(gè)demo,也有許多token相關(guān)的工具如incognito等
當(dāng)獲取管理員權(quán)限后,我們可以列出系統(tǒng)中進(jìn)程對(duì)應(yīng)的token:

然后竊取指定進(jìn)程的token來(lái)運(yùn)行我們的程序,如直接運(yùn)行上線馬

如果想要拿回程序輸出的話,則可以通過(guò)管道等進(jìn)程間通信的方法來(lái)回顯輸出

如果拿到一臺(tái)機(jī)器有域管的進(jìn)程,那么我們可以直接竊取域管進(jìn)程的token來(lái)進(jìn)行DCSync攻擊

?
0x04 參考鏈接
https://docs.microsoft.com/
https://github.com/gentilkiwi/mimikatz/
https://www.ired.team/offensive-security/privilege-escalation/t1134-access-token-manipulation
https://www.slideshare.net/JustinBui5/understanding-windows-access-token-manipulation
https://posts.specterops.io/understanding-and-defending-against-access-token-theft-finding-alternatives-to-winlogon-exe-80696c8a73b