通過查找非權限進程中的泄漏句柄來查找權限升級和UAC
在某些情況下,具有高完整性或系統完整性的進程會向權限進程/線程/令牌請求句柄,然后生成低完整性進程。如果這些句柄足夠強大,且類型正確,并且由子進程繼承,我們可以從另一個進程復制它們,然后濫用它們來升級權限或繞過UAC。在這篇文章中,我們將介紹如何尋找和濫用這種漏洞。
介紹
本質上,這個想法是看看我們是否可以自動找到擁有高完整性(也就是提升)或SYSTEM進程的權限句柄的非權限進程,然后檢查我們是否可以作為一個非權限用戶附加到這些進程上,并復制這些句柄,以便以后濫用它們。我們的工具會受到哪些限制?
1.它必須作為中等完整性進程運行;
2. 進程令牌中沒有 SeDebugPrivilege(中等完整性的進程默認沒有這個權限);
3. 沒有 UAC 繞過,因為它也必須適用于非管理用戶;
這個過程有點復雜,我們將經歷的步驟或多或少如下:
1.枚舉所有進程持有的所有句柄;
2.過濾掉我們不感興趣的句柄,現在我們只關注進程、線程和令牌的句柄,因為它們更容易被武器化;
3.過濾掉引用低完整性進程/線程/令牌的句柄;
4.過濾掉完整性大于中等的進程持有的句柄,除非獲得SeDebugPrivilege,否則我們不能附加到它們上,這違背了本文的目的;
5.復制其余的句柄并將它們導入我們的進程,并試圖濫用它們來升級權限或者至少繞過UAC;
當然,我們不太可能在一臺全新的Windows設備上滿足這些條件,所以為了避免這個問題,我將使用一個我專門為此目的編寫的易受攻擊的應用程序。
句柄處理
正如我在這個Twitter線程中簡要討論的那樣,Windows是一個基于對象的操作系統,這意味著每個實體(進程、線程、互斥鎖等)在內核中都以數據結構的形式有一個“對象”表示。例如,對于進程,該數據結構的類型是_EPROCESS。作為存在于內核空間的數據,普通的用戶模式代碼無法直接與這些數據結構交互,因此操作系統公開了一個間接機制,該機制依賴于特殊的HANDLE類型變量以及用于服務的 SC_HANDLE 等派生類型。句柄只不過是內核空間表中的索引,對每個進程來說都是私有的。表中的每一項都包含了它所指向的對象的地址以及該句柄對該對象的訪問級別。這個表由每個進程的_EPROCESS結構的ObjectTable成員(它的類型是_HANDLE_TABLE*,因此它指向一個_HANDLE_TABLE)指向。
為了更容易理解,讓我們看一個例子。要獲得進程的句柄,我們可以使用OpenProcess Win32 API,定義如下:
它需要3個參數:
dwDesiredAccess是一個DWORD,它指定了我們希望對我們試圖打開的進程擁有的訪問級別;
bInheritHandle是一個布爾值,如果設置為TRUE,將使句柄可繼承,這意味著調用進程在子進程生成時將返回的句柄復制給子進程(以防我們的程序調用CreateProcess之類的函數);
dwProcessId是一個DWORD,用于指定我們想打開哪個進程(通過提供它的PID);
在下一行中,我將嘗試打開系統進程(它始終具有 PID 4)的句柄,向內核指定我希望句柄擁有盡可能少的特權,只需要查詢有關信息的子集進程(PROCESS_QUERY_LIMITED_INFORMATION),并且我希望該程序的子進程繼承返回的句柄(TRUE)。
OpenProcess返回的System進程的句柄(如果它沒有因為某種原因失敗)被放入hProcess變量中以供以后使用。
在后臺,內核執行一些安全檢查,如果這些檢查通過,則獲取所提供的PID,解析相關_EPROCESS結構的地址,并將其復制到句柄表中的一個新條目中。之后,它將訪問掩碼(即提供的訪問級別)復制到相同的條目中,并將條目值返回給調用代碼。
當你調用其他函數(如OpenThread和OpenToken)時,也會發生類似的事情。
查看句柄
正如我們前面介紹的,句柄本質上是表的索引。每個條目都包含句柄所引用對象的地址以及句柄的訪問級別。我們可以使用 Process Explorer 或 Process Hacker 等工具查看這些信息:
從這個 Process Explorer 屏幕截圖中,我們可以獲得一些信息:
紅框:句柄所指的對象類型;
藍色框:句柄值(表項的實際索引);
黃色框:句柄所指對象的地址;
綠色框:訪問掩碼及其解碼值(訪問掩碼是在 Windows.h 頭文件中定義的宏),這告訴我們在對象上授予句柄持有者哪些特權;
有很多方法可以獲得這些信息,不一定需要使用在內核模式下運行的代碼。在這些方法中,最實用和最有用的是依賴原生 API NtQuerySystemInformation,當調用它時傳遞 SystemHandleInformation (0x10) 值作為其第一個參數,返回一個指向 SYSTEM_HANDLE 變量數組的指針,其中每個變量都引用一個由系統上的進程打開的句柄。
讓我們來看看用c++實現它的一種可能的方法。
在這段代碼中,我們使用以下變量:
queryInfoStatus 將保存 NtQuerySystemInformation 的返回值;
tempHandleInfo 將保存有關系統 NtQuerySystemInformation 為我們獲取的所有句柄的數據;
handleInfoSize 是對所說數據量的“猜測”。不要擔心,因為每次 NtQuerySystemInformation 將返回 STATUS_INFO_LENGTH_MISMATCH 時這個變量都會加倍,這是一個告訴我們分配的空間不夠的值;
handleInfo 是指向內存位置的指針 NtQuerySystemInformation 將填充我們需要的數據;
不要對這里的 while 循環感到困惑,正如我們所說,我們只是反復調用函數,直到分配的內存空間足夠大,可以容納所有的數據。在使用Windows本機API時,這種類型的操作非常普遍。
NtQuerySystemInformation 獲取的數據可以通過簡單的迭代來解析,如下所示:
從代碼中可以看出,變量句柄是 SYSTEM_HANDLE 類型的結構(自動從代碼中刪除)有許多成員提供有關它所引用的句柄的有用信息。最有趣的成員是:
ProcessId:持有句柄的進程;
Handle:持有句柄本身的進程內部的句柄值;
Object:句柄指向的對象在內核空間中的地址;
ObjectTypeNumber:一個未記錄的 BYTE 變量,用于標識句柄所指對象的類型。為了解釋它,需要進行一些逆向工程和挖掘,只要說進程由值 0x07 標識,線程由 0x08 標識,令牌由 0x05 標識就足夠了;
GrantedAccess 句柄授予的對內核對象的訪問級別,對于進程,你可以找到諸如 PROCESS_ALL_ACCESS、PROCESS_CREATE_PROCESS 等值。
讓我們運行上述代碼并查看其輸出結果:
我們可以從對象類型的 0x7 值推斷出,在這段摘錄中,我們看到 PID 為 4 的進程(即任何 Windows 機器上的系統進程)當前已打開 3 個句柄。所有這些句柄都引用進程類型的內核對象,每個都有自己的內核空間地址,但只有第一個是特權句柄,正如你可以從其值推斷出的那樣, 0x1fffff,這是 PROCESS_ALL_ACCESS 翻譯的內容。不幸的是,在我的研究中,我發現沒有直接的方法可以直接提取 SYSTEM_HANDLE 結構的 ObjectAddress 成員所指向的進程的 PID。稍后我們將看到一個巧妙的技巧來規避這個問題,但現在讓我們使用 Process Explorer 檢查它正在使用哪個進程。
正如你所看到的,值為0x828的句柄的類型是process,它引用進程services.exe。對象地址和被授予的訪問也都簽出了,如果你查看圖像的右側,你將看到解碼的訪問掩碼顯示PROCESS_ALL_ACCESS,正如預期的那樣。
這是非常有趣的,因為它本質上允許我們查看任何進程的句柄表,而不管它的安全上下文和PP(L)級別。
從目標進程的對象地址獲取目標進程的PID
如上所述,我沒有找到一種方法來取回給定進程的 SYSTEM_HANDLE 進程的 PID,但我確實找到了一個有趣的解決方法。讓我們先來看看一些假設:
1.SYSTEM_HANDLE結構包含Object成員,該成員保存內核對象地址,該地址在內核空間中;
2.在Windows上,所有進程都有自己的地址空間,但是地址空間的內核空間部分(64位進程的最大128TB)對所有進程是相同的。內核空間中的地址在所有進程中保存相同的數據;
3.提到進程的句柄時,SYSTEM_HANDLE的Object成員指向進程本身的_EPROCESS結構;
4.每個進程只有一個 _EPROCESS 結構;
5.我們可以通過調用 OpenProcess 并將 PROCESS_QUERY_LIMITED_INFORMATION 指定為所需的訪問值來獲取任何進程的句柄,而不管其安全上下文如何;
從這些假設中,我們可以推斷出以下信息:
1.如果句柄在同一個對象上打開,則兩個不同 SYSTEM_HANDLE 結構的 Object 成員將相同,而與持有該句柄的進程無關,例如,由兩個不同進程在同一文件上打開的兩個句柄將具有相同的 Object 值:
1.1由兩個不同進程打開的同一進程的兩個句柄將具有匹配的 Object 值;
1.2線程、令牌等也是如此;
2.當調用 NtQuerySystemInformation 時,我們可以枚舉我們自己的進程持有的句柄;
如果我們通過 OpenProcess 獲得一個進程的句柄,我們就知道該進程的 PID,并且通過 NtQuerySystemInformation,它的 _EPROCESS 的內核空間地址
你能看到我們要去哪里嗎?如果我們設法打開一個對所有進程具有訪問 PROCESS_QUERY_LIMITED_INFORMATION 的句柄,然后通過 NtQuerySystemInformation 檢索所有系統句柄,我們就可以過濾掉所有不屬于我們進程的句柄,并從那些屬于我們進程的句柄中提取對象值并在它與生成的 PID 之間進行匹配。當然,線程也可以這樣做,只使用 OpenThread 和 THREAD_QUERY_INFORMATION_LIMITED。
為了有效地打開系統上的所有進程和線程,我們可以依賴 TlHelp32.h 庫的例程,它會允許我們拍攝系統上所有進程和線程的快照,并遍歷該快照以獲取拍攝快照時運行的進程和線程的 PID 和 TID(線程 ID)。
下面的代碼塊顯示了我們如何獲取所述快照并遍歷它以獲取所有進程的 PID。
首先定義一個std::map,這是c++中的一個類似字典的類,它允許我們跟蹤指向PID的句柄,我們將其稱為 mHandleId。
完成后,我們使用 CreateToolhelp32Snapshot 獲取有關進程的系統狀態快照,并指定我們只需要進程(通過 TH32CS_SNAPPROCESS 參數)。這個快照被分配給快照變量,它的類型是 wil::unique_handle,它是 WIL 庫的一個 C++ 類,它使我們擺脫了在使用句柄后必須正確清理句柄的負擔。完成后,我們定義并初始化一個名為 processEntry 的 PROCESSENTRY32W 變量,一旦我們開始遍歷快照,它將保存我們正在檢查的進程的信息。
通過調用 Process32FirstW 并用快照中第一個進程的數據填充 processEntry。對于每個進程,我們嘗試在其 PID 上使用 PROCESS_QUERY_LIMITED_INFORMATION 調用 OpenProcess,如果成功,我們將句柄 - PID 對存儲在 mHandleId 映射中。
在每個 while 循環中,我們執行 Process32NextW 并用新進程填充 processEntry 變量,直到它返回 false 并且我們退出循環。現在,我們的句柄和它們指向的進程的 PID 之間有一個 1 對 1 的映射。現在進入第二階段!
現在是獲取所有系統句柄并過濾掉不屬于我們進程的句柄的時候了,我們已經了解了如何檢索所有句柄,現在只需檢查每個 SYSTEM_HANDLE 并將其 ProcessId 成員與我們的進程的 PID 進行比較,可通過恰當命名的 GetCurrentProcessId 函數獲得。然后,我們以與處理句柄-PID 對類似的方式存儲屬于我們進程的那些 SYSTEM_HANDLE 的 Object 和 Handle 成員的值,使用我們稱為 mAddressHandle 的映射。
你可能想知道為什么使用 switch 語句而不是簡單的 if。一些代碼已被刪除,因為這些是我們高級持久性 Tortellini 專門為尋找我們在文章開頭提到的漏洞而編寫的工具的摘錄。
現在我們已經填充了兩個映射,當我們只知道它的 _EPROCESS 地址時,獲取一個進程的 PID 是一件輕而易舉的事。
我們首先將對象的地址保存在地址變量中,然后使用 find 方法在 mAddressHandle 映射中查找該地址,該方法將返回< uint64_t,HANDLE >。這對包含地址和它對應的句柄。我們通過保存對成員的值來獲取句柄second并將其保存在foundHandle變量中。之后,只需要做我們剛才所做的事情,但是使用mHandleId映射和handlePid變量將保存進程的 PID,其地址是我們開始的那個進程。
現在我們有了一種可靠的方法來匹配地址和 PID,我們需要專門尋找那些完整性小于高進程持有有趣的句柄的情況,這些句柄與完整性等于或大于高的進程保持一致。但是從安全的角度來看,是什么讓句柄“有趣”呢?我們將關注的句柄是具有以下訪問掩碼的句柄:
PROCESS_ALL_ACCESS
PROCESS_CREATE_PROCESS
PROCESS_CREATE_THREAD
PROCESS_DUP_HANDLE
PROCESS_VM_WRITE
如果你在非特權進程中找到具有至少一個此訪問掩碼的特權進程的句柄,那非常幸運。讓我們看看我們如何做到這一點。
在這段代碼中,我們首先定義一個名為 vSysHandle 的 std::vector,它將保存有趣的 SYSTEM_HANDLE。之后,我們開始對 NtQuerySystemInformation 返回的數據進行常規迭代,只是這次我們跳過了當前進程持有的句柄。然后,我們通過我編寫的名為 GetTargetIntegrityLevel 的幫助函數檢查持有我們當前正在分析的句柄的進程的完整性級別。這個函數基本上返回一個 DWORD,告訴我們與它作為參數接收的 PID 相關聯的令牌的完整性級別,并且改編自許多在線可用的 PoC 和 MSDN 函數。
一旦我們檢索到進程的完整性級別,我們要確保它小于高完整性,因為我們感興趣的是持有感興趣的句柄的中完整性或低完整性進程,我們還要確保我們正在處理的SYSTEM_HANDLE類型是進程(0x7)。檢查后,我們轉到檢查句柄授予的訪問權限。如果句柄不是PROCESS_ALL_ACCESS或不包含任何指定的標志,則跳過它。否則,我們更進一步,檢索句柄所指進程的 PID,并獲取其完整性級別。如果它是高完整性或更高的(例如SYSTEM),我們將SYSTEM_HANDLE保存在我們的vsyhandle中供以后使用。
首先,我們打開持有權限句柄的進程,然后復制該句柄。
這是相當簡單的,首先,你使用PROCESS_DUP_HANDLE訪問權限打開進程,這是復制句柄所需的最小權限,然后在該進程上調用DuplicateHandle,告訴函數你希望復制保存在syhandle中的句柄,并將其保存到clonedHandle變量中的當前進程中。
通過這種方式,我們的進程現在處于權限句柄的控制之下,我們可以使用它來生成一個新進程,把它的父進程偽裝成該句柄所指向的權限進程,從而使新進程繼承它的安全上下文,并獲得命令shell等。
讓我們看看它的實際應用:
本文翻譯自:https://aptw.tf/2022/02/10/leaked-handle-hunting.html