为学大病在好名。
0x00 摘要 去年在分析CVE-2016-1757 时,初步的接触了Mach
在IPC
系统中使用的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
就是用于找到这个队列的索引。
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
。
在内核中代码中,namep
是mach_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。
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; ipc_port_init(port, space, name); [...] *namep = name; <--namep是从这里来的。 *portp = port; return KERN_SUCCESS; }
很明显,port
和name
两个变量都是由函数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; [...] object = io_alloc(otype); [...] *namep = CAST_MACH_PORT_TO_NAME(object); kr = ipc_entry_alloc(space, namep, &entry); if (kr != KERN_SUCCESS) { io_free(otype, object); return kr; } entry->ie_bits |= type | urefs; entry->ie_object = object; ipc_entry_modified(space, *namep, entry); io_lock(object); object->io_references = 1 ; 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 space a 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 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) boolean_t is_active; boolean_t is_growing; ipc_entry_t is_table; ipc_entry_num_t is_table_size; struct ipc_table_size *is_table_next; struct ipc_splay_tree is_tree; ipc_entry_num_t is_tree_total; ipc_entry_num_t is_tree_small; ipc_entry_num_t is_tree_hash; 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_entry_bits_t ie_bits; mach_port_index_t ie_index; union { mach_port_index_t next; ipc_table_index_t request; } index; };
用一张书上的图,很清楚的表明了他们之间的关系。
1.3.2 ipc_port 相对而言ipc_port的数据结构就较为简单了。在复制给space
的ie_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_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 , NULL ); }
这里同样的用书上的一张图就可以很简单的解释清楚了。
0x02 POC的分析 POC原来的writeup在这里 。
原文已经解释的非常清楚了,我就不画蛇添足了。简单记录一下我自己在分析的过程中的一些问题。
2.1 port的user reference计数代表了什么? 一个port
的user reference
只表示了某个entry
在task
的space
中被多少个地方使用,和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; [...] 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 ; ipc_entry_modified(space, name, entry); is_write_unlock(space); } [...] return KERN_SUCCESS; }
所以当entry
计数为1的时候,调用了ipc_entry_dealloc
,ipc_entry_dealloc
不会将entry
对应的内存释放,而是将其放入一个free_list
等待重复使用。entry
的内存不会释放,而且entry
在is_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) { <---- (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]; 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); 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); assert(MACH_PORT_VALID(new_name)); *namep = new_name; *entryp = entry; return KERN_SUCCESS; }
通过[1]可以看到,正如2.2节提到的一样,entry
在table
中的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_space
和ipc_entry
状态变化。
接下来就要分析学习CVE-2016-7644,通过对CVE-2016-7644的分析学习,可以更加深入的理解port在内核中状态的变化。也就是port
自身的port->srights
和io_reference
的状态变化及漏洞的利用。
参考
《Mac OS X Internals》
Broken kernel mach port name uref