iOS Exploiting

{{#include ../../banners/hacktricks-training.md}}

iOS Exploit Mitigations

  • Code Signing in iOS works by requiring every piece of executable code (apps, libraries, extensions, etc.) to be cryptographically signed with a certificate issued by Apple. When code is loaded, iOS verifies the digital signature against Apples trusted root. If the signature is invalid, missing, or modified, the OS refuses to run it. This prevents attackers from injecting malicious code into legitimate apps or running unsigned binaries, effectively stopping most exploit chains that rely on executing arbitrary or tampered code.
  • CoreTrust is the iOS subsystem responsible for enforcing code signing at runtime. It directly verifies signatures using Apples root certificate without relying on cached trust stores, meaning only binaries signed by Apple (or with valid entitlements) can execute. CoreTrust ensures that even if an attacker tampers with an app after installation, modifies system libraries, or tries to load unsigned code, the system will block execution unless the code is still properly signed. This strict enforcement closes many post-exploitation vectors that older iOS versions allowed through weaker or bypassable signature checks.
  • Data Execution Prevention (DEP) marque les régions mémoire comme non-exécutables à moins qu'elles ne contiennent explicitement du code. Cela empêche les attaquants d'injecter du shellcode dans des régions de données (comme le stack ou le heap) et de l'exécuter, les forçant à s'appuyer sur des techniques plus complexes comme le ROP (Return-Oriented Programming).
  • ASLR (Address Space Layout Randomization) randomise les adresses mémoire du code, des libraries, du stack et du heap à chaque exécution du système. Cela rend beaucoup plus difficile pour un attaquant de prédire où se trouvent des instructions ou gadgets utiles, brisant de nombreuses chaînes d'exploit qui dépendent d'agencements mémoire fixes.
  • KASLR (Kernel ASLR) applique le même concept de randomisation au kernel iOS. En mélangeant l'adresse de base du kernel à chaque boot, il empêche les attaquants de localiser de façon fiable les fonctions ou structures du kernel, augmentant la difficulté des exploits au niveau kernel qui viserait à prendre le contrôle complet du système.
  • Kernel Patch Protection (KPP), aussi connu sous AMCC (Apple Mobile File Integrity) sur iOS, surveille en continu les pages de code du kernel pour s'assurer qu'elles n'ont pas été modifiées. Si une altération est détectée — comme un exploit essayant de patcher des fonctions kernel ou d'insérer du code malveillant — l'appareil panique et redémarre immédiatement. Cette protection rend les exploits kernel persistants beaucoup plus difficiles, car les attaquants ne peuvent pas simplement hooker ou patcher les instructions du kernel sans provoquer un crash système.
  • Kernel Text Readonly Region (KTRR) est une fonctionnalité de sécurité matérielle introduite sur les appareils iOS. Elle utilise le contrôleur mémoire du CPU pour marquer la section de code (text) du kernel comme en lecture seule de façon permanente après le boot. Une fois verrouillée, même le kernel lui-même ne peut pas modifier cette région mémoire. Cela empêche les attaquants — et même du code privilégié — de patcher les instructions du kernel à l'exécution, fermant une grande classe d'exploits qui reposaient sur la modification directe du code kernel.
  • Pointer Authentication Codes (PAC) utilisent des signatures cryptographiques insérées dans des bits inutilisés des pointeurs pour vérifier leur intégrité avant utilisation. Lorsqu'un pointeur (comme une adresse de retour ou un function pointer) est créé, le CPU le signe avec une clé secrète ; avant la déréférence, le CPU vérifie la signature. Si le pointeur a été altéré, la vérification échoue et l'exécution s'arrête. Cela empêche les attaquants de forger ou réutiliser des pointeurs corrompus dans des exploits de corruption mémoire, rendant des techniques comme le ROP ou le JOP beaucoup plus difficiles à réaliser de manière fiable.
  • Privilege Access never (PAN) est une fonctionnalité matérielle qui empêche le kernel (mode privilégié) d'accéder directement à la mémoire user-space sauf s'il active explicitement cet accès. Cela arrête les attaquants ayant obtenu de l'exécution de code kernel de lire ou écrire facilement la mémoire utilisateur pour escalader des privilèges ou voler des données sensibles. En imposant une séparation stricte, PAN réduit l'impact des exploits kernel et bloque de nombreuses techniques courantes d'escalade de privilèges.
  • Page Protection Layer (PPL) est un mécanisme de sécurité iOS qui protège les régions mémoire critiques gérées par le kernel, surtout celles liées au code signing et aux entitlements. Il applique des protections d'écriture strictes en utilisant la MMU (Memory Management Unit) et des vérifications additionnelles, garantissant que même le code kernel privilégié ne peut pas modifier arbitrairement des pages sensibles. Cela empêche les attaquants ayant obtenu l'exécution au niveau kernel de manipuler des structures critiques pour la sécurité, rendant la persistance et les contournements du code-signing significativement plus difficiles.

Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)

The kernel used a zone allocator (kalloc) divided into fixed-size "zones." Each zone only stores allocations of a single size class.

From the screenshot:

Zone Name Element Size Example Use
default.kalloc.16 16 bytes Très petites structures du kernel, pointeurs.
default.kalloc.32 32 bytes Petites structures, en-têtes d'objets.
default.kalloc.64 64 bytes Messages IPC, tout petits buffers kernel.
default.kalloc.128 128 bytes Objets moyens comme des parties de OSObject.
default.kalloc.256 256 bytes Messages IPC plus grands, tableaux, structures de périphériques.
default.kalloc.1280 1280 bytes Grandes structures, métadonnées IOSurface/graphics.

How it worked:

  • Each allocation request gets rounded up to the nearest zone size. (E.g., a 50-byte request lands in the kalloc.64 zone).
  • Memory in each zone was kept in a free list — chunks freed by the kernel went back into that zone.
  • If you overflowed a 64-byte buffer, youd overwrite the next object in the same zone.

This is why heap spraying / feng shui was so effective: you could predict object neighbors by spraying allocations of the same size class.

The freelist

Inside each kalloc zone, freed objects werent returned directly to the system — they went into a freelist, a linked list of available chunks.

  • When a chunk was freed, the kernel wrote a pointer at the start of that chunk → the address of the next free chunk in the same zone.

  • The zone kept a HEAD pointer to the first free chunk.

  • Allocation always used the current HEAD:

  1. Pop HEAD (return that memory to the caller).

  2. Update HEAD = HEAD->next (stored in the freed chunks header).

  • Freeing pushed chunks back:

  • freed_chunk->next = HEAD

  • HEAD = freed_chunk

So the freelist was just a linked list built inside the freed memory itself.

Normal state:

Zone page (64-byte chunks for example):
[ A ] [ F ] [ F ] [ A ] [ F ] [ A ] [ F ]

Freelist view:
HEAD ──► [ F ] ──► [ F ] ──► [ F ] ──► [ F ] ──► NULL
(next ptrs stored at start of freed chunks)

Exploiter la freelist

Parce que les 8 premiers octets d'un free chunk = freelist pointer, un attaquant pourrait le corrompre :

  1. Heap overflow dans un freed chunk adjacent → écraser son “next” pointer.

  2. Use-after-free écriture dans un freed object → écraser son “next” pointer.

Ensuite, lors de la prochaine allocation de cette taille :

  • L'allocateur retire le chunk corrompu.

  • Suit le “next” pointer fourni par l'attaquant.

  • Retourne un pointer vers de la mémoire arbitraire, permettant des fake object primitives ou un targeted overwrite.

Exemple visuel de freelist poisoning:

Before corruption:
HEAD ──► [ F1 ] ──► [ F2 ] ──► [ F3 ] ──► NULL

After attacker overwrite of F1->next:
HEAD ──► [ F1 ]
(next) ──► 0xDEAD_BEEF_CAFE_BABE  (attacker-chosen)

Next alloc of this zone → kernel hands out memory at attacker-controlled address.

This freelist design made exploitation highly effective pre-hardening: predictable neighbors from heap sprays, raw pointer freelist links, and no type separation allowed attackers to escalate UAF/overflow bugs into arbitrary kernel memory control.

Heap Grooming / Feng Shui

Le but du heap grooming est de façonner la disposition du heap afin que lorsqu'un attaquant déclenche un overflow ou un use-after-free, l'objet cible (victim) se trouve juste à côté d'un objet contrôlé par l'attaquant.
De cette façon, lorsqu'une corruption mémoire survient, l'attaquant peut de manière fiable écraser l'objet victim avec des données contrôlées.

Étapes :

  1. Spray allocations (fill the holes)
  • Avec le temps, le kernel heap se fragmente : certaines zones ont des trous où d'anciens objets ont été libérés.
  • L'attaquant commence par effectuer de nombreuses allocations factices pour remplir ces espaces, de sorte que le heap devienne « compact » et prévisible.
  1. Force new pages
  • Une fois les trous remplis, les allocations suivantes doivent provenir de nouvelles pages ajoutées à la zone.
  • Des pages neuves signifient que les objets seront regroupés, et non dispersés dans une mémoire fragmentée plus ancienne.
  • Cela donne à l'attaquant un bien meilleur contrôle sur les voisins.
  1. Place attacker objects
  • L'attaquant effectue à nouveau un spray, créant de nombreux objets contrôlés par l'attaquant dans ces nouvelles pages.
  • Ces objets sont prévisibles en taille et en emplacement (puisqu'ils appartiennent tous à la même zone).
  1. Free a controlled object (make a gap)
  • L'attaquant libère délibérément un de ses propres objets.
  • Cela crée un « trou » dans le heap, que l'allocateur réutilisera pour la prochaine allocation de cette taille.
  1. Victim object lands in the hole
  • L'attaquant force le kernel à allouer l'objet victim (celui qu'il veut corrompre).
  • Puisque le trou est le premier emplacement disponible dans le freelist, le victim est placé exactement là où l'attaquant a libéré son objet.
  1. Overflow / UAF into victim
  • À présent, l'attaquant a des objets contrôlés autour du victim.
  • En overflowant depuis un de ses propres objets (ou en réutilisant un objet freed), il peut de manière fiable écraser les champs mémoire du victim avec des valeurs choisies.

Pourquoi ça marche :

  • Predictabilité de l'allocateur de zone : les allocations de la même taille proviennent toujours de la même zone.
  • Comportement du freelist : les nouvelles allocations réutilisent d'abord le chunk le plus récemment freed.
  • Heap sprays : l'attaquant remplit la mémoire avec un contenu prévisible et contrôle la disposition.
  • Résultat final : l'attaquant contrôle où l'objet victim atterrit et quelles données se trouvent à côté de lui.

Modern Kernel Heap (iOS 15+/A12+ SoCs)

Apple a durci l'allocateur et rendu le heap grooming beaucoup plus difficile :

1. From Classic kalloc to kalloc_type

  • Before: un seul zone kalloc.<size> existait pour chaque classe de taille (16, 32, 64, … 1280, etc.). Tout objet de cette taille y était placé → les objets attaquants pouvaient se retrouver à côté d'objets kernel privilégiés.
  • Now:
  • Les objets kernel sont alloués depuis des typed zones (kalloc_type).
  • Chaque type d'objet (par ex. ipc_port_t, task_t, OSString, OSData) dispose de sa propre zone dédiée, même s'ils ont la même taille.
  • Le mapping entre type d'objet ↔ zone est généré par le système kalloc_type à la compilation.

Un attaquant ne peut plus garantir que des données contrôlées (OSData) se retrouvent adjacentes à des objets kernel sensibles (task_t) de la même taille.

2. Slabs and Per-CPU Caches

  • Le heap est divisé en slabs (pages de mémoire découpées en chunks de taille fixe pour cette zone).
  • Chaque zone a un cache par CPU pour réduire la contention.
  • Chemin d'allocation :
  1. Essayer le cache per-CPU.
  2. Si vide, prendre du global freelist.
  3. Si le freelist est vide, allouer un nouveau slab (une ou plusieurs pages).
  • Avantage : cette décentralisation rend les heap sprays moins déterministes, car les allocations peuvent être satisfaites à partir des caches de différents CPUs.

3. Randomization inside zones

  • À l'intérieur d'une zone, les éléments freed ne sont pas rendus dans un simple ordre FIFO/LIFO.
  • XNU moderne utilise des pointeurs freelist encodés (safe-linking comme Linux, introduit vers iOS 14).
  • Chaque pointeur freelist est XOR-encodé avec un cookie secret propre à la zone.
  • Cela empêche les attaquants de forger un faux pointeur freelist s'ils obtiennent une primitive d'écriture.
  • Certaines allocations sont randomisées dans leur placement au sein d'un slab, donc le spray ne garantit pas l'adjacence.

4. Guarded Allocations

  • Certains objets kernel critiques (par ex. credentials, task structures) sont alloués dans des guarded zones.
  • Ces zones insèrent des guard pages (mémoire non mappée) entre les slabs ou utilisent des redzones autour des objets.
  • Tout overflow dans la guard page déclenche un fault → panic immédiat au lieu d'une corruption silencieuse.

5. Page Protection Layer (PPL) and SPTM

  • Même si vous contrôlez un objet freed, vous ne pouvez pas modifier toute la mémoire kernel :
  • PPL (Page Protection Layer) impose que certaines régions (par ex. données de code signing, entitlements) soient en lecture seule même pour le kernel luimême.
  • Sur les appareils A15/M2+, ce rôle est remplacé/renforcé par SPTM (Secure Page Table Monitor) + TXM (Trusted Execution Monitor).
  • Ces couches matérielles signifient que les attaquants ne peuvent pas escalader depuis une simple corruption de heap vers la mise à jour arbitraire de structures de sécurité critiques.

6. Large Allocations

  • Toutes les allocations ne passent pas par kalloc_type.
  • Les très grosses requêtes (audessus d'environ ~16KB) contournent les typed zones et sont servies directement depuis la kernel VM (kmem) via des allocations de pages.
  • Celles-ci sont moins prévisibles, mais aussi moins exploitables, puisqu'elles ne partagent pas de slabs avec d'autres objets.

7. Allocation Patterns Attackers Target

Même avec ces protections, les attaquants cherchent toujours :

  • Reference count objects : si vous pouvez altérer les compteurs retain/release, vous pouvez provoquer des use-after-free.
  • Objects with function pointers (vtables) : corrompre l'un d'eux donne toujours du contrôle de flux.
  • Shared memory objects (IOSurface, Mach ports) : ces objets restent des cibles car ils font le pont user ↔ kernel.

Mais — contrairement à avant — vous ne pouvez plus simplement sprayer OSData en espérant qu'il se retrouve voisin d'un task_t. Il faut des bugs spécifiques au type ou des info leaks pour réussir.

Example: Allocation Flow in Modern Heap

Supposons que userspace appelle IOKit pour allouer un objet OSData :

  1. Type lookupOSData mappe vers la zone kalloc_type_osdata (taille 64 octets).
  2. Vérifier le cache per-CPU pour des éléments libres.
  • Si trouvé → en retourner un.
  • Si vide → passer au global freelist.
  • Si le freelist est vide → allouer un nouveau slab (page de 4KB → 64 chunks de 64 octets).
  1. Retourner le chunk à l'appelant.

Freelist pointer protection :

  • Chaque chunk freed stocke l'adresse du prochain chunk libre, mais encodée avec une clé secrète.
  • Écraser ce champ avec des données d'attaquant ne fonctionnera pas à moins de connaître la clé.

Comparison Table

Feature Old Heap (Pre-iOS 15) Modern Heap (iOS 15+ / A12+)
Allocation granularity Fixed size buckets (kalloc.16, kalloc.32, etc.) Size + type-based buckets (kalloc_type)
Placement predictability High (same-size objects side by side) Low (same-type grouping + randomness)
Freelist management Raw pointers in freed chunks (easy to corrupt) Encoded pointers (safe-linking style)
Adjacent object control Easy via sprays/frees (feng shui predictable) Hard — typed zones separate attacker objects
Kernel data/code protections Few hardware protections PPL / SPTM protect page tables & code pages
Exploit reliability High with heap sprays Much lower, requires logic bugs or info leaks

(Old) Physical Use-After-Free via IOSurface

{{#ref}} ios-physical-uaf-iosurface.md {{#endref}}


Ghidra Install BinDiff

Download BinDiff DMG from https://www.zynamics.com/bindiff/manual and install it.

Open Ghidra with ghidraRun and go to File --> Install Extensions, press the add button and select the path /Applications/BinDiff/Extra/Ghidra/BinExport and click OK and installez-le même s'il y a une incompatibilité de version.

Using BinDiff with Kernel versions

  1. Go to the page https://ipsw.me/ and download the iOS versions you want to diff. These will be .ipsw files.
  2. Decompress until you get the bin format of the kernelcache of both .ipsw files. You have information on how to do this on:

{{#ref}} ../../macos-hardening/macos-security-and-privilege-escalation/mac-os-architecture/macos-kernel-extensions.md {{#endref}}

  1. Open Ghidra with ghidraRun, create a new project and load the kernelcaches.
  2. Open each kernelcache so they are automatically analyzed by Ghidra.
  3. Then, on the project Window of Ghidra, right click each kernelcache, select Export, select format Binary BinExport (v2) for BinDiff and export them.
  4. Open BinDiff, create a new workspace and add a new diff indicating as primary file the kernelcache that contains the vulnerability and as secondary file the patched kernelcache.

Finding the right XNU version

If you want to check for vulnerabilities in a specific version of iOS, you can check which XNU release version the iOS version uses at [https://www.theiphonewiki.com/wiki/kernel]https://www.theiphonewiki.com/wiki/kernel).

For example, the versions 15.1 RC, 15.1 and 15.1.1 use the version Darwin Kernel Version 21.1.0: Wed Oct 13 19:14:48 PDT 2021; root:xnu-8019.43.1~1/RELEASE_ARM64_T8006.

{{#include ../../banners/hacktricks-training.md}}