CVE-2016-7637---再谈Mach IPC

为学大病在好名。

0x00 摘要

​ 去年在分析CVE-2016-1757时,初步的接触了MachIPC系统中使用的Message,在分析最近的一系列与IPC模块相关的漏洞时,又加强对IPC模块的理解,所以通过一到两篇文章梳理一下最近的学习总结与心得体会。

​ 关于CVE-2016-7637这个漏洞的描述有很多资料了,是一个攻击Mach的内核IPC模块的漏洞,本文最后会对漏洞做出比较详细的解释,这里给出几个链接,不熟悉这个漏洞的读者可以先了解一下。

黑云压城城欲摧 - 2016年iOS公开可利用漏洞总结

mach portal漏洞利用的一些细节

Broken kernel mach port name uref

0x01 什么是Port

​ 对于一般的开发者,在用到Port的时候可以简单的将Port理解为进程间通信所使用的类似于Socket的东西,可以用他来发送消息,也可以用来接收消息。

​ 下面我们来一步一步的构建对整个模块的理解。

​ 已经知道Port是什么的读者可以跳过这一个部分。

1.1 利用Port传递数据

Port最简单的可以理解为一个内核中的消息队列。不同的Task通过这个消息队列相互传递数据。而Port就是用于找到这个队列的索引。

利用Port传递数据

1.2 Port、Port Name 与 Right

通过函数mach_port_allocate就可以在内核中建立一个消息队列,并获取一个与之对应的的Port。代码如下。

1
2
mach_port_t p;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);

通过查看内核源码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* Purpose:
* Allocates a right in a space. Like mach_port_allocate_name,
* except that the implementation picks a name for the right.
* The name may be any legal name in the space that doesn't
* currently denote a right.
*/

kern_return_t
mach_port_allocate(
ipc_space_t space,
mach_port_right_t right,
mach_port_name_t *namep)
{
kern_return_t kr;
mach_port_qos_t qos = qos_template;

kr = mach_port_allocate_full (space, right, MACH_PORT_NULL,
&qos, namep);
return (kr);
}

仔细看的话会发现,对我们刚刚申请的p出现了好几个解释,一下就晕了。

  • 在应用层代码中,p被定义为mach_port_t
  • 在内核中代码中,namepmach_port_name_t
  • 在注释中又说,Allocates a RIGHT in a space。

1.2.1 Port与Port Name

通过调试器观察一下,可以发现,

1
(lldb) p p
(mach_port_t) $0 = 3331
1
* frame #0: 0xffffff801d4ee11d kernel`mach_port_allocate_full(space=0xffffff8024ceac00, right=1, proto=0x0000000000000000, qosp=0xffffff887d7b3ef0, namep=0xffffff887d7b3eec

在应用层的mach_port_t是一个已经经过代码处理的类似于Socket的一个整数,来表示这个Port

namep在内核之中是一个地址,指向了一块用来索引Port的内存,具体的实现在本文的后面会有更详细的解释。

1.2.2 Right

这里注释所说的Right简单的理解,其实是一个Port和对这个Port进行访问的权限。每一个Port代表的消息队列并不是可以任意访问的,需要有对这个队列的访问权限。各种权限在头文件中的定义如下。

1
2
3
4
5
6
#define MACH_PORT_RIGHT_SEND      ((mach_port_right_t) 0)
#define MACH_PORT_RIGHT_RECEIVE ((mach_port_right_t) 1)
#define MACH_PORT_RIGHT_SEND_ONCE ((mach_port_right_t) 2)
#define MACH_PORT_RIGHT_PORT_SET ((mach_port_right_t) 3)
#define MACH_PORT_RIGHT_DEAD_NAME ((mach_port_right_t) 4)
#define MACH_PORT_RIGHT_NUMBER ((mach_port_right_t) 5)

每种Right都有不同的含义,可以自行查阅文档。

这里需要简单的提一下的就是每一个Port都有且只有Task对其拥有RECEIVE的权限,SEND的权限不限。拥有MACH_PORT_RIGHT_RECEIVE时也可以对Port进行消息的Send。

PORT_NAME_RIGHT

1.3 Port的具体实现

阅读mach_port_allocate_full函数的源码,最终在一个port的创建流程中,最主要的函数是ipc_port_alloc以及在其实现中调用的ipc_object_alloc

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
kern_return_t
ipc_port_alloc(
ipc_space_t space,
mach_port_name_t *namep,
ipc_port_t *portp)
{
ipc_port_t port;
mach_port_name_t name;
kern_return_t kr;

kr = ipc_object_alloc(space, IOT_PORT,
MACH_PORT_TYPE_RECEIVE, 0,
&name, (ipc_object_t *) &port);
if (kr != KERN_SUCCESS)
return kr;

/* port and space are locked */
ipc_port_init(port, space, name);
[...]

*namep = name; <--namep是从这里来的。
*portp = port;

return KERN_SUCCESS;
}

很明显,portname两个变量都是由函数ipc_object_alloc中获取的。关键源码如下。

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
38
39
40
41
42
kern_return_t
ipc_object_alloc(
ipc_space_t space,
ipc_object_type_t otype,
mach_port_type_t type,
mach_port_urefs_t urefs,
mach_port_name_t *namep,
ipc_object_t *objectp)
{
ipc_object_t object;
ipc_entry_t entry;
kern_return_t kr;

[...]

//从zone中申请内存,这个object就是ipc_port
object = io_alloc(otype);

[...]

//获取namep和entry
*namep = CAST_MACH_PORT_TO_NAME(object);
kr = ipc_entry_alloc(space, namep, &entry);
if (kr != KERN_SUCCESS) {
io_free(otype, object);
return kr;
}
/* space is write-locked */

//设置关键的参数
entry->ie_bits |= type | urefs;
entry->ie_object = object;
ipc_entry_modified(space, *namep, entry);

io_lock(object);

object->io_references = 1; /* for entry, not caller */
object->io_bits = io_makebits(TRUE, otype, 0);

*objectp = object;
return KERN_SUCCESS;
}

1.3.1 IPC Space和IPC Entry

​ 细心的读者会发现前面有个叫做space的参数没有解释。这里又出现了一个新的结构叫做entry。他们是有关系的,这里我们来一起解释一下。

Each task has a private IPC spacea namespace for portsthat is represented by the ipc_space structure in the kernel.

Mac OS X Internals

每一个Task都有一个自己独立的IPC的数据空间,就是这里的space。他的数据结构是定义如下。

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
38
39
40
41
// osfmk/ipc/ipc_space.h

typedef natural_t ipc_space_refs_t;

struct ipc_space {
decl_mutex_data(,is_ref_lock_data)
ipc_space_refs_t is_references;

decl_mutex_data(,is_lock_data)

// is the space active?
boolean_t is_active;

// is the space growing?
boolean_t is_growing;

// table (array) of IPC entries
// 这个是最重要的,存放了所有的entry
ipc_entry_t is_table;

// current table size
ipc_entry_num_t is_table_size;

// information for larger table
struct ipc_table_size *is_table_next;

// splay tree of IPC entries (can be NULL)
struct ipc_splay_tree is_tree;

// number of entries in the tree
ipc_entry_num_t is_tree_total;

// number of "small" entries in the tree
ipc_entry_num_t is_tree_small;

// number of hashed entries in the tree
ipc_entry_num_t is_tree_hash;

// for is_fast_space()
boolean_t is_fast;
};

而我们研究的对象 Mach Ports全都存储在is_table这个数组中,这个数组就是由ipc_entry组成的。

1
2
3
4
5
6
7
8
9
struct ipc_entry {
struct ipc_object *ie_object; //ipc_port_t
ipc_entry_bits_t ie_bits; //gen|0|0|0|capability|user reference
mach_port_index_t ie_index;
union {
mach_port_index_t next; /* next in freelist, or... */
ipc_table_index_t request; /* dead name request notify */
} index;
};

用一张书上的图,很清楚的表明了他们之间的关系。

1.3.2 ipc_port

相对而言ipc_port的数据结构就较为简单了。在复制给spaceie_object之后,通过ipc_port_init函数的初始化,就完成了port的创建了。

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
ipc_port_init(
ipc_port_t port,
ipc_space_t space,
mach_port_name_t name)
{
/* port->ip_kobject doesn't have to be initialized */

port->ip_receiver = space;
port->ip_receiver_name = name;

port->ip_mscount = 0;
port->ip_srights = 0;
port->ip_sorights = 0;

port->ip_nsrequest = IP_NULL;
port->ip_pdrequest = IP_NULL;
port->ip_requests = IPR_NULL;

port->ip_premsg = IKM_NULL;
port->ip_context = 0;

port->ip_sprequests = 0;
port->ip_spimportant = 0;
port->ip_impdonation = 0;
port->ip_tempowner = 0;

port->ip_guarded = 0;
port->ip_strict_guard = 0;
port->ip_impcount = 0;

port->ip_reserved = 0;

ipc_mqueue_init(&port->ip_messages,
FALSE /* !set */, NULL /* no reserved link */);
}

这里同样的用书上的一张图就可以很简单的解释清楚了。

ipc_port

0x02 POC的分析

​ POC原来的writeup在这里

​ 原文已经解释的非常清楚了,我就不画蛇添足了。简单记录一下我自己在分析的过程中的一些问题。

2.1 port的user reference计数代表了什么?

​ 一个portuser reference只表示了某个entrytaskspace中被多少个地方使用,和entry实际指向哪个port没有关系。

2.2 ipc_right_dealloc函数是只释放了entry还是同时也在内存中释放了port?

ipc_right_dealloc函数相关部分源码如下:

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
38
39
40
41
42
43
44
45
46
47
48
kern_return_t
ipc_right_dealloc(
ipc_space_t space,
mach_port_name_t name,
ipc_entry_t entry)
{
ipc_port_t port = IP_NULL;
ipc_entry_bits_t bits;
mach_port_type_t type;

bits = entry->ie_bits;
type = IE_BITS_TYPE(bits);

assert(is_active(space));
switch (type) {
[...]
case MACH_PORT_TYPE_SEND: {
[...]
port = (ipc_port_t) entry->ie_object;
[...]
//如果在task内entry的reference已经为1了就
//释放entry
//如果计数不为1,就将计数减一
if (IE_BITS_UREFS(bits) == 1) {
if (--port->ip_srights == 0) {
nsrequest = port->ip_nsrequest;
if (nsrequest != IP_NULL) {
port->ip_nsrequest = IP_NULL;
mscount = port->ip_mscount;
}
}

[...]
entry->ie_object = IO_NULL;
ipc_entry_dealloc(space, name, entry);
is_write_unlock(space);
ip_release(port);

} else {
ip_unlock(port);
entry->ie_bits = bits-1; /* decrement urefs */
ipc_entry_modified(space, name, entry);
is_write_unlock(space);
}
[...]

return KERN_SUCCESS;
}

所以当entry计数为1的时候,调用了ipc_entry_deallocipc_entry_dealloc不会将entry对应的内存释放,而是将其放入一个free_list等待重复使用。entry的内存不会释放,而且entryis_table中的index也并不会改变,只是被放到了一个结构管理的队列中去了。

对于Port来说,内核调用了ip_release,这个函数的作用是减少ipc_object自身的reference,如果port的索引变为0了,那就会被释放,如果系统中还有其他的进程在使用这个port,那么这个port就不会被释放。

2.3 如何通过调试器调试漏洞触发的现场?

一开始我想的方法是对ipc_right_copyout下条件断点,条件是entry->ie_bits&0xffff == 0xfffe。但是因为ipc_right_copyout这个函数在内核中的调用太过于频繁,导致虚拟机跑太卡了。

只能通过逆向,在汇编代码处下断点。(内核版本10.12_16A323)

对应的就是出bug的代码段。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (urefs+1 == MACH_PORT_UREFS_MAX) {
if (overflow) {
/* leave urefs pegged to maximum */ <---- (1)

port->ip_srights--;
ip_unlock(port);
ip_release(port);
return KERN_SUCCESS;
}

ip_unlock(port);
return KERN_UREFS_OVERFLOW;
}

所以通过断点

1
b *(0xffffff80002e6fbb + kslide)

就可以得到漏洞触发时的情况。

2.4 port替换的原理是什么?

port是通过port name在task中来获取的。在前文中提到,namep是通过函数ipc_entry_alloc来获取的。查看获取到namep的核心代码如下:

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
38
39
40
41
42
43
44
kern_return_t
ipc_entry_claim(
ipc_space_t space,
mach_port_name_t *namep,
ipc_entry_t *entryp)
{
ipc_entry_t entry;
ipc_entry_t table;
mach_port_index_t first_free;
mach_port_gen_t gen;
mach_port_name_t new_name;

table = &space->is_table[0];

first_free = table->ie_next;
assert(first_free != 0);

entry = &table[first_free]; //[1]
table->ie_next = entry->ie_next;
space->is_table_free--;

assert(table->ie_next < space->is_table_size);

/*
* Initialize the new entry. We need only
* increment the generation number and clear ie_request.
*/

gen = IE_BITS_NEW_GEN(entry->ie_bits); //[2]
entry->ie_bits = gen;
entry->ie_request = IE_REQ_NONE;

/*
* The new name can't be MACH_PORT_NULL because index
* is non-zero. It can't be MACH_PORT_DEAD because
* the table isn't allowed to grow big enough.
* (See comment in ipc/ipc_table.h.)
*/

new_name = MACH_PORT_MAKE(first_free, gen); //[3]
assert(MACH_PORT_VALID(new_name));
*namep = new_name;
*entryp = entry;

return KERN_SUCCESS;
}

通过[1]可以看到,正如2.2节提到的一样,entrytable中的index是不变的。

通过[2]可以看到,每次使用entry来存放一个新port时,gen的值会加1。

通过[3]可以看到,namep就是通过这两个参数生成的。

因为index是不变的,所以在通过漏洞释放target_port之后,不断的对目标entry进行申请和释放,就可以通过整形溢出,使得gen变成和taget_port释放之前使用的entry相同。

那么就实现了在port_name不变的情况下替换了port的内核对象。

0x03 小结

通过CVE-2016-7637的分析和研究加深了对port这个数据结构的理解,并且通过对poc的分析,体现了一个port在单个task中的状态变化,实际上是ipc_spaceipc_entry状态变化。

接下来就要分析学习CVE-2016-7644,通过对CVE-2016-7644的分析学习,可以更加深入的理解port在内核中状态的变化。也就是port自身的port->srightsio_reference的状态变化及漏洞的利用。

参考

  1. 《Mac OS X Internals》
  1. Broken kernel mach port name uref
文章目录
  1. 1. 0x00 摘要
  2. 2. 0x01 什么是Port
    1. 2.1. 1.1 利用Port传递数据
    2. 2.2. 1.2 Port、Port Name 与 Right
      1. 2.2.1. 1.2.1 Port与Port Name
      2. 2.2.2. 1.2.2 Right
    3. 2.3. 1.3 Port的具体实现
      1. 2.3.1. 1.3.1 IPC Space和IPC Entry
      2. 2.3.2. 1.3.2 ipc_port
  3. 3. 0x02 POC的分析
    1. 3.1. 2.1 port的user reference计数代表了什么?
    2. 3.2. 2.2 ipc_right_dealloc函数是只释放了entry还是同时也在内存中释放了port?
    3. 3.3. 2.3 如何通过调试器调试漏洞触发的现场?
    4. 3.4. 2.4 port替换的原理是什么?
  4. 4. 0x03 小结
  5. 5. 参考
,