確定當前CPU雖然這不是一個真正的“技巧”,但為了匹配dblmap中的正確結構地址,有必要識別當前CPU編號。每個內核都有自己的GDT(在dblmap中),因此可以使用sgdt指令來確定當前的CPU。然後可以循環,直到看到“正確的”GDT。
需要執行此操作的一個示例是在使用有效負載填充LDT 之後,任務需要在正確的內核上執行,然後有效負載才會出現在dblmap 中該內核的LDT 中。
使用dblmap 簡化實際的漏洞利用為了提供一個演示dblmap 實用性的具體示例,我們將展示如何通過使用dblmap 而不是其過度複雜的多個洩漏原語構造來大大簡化我們的Pwn2Own 2021 內核漏洞利用。 GitHub 上提供了此變體的源代碼以及完整的Safari 到內核鏈的其餘部分。
bug/exploit 的完整細節在之前的一篇文章中已經介紹過:1.我們對已釋放的內核緩衝區進行任意寫入操作;2.沒有內核信息洩露。
我們可以使用包含OSObject 指針的新OSArray 的後備存儲輕鬆回收已釋放的內核緩衝區。這些是C++ 對象,因此破壞數組中的內核數據指針以指向假對象應該足以通過虛擬調用劫持控制流。
多虧了dblmap,我們可以將已知的數據放在已知的內核地址。這意味著我們可以在LDT中構造偽內核對象及其虛函數表,而不需要真正的內核文本或數據洩漏。
為了簡單起見,我們選擇CPU 0作為“正確”的CPU,它的LDT對應於內核中的master_ldt符號。
現在我們已經能夠劫持內核控制流,我們需要研究調用時的寄存器狀態,以及如何將其轉換為對dblmap函數的有用調用。
被劫持的內核調用網站的參數控制在這種情況下,將生成一個損壞的OSArray的副本,我們首先能夠在OSArray:initWithObjects()中劫持控件,它被傳遞給包含損壞對象的備份存儲。遍歷損壞的支持存儲,並為每個對象調用taggedRetain()虛函數。
調用的第一個參數自然是this指針,它將指向LDT中的假對象;
第二個參數是“tag”,它將是OSCollection:gMetaClass;
沒有明確的第三個參數,但我們仍然可以查看函數是如何編譯的,並確定調用網站的rdx(根據調用約定的第三個參數)中可能包含的內容。
它恰好在count++操作期間使用,這意味著它將等於假對像在已損壞數組中的索引加1。換句話說,如果破壞數組中第i個索引的對象,對應調用的第三個參數將是i+1。這讓我們可以控制第三個參數,只要它是一個相對較小的非零整數。
由於第二個參數OSCollection:gMetaClass 是OSMetaClass 的一個實例,一個C++ 對象,它的第一個字段是虛函數表。使用第三個參數8調用memcpy()會相對簡單,它會將虛函數表複製到LDT中。然後可以使用i386_get_ldt()讀出虛函數表,這將導致文本洩漏。
然後,我們可以使用OSSerializer:serialize()將帶有受控制的this參數的調用轉換為帶有3個受控制參數的任意函數調用(假設可以滿足LDT限制)。這可能足以獲得足夠的洩漏,以消除對LDT的依賴,並從那裡繼續利用。
在LDT 中設置任意位hibernate_page_bitset() 函數的簽名是:
第二個布爾參數set決定函數是設置位還是清除位。在我們的例子中,它將為真,因為OSCollection:gMetaClass是非零的。
第三個參數page指定要設置或清除的位。如前所述,我們可以將其設為任何小的非零整數。
第一個參數是LDT中的假對象,它的結構如下:
簡而言之,它是一個可變大小的位圖數組,其中每個位圖都與一個位索引範圍相關聯。
由於我們控制了偽結構和位索引,因此調用這個函數允許我們在LDT中設置任意位。
應該注意的是,dblmap 中LDT 的內存內容是短暫的,如果當前任務(進程/線程)被搶占,它們就變得無關緊要。
當一個新任務開始在CPU上執行時,如果它有一個LDT,它將被複製到dblmap中,覆蓋現有的已損壞的內容。同樣,當重新調度被搶占的任務(具有先前損壞的LDT)時,複製到dblmap中的LDT來自堆分配結構,從而有效地消除了損壞。
在本文的示例中,你基本上可以忽略這個細節,因為失敗不會有任何懲罰或崩潰,如果需要的話,我們可以重新執行被劫持的bitset調用。另外,OSArray:initWithObjects()在一個循環中遍歷已損壞的後台存儲,這意味著我們可以劫持數組中每個插槽的虛擬調用,並在一次傳遞中多次調用bitset函數。我們不需要每次設置一個位時都來回切換到用戶空間,這意味著經過的時間更少,我們的任務被搶占的機會也就越低。
另一種選擇是破壞LDT 中的前3 個描述符之一。這些是硬編碼的,不能用i386_set_ldt() 修改:
由於它們預計不會更改,因此每次在CPU 上執行新任務時不會重置這些條目。任何dblmap 的LDT(每個CPU 一個)中對這3 個描述符的任何損壞都將在搶占期間持續存在。
構建調用門在LDT中設置比特聽起來很強大,但我們實際上可以用它做什麼?
請記住,有兩種類型的描述符:用於內存區域的代碼/數據描述符和系統描述符。 LDT中唯一允許的系統描述符是調用門。正如英特爾手冊所述:調用門有助於在不同權限級別之間進行程序控制的受控傳輸。
它們主要由3個方面定義:
1.訪問門所需的權限級別;
2.目標代碼段選擇器;
3.目標入口點;
二進制格式:
當從用戶空間(ring3)通過呼叫門進行遠程呼叫時,大致會發生以下情況:
1.檢查權限(調用門描述符的DPL 字段必須為3);
2.目標代碼段描述符的DPL 字段成為新的權限級別;
3.使用新的權限級別從TSS 中選擇一個新的堆棧指針;
4.將舊的ss 和rsp 推入新堆棧;
5.將舊的cs和rip推入新堆棧;
6.從調用門選擇器和入口點設置cs和rip。
構造一個可以被ring3 (DPL為3)訪問的調用門,並為64位內核代碼(0x8)指定代碼段選擇器,這將允許我們對指定的任何地址執行遠程調用,如ring0。
濫用調用門來利用內核漏洞之前已經被證明過,但是在32 位Windows 的上下文中,當時SMEP、SMAP和頁表隔離並未受到關注。
洩露kernel slide當我們觸發遠程調用時,Supervisor Mode Execution Prevention (SMEP)阻止我們跳轉到用戶空間中的可執行代碼。頁表仍然處於用戶模式,其中唯一映射的內核空間頁用於dblmap。這就留下了跳到__HIB文本部分中的現有代碼的唯一選項。
我們將遠程調用指向ks_64bit_return()的中間,它包含以下指令序列:
請記住,我們擁有完全的寄存器控制(rsp除外,它將是內核堆棧),因此對r15解引用的第一條指令將給我們一個任意的讀原語。我們只需將r15適當地設置為要讀取的地址,進行遠程調用,在返回到用戶空間時,r15將包含解除引用的數據。
這可能導致從dblmap中洩漏一個函數指針,從而暴露kernel slide。具體來說,我們可以從idt64_hndl_table0中洩漏ks_dispatch()指針。
這裡有一點需要注意的是,從遠程調用返回通常是通過遠返回指令retf 完成的,而不是iretq 中斷返回。 堆棧佈局將與預期略有不同:
rflags 將從舊的rsp 中設置,而rsp 將使用舊的ss 填充,而ss 將從之後發生在堆棧上的任何內容中填充。這意味著:
在進行遠程調用之前,可以通過將RSP設置為Rflags值來“恢復”Rflags;
從舊的ss中設置RSP是沒有問題的,我們可以自己恢復RSP;
加載到ss的堆棧上的下一個值將是一個內核指針,這將是一個無效的選擇器,並將觸發一個異常。然而,這個異常將發生在中斷返回後的ring3中,所以我們可以簡單地使用預期的Mach異常處理行為來捕獲它。
可以使用thread_set_exception_ports()來為EXC_BAD_ACCESS註冊一個異常端口。我們生成一個線程來等待異常消息,然後用包含正確的ss選擇器的消息內容來響應,從而允許遠程調用線程繼續。
控制頁表,控制一切kernel slide不僅顯示內核基址的虛擬地址,而且還顯示其物理地址。這為我們提供了CPU 0的LDT的物理地址,或者換句話說,就是受控數據的物理地址。
這為我們提供了一個非常強大的工具:如果我們重構調用門以跳轉到mov cr3指令,我們將立即獲得任意的ring0代碼執行。
我們所需要做的就是確保我們正確地設置了我們的偽頁表(駐留在LDT 中):
1.mov cr3後面指令的虛擬地址應該映射到傳入LDT的shellcode的物理地址;
2.內核堆棧應該是可映射和可寫的(對於CPU 0,這是__HIB 段中的符號master_sstk);
3.根據經驗,這是不必要的,但是為了安全起見,應該映射GDT(對於CPU 0,符號master_gdt)。
注意,這只適用於CPU 0,它的LDT靜態分配在__HIB段中。其他CPU 的LDT 從內核堆分配,虛擬別名插入到dblmap 的頁表中。這些堆分配的物理地址不能直接從kernel slide中推斷出來。
內核Shellcode由於對LDT 描述符的限制,我們傳入LDT的shellcode將由任意6字節的塊組成。如果我們使用2 個字節進行短跳轉(EB + 偏移量),這會留下4 個任意字節的塊。
雖然理論上這已經結束了,但在“正常”頁表狀態下更容易獲得不受限制的shellcode執行。為了實現這一目標,一個簡單的解決方案是使用受限制的shellcode禁用SMEP和SMAP(它們只是CR4寄存器中的位),然後返回到用戶空間。然後,我們可以像以前一樣觸發一個被劫持的虛擬內核調用,但這一次跳轉到用戶空間中的任意shellcode。
一個小細節是每個CPU都有控制寄存器,所以只有在執行LDT shellcode的CPU上才會禁用SMEP和SMAP。
另一個實現細節是如何干淨地返回到用戶空間,這將需要恢復原始頁表。我們通過讓shellcode執行以下操作來實現:
1.從dblmap 讀取cpshadows[i].cpu_ucr3 到rax;
2.修改堆棧以形成有效的iretq 佈局;
3.跳轉到ks_64bit_return() 執行cr3 切換(來自rax),然後執行iretq;
我們介紹了dblmap 如何大大降低KASLR 的功效,提供幾個有趣的內核調用目標、主機走私內核shellcode 等。
Recommended Comments