在某些情況下,具有高完整性或系統完整性的進程會向權限進程/線程/令牌請求句柄,然後生成低完整性進程。如果這些句柄足夠強大,且類型正確,並且由子進程繼承,我們可以從另一個進程複製它們,然後濫用它們來升級權限或繞過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等。
讓我們看看它的實際應用: