-
-
[翻译]task_t指针重大风险预报——PoC task_t considered harmful - many XNU EoPs
-
发表于:
2016-12-30 12:37
5745
-
[翻译]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交互。
我们就可以得到IOKit对象,并通过task struct指针,与一个euid 为0的进程的虚拟内存进行交互。
(还有一种攻击方式:你也可以通过强制产生一个恢复task struct的服务程序来逃逸沙盒)
(你也可以通过强制launchd生成一个将会重新利用已被释放的task struct的服务,来实现沙盒逃逸。)
反之若这些IOKit驱动程序引用task struct,没关系!
当然,再进一步,即使这些IOKit驱动程序对task struct作了引用,也无所谓!
(至少在没有suid二进制程序运行时)
(至少在suid二进制程序运行时没有问题。)
因为用户端的用户空间客户端在time A拥有发送至task port的权限,但当从task port 传递至IOKit并不意味着仍然有发送权限,仅仅是因为IOKIt驱动程序实际调用的是task struct指针。
就IOSurface而言,这个允许我们方便的发送任意代码至虚拟内存euid为0的读写区域。
以IOSurface为例,这使得我们可以轻松的map euid为0的进程的虚拟内存里的任意可读写区域,并且重新写入。
大量IOKit驱动程序存储taks struct指针,使用它们操作用户空间虚拟内存(如ioacceleratorFamily2,IOthunderboltFamily,IOSurface)或者依赖于taks struct指针去执行权限检测(如IOHIDFamily)
另外一个有趣的例子是stack中的task struct指针
MIG文件中相对应的用户层/内核层中的task port如下格式
type task_t = mach_port_t
#if KERNEL_SERVER
intran: task_t convert_port_to_task(mach_port_t)
convert_port_to_task 如下:
task_t
convert_port_to_task(
ipc_port_t port)
{
task_t task = TASK_NULL;
if (IP_VALID(port)) {
ip_lock(port);
if ( ip_active(port) &&
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指针就依然有效。
这就产生了大量的有趣的条件竞争。
grep所有.defs文件的源代码,需要一个task_t来找到它们;-)
在这个exp中,我将证明最有趣的环节:task_threads
让我们一起来看一下task_threads实际是如何工作的,包括由MIG产生的核心代码。
在 task_server.c(一个自动产生的文件,若找不到该文件,先build XNU)
target_task = convert_port_to_task(In0P->Head.msgh_request_port);
RetCode = task_threads(target_task, (thread_act_array_t *)&(OutP->act_list.address), &OutP->act_listCnt);
task_deallocate(target_task);
This gives us back the task struct from the task port then calls task_threads:
(unimportant bits removed)
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); i < actual;
++i, thread = (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] = convert_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 task)
{
ipc_port_t old_kport, new_kport;
ipc_port_t old_sself;
ipc_port_t old_exc_actions[EXC_TYPES_COUNT];
int i;
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 = ipc_port_make_send(new_kport);
ipc_kobject_set(old_kport, IKO_NULL, IKOT_NONE); <-- point (1)
... then calls:
ipc_thread_reset(
thread_t thread)
{
ipc_port_t old_kport, new_kport;
ipc_port_t old_sself;
ipc_port_t old_exc_actions[EXC_TYPES_COUNT];
boolean_t has_old_exc_actions = FALSE;
int i;
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; <-- point (2)
Point (1)从旧的task port清除task struct pointer,然后重新分配一个新的port给task
Point (2)对应的 thread port同上.
调用执行exec的进程B和处理task_threads()的进程A以及imagine
下面是执行过程:
Process A: target_task = convert_port_to_task(In0P->Head.msgh_request_port); //
得到指向B进程的task struct 指针
Process B: ipc_kobject_set(old_kport, IKO_NULL, IKOT_NONE); //
B进程使旧的task port失效以至于不(再)拥有task struct的指针
Process B: thread->ith_self = new_kport //
B进程重新分配一个新的thread ports和激活(并设置)他们
Process A: ((ipc_port_t *) thread_list)[i] = convert_thread_to_port(thread_list[i]); // A进程读取和转变为新的 thread port 对象!
这里最基本的问题不是这个特殊的资源(条件)竞争,事实上是当最先指定一个task struct指针后,你不能依赖拥有一个相同的euid的task struct 指针。
exploit:
这段利用代码说明一个euid为0进程的thread port竞争资源,
这段poc仅仅利用了这种条件竞争来得到一个euid为0的程序的线程端口。
一旦运行利用代码,我仅仅需要跟随(放置了)一小段ROP payload插入ret-slide。然后使用thread port 设置RIP到gadget添加了大量的rsp、X,随后会弹出shell,只需要运行一段时间,将会出现竞争情况。
测试系统 MacBookAir5,2 OS X 10.11.5(15F34)
在mac os10.12更优化的利用代码,对于内核版本不高于10.12的都有效。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2019-9-14 20:42
被kanxue编辑
,原因: