浅析mach_portal(1)— CVE-2016-7637

由于前一段时间的iOS 10.x 越狱又大放光芒,这次的主角mach_portal,主要是通过mach port的三个漏洞再加上luca添加了kpp绕过完成的越狱,刚好前一段时间粗略学习过mach ipc的一些东西,作为巩固,我们来一步步探究mach_portal

这次的被利用的三个mach_port的漏洞分别为CVE-2016-7637CVE-2016-7644CVE-2016-7661,我们一个一个来看

有关初探mach ipc的可以看这里

CVE-2016-7637

基本概念

launchd

launchd是系统中第一个用户态进程,负责直接或间接的启动系统中的其他进程。他是由内核直接启动的

系统范围和用户范围的launch

  1. 用户范围的launch是在用户登录的时候执行的,每通过ssh登录一个用户就会创建一个launch进程,但是在iOS中只有一个launch>>系统范围的launch。

  2. 系统范围的launch(PID 1)是系统上唯一一个不能被终止的进程,如果这个launch进程被杀死了,就会导致kernel panic ,。只有当系统关闭的时候,launch才是最后一个退出的进程。

launch的核心任务是根据预定的安排或实际的需要来加载其他的应用程序,launch分为两种不同的后台服务:

  1. Launch Daemon是系统级的后台服务,一般和用户没有交互,由系统自动启动
  2. Launch Agent是Launch Daemon的一种,但Agent是用户级别的后台服务,只有在用户登录时才会启动,所以会和用户交互,甚至有的还有GUI。
    ps:在iOS中只有 Launch Daemon

在iOS中有两个特殊的Launch Daemon,SpringBoard和lockdownd。

springboard

由于iOS不需要登录系统,所以springboard是自启动,同时作为一个GUI shell提供了用户界面

springboard在创建GUI的时候,会在/Applications和/var/mobile/Applications目录下寻找应用,读取每一个应用的Info.plist文件,找到CFBUndleIcons属性对应的应用程序图标并在设备上显示这些图标。

lockdownd

lockdownd和launch一样以root身份运行在其他进程之前。lockdownd由launch启动,负责设备激活、备份、奔溃报告、设备同步等,但又像一个小型的launch有着自己要启动的服务列表

1
2
3
4
5
6
7
8
<key>com.apple.afc2</key>
<dict>
<key>AllowUnactivatedService</key> <true />
<key>Label</key>
<string>com.apple.afc2</string> <key>ProgramArguments</key> <array>
<string>/usr/libexec/afcd</string> <string>--lockdown</string> <string>-d</string> <string>/</string>
</array>
</dict>

port

前文中我们已经讲过,portmach中几乎所有其他对象实现的基础,消息在端口之间传递,并允许对消息进行各种操作,在消息的基础上又实现了mach内核的ipc

我们首先看看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
36
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;
#if MACH_ASSERT
uintptr_t buf[IP_CALLSTACK_MAX];
ipc_port_callstack_init_debug(&buf[0], IP_CALLSTACK_MAX);
#endif /* MACH_ASSERT */
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);
#if MACH_ASSERT
ipc_port_init_debug(port, &buf[0], IP_CALLSTACK_MAX);
#endif /* MACH_ASSERT */
/* unlock space after init */
is_write_unlock(space);
*namep = name;
*portp = port;
return KERN_SUCCESS;
}

可以看到在ipc_port_alloc中主要有两个变量port和name,在ipc_port_init初始化时则还有一个space,那么先看看这个space变量到底是做什么的呢,根据字面意思猜测space是一个ipc通讯中的独立的空间

portname则是通过ipc_object_alloc获取的(ipc_object.c)

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
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;
......
/* 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;
}

在ipc_space.h我们看到有关space的数据结构

1
2
3
4
5
6
7
8
9
10
11
struct ipc_space {
lck_spin_t is_lock_data;
ipc_space_refs_t is_bits; /* holds refs, active, growing */
ipc_entry_num_t is_table_size; /* current size of table */
ipc_entry_num_t is_table_free; /* count of free elements */
ipc_entry_t is_table; /* an array of entries */
task_t is_task; /* associated task */
struct ipc_table_size *is_table_next; /* info for larger table */
ipc_entry_num_t is_low_mod; /* lowest modified entry during growth */
ipc_entry_num_t is_high_mod; /* highest modified entry during growth */
};

ipc_space.c中我们可以看到创建一个spcae时有一个table,而这个table存放了ipc port 的内容并且这个table是由ipc_entry_t创建的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kern_return_t
ipc_space_create(
ipc_table_size_t initial,
ipc_space_t *spacep)
{
ipc_space_t space;
ipc_entry_t table;
ipc_entry_num_t new_size;
mach_port_index_t index;
space = is_alloc();
if (space == IS_NULL)
return KERN_RESOURCE_SHORTAGE;
table = it_entries_alloc(initial);
if (table == IE_NULL) {
is_free(space);
return KERN_RESOURCE_SHORTAGE;
}
......

再来看看ipc_entry的数据结构

1
2
3
4
5
6
7
8
9
struct ipc_entry {
struct ipc_object *ie_object;
ipc_entry_bits_t ie_bits;
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
2
#define IE_BITS_UREFS_MASK 0x0000ffff /* 16 bits of user-reference */
#define IE_BITS_UREFS(bits) ((bits) & IE_BITS_UREFS_MASK)

ipc_entry有一个指向ipc_objectie_bits指针,ie_bits字段的低16位是ipc_entryuref数量,最大值是0xFFFF

当一个进程接收到一个“复杂消息”时,有一个ipc_right_copyout函数会复制当前端口的权限到taskipc_entrytable

以下是ipc_right_copyout处理发送消息权限的代码

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
if (bits & MACH_PORT_TYPE_SEND) {
mach_port_urefs_t urefs = IE_BITS_UREFS(bits);
assert(port->ip_srights > 1);
assert(urefs > 0);
assert(urefs < MACH_PORT_UREFS_MAX);
if (urefs+1 == MACH_PORT_UREFS_MAX) {
if (overflow) {
/* leave urefs pegged to maximum */
port->ip_srights--;
ip_unlock(port);
ip_release(port);
return KERN_SUCCESS;
}
ip_unlock(port);
return KERN_UREFS_OVERFLOW;
}
port->ip_srights--;
ip_unlock(port);
ip_release(port);
......
entry->ie_bits = (bits | MACH_PORT_TYPE_SEND) + 1;
ipc_entry_modified(space, name, entry);
break;
}

以上代码中可以看出uref的值不会超过0xFFFE,在最下面的几行可以看到,对uref的数量没有限制,这样就容易造成溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case MACH_MSG_OOL_PORTS_DESCRIPTOR: {
mach_port_t *ports;
mach_msg_ool_ports_descriptor_t *dsc;
mach_msg_type_number_t j;
/*
* Destroy port rights carried in the message
*/
dsc = &daddr->ool_ports;
ports = (mach_port_t *) dsc->address;
for (j = 0; j < dsc->count; j++, ports++) {
mach_msg_destroy_port(*ports, dsc->disposition);
}

漏洞利用

具体的利用,在Ian Beer提供的write up中很详细了

当进程收到某条含有OOL描述符的复杂消息时,这条消息由于不符合进程的接收标准被销毁时,mach_msg_destroy_port会释放所有的port right,而当每条这种复杂消息被销毁时,其中的port right都会被释放,进而会减少uref的值,当uref为0时,ipc_entry被释放到free list中并被标记为“空闲状态”,由此一来这个ipc_entry可以被用来指向另一个ipc_object

所以说,在’Ian Beer‘的write up中,主要是通过launch中的com.apple.CoreServices.coreservicesd服务,由于launch包含这个服务的发送权限,这样一来,可以

  • 先通过漏洞,释放这个port right,
  • 然后注册大量的虚拟服务,利用类似于堆喷的手段,使其中的某个服务代替了launch中的com.apple.CoreServices.coreservicesd服务,
  • 于是launch会把send right发向我们的虚拟服务,
  • 当有任何进程查找com.apple.CoreServices.coreservicesd时,就会把我们具有接收权限的端口发送给他
  • 然后我们就可以通过中间人,来监听任意进程与com.apple.CoreServices.coreservicesd的通信
  • 当我们发送一些root服务到内核中,并通过中间人监听其通信,就可以在launch中启动一个新的root权限的线程。
文章目录
  1. 1. CVE-2016-7637
    1. 1.1. 基本概念
      1. 1.1.1. launchd
        1. 1.1.1.1. 系统范围和用户范围的launch
        2. 1.1.1.2. 在iOS中有两个特殊的Launch Daemon,SpringBoard和lockdownd。
          1. 1.1.1.2.1. springboard
          2. 1.1.1.2.2. lockdownd
      2. 1.1.2. port
    2. 1.2. 漏洞利用
|