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
個人學習記錄-常用抓包工具/技術的總結

TAG:iOS | iOS应用 | 网络安全 |