隨著內核地址空間佈局隨機化(KASLR)被現代系統廣泛採用,獲取信息洩漏是大多數權限升級攻擊的必要組成部分。本文我們將介紹XNU(蘋果macOS使用的內核)的實現細節,它可以消除許多內核漏洞中對專用信息洩露漏洞的需要。
關鍵在於內核Mach-O的__HIB段,它包含系統休眠和底層CPU管理的函數子集和數據結構,總是映射在一個已知的地址。
我們將首先介紹__HIB段以及它可能被濫用的各種方式。使用我們的Pwn2Own 2021內核漏洞作為一個真實的例子,展示如何簡化該漏洞。
濫用基本操作系統設計可以實現漏洞利用原語
__HIB 段:休眠和內核入口
__HIB段的名字來源於“休眠(hibernation)”,似乎它的主要目的是處理從休眠映像中恢復系統。
它的其他主要功能是處理內核的低級進入和退出,即通過中斷或系統調用。它包含中斷描述符表(IDT) 和相應的中斷處理程序存根。
它同樣包含通過syscall和sysenter指令進入的存根。這些入口點是通過編寫相應的MSR 來設置的:
這個代碼片段使用DBLMAP宏引用“DBLMAP”中的每個函數的別名。
XNU dblmapdblmap是一個虛擬內存區域,別名為組成__HIB段的相同物理頁。
dblmap在doublemap_init()中初始化,它將虛擬別名插入到構成__HIB段的所有物理頁的頁表中。 dblmap 的基數是用8 位熵隨機化的。
別名防止了使用sidt指令的瑣碎的KASLR繞過,sidt指令將揭示IDT的內核地址。這個指令不是權限指令,如果在ring3(用戶模式)中執行不會導致異常,除非在處理器上啟用了用戶模式指令保護(UMIP)(不在XNU 中):
用戶模式指令預防(CR4的第11位)設置後,如果CPL 0: SGDT、SIDT、SLDT、SMSW和STR,則不能執行以下指令,如果試圖執行該值,將導致通用保護異常(#GP)。
由於IDT/GDT/LDT/etc 位於dblmap 別名中,因此知道它們在dblmap 中的地址不會顯示它們在正常內核映射中的地址。但是,它們確實會洩漏dblmap 信息。
緩解失敗dblmap 的一個輔助目的是它起到緩解Meltdown 的作用。各種操作系統採用的常用技術是在執行用戶空間代碼時從頁表中刪除內核映射,然後在進行中斷/系統調用/等時重新映射內核。 Linux 將其實現稱為內核頁表隔離(KPTI),在Microsoft Windows 上,它的對等物被稱為KVA Shadow。
兩種實現方式的相似之處在於,在執行用戶代碼時,只有一小部分內核頁面出現在頁表中。該子集包含處理中斷和系統調用以及在用戶和內核頁表之間來迴轉換所需的代碼/數據。在XNU 的情況下,“小子集”正是dblmap。
為了進行頁表轉換,中斷調度代碼需要知道正確的頁表地址以進行切換。換句話說,要向cr3寫入什麼值,即保存頂級分頁結構的物理地址的控制寄存器。
這些值駐留在__HIB 段的數據部分之一中的每個CPU 數據結構中:
cpshadows[cpu].cpu_ucr3 : 用於userspace + dblmap;
cpshadows[cpu].cpu_shadowtask_cr3 :用於userspace + full kernel;
從用戶空間進入後,中斷調度代碼會將cr3 切換到cpu_shadowtask_cr3,然後在返回用戶空間之前切換回cpu_ucr3。
由於在執行用戶代碼時dblmap 被映射到頁表中,因此理論上dblmap 中的任何內容都可以使用Meltdown 讀取。這就產生了dblmap 中是否存在任何“敏感”數據的問題。
值得注意的是,有幾個函數指針以數據的形式出現,用於從dblmap跳轉到正常映射的內核函數。洩露其中任何一個都足以確定kernel slide(用於描述隨機內核基址地址的XNU 術語)並破壞KASLR。
有趣的是,KASLR 在Windows 和Linux 上都可以通過Meltdown 進行類似的攻擊:
同樣的策略也適用於運行有漏洞的英特爾硬件的最新版本的macOS。只需使用libkdump從dbblmap讀取即可。簡而言之,Meltdown 可用於在除最後一代基於Intel 的Mac 機型之外的所有設備上破壞KASLR,該機型附帶Ice Lake 處理器,其中包含針對Meltdown 的CPU 級修復。
雖然KASLR 在基於Intel 的Mac 上無疑是一個重要且幾乎普遍的突破,但我們現在將討論dblmap 提供的其他幾個有用的內核利用原語,這些原語適用於包括Ice Lake 在內的所有版本。
LDT——已知內核地址的受控數據從利用的角度來看,也許dblmap 最有趣的工件是它為每個CPU 內核保存LDT,它可以包含幾乎可以從用戶模式控制的任意數據,並且位於dblmap中的固定偏移位置。
LDT/GDTLDT 和GDT 包含許多段描述符。每個描述符要么是系統描述符,要么是標準代碼/數據描述符。
代碼/數據描述符是一個8 字節的結構,描述了具有某些訪問權限(即它是否可讀/可寫/可執行,以及可以訪問它的特權級別)的內存區域(由基地址和限制指定)。這主要是一個32 位保護模式的概念;如果執行64 位代碼,則忽略基地址和限制。描述符可以描述64 位代碼段,但不使用基本/限制。
系統描述符是特殊的,大多數擴展為16 字節以適應指定64 位地址。它們可以是以下類型之一:
LDT :指定LDT 的地址和大小;
任務狀態段(TSS) :指定TSS 的地址,該結構包含與權限級別切換相關的信息結構;
調用、中斷或陷阱門:指定遠程調用、中斷或陷阱的入口點;
LDT中唯一允許的系統描述符是調用門,我們稍後會探討。
段描述符使用16 位段選擇器引用:
例如,cs寄存器(代碼段)是一個選擇器,它引用當前正在執行的任何代碼的段。通過在GDT/LDT中為64位和32位代碼設置不同的描述符,操作系統可以通過使用適當的選擇器在受保護模式(32位代碼)和長模式(64位代碼)之間進行用戶空間跳轉。特別是在macOS上,硬編碼的選擇器是:
0x2b:第5個GDT 條目,ring3——64 位用戶代碼;
0x1b:第3個GDT 條目,ring3——32 位用戶代碼;
0x08:第1個GDT 條目,ring0——64 位內核代碼;
0x50:第10個GDT 條目,ring0——32 位內核代碼;
LDT在macOS上的應用進程可以通過調用i386_set_ldt()在其用戶模式的LDT中設置描述符。它調用的對應內核函數是i386_set_ldt_impl()。
此函數對LDT 中允許的描述符類型施加了一些限制。描述符必須為空,或者指定一個用戶空間的32位代碼/數據段:
就二進制格式而言,這轉換為對每個8字節描述符的以下限制:
字節5 (dp-access):必須是0,1,或者有高nibble0xf(值匹配0xf)。
字節6 (dp-granularity): 位5不能設置(掩碼0x20)。
使用LDT在已知的內核地址創建偽內核對象,這些限制不會太嚴格。唯一真正的問題是能否在這樣的對像中構造有效的內核指針。
為了解決這個問題,我們可以將假對象錯位6 個字節。這將使dp-access 與內核指針的高字節(將為0xff)重疊。唯一的限制是指針的低字節(與dp-granularity 重疊)不能設置位5 。
成功設置LDT描述符將創建一個堆分配的LDT結構,該結構的指針存儲在當前任務結構的task-i386_ldt字段中。
每當發生上下文切換或手動更新LDT時,內核通過user_ldt_set()將來自該結構的描述符複製到實際的LDT(在dblmap中)。
進程可以使用i386_get_ldt()查詢當前的LDT條目,調用內核函數i386_get_ldt_impl()。這將直接從實際LDT(在dblmap中)複製描述符,而不是從LDT結構體複製描述符。
已知內核地址上的已知代碼在編寫沒有內核代碼地址洩漏的macOS 內核漏洞利用時,__HIB 文本部分中的函數子集可能會用作有用的調用目標。
dblmap 中一些值得注意的函數(在固定偏移處)是:
memcpy(), bcopy();
memset(), memset_word(), bzero();
hibernate_page_bitset()——設置或清除位圖結構中的位;
hibernate_page_bitmap_pin()——可以將32 位int 從第一個結構參數複製到第二個指針參數;
hibernate_sum_page()——從它的第一個參數返回一個32 位int 作為一個數組,使用第二個作為索引;
hibernate_scratch_read(),hibernate_scratch_write()——類似於memcpy,但第一個參數是一個結構;
當控制流被劫持時,這些函數對內核利用的效用將嚴重依賴於特定的上下文。上面列出的大多數內核調用目標需要3個參數。如果一個給定的漏洞允許按順序進行多個受控調用,你還可以使用早期調用來設置寄存器/狀態,以便在以後的調用中使用。
無論情況如何,dblmap通常可以用來將一個簡單的內核控制流劫持原語轉換為同時產生真正內核文本(代碼地址)洩漏的內容。
其他dblmap技巧已知內核地址上真正任意的64位值我們已經介紹了dblmap如何允許我們輕鬆地在LDT中放置幾乎任意的數據。
但是,假設你需要一個真正任意的64位值(可能你需要的函數指針位為5,並且不符合LDT 限制)。當進行系統調用時,調度存根會將rsp(用戶空間堆棧指針)保存在dblmap 的臨時位置(cpshadows[cpu].cpu_uber.cu_tmp)。用戶rsp 值也將放置在dblmap 中的中斷堆棧上(對於cpu 0 的master_sstk 或scdtables[cpu])。
雖然不是其預期用途,但rsp 可以被視為通用寄存器,並分配任意64 位值。除非需要在系統調用之前和之後設置/恢復rsp 給開髮帶來不便,否則這會在已知地址處提供任意64 位值。
將內核指針洩漏到LDTLDT的另一個有用的方面是,它可以用作真正的內核地址洩漏的可寫位置,可以使用i386_get_ldt()從用戶模式查詢和讀取。假設你能夠使用劫持函數調用將任意地址複製到LDT中。將dblmap中的一個函數指針複製到LDT中,然後查詢LDT的適當範圍,將獲得文本洩漏。
或者作為另一個例子,也許你劫持的函數調用的第一個參數是LDT中的假對象,第二個參數是某個有效的c++對象,第三個參數是任何足夠小的整數。調用memcpy() 會將vtable和一些對象的字段複製到LDT。查詢LDT 然後將這些洩漏讀取到用戶空間。
記住,每個CPU核有一個LDT,在macOS上沒有方法將進程固定到任何特定的核上。如果一個線程執行被劫持的函數調用,將洩漏複製到它的LDT中,然後在調用i386_get_ldt()之前被搶占,那麼洩漏實際上將丟失。
Recommended Comments