task_t指針重大風險預報-修復建議篇
看雪首發:【翻譯】task_t指針重大風險預報-修復建議篇-『iOS安全』-看雪安全論壇
引言:大家都知道知名義大利天才少年Luca放出來的針對<=10.2版本的yalu越獄使用的是對kernel port的buffer overflow拿到了kernel_task_port,本文對類似的task_t指針做出了針對性的分析,從mach埠背景知識,到IOkit的相關處理,最終如何利用在堆棧上寫出Exploit,最終甚至給蘋果團隊給出了修復漏洞的建議,由淺入深,偏辟入里,值得推薦。
本文分三篇推出,分別是分析篇,Exploit篇,和修復建議篇。
譯者註:
·一些諸如bug,exploit之類的行話選擇性的翻譯,這通常取決於句子的流暢性。
·不確定的地方在括弧中附註了原句
·超鏈接附在括弧內,方便查看
by ruanbonan
修復建議篇
XNU不是Unix也不是Mach
在純凈的Mach微內核中,使舊任務埠無效已經能夠很充分地避免任何其他進程通過執行許可權提升來保持對任務的控制,但是XNU不是微內核。之前我們看了把mach任務埠轉換成任務結構體指針的內核函數convert_port_to_task。這個指針可以被使用,並在內核中傳遞,而不沒有發送消息的開銷。例如,當IOKit想要控制一個進程的虛擬內存時,它直接調用相關的內核函數即可,而不必發送mach信息給mach_vm MIG子系統(理論上來說是可以這樣做的)。
要弄明白這個機制還可以這樣想:所有工作在內核中的MIG子系統(IOKit,mach_vm,tasks,threads,semaphores等等)相互之間都直接連接。他們可以簡單地調用目標函數,而不必通過MIG IPC層。這明顯快了很多,但是帶來了開銷。
所有task_t指針都是潛在的安全bug
權衡來看,沒有一個有權使用資源的中心點能夠被去除。在內核中,當特權執行發生時,它們不能僅僅讓一個任務埠無效,就期望這會起作用,因為內核內部的MIG子系統不使用任務埠,僅僅在處於用戶/內核邊界時,它們會在任務埠和任務結構體指針之間做轉化。內核不知道在哪裡所有的內核指針會指向一個任務結構體;它無法期望去使它們無效化。
這是一個比最初的引用計數bug嚴重的多的問題。當一個許可權提升操作發生時,execve不能創建一個新進程;任務結構體保持原狀,僅僅是特權改變了。這意味著內核中每一個單獨的task_t指針都是一個潛在的安全bug——在你獲得它的許可權後,沒有鎖機制讓你斷言一個任務結構體的許可權未曾改變,內核代碼在某時獲得一個任務結構體的許可權也不意味著之後它應該具有這個許可權。
在堆上:重寫IOSurface Exploit
實際上,為了讓最初的IOSurface Exploit在正確的task_reference(owningTask調用時也能有效,我們僅僅需要稍微改變一下。我們不再讓子進程把它的任務埠傳回給父進程,而是在子進程中創建一個IOSurfaceRootUserClient(正確地使用子進程自己的任務埠),然後把這個userclient埠傳回給父進程。
然後子進程通過execve來執行一個帶有suid許可權的程序,這將會把任務的EUID設置為0,且不釋放任務結構體。父進程依然有給IOSurfaceRootUserClient發送消息的許可權,並且這個userclient的owningTask現在的EUID是0.父進程可以像之前那樣繼續執行,阻塞子進程,映射目標的libc __DATA段,覆蓋一個函數指針並且取消對子進程的阻塞,這樣子進程會嘗試退出,並執行ROP棧代碼。這個新的Exploit也繞過了10.11.6中加入的緩解策略,該緩解策略禁止創建帶有其他任務的任務埠的userclient。
注意,這個Exploit沒有失敗的案例——沒有競態條件需要獲得,沒有可能出錯的UAF。這個Exploit應該在所有版本低於10.11.6的OS X上生效。
這個原始的exploit比UAF的威力稍微小了一些,UAF可以幫助你從非常嚴格的沙箱中逃逸,而在這個exploit中你的確需要調用execve。這些在堆上儲存task_t指針的IOKit對象僅僅是冰山一角。
在棧上:利用task_threads
回到用戶/內核邊界處,當convert_port_to_task把一個從用戶空間收到的任務埠轉換成任務結構體指針時,這個任務可能執行一個suid或者有許可權的二進位程序來提升它的許可權。即使這個任務結構體指針沒有存儲在堆上,仍然可能存在可以利用的bug。案例之一是下面的內核MIG task_threads方法:
kern_return_t
task_threads(
task_t target_task,
thread_act_array_t *act_list,
mach_msg_type_number_t *act_listCnt );
被賦予一個任務埠的發送許可權的情況下,這個方法返回這個任務所有線程的線程埠的發送許可權。下面是內核中MIG自動生成的代碼片段:
target_task = convert_port_to_task(
In0P->Head.msgh_request_port);n // (1)
RetCode = task_threads(
target_task,
(thread_act_array_tn *)&(OutP->act_list.address),
&OutP->act_listCnt);
task_deallocate(target_task);
我們可以看到任務埠被轉換成了任務結構體指針,它接下來被存儲在局部變數 target_task中,這個局部變數的生存周期是這個函數調用的生存周期。
下面是來自task_threads的相關代碼:
task_threads(
task_t task,
thread_act_array_tn *threads_out,
mach_msg_type_number_tn *count)
{
...
for (thread =n (thread_t)queue_first(&task->threads);
in < actual;
++i,n thread = (thread_t)queue_next(&thread->task_threads)) {
thread_reference_internal(thread);
thread_list[j++]n = thread;
}
...
for (i = 0; in < actual; ++i)
((ipc_port_tn *) thread_list)[i] = convert_thread_to_port(thread_list[i]); // (2)
}
...
}
這段代碼在線程列表中不斷循環迭代地收集struct thread指針,然後把那些結構體線程轉化為線程埠,並返回。代碼中有少量的鎖,但是它們是不相關的。
如果任務同時正在執行一個帶有suid標誌的程序,會發生什麼?
相關的exec代碼部分有兩點,在ipc_task_reset和ipc_thread_reset中:
void
ipc_task_reset(
task_tn task)
{
ipc_port_t old_kport,n new_kport;
ipc_port_t old_sself;
ipc_port_tn old_exc_actions[EXC_TYPES_COUNT];
int i;
new_kport =n ipc_port_alloc_kernel();
if (new_kport == IP_NULL)
panic("ipc_task_reset");
itk_lock(task);
old_kport =n task->itk_self;
if (old_kport == IP_NULL) {
itk_unlock(task);
ipc_port_dealloc_kernel(new_kport);
return;
}
task->itk_self =n new_kport;
old_sself =n task->itk_sself;
task->itk_sself =n ipc_port_make_send(new_kport);
ipc_kobject_set(old_kport,n IKO_NULL, IKOT_NONE); // (3)
緊跟著的是對ipc_thread_reset的調用:
ipc_thread_reset(
thread_t thread)
{
ipc_port_t old_kport,n new_kport;
ipc_port_t old_sself;
ipc_port_tn old_exc_actions[EXC_TYPES_COUNT];
boolean_tn has_old_exc_actions = FALSE;
intn i;
new_kport =n ipc_port_alloc_kernel();
if (new_kport == IP_NULL)
panic("ipc_task_reset");
thread_mtx_lock(thread);
old_kport =n thread->ith_self;
if (old_kport == IP_NULL) {
thread_mtx_unlock(thread);
ipc_port_dealloc_kernel(new_kport);
return;
}
thread->ith_self =n new_kport; // (4)
我們把執行exec的進程命名為B,調用task_threads()的進程命名為A,想像下面的交叉執行過程:
A:
target_task = convert_port_to_task(
In0P->Head.msgh_request_port);n // (1)
A從棧上獲得了指向進程B的任務結構體的指針。
B:
ipc_kobject_set(old_kport, IKO_NULL,n IKOT_NONE); // (3)
B執行了帶有suid許可權的程序,並且使舊任務埠無效,因此它不再擁有任務結構體指針。
B:
thread->ith_self = new_kport; // (4)
B分配了新的線程埠並啟動
A:
((ipc_port_t *) thread_list)[i] =n convert_thread_to_port(thread_list[i]); // (2)
A為B的特權線程讀入並轉換新的線程埠對象,這給了A一個特權線程埠。
一個線程埠的發送許可權會給你完整的寄存器控制許可權。這個exploit和之前的兩個執行模式有些類似,不同的是一旦它獲得線程埠,它可以直接把RIP指向我們的gadget地址,而不必覆蓋一個函數指針。競態窗口非常小,所以需要一個很特別的交叉執行才可以,但是這是可以實現的。查看exploit(鏈接:https://bugs.chromium.org/p/project-zero/issues/attachment?aid=237182)和最初的bug報告(鏈接:837 - task_t considered harmful - many XNU EoPs - project-zero - Monorail)。
第二輪緩解策略
iOS 10/MacOS 10.12 引入了另外的緩解策略,同樣可以繞過。
首先,在IOKit方面,userclient的生命周期現在直接與創建的任務綁定。其次,ipc_kobject服務有一處緩解措施來檢測如果MIG內核方法因為競態導致了execve調用,就強制使這個方法執行失敗:
/*
* Check if the port is a task port, if its an task port then
* snapshot the task exec token before the mign routine call.
*/
ipc_port_t port =n request->ikm_header->msgh_remote_port;
if (IP_VALID(port) && ip_kotype(port)n == IKOT_TASK) {
task =n convert_port_to_task_with_exec_token(port, &exec_token);
}
(*ptr->routine)(request->ikm_header,n reply->ikm_header);
/* Check if the exec token changed during then mig routine */
if (task != TASK_NULL) {
if (exec_token != task->exec_token) {
exec_token_changed = TRUE;
}
task_deallocate(task);
}
緩解策略中有三處缺陷:
1. 它僅僅審查了第一個參數,但是有的內核MIG方法會在其他位置接受一個任務埠。
2. 它僅僅檢查任務埠,然而thread_ports也受到了相似的影響。
3. 它僅僅緩解了那些我們需要獲得MIG調用返回資源(比如埠)的bug。但是還有大量的其他方法是直接修改進程狀態,而非返回新埠。
繞過第二輪緩解策略
雖然我們不再能夠直接通過task_threads獲得新的線程埠,還是有一些繞彎子的途徑來達到目的。我們僅僅需要一個能夠修改狀態,而不是直接返回一些有用的東西(比如任務埠)的API。
task_set_exception_port允許我們為一個任務設置一個異常埠。當異常拋出時(比如非法訪問內存)內核會發送一個異常消息給註冊過的異常處理常式。對於我們很重要的是,這個異常消息包含了任務以及造成異常的線程的線程埠。
與內核中絕大多數地方帶有一個task_t在棧上一樣,這個API也存在有漏洞的競態條件。在進程A中我們持續調用task_set_exception_ports()來傳遞進程B的任務埠,同時B execve執行一個帶有suid許可權的程序:
mig_internal novalue _Xtask_set_exception_ports(
mach_msg_header_t *InHeadP,
mach_msg_header_t *OutHeadP) {
...
task =n convert_port_to_task(In0P->Head.msgh_request_port); // (1)
OutP->RetCode =
task_set_exception_ports(task,
In0P->exception_mask,
In0P->new_port.name,
In0P->behavior,
In0P->new_flavor);
task_deallocate(task);
...
kern_return_t
task_set_exception_ports(
task_tn task,
exception_mask_tn exception_mask,
ipc_port_tn new_port,
exception_behavior_t new_behavior,
thread_state_flavor_t new_flavor)
{
...
itk_lock(task); // (2)
for (i = FIRST_EXCEPTION; i <n EXC_TYPES_COUNT; ++i) {
if ((exception_mask & (1n << i)) ) {
old_port[i] =n task->exc_actions[i].port;
task->exc_actions[i].portn = ipc_port_copy_send(new_port); // (3)
task->exc_actions[i].behaviorn = new_behavior;
task->exc_actions[i].flavorn = new_flavor;
task->exc_actions[i].privilegedn = privileged;
}
...
itk_unlock(task);
...
進程B調用execve來執行一個特權suid程序:
ipc_task_reset(
task_t task)
{
...
itk_lock(task); // (4)
...
ip_lock(old_kport);
ipc_kobject_set_atomically(old_kport,n IKO_NULL, IKOT_NONE); // (5)
task->exec_token += 1;
ip_unlock(old_kport);
ipc_kobject_set(new_kport,n (ipc_kobject_t) task, IKOT_TASK);
for (i = FIRST_EXCEPTION; i <n EXC_TYPES_COUNT; i++) {
...
ifn (!task->exc_actions[i].privileged) {
old_exc_actions[i]n = task->exc_actions[i].port;
task->exc_actions[i].portn = IP_NULL; // (6)
}
}
itk_unlock(task); //(7)
我們尋找下面這樣的交叉執行情景:
A:
task =n convert_port_to_task(In0P->Head.msgh_request_port); // (1)
B:
itk_lock(task); // (4)
ipc_kobject_set_atomically(old_kport,n IKO_NULL, IKOT_NONE); // (5)
task->exc_actions[i].port = IP_NULL; // (6)
itk_unlock(task); //(7)
A:
itk_lock(task); // (2)
task->exc_actions[i].port =n ipc_port_copy_send(new_port); // (3)
我們很容易在競態條件取得對task_threads的先機,因為鎖保證了所有對我們有利的事情。我們要做的僅僅是循環調用task_set_exception_ports並且希望(4)處的B接過任務鎖之前,(1)能夠被A調用。實踐中Exp在幾微秒內就可以贏得競態條件。
最後的工作實際上是確保當贏得競態條件時,我們強制子進程引發一個異常,把它的任務和線程埠發給我們。我們可以通過在執行suid目標前帶一個非常小的值調用setrlimit(RLIMIT_STACK)
來實現。這意味著我們將要執行的二進位程序的棧空間很小,很快就會導致段錯誤。
在父進程中,一旦task_set_exception_port調用失敗,我們就嘗試從異常埠接收消息,設置一個短的timeout。如果接收到消息,我們在競態中取得先機,這個消息中包含euid為0的進程的任務和線程埠。這種情況下,Exp在任務中分配了一些RWX內存,並把一個shellcode拷貝到這個地方。shellcode做的事情如下:
struct rlimit lim = {0x1000000,n 0x1000000};
setrlimit(RLIMIT_STACK, lim);
setuid(0);
char* argv[2] = {"/bin/bash",n 0};
execve("/bin/bash", argv, 0);
shellcode把棧長度設置回一個很大的值,用setuid(0)n來避免bash丟失許可權,最後打開一個shell。
這個Exp應該會在MacOS/OS X 版本<=10.12.0時穩定工作。
最後的修復
這不是一類容易修復的bug。XNU的設計導致了task_t指針到處存在,而且我們提到的問題不隻影響到task_t指針;線程也會受到這個問題的影響。蘋果決定重構在裝載二進位程序時用來分配新任務和線程結構體的execve的代碼,應該會解決問題。這是一個工作量相當大的事情,蘋果為修復這些bug投入的努力值得讚賞,我期待在MacOS 10.12.1版本源碼中看到新的代碼。
譯者:ruanbonan
原文鏈接:https://googleprojectzero.blogspot.kr/2016/10/taskt-considered-harmful.html原文作者:lan Beer,Project Zero
微信公眾號:看雪iOS安全小組
我們的微博:http://weibo.com/pediyiosteam
我們的知乎:http://zhihu.com/people/pediyiosteam
加入我們:看雪iOS安全小組成員募集中:iOS安全小組成員招募中-『iOS安全』-看雪安全論壇
n[看雪iOS安全小組]置頂嚮導集合貼: [新人請看][看雪iOS安全小組]置頂嚮導集合貼-『iOS安全』-看雪安全論壇· n
推薦閱讀:
※iOS有哪些好用的貼紙應用?
※iOS 上有哪些成功的應用是個人開發者開發的?
※iOS 上有哪些精美、優雅到藝術品水平的應用?
※ElemeUED Post #2
TAG:iOS |