task_t指針重大風險預報——PoC task_t considered harmful - many XNU EoPs
CVE-2016-1757 是由於exec運行期間資源條件競爭導致port失效而產生的安全漏洞
CVE-2016-1757是一個涉及到在exec操作期間,埠結構順序失效的條件競爭漏洞。
詳情:
當一個suid二進位程序被執行,儘管task struct與執行程序前狀態保持一致,但是它執行前的task 和task port確實已失效。
當執行一個suid二進位程序時,雖然這個任務的舊任務以及線程埠已經無效,但是它的任務結構卻還保持著相同的狀態。
在此期間,執行前task沒有自我複製和產生一個新的task。
如果沒有fork或者創建新的任務,
這就意味著任何指向之前task struct的指針如今指向一個進程euid為0的進程的task struct。(即擁有root許可權的執行環境)
許多IOKit驅動程序都保存著task struct指針作為它們的一部分,可以參考我之前的bug報告中的一些例子。
在這些例子中,我提到了另一個bug,若IOKit驅動程序未引用task struct,則如果殺死相應的task,然後fork和執行一段suid root二進位程序,
我們能夠通過一個euid 為0的虛擬內存的task struct指針獲得IOKit object交互。n
我們就可以得到IOKit對象,並通過task struct指針,與一個euid 為0的進程的虛擬內存進行交互。n
(還有一種攻擊方式:你也可以通過強制產生一個恢復task struct的服務程序來逃逸沙盒)n
(你也可以通過強制launchd生成一個將會重新利用已被釋放的task struct的服務,來實現沙盒逃逸。)n
反之若這些IOKit驅動程序引用task struct,沒關係!n
當然,再進一步,即使這些IOKit驅動程序對task struct作了引用,也無所謂!n
(至少在沒有suid二進位程序運行時)n
(至少在suid二進位程序運行時沒有問題。)n
因為用戶端的用戶空間客戶端在time A擁有發送至task port的許可權,但當從task port 傳遞至IOKit並不意味著仍然有發送許可權,僅僅是因為IOKIt驅動程序實際調用的是task struct指針。n
就IOSurface而言,這個允許我們方便的發送任意代碼至虛擬內存euid為0的讀寫區域。n
以IOSurface為例,這使得我們可以輕鬆的map euid為0的進程的虛擬內存里的任意可讀寫區域,並且重新寫入。n
大量IOKit驅動程序存儲taks struct指針,使用它們操作用戶空間虛擬內存(如ioacceleratorFamily2,IOthunderboltFamily,IOSurface)或者依賴於taks struct指針去執行許可權檢測(如IOHIDFamily)n
另外一個有趣的例子是stack中的task struct指針n
MIG文件中相對應的用戶層/內核層中的task port如下格式n
type task_t = mach_port_tn
#if KERNEL_SERVERn
intran: task_t convert_port_to_task(mach_port_t)n
convert_port_to_task 如下:n
task_tn
convert_port_to_task(n
ipc_port_t port)n
{n
task_t task = TASK_NULL;n
if (IP_VALID(port)) {n
ip_lock(port);n
if ( ip_active(port) &&n
ip_kotype(port) == IKOT_TASK ) {
task = (task_t)port->ip_kobject;
assert(task != TASK_NULL);
task_reference_internal(task);
}
ip_unlock(port);
}
return (task);
}
task port 轉變為相對應的task struct指針,該指針引用於task struct,但僅僅是確保它不被釋放,
而非為了執行二進位程序導致它自己的euid不變。
而不是保證它的euid不會變成suid root程序執行的結果。
儘管task port不再有效,但只要port lock解除鎖定,task就可以執行標記為suid的二進位程序,task strut指針就依然有效。
這就產生了大量的有趣的條件競爭。
ngrep所有.defs文件的源代碼,需要一個task_t來找到它們;-)
在這個exp中,我將證明最有趣的環節:task_threads
讓我們一起來看一下task_threads實際是如何工作的,包括由MIG產生的核心代碼。
在 task_server.c(一個自動產生的文件,若找不到該文件,先build XNU)
target_task =nconvert_port_to_task(In0P->Head.msgh_request_port);
RetCode = task_threads(target_task,n(thread_act_array_t *)&(OutP->act_list.address),n&OutP->act_listCnt);
task_deallocate(target_task);
This gives us backnthe task struct from the task port then calls task_threads:
(unimportant bitsnremoved)
task_threads(
task_t task,
thread_act_array_t *threads_out,
mach_msg_type_number_t *count)
{
...
for (thread = (thread_t)queue_first(&task->threads);ni < actual;
++i, thread =n(thread_t)queue_next(&thread->task_threads)) {
thread_reference_internal(thread);
thread_list[j++] = thread;
}
...
for (i = 0; i < actual; ++i)
((ipc_port_t *) thread_list)[i] =nconvert_thread_to_port(thread_list[i]);
}
...
}
task_threads利用task struct 指針通過threads列表迭代threads(來遍歷線程列表),
然後creates發送指令給task_threads,task_threads發送指令返回給用戶空間,
然後賦予它們發送許可權,隨後在用戶空間得到發送返回
過程中出現鎖定和解鎖,但是鎖定和解鎖是不相關的。
如果task同時運行suid標記為root的二進程代碼會發生什麼?
執行代碼相關聯的兩部分主要是ipc_task_reset和ipc_thread_reset
void
ipc_task_reset(
task_t ntask)
{
ipc_port_t old_kport, new_kport;
ipc_port_t old_sself;
ipc_port_tnold_exc_actions[EXC_TYPES_COUNT];
intni;
new_kport = ipc_port_alloc_kernel();
if (new_kport == IP_NULL)
panic("ipc_task_reset");
itk_lock(task);
old_kport = task->itk_self;
if (old_kport == IP_NULL) {
itk_unlock(task);
ipc_port_dealloc_kernel(new_kport);
return;
}
task->itk_self = new_kport;
old_sself = task->itk_sself;
task->itk_sself =nipc_port_make_send(new_kport);
ipc_kobject_set(old_kport, IKO_NULL,nIKOT_NONE); <-- point (1)
... then calls:
ipc_thread_reset(
thread_t nthread)
{
ipc_port_t old_kport, new_kport;
ipc_port_t old_sself;
ipc_port_tnold_exc_actions[EXC_TYPES_COUNT];
boolean_t nhas_old_exc_actions = FALSE;
int ni;
new_kport = ipc_port_alloc_kernel();
if (new_kport == IP_NULL)
panic("ipc_task_reset");
thread_mtx_lock(thread);
old_kport = thread->ith_self;
if (old_kport == IP_NULL) {
thread_mtx_unlock(thread);
ipc_port_dealloc_kernel(new_kport);
return;
}
thread->ith_self = new_kport; <--npoint (2)
Point (1)從舊的task port清除task struct pointer,然後重新分配一個新的port給task
Point (2)對應的 thread port同上.
調用執行exec的進程B和處理task_threads()的進程A以及imagine
下面是執行過程:
ProcessnA: target_task = convert_port_to_task(In0P->Head.msgh_request_port); //
得到指向B進程的task struct 指針
Process B: ipc_kobject_set(old_kport,nIKO_NULL, IKOT_NONE); //
B進程使舊的task port失效以至於不(再)擁有task struct的指針
Process B: thread->ith_self = new_kport //n
B進程重新分配一個新的thread ports和激活(並設置)他們
Process A: ((ipc_port_t *) thread_list)[i] =nconvert_thread_to_port(thread_list[i]); // A進程讀取和轉變為新的 thread port 對象!
這裡最基本的問題不是這個特殊的資源(條件)競爭,事實上是當最先指定一個task struct指針後,你不能依賴擁有一個相同的euid的task struct 指針。
exploit:n
這段利用代碼說明一個euid為0進程的thread port競爭資源,n
這段poc僅僅利用了這種條件競爭來得到一個euid為0的程序的線程埠。n
一旦運行利用代碼,我僅僅需要跟隨(放置了)一小段ROP payload插入ret-slide。然後使用thread port 設置RIP到gadget添加了大量的rsp、X,隨後會彈出shell,只需要運行一段時間,將會出現競爭情況。n
測試系統 nMacBookAir5,2 OS X 10.11.5(15F34)
在mac os10.12更優化的利用代碼,對於內核版本不高於10.12的都有效。
推薦閱讀:
※雲出血(Cloudbleed):各知名互聯網品牌泄露用戶密鑰和敏感信息
※如何看待 2014 年 12 月 25 日網傳 12306 賬號信息泄漏(含明文密碼)一事?
※RETracer: Triaging Crashes by Reverse Execution from Partial Memory Dumps - Week 7
※個人學習記錄-常用抓包工具/技術的總結