由于前一段时间的iOS 10.x 越狱又大放光芒,这次的主角mach_portal
,主要是通过mach port
的三个漏洞再加上luca添加了kpp绕过完成的越狱,刚好前一段时间粗略学习过mach ipc
的一些东西,作为巩固,我们来一步步探究mach_portal
…
这次的被利用的三个mach_port的漏洞分别为CVE-2016-7637
、CVE-2016-7644
、CVE-2016-7661
,我们一个一个来看
有关初探mach ipc
的可以看这里
CVE-2016-7637
基本概念
launchd
launchd是系统中第一个用户态进程,负责直接或间接的启动系统中的其他进程。他是由内核直接启动的
系统范围和用户范围的launch
用户范围的launch是在用户登录的时候执行的,每通过ssh登录一个用户就会创建一个launch进程,但是在iOS中只有一个launch>>系统范围的launch。
系统范围的launch(PID 1)是系统上唯一一个不能被终止的进程,如果这个launch进程被杀死了,就会导致kernel panic ,。只有当系统关闭的时候,launch才是最后一个退出的进程。
launch的核心任务是根据预定的安排或实际的需要来加载其他的应用程序,launch分为两种不同的后台服务:
- Launch Daemon是系统级的后台服务,一般和用户没有交互,由系统自动启动
- 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
前文中我们已经讲过,port
是mach
中几乎所有其他对象实现的基础,消息在端口之间传递,并允许对消息进行各种操作,在消息的基础上又实现了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通讯中的独立的空间
而port
和name
则是通过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_object
的ie_bits
指针,ie_bits
字段的低16位是ipc_entry
的uref
数量,最大值是0xFFFF
当一个进程接收到一个“复杂消息”时,有一个ipc_right_copyout
函数会复制当前端口的权限到task
的ipc_entry
的table
以下是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权限的线程。