|
引言:大家都知道知名意大利天才少年Luca放出来的针对Head.msgh_request_port); // (1)
RetCode = task_threads(
target_task,
(thread_act_array_t *)&(OutP->act_list.address),
&OutP->act_listCnt);
task_deallocate(target_task);
我们可以看到任务端口被转换成了任务结构体指针,它接下来被存储在局部变量 target_task中,这个局部变量的生存周期是这个函数调用的生存周期。
下面是来自task_threads的相关代码:
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>); // (2)
}
...
}
这段代码在线程列表中不断循环迭代地收集struct thread指针,然后把那些结构体线程转化为线程端口,并返回。代码中有少量的锁,但是它们是不相关的。
如果任务同时正在执行一个带有suid标志的程序,会发生什么?
相关的exec代码部分有两点,在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); // (3)
紧跟着的是对ipc_thread_reset的调用:
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; // (4)
我们把执行exec的进程命名为B,调用task_threads()的进程命名为A,想象下面的交叉执行过程:
A:
target_task = convert_port_to_task(
In0P->Head.msgh_request_port); // (1)
A从栈上获得了指向进程B的任务结构体的指针。
B:
ipc_kobject_set(old_kport, IKO_NULL, IKOT_NONE); // (3)
B执行了带有suid权限的程序,并且使旧任务端口无效,因此它不再拥有任务结构体指针。
B:
thread->ith_self = new_kport; // (4)
B分配了新的线程端口并启动
A:
((ipc_port_t *) thread_list)<i> = 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 a task port then
* snapshot the task exec token before the mig routine call.
*/
ipc_port_t port = request->ikm_header->msgh_remote_port;
if (IP_VALID(port) && ip_kotype(port) == IKOT_TASK) {
task = convert_port_to_task_with_exec_token(port, &exec_token);
}
(*ptr->routine)(request->ikm_header, reply->ikm_header);
/* Check if the exec token changed during the 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 = 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_t task,
exception_mask_t exception_mask,
ipc_port_t new_port,
exception_behavior_t new_behavior,
thread_state_flavor_t new_flavor)
{
...
itk_lock(task); // (2)
for (i = FIRST_EXCEPTION; i < EXC_TYPES_COUNT; ++i) {
if ((exception_mask & (1 exc_actions<i>.port;
task->exc_actions<i>.port = ipc_port_copy_send(new_port); // (3)
task->exc_actions<i>.behavior = new_behavior;
task->exc_actions<i>.flavor = new_flavor;
task->exc_actions<i>.privileged = 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, IKO_NULL, IKOT_NONE); // (5)
task->exec_token += 1;
ip_unlock(old_kport);
ipc_kobject_set(new_kport, (ipc_kobject_t) task, IKOT_TASK);
for (i = FIRST_EXCEPTION; i < EXC_TYPES_COUNT; i++) {
...
if (!task->exc_actions<i>.privileged) {
old_exc_actions<i> = task->exc_actions<i>.port;
task->exc_actions<i>.port = IP_NULL; // (6)
}
}
itk_unlock(task); //(7)
我们寻找下面这样的交叉执行情景:
A:
task = convert_port_to_task(In0P->Head.msgh_request_port); // (1)
B:
itk_lock(task); // (4)
ipc_kobject_set_atomically(old_kport, 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 = 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, 0x1000000};
setrlimit(RLIMIT_STACK, lim);
setuid(0);
char* argv[2] = {"/bin/bash", 0};
execve("/bin/bash", argv, 0);
shellcode把栈长度设置回一个很大的值,用setuid(0)来避免bash丢失权限,最后打开一个shell。
这个Exp应该会在MacOS/OS X 版本 |
|