Exploitation iOS

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Mitigations des exploits iOS

1. Code Signing / Vérification de signature à l’exécution

Introduit tôt (iPhone OS → iOS) Ceci est l’une des protections fondamentales : tout code exécutable (apps, dynamic libraries, JIT-ed code, extensions, frameworks, caches) doit être signé cryptographiquement par une chaîne de certificats enracinée dans la confiance d’Apple. À l’exécution, avant de charger un binaire en mémoire (ou avant d’effectuer des sauts à travers certaines frontières), le système vérifie sa signature. Si le code est modifié (bit-flipped, patché) ou non signé, le chargement échoue.

  • Empêche : l’étape « classic payload drop + execute » dans les chaînes d’exploit ; injection de code arbitraire ; modification d’un binaire existant pour y insérer une logique malveillante.
  • Détail du mécanisme :
  • Le Mach-O loader (et dynamic linker) vérifie les pages de code, les segments, les entitlements, les team IDs, et que la signature couvre le contenu du fichier.
  • Pour les régions mémoire comme les JIT caches ou le code généré dynamiquement, Apple impose que les pages soient signées ou validées via des APIs spéciales (par ex. mprotect avec des contrôles de code-sign).
  • La signature inclut les entitlements et identifiants ; l’OS fait respecter que certaines APIs ou capacités privilégiées requièrent des entitlements spécifiques qui ne peuvent pas être falsifiés.
Exemple Supposons qu’un exploit obtienne de l’exécution de code dans un process et tente d’écrire du shellcode dans le heap puis d’y sauter. Sur iOS, cette page devrait être marquée exécutable **et** satisfaire les contraintes de code-sign. Puisque le shellcode n’est pas signé par le certificat d’Apple, le saut échoue ou le système refuse de rendre cette région mémoire exécutable.

2. CoreTrust

Introduit autour de l’ère iOS 14+ (ou progressivement sur les appareils plus récents / versions iOS ultérieures) CoreTrust est le sous-système qui effectue la validation de signature à l’exécution des binaires (incluant les binaires système et ceux des utilisateurs) contre le certificat racine d’Apple plutôt que de se fier à des stores de confiance userland mis en cache.

  • Empêche : le tampering post-installation des binaires, les techniques de jailbreak qui tentent de remplacer ou patcher des librairies système ou des apps utilisateur ; tromper le système en remplaçant des binaires de confiance par des équivalents malveillants.
  • Détail du mécanisme :
  • Au lieu de faire confiance à une base de confiance locale ou à un cache de certificats, CoreTrust récupère ou se réfère directement au root d’Apple ou vérifie des certificats intermédiaires dans une chaîne sécurisée.
  • Il garantit que les modifications (par ex. dans le filesystem) d’un binaire existant sont détectées et rejetées.
  • Il lie les entitlements, team IDs, flags de code signing et autres métadonnées au binaire au moment du chargement.
Exemple Un jailbreak pourrait tenter de remplacer `SpringBoard` ou `libsystem` par une version patchée pour obtenir de la persistance. Mais lorsque le loader de l’OS ou CoreTrust vérifie, il remarque la discordance de signature (ou des entitlements modifiés) et refuse d’exécuter.

3. Data Execution Prevention (DEP / NX / W^X)

Introduit dans de nombreux OS plus tôt ; iOS a eu NX-bit / w^x depuis longtemps DEP impose que les pages marquées écrites (pour des données) soient non-exécutables, et que les pages marquées exécutables soient non-écrites. On ne peut pas simplement écrire du shellcode dans une région heap ou stack puis l’exécuter.

  • Empêche : l’exécution directe de shellcode ; le classique buffer-overflow → saut vers shellcode injecté.
  • Détail du mécanisme :
  • Le MMU / les flags de protection mémoire (via les tables de pages) appliquent la séparation.
  • Toute tentative de marquer une page writable en executable déclenche une vérification système (et est soit interdite soit nécessite une approbation de code-sign).
  • Dans beaucoup de cas, rendre des pages exécutables requiert de passer par des APIs OS qui imposent des contraintes ou contrôles additionnels.
Exemple Un overflow écrit du shellcode sur le heap. L’attaquant tente `mprotect(heap_addr, size, PROT_EXEC)` pour le rendre exécutable. Mais le système refuse ou valide que la nouvelle page doit passer les contraintes de code-sign (ce que le shellcode ne peut pas).

4. Address Space Layout Randomization (ASLR)

Introduit lors de l’ère iOS ~4–5 (approximativement iOS 4–5) ASLR randomise les adresses de base des régions mémoires clés : libraries, heap, stack, etc., à chaque lancement de processus. Les adresses des gadgets bougent d’un exécution à l’autre.

  • Empêche : le hardcoding des adresses de gadgets pour ROP/JOP ; les chaînes d’exploit statiques ; sauts aveugles vers des offsets connus.
  • Détail du mécanisme :
  • Chaque library / module dynamique chargé est rebasé à un offset aléatoire.
  • Les pointeurs de base du stack et du heap sont randomisés (avec certaines limites d’entropie).
  • Parfois d’autres régions (ex. allocations mmap) sont aussi randomisées.
  • Combiné aux mitigations d’information-leak, cela force l’attaquant à d’abord leak une adresse ou un pointeur pour découvrir les bases à l’exécution.
Exemple Une chaîne ROP attend un gadget à `0x….lib + offset`. Mais comme `lib` est relocée différemment à chaque exécution, la chaîne codée en dur échoue. Un exploit doit d’abord leak la base du module avant de calculer les adresses des gadgets.

5. Kernel Address Space Layout Randomization (KASLR)

Introduit approximativement (iOS 5 / iOS 6) Analogique à ASLR en espace utilisateur, KASLR randomise la base du kernel text et d’autres structures kernel au démarrage.

  • Empêche : les exploits kernel qui reposent sur l’emplacement fixe du code ou des données kernel ; les exploits kernel statiques.
  • Détail du mécanisme :
  • À chaque boot, la base du kernel est randomisée (dans une plage).
  • Les structures de données kernel (comme task_structs, vm_map, etc.) peuvent aussi être déplacées ou décalées.
  • Les attaquants doivent d’abord leak des pointeurs kernel ou utiliser des vulnérabilités d’information disclosure pour calculer les offsets avant de détourner des structures ou du code kernel.
Exemple Une vuln locale vise à corrompre un pointeur de fonction kernel (par ex. dans un `vtable`) à `KERN_BASE + offset`. Mais comme `KERN_BASE` est inconnu, l’attaquant doit d’abord le leak (par ex. via une primitive de lecture) avant de calculer la bonne adresse à corrompre.

6. Kernel Patch Protection (KPP / AMCC)

Introduit dans les iOS récents / hardware de la série A (post environ iOS 15–16 ou puces plus récentes) KPP (aka AMCC) surveille en continu l’intégrité des pages de kernel text (via hash ou checksum). Si elle détecte du tampering (patches, hooks inline, modifications de code) en dehors de fenêtres autorisées, elle déclenche un kernel panic ou un reboot.

  • Empêche : le patching kernel persistant (modification d’instructions kernel), les hooks inline, l’écrasement statique de fonctions.
  • Détail du mécanisme :
  • Un module hardware ou firmware surveille la région de kernel text.
  • Il re-hashe périodiquement ou à la demande les pages et compare aux valeurs attendues.
  • Si des mismatches surviennent en dehors de fenêtres de mise à jour bénignes, il panique l’appareil (pour éviter des patches malveillants persistants).
  • Les attaquants doivent soit éviter les fenêtres de détection soit utiliser des voies de patch légitimes.
Exemple Un exploit tente de patcher le prologue d’une fonction kernel (par ex. `memcmp`) pour intercepter les appels. Mais KPP remarque que le hash de la page de code ne correspond plus à la valeur attendue et déclenche un kernel panic, faisant planter l’appareil avant que le patch ne se stabilise.

7. Kernel Text Read‐Only Region (KTRR)

Introduit dans les SoC modernes (post ~A12 / hardware plus récent) KTRR est un mécanisme appliqué par le hardware : une fois le kernel text verrouillé tôt au démarrage, il devient en lecture seule depuis EL1 (le kernel), empêchant toute écriture ultérieure sur les pages de code.

  • Empêche : toute modification du code kernel après le boot (par ex. patching, injection de code in-place) au niveau de privilège EL1.
  • Détail du mécanisme :
  • Pendant le boot (stade secure/bootloader), le contrôleur mémoire (ou une unité hardware sécurisée) marque les pages physiques contenant le kernel text comme read-only.
  • Même si un exploit obtient des privilèges kernel complets, il ne peut pas écrire dans ces pages pour patcher des instructions.
  • Pour les modifier, l’attaquant doit d’abord compromettre la chaîne de boot, ou subvertir KTRR lui-même.
Exemple Un exploit d’élévation de privilèges saute en EL1 et écrit un trampoline dans une fonction kernel (par ex. dans le handler `syscall`). Mais parce que les pages sont verrouillées en lecture seule par KTRR, l’écriture échoue (ou déclenche une faute), donc les patches ne sont pas appliqués.

8. Pointer Authentication Codes (PAC)

Introduit avec ARMv8.3 (hardware), Apple à partir d’A12 / iOS ~12+

  • PAC est une fonctionnalité hardware introduite dans ARMv8.3-A pour détecter la falsification des valeurs de pointeurs (return addresses, function pointers, certains data pointers) en incorporant une petite signature cryptographique (un “MAC”) dans les bits de poids fort inutilisés du pointeur.
  • La signature (“PAC”) est calculée sur la valeur du pointeur plus un modifier (une valeur de contexte, ex. stack pointer ou autre donnée distincte). Ainsi la même valeur de pointeur dans des contextes différents obtient un PAC différent.
  • Au moment de l’utilisation, avant de déréférencer ou de brancher via ce pointeur, une instruction d’authenticate vérifie le PAC. Si valide, le PAC est retiré et on obtient le pointeur pur ; si invalide, le pointeur devient “poisoned” (ou une faute est levée).
  • Les clés utilisées pour produire/valider les PAC résident dans des registres privilégiés (EL1, kernel) et ne sont pas lisibles depuis le mode user.
  • Parce que tous les 64 bits d’un pointeur ne sont pas utilisés dans beaucoup de systèmes (ex. espace d’adresses 48-bit), les bits supérieurs sont “libres” et peuvent contenir le PAC sans altérer l’adresse effective.

Base architecturale & types de clés

  • ARMv8.3 introduit cinq clés 128-bit (chacune implémentée via deux registres système 64-bit) pour pointer authentication.

  • APIAKey — pour instruction pointers (domaine “I”, clé A)

  • APIBKey — seconde clé instruction pointer (domaine “I”, clé B)

  • APDAKey — pour data pointers (domaine “D”, clé A)

  • APDBKey — pour data pointers (domaine “D”, clé B)

  • APGAKey — clé “générique”, pour signer des blobs non-pointer ou usages génériques

  • Ces clés sont stockées dans des registres systèmes privilégiés (accessibles seulement en EL1/EL2, etc.), non accessibles depuis user mode.

  • Le PAC est calculé via une fonction cryptographique (ARM suggère QARMA comme algorithme) en utilisant :

  1. La valeur du pointeur (portion canonique)
  2. Un modifier (valeur de contexte, comme un salt)
  3. La clé secrète
  4. Un certain logic interne de tweak Si le PAC résultant correspond à ce qui est stocké dans les bits supérieurs du pointeur, l’authentification réussit.

Familles d’instructions

La convention de nommage est : PAC / AUT / XPAC, puis des lettres de domaine.

  • PACxx instructions signent un pointeur et insèrent un PAC
  • AUTxx instructions authentifient + retirent (valident et enlèvent le PAC)
  • XPACxx instructions retirent sans valider

Domaines / suffixes :

MnémoniqueSignification / DomaineClé / DomaineExemple d’utilisation en assembleur
PACIASigner instruction pointer avec APIAKey“I, A”PACIA X0, X1 — signer le pointeur dans X0 en utilisant APIAKey avec le modifier X1
PACIBSigner instruction pointer avec APIBKey“I, B”PACIB X2, X3
PACDASigner data pointer avec APDAKey“D, A”PACDA X4, X5
PACDBSigner data pointer avec APDBKey“D, B”PACDB X6, X7
PACG / PACGASignature générique (non-pointer) avec APGAKey“G”PACGA X8, X9, X10 (signer X9 avec le modifier X10 dans X8)
AUTIAAuthentifier instruction pointer signé APIA & retirer PAC“I, A”AUTIA X0, X1 — vérifier PAC sur X0 en utilisant le modifier X1, puis retirer
AUTIBAuthentifier domaine APIB“I, B”AUTIB X2, X3
AUTDAAuthentifier data pointer signé APDA“D, A”AUTDA X4, X5
AUTDBAuthentifier data pointer signé APDB“D, B”AUTDB X6, X7
AUTGAAuthentifier générique / blob (APGA)“G”AUTGA X8, X9, X10 (valider générique)
XPACIRetirer PAC (instruction pointer, sans validation)“I”XPACI X0 — enlever le PAC de X0 (domaine instruction)
XPACDRetirer PAC (data pointer, sans validation)“D”XPACD X4 — enlever le PAC du data pointer dans X4

Il existe des formes spécialisées / alias :

  • PACIASP est un raccourci pour PACIA X30, SP (signer le link register en utilisant SP comme modifier)
  • AUTIASP est AUTIA X30, SP (authentifier le link register avec SP)
  • Des formes combinées comme RETAA, RETAB (authentifier-et-retourner) ou BLRAA (authentifier & branch) existent dans les extensions ARM / support compilateur.
  • Aussi des variantes à modifier implicite zéro : PACIZA / PACIZB où le modifier est implicitement zéro, etc.

Modifiers

Le but principal du modifier est de lier le PAC à un contexte spécifique pour que la même adresse signée dans différents contextes produise des PAC différents. C’est comme ajouter un salt à un hash.

Ainsi :

  • Le modifier est une valeur de contexte (un autre registre) qui est mixée dans le calcul du PAC. Choix typiques : le stack pointer (SP), un frame pointer, ou un ID d’objet.
  • Utiliser SP comme modifier est courant pour le signing des return addresses : le PAC devient lié à la frame de pile spécifique. Si vous essayez de réutiliser le LR dans une autre frame, le modifier change, donc la validation PAC échoue.
  • La même valeur de pointeur signée avec des modifiers différents produit des PACs différents.
  • Le modifier n’a pas besoin d’être secret, mais idéalement il n’est pas contrôlé par l’attaquant.
  • Pour des instructions qui signent ou valident des pointeurs où aucun modifier significatif n’existe, certaines formes utilisent zéro ou une constante implicite.

Personnalisations & observations Apple / iOS / XNU

  • L’implémentation PAC d’Apple inclut des diversificateurs par boot de sorte que les clés ou tweaks changent à chaque démarrage, empêchant la réutilisation entre boots.
  • Ils incluent aussi des mitigations cross-domain pour empêcher que des PACs signés en user mode soient facilement réutilisés en kernel mode, etc.
  • Sur Apple M1 / Apple Silicon, le reverse engineering a montré qu’il y a neuf types de modifiers et des registres système Apple-spécifiques pour le contrôle des clés.
  • Apple utilise PAC à travers de nombreux sous-systèmes kernel : signing des return addresses, intégrité des pointeurs dans les données kernel, contextes de thread signés, etc.
  • Google Project Zero a montré comment, sous une primitive de lecture/écriture mémoire puissante dans le kernel, on pouvait forger des PACs kernel (pour les clés A) sur des appareils de l’ère A12, mais Apple a patché beaucoup de ces voies.
  • Dans le système d’Apple, certaines clés sont globales au kernel, tandis que les processus user peuvent obtenir de l’aléa de clé par process.

Contournements PAC

  1. PAC kernel-mode : théorique vs contournements réels
  • Parce que les clés PAC kernel et la logique sont strictement contrôlées (registres privilégiés, diversificateurs, isolation de domaine), forger arbitrairement des pointeurs kernel signés est très difficile.
  • Azad (2020) “iOS Kernel PAC, One Year Later” rapporte qu’en iOS 12-13, il a trouvé quelques contournements partiels (signing gadgets, réutilisation d’états signés, branches indirectes non protégées) mais pas de bypass générique complet. bazad.github.io
  • Les personnalisations d’Apple décrites dans “Dark Magic” réduisent encore les surfaces exploitables (domain switching, bits d’activation par clé). i.blackhat.com
  • Il existe un contournement connu kernel PAC CVE-2023-32424 sur Apple silicon (M1/M2) rapporté par Zecao Cai et al. i.blackhat.com
  • Mais ces bypasss reposent souvent sur des gadgets très spécifiques ou des bugs d’implémentation ; ils ne sont pas des contournements généraux.

Ainsi le PAC kernel est considéré très robuste, bien que pas parfait.

  1. Techniques de contournement PAC en user-mode / runtime

Celles-ci sont plus courantes, et exploitent des imperfections dans la manière dont PAC est appliqué ou utilisé dans le linking dynamique / runtime frameworks. Ci-dessous des classes avec exemples.

2.1 Shared Cache / problèmes de clé A

  • Le dyld shared cache est un grand blob pré-lié de frameworks et librairies système. Parce qu’il est si largement partagé, les function pointers à l’intérieur du shared cache sont “pré-signés” puis utilisés par de nombreux processus. Les attaquants ciblent ces pointeurs déjà signés comme des “PAC oracles”.

  • Certaines techniques de contournement tentent d’extraire ou de réutiliser des pointeurs signés par la clé A présents dans le shared cache et de les réutiliser dans des gadgets.

  • La présentation “No Clicks Required” décrit la construction d’un oracle sur le shared cache pour inférer des adresses relatives et combiner cela avec des pointeurs signés pour contourner PAC. saelo.github.io

  • De plus, les imports de function pointers depuis des librairies partagées en userspace se sont révélés parfois insuffisamment protégés par PAC, permettant à un attaquant d’obtenir des function pointers sans changer leur signature. (entrée bug Project Zero) bugs.chromium.org

2.2 dlsym(3) / résolution de symboles dynamique

  • Un contournement connu consiste à appeler dlsym() pour obtenir un pointeur de fonction déjà signé (signé par la clé A, diversifier zéro) puis l’utiliser. Parce que dlsym retourne un pointeur légitime signé, l’utiliser contourne le besoin de forger un PAC.

  • Le blog d’Epsilon détaille comment certains bypass exploitent cela : appeler dlsym("someSym") renvoie un pointeur signé et peut être utilisé pour des appels indirects. blog.epsilon-sec.com

  • Synacktiv dans “iOS 18.4 — dlsym considered harmful” décrit un bug : certains symboles résolus via dlsym sur iOS 18.4 retournent des pointeurs incorrectement signés (ou avec des diversificateurs buggy), permettant un contournement PAC non voulu. Synacktiv

  • La logique dans dyld pour dlsym inclut : quand result->isCode, ils signent le pointeur retourné avec __builtin_ptrauth_sign_unauthenticated(..., key_asia, 0), i.e. contexte zéro. blog.epsilon-sec.com

Ainsi, dlsym est un vecteur fréquent dans les contournements PAC en user-mode.

2.3 Autres relocations DYLD / runtime

  • Le loader DYLD et la logique de relocation dynamique sont complexes et parfois mappent temporairement des pages en read/write pour effectuer des relocations, puis les remettent en lecture seule. Les attaquants exploitent ces fenêtres. La présentation de Synacktiv décrit “Operation Triangulation”, un contournement basé sur le timing du PAC via des relocations dynamiques. Synacktiv

  • Les pages DYLD sont maintenant protégées avec SPRR / VM_FLAGS_TPRO (certains flags de protection pour dyld). Mais les versions antérieures avaient des gardes plus faibles. Synacktiv

  • Dans les chaînes d’exploit WebKit, le loader DYLD est souvent une cible pour le contournement PAC. Les slides mentionnent que beaucoup de bypasss PAC ont visé le loader DYLD (via relocation, interposer hooks). Synacktiv

2.4 NSPredicate / NSExpression / ObjC / SLOP

  • Dans les chaînes d’exploit userland, des méthodes du runtime Objective-C telles que NSPredicate, NSExpression ou NSInvocation sont utilisées pour faire transiter des appels de contrôle sans besoin apparent de forger des pointeurs.

  • Sur les anciens iOS (avant PAC), un exploit utilisait des fake NSInvocation objects pour appeler des selectors arbitraires sur de la mémoire contrôlée. Avec PAC, des modifications sont nécessaires. Mais la technique SLOP (SeLector Oriented Programming) a été étendue sous PAC aussi. Project Zero

  • La technique originale SLOP permettait d’enchaîner des appels ObjC en créant des invocations factices ; le bypass repose sur le fait que les ISA ou selector pointers sont parfois pas complètement protégés par PAC. Project Zero

  • Dans des environnements où pointer authentication est appliqué partiellement, les méthodes / selectors / target pointers peuvent ne pas toujours bénéficier de la protection PAC, laissant une marge pour contournement.

Exemple de flux

Example Signing & Authenticating ``` ; Example: function prologue / return address protection my_func: stp x29, x30, [sp, #-0x20]! ; push frame pointer + LR mov x29, sp PACIASP ; sign LR (x30) using SP as modifier ; … body … mov sp, x29 ldp x29, x30, [sp], #0x20 ; restore AUTIASP ; authenticate & strip PAC ret

; Example: indirect function pointer stored in a struct ; suppose X1 contains a function pointer PACDA X1, X2 ; sign data pointer X1 with context X2 STR X1, [X0] ; store signed pointer

; later retrieval: LDR X1, [X0] AUTDA X1, X2 ; authenticate & strip BLR X1 ; branch to valid target

; Example: stripping for comparison (unsafe) LDR X1, [X0] XPACI X1 ; strip PAC (instruction domain) CMP X1, #some_label_address BEQ matched_label

</details>

<details>
<summary>Example</summary>
Un buffer overflow écrase une adresse de retour sur la stack. L'attaquant écrit l'adresse du gadget cible mais ne peut pas calculer le PAC correct. Quand la fonction retourne, l'instruction CPU `AUTIA` génère une faute à cause du mismatch du PAC. La chaîne échoue.
L'analyse de Project Zero sur A12 (iPhone XS) a montré comment le PAC d'Apple est utilisé et des méthodes pour forger des PACs si un attaquant dispose d'un primitive de lecture/écriture mémoire.
</details>


### 9. **Branch Target Identification (BTI)**
**Introduced with ARMv8.5 (later hardware)**
BTI est une fonctionnalité matérielle qui vérifie les **indirect branch targets** : lorsqu'on exécute `blr` ou des appels/sauts indirects, la cible doit commencer par un **BTI landing pad** (`BTI j` ou `BTI c`). Sauter vers des adresses de gadget qui n'ont pas le landing pad déclenche une exception.

L'implémentation de LLVM note trois variantes d'instructions BTI et comment elles se mappent aux types de branchement.

| BTI Variant | What it permits (which branch types) | Typical placement / use case |
|-------------|----------------------------------------|-------------------------------|
| **BTI C** | Targets of *call*-style indirect branches (e.g. `BLR`, or `BR` using X16/X17) | Put at entry of functions that may be called indirectly |
| **BTI J** | Targets of *jump*-style branches (e.g. `BR` used for tail calls) | Placed at the beginning of blocks reachable by jump tables or tail-calls |
| **BTI JC** | Acts as both C and J | Can be targeted by either call or jump branches |

- Dans du code compilé avec branch target enforcement, les compilateurs insèrent une instruction BTI (C, J, ou JC) à chaque target d'indirect-branch valide (début de fonctions ou blocs atteignables par des sauts) de sorte que les indirect branches réussissent uniquement vers ces endroits.
- Les **direct branches / calls** (i.e. adresses fixes `B`, `BL`) ne sont **pas restreintes** par BTI. L'hypothèse est que les pages de code sont de confiance et que l'attaquant ne peut pas les modifier (donc les direct branches sont sûres).
- De plus, les instructions **RET / return** ne sont généralement pas restreintes par BTI parce que les adresses de retour sont protégées via PAC ou des mécanismes de return signing.

#### Mechanism and enforcement

- Quand le CPU décode un **indirect branch (BLR / BR)** dans une page marquée “guarded / BTI-enabled,” il vérifie si la première instruction de l'adresse cible est un BTI valide (C, J, ou JC selon autorisation). Si ce n'est pas le cas, une **Branch Target Exception** se produit.
- L'encodage de l'instruction BTI est conçu pour réutiliser des opcodes précédemment réservés pour des NOPs (dans les versions ARM antérieures). Ainsi les binaires BTI-enabled restent rétro-compatibles : sur du hardware sans support BTI, ces instructions agissent comme des NOPs.
- Les passes du compilateur qui ajoutent des BTI les insèrent uniquement là où c'est nécessaire : fonctions pouvant être appelées indirectement, ou basic blocks ciblés par des sauts.
- Certains patchs et code LLVM montrent que BTI n'est pas inséré pour *tous* les basic blocks — seulement ceux qui sont des branch targets potentiels (e.g. provenant de switch / jump tables).

#### BTI + PAC synergy

PAC protège la valeur du pointeur (la source) — il garantit que la chaîne d'appels indirects / retours n'a pas été altérée.

BTI garantit que même un pointeur valide ne peut cibler que des entry points correctement marqués.

Combinés, l'attaquant a besoin à la fois d'un pointeur valide avec le PAC correct et que la cible ait un BTI placé là. Cela augmente la difficulté de construire des gadgets d'exploit.

#### Example


<details>
<summary>Example</summary>
Un exploit tente de pivoter vers un gadget à `0xABCDEF` qui ne commence pas par `BTI c`. Le CPU, lors de l'exécution de `blr x0`, vérifie la cible et provoque une faute parce que l'alignement/instruction initiale n'inclut pas un landing pad valide. Ainsi beaucoup de gadgets deviennent inutilisables sauf s'ils incluent le préfixe BTI.
</details>


### 10. **Privileged Access Never (PAN) & Privileged Execute Never (PXN)**
**Introduced in more recent ARMv8 extensions / iOS support (for hardened kernel)**

#### PAN (Privileged Access Never)

- **PAN** est une fonctionnalité introduite dans **ARMv8.1-A** qui empêche le **privileged code** (EL1 ou EL2) de **lire ou écrire** la mémoire marquée comme **user-accessible (EL0)**, à moins que PAN soit explicitement désactivé.
- L'idée : même si le kernel est trompé ou compromis, il ne peut pas déréférencer arbitrairement des pointeurs user-space sans d'abord *clear* PAN, réduisant ainsi les risques d'exploits de type **`ret2usr`** ou d'abus de buffers contrôlés par l'utilisateur.
- Quand PAN est activé (PSTATE.PAN = 1), toute instruction privileged de load/store accédant une adresse virtuelle “accessible à EL0” déclenche une **permission fault**.
- Le kernel, quand il doit légitimement accéder à la mémoire user (e.g. copier des données vers/depuis des buffers utilisateur), doit **désactiver temporairement PAN** (ou utiliser des instructions de “unprivileged load/store”) pour permettre cet accès.
- Dans Linux sur ARM64, le support PAN a été introduit vers 2015 : des patches du kernel ont ajouté la détection de la feature, et remplacé `get_user` / `put_user` etc. par des variantes qui clear PAN autour des accès mémoire utilisateur.

**Nuance clé / limitation / bug**
- Comme noté par Siguza et d'autres, un bug de spécification (ou un comportement ambigu) dans la conception ARM fait que les mappings utilisateur execute-only (`--x`) peuvent **ne pas déclencher PAN**. En d'autres termes, si une page utilisateur est marquée exécutable mais sans permission de lecture, la tentative de lecture du kernel pourrait contourner PAN parce que l'architecture considère que “accessible à EL0” nécessite la permission de lecture, pas seulement d'exécution. Cela conduit à un contournement de PAN dans certaines configurations.
- À cause de cela, si iOS / XNU permet des pages utilisateur execute-only (comme certains setups JIT ou code-cache), le kernel pourrait lire accidentellement depuis elles même avec PAN activé. C'est une zone subtile exploitables dans certains systèmes ARMv8+.

#### PXN (Privileged eXecute Never)

- **PXN** est un flag de page table (dans les entrées de page, leaf ou block) qui indique que la page est **non exécutable en mode privilégié** (i.e. quand EL1 exécute).
- PXN empêche le kernel (ou tout code privilégié) de sauter vers ou d'exécuter des instructions depuis des pages user même si le contrôle est détourné. En pratique, cela empêche une redirection de contrôle au niveau kernel vers de la mémoire user.
- Combiné avec PAN, cela garantit que :
1. Le kernel ne peut pas (par défaut) lire ou écrire les données user (PAN)
2. Le kernel ne peut pas exécuter le code user (PXN)
- Dans le format de page table ARMv8, les entrées leaf ont un bit `PXN` (et aussi `UXN` pour unprivileged execute-never) dans leurs bits d'attribut.

Ainsi même si le kernel a un pointeur de fonction corrompu pointant vers la mémoire user, et tente de brancher là, le bit PXN provoquerait une faute.

#### Memory-permission model & how PAN and PXN map to page table bits

Pour comprendre comment PAN / PXN fonctionnent, il faut voir comment la traduction et le modèle de permission ARM s'articulent (simplifié) :

- Chaque entrée de page ou block a des champs d'attributs incluant **AP[2:1]** pour les permissions d'accès (read/write, privileged vs unprivileged) et des bits **UXN / PXN** pour les restrictions execute-never.
- Quand PSTATE.PAN est 1 (activé), le hardware applique des sémantiques modifiées : les accès privilégiés aux pages marquées comme “accessible par EL0” (i.e. user-accessible) sont interdits (fault).
- À cause du bug mentionné, les pages marquées uniquement exécutable (sans permission de lecture) peuvent ne pas compter comme “accessible par EL0” sous certaines implémentations, contournant ainsi PAN.
- Quand le bit PXN d'une page est réglé, même si le fetch d'instruction vient d'un niveau de privilège supérieur, l'exécution est interdite.

#### Kernel usage of PAN / PXN in a hardened OS (e.g. iOS / XNU)

Dans un design de kernel durci (comme ce que Apple pourrait utiliser) :

- Le kernel active PAN par défaut (donc le code privilégié est contraint).
- Dans les chemins qui doivent légitimement lire/écrire des buffers utilisateur (e.g. copie de buffer d'appel système, I/O, read/write user pointer), le kernel désactive temporairement **PAN** ou utilise des instructions spéciales pour outrepasser.
- Après avoir fini l'accès aux données utilisateur, il doit réactiver PAN.
- PXN est appliqué via les page tables : les pages user ont PXN = 1 (donc le kernel ne peut pas les exécuter), les pages kernel n'ont pas PXN (donc le code kernel peut s'exécuter).
- Le kernel doit s'assurer qu'aucun chemin d'exécution ne provoque l'exécution dans des zones mémoire user (ce qui contournerait PXN) — ainsi les chaînes d'exploit reposant sur “jump into user-controlled shellcode” sont bloquées.

À cause du contournement PAN via les pages execute-only, dans un système réel, Apple pourrait désactiver ou interdire les pages user execute-only, ou patcher autour de la faiblesse de spécification.


#### Attack surfaces, bypasses, and mitigations

- **PAN bypass via execute-only pages** : comme discuté, la spécification laisse un vide : les pages user en execute-only (pas de lecture) pourraient ne pas être considérées comme “accessible à EL0,” donc PAN ne bloquera pas les lectures du kernel depuis ces pages sous certaines implémentations. Cela donne à l'attaquant une voie inhabituelle pour fournir des données via des sections “execute-only”.
- **Temporal window exploit** : si le kernel désactive PAN pour une fenêtre plus longue que nécessaire, une course ou un chemin malveillant pourrait exploiter cette fenêtre pour effectuer des accès mémoire utilisateur non désirés.
- **Forgotten re-enable** : si des chemins de code oublient de réactiver PAN, des opérations kernel ultérieures pourraient accéder incorrectement à la mémoire user.
- **Misconfiguration of PXN** : si les page tables ne définissent pas PXN sur les pages user ou mappent incorrectement des pages de code user, le kernel pourrait être trompé pour exécuter du code user.
- **Speculation / side-channels** : analogue aux contournements spéculatifs, il peut exister des effets microarchitecturaux transitoires qui provoquent une violation transitoire des checks PAN / PXN (même si de telles attaques dépendent fortement du design du CPU).
- **Complex interactions** : Dans des features plus avancées (e.g. JIT, shared memory, regions de code just-in-time), le kernel peut nécessiter un contrôle fin pour permettre certains accès mémoire ou exécutions dans des régions mappées en user ; concevoir cela en toute sécurité sous les contraintes PAN/PXN est non trivial.


#### Example

<details>
<summary>Code Example</summary>
Here are illustrative pseudo-assembly sequences showing enabling/disabling PAN around user memory access, and how a fault might occur.

// Suppose kernel entry point, PAN is enabled (privileged code cannot access user memory by default)

; Kernel receives a syscall with user pointer in X0 ; wants to read an integer from user space mov X1, X0 ; X1 = user pointer

; disable PAN to allow privileged access to user memory MSR PSTATE.PAN, #0 ; clear PAN bit, disabling the restriction

ldr W2, [X1] ; now allowed load from user address

; re-enable PAN before doing other kernel logic MSR PSTATE.PAN, #1 ; set PAN

; … further kernel work …

; Later, suppose an exploit corrupts a pointer to a user-space code page and jumps there BR X3 ; branch to X3 (which points into user memory)

; Because the target page is marked PXN = 1 for privileged execution, ; the CPU throws an exception (fault) and rejects execution

Si le kernel n'avait **pas** activé PXN sur cette user page, alors la branche pourrait réussir — ce qui serait insécurisé.

Si le kernel oublie de réactiver PAN après un accès à la mémoire user, cela ouvre une fenêtre où la logique kernel suivante pourrait lire/écrire accidentellement de la mémoire user arbitraire.

Si le user pointer pointe vers une execute-only page (user page avec uniquement la permission execute, pas read/write), sous le bug de la spec PAN, `ldr W2, [X1]` pourrait **ne pas** faulter même avec PAN activé, permettant un bypass exploit, selon l'implémentation.

</details>

<details>
<summary>Example</summary>
Une vulnérabilité kernel tente de prendre un function pointer fourni par l'user et de l'appeler dans le kernel context (c.-à-d. `call user_buffer`). Sous PAN/PXN, cette opération est interdite ou provoque une fault.
</details>

---

### 11. **Top Byte Ignore (TBI) / Pointer Tagging**
**Introduced in ARMv8.5 / newer (or optional extension)**
TBI means the top byte (most-significant byte) of a 64-bit pointer is ignored by address translation. This lets OS or hardware embed **tag bits** in the pointer’s top byte without affecting the actual address.

- TBI stands for **Top Byte Ignore** (sometimes called *Address Tagging*). It is a hardware feature (available in many ARMv8+ implementations) that **ignores the top 8 bits** (bits 63:56) of a 64-bit pointer when performing **address translation / load/store / instruction fetch**.
- In effect, the CPU treats a pointer `0xTTxxxx_xxxx_xxxx` (where `TT` = top byte) as `0x00xxxx_xxxx_xxxx` for the purposes of address translation, ignoring (masking off) the top byte. The top byte can be used by software to store **metadata / tag bits**.
- This gives software “free” in-band space to embed a byte of tag in each pointer without altering which memory location it refers to.
- The architecture ensures that loads, stores, and instruction fetch treat the pointer with its top byte masked (i.e. tag stripped off) before performing the actual memory access.

Thus TBI decouples the **logical pointer** (pointer + tag) from the **physical address** used for memory operations.

#### Why TBI: Use cases and motivation

- **Pointer tagging / metadata**: You can store extra metadata (e.g. object type, version, bounds, integrity tags) in that top byte. When you later use the pointer, the tag is ignored at hardware level, so you don’t need to strip manually for the memory access.
- **Memory tagging / MTE (Memory Tagging Extension)**: TBI is the base hardware mechanism that MTE builds on. In ARMv8.5, the **Memory Tagging Extension** uses bits 59:56 of the pointer as a **logical tag** and checks it against an **allocation tag** stored in memory.
- **Enhanced security & integrity**: By combining TBI with pointer authentication (PAC) or runtime checks, you can force not just the pointer value but also the tag to be correct. An attacker overwriting a pointer without the correct tag will produce a mismatched tag.
- **Compatibility**: Because TBI is optional and tag bits are ignored by hardware, existing untagged code continues to operate normally. The tag bits effectively become “don’t care” bits for legacy code.

#### Example
<details>
<summary>Example</summary>
A function pointer included a tag in its top byte (say `0xAA`). An exploit overwrites the pointer low bits but neglects the tag, so when the kernel verifies or sanitizes, the pointer fails or is rejected.
</details>

---

### 12. **Page Protection Layer (PPL)**
**Introduced in late iOS / modern hardware (iOS ~17 / Apple silicon / high-end models)** (some reports show PPL circa macOS / Apple silicon, but Apple is bringing analogous protections to iOS)

- PPL is designed as an **intra-kernel protection boundary**: even if the kernel (EL1) is compromised and has read/write capabilities, **it should not be able to freely modify** certain **sensitive pages** (especially page tables, code-signing metadata, kernel code pages, entitlements, trust caches, etc.).
- It effectively creates a **“kernel within the kernel”** — a smaller trusted component (PPL) with **elevated privileges** that alone can modify protected pages. Other kernel code must call into PPL routines to effect changes.
- This reduces the attack surface for kernel exploits: even with full arbitrary R/W/execute in kernel mode, exploit code must also somehow get into the PPL domain (or bypass PPL) to modify critical structures.
- On newer Apple silicon (A15+ / M2+), Apple is transitioning to **SPTM (Secure Page Table Monitor)**, which in many cases replaces PPL for page-table protection on those platforms.

Here’s how PPL is believed to operate, based on public analysis:

#### Use of APRR / permission routing (APRR = Access Permission ReRouting)

- Apple hardware uses a mechanism called **APRR (Access Permission ReRouting)**, which allows page table entries (PTEs) to contain small indices, rather than full permission bits. Those indices are mapped via APRR registers to actual permissions. This allows dynamic remapping of permissions per domain.
- PPL leverages APRR to segregate privilege within kernel context: only the PPL domain is permitted to update the mapping between indices and effective permissions. That is, when non-PPL kernel code writes a PTE or tries to flip permission bits, the APRR logic disallows it (or enforces read-only mapping).
- PPL code itself runs in a restricted region (e.g. `__PPLTEXT`) which is normally non-executable or non-writable until entry gates temporarily allow it. The kernel calls PPL entry points (“PPL routines”) to perform sensitive operations.

#### Gate / Entry & Exit

- When the kernel needs to modify a protected page (e.g. change permissions of a kernel code page, or modify page tables), it calls into a **PPL wrapper** routine, which does validation and then transitions into the PPL domain. Outside that domain, the protected pages are effectively read-only or non-modifiable by the main kernel.
- During PPL entry, the APRR mappings are adjusted so that memory pages in the PPL region are set to **executable & writable** within PPL. Upon exit, they are returned to read-only / non-writable. This ensures that only well-audited PPL routines can write to protected pages.
- Outside PPL, attempts by kernel code to write to those protected pages will fault (permission denied) because the APRR mapping for that code domain doesn’t permit writing.

#### Protected page categories

The pages that PPL typically protects include:

- Page table structures (translation table entries, mapping metadata)
- Kernel code pages, especially those containing critical logic
- Code-sign metadata (trust caches, signature blobs)
- Entitlement tables, signature enforcement tables
- Other high-value kernel structures where a patch would allow bypassing signature checks or credentials manipulation

The idea is that even if the kernel memory is fully controlled, the attacker cannot simply patch or rewrite these pages, unless they also compromise PPL routines or bypass PPL.


#### Known Bypasses & Vulnerabilities

1. **Project Zero’s PPL bypass (stale TLB trick)**

- A public writeup by Project Zero describes a bypass involving **stale TLB entries**.
- The idea:

1. Allocate two physical pages A and B, mark them as PPL pages (so they are protected).
2. Map two virtual addresses P and Q whose L3 translation table pages come from A and B.
3. Spin a thread to continuously access Q, keeping its TLB entry alive.
4. Call `pmap_remove_options()` to remove mappings starting at P; due to a bug, the code mistakenly removes the TTEs for both P and Q, but only invalidates the TLB entry for P, leaving Q’s stale entry live.
5. Reuse B (page Q’s table) to map arbitrary memory (e.g. PPL-protected pages). Because the stale TLB entry still maps Q’s old mapping, that mapping remains valid for that context.
6. Through this, the attacker can put writable mapping of PPL-protected pages in place without going through PPL interface.

- This exploit required fine control of physical mapping and TLB behavior. It demonstrates that a security boundary relying on TLB / mapping correctness must be extremely careful about TLB invalidations and mapping consistency.

- Project Zero commented that bypasses like this are subtle and rare, but possible in complex systems. Still, they regard PPL as a solid mitigation.

2. **Other potential hazards & constraints**

- If a kernel exploit can directly enter PPL routines (via calling the PPL wrappers), it might bypass restrictions. Thus argument validation is critical.
- Bugs in the PPL code itself (e.g. arithmetic overflow, boundary checks) can allow out-of-bounds modifications inside PPL. Project Zero observed that such a bug in `pmap_remove_options_internal()` was exploited in their bypass.
- The PPL boundary is irrevocably tied to hardware enforcement (APRR, memory controller), so it's only as strong as the hardware implementation.



#### Example
<details>
<summary>Code Example</summary>
Here’s a simplified pseudocode / logic showing how a kernel might call into PPL to modify protected pages:
```c
// In kernel (outside PPL domain)
function kernel_modify_pptable(pt_addr, new_entry) {
// validate arguments, etc.
return ppl_call_modify(pt_addr, new_entry)  // call PPL wrapper
}

// In PPL (trusted domain)
function ppl_call_modify(pt_addr, new_entry) {
// temporarily enable write access to protected pages (via APRR adjustments)
aprr_set_index_for_write(PPL_INDEX)
// perform the modification
*pt_addr = new_entry
// restore permissions (make pages read-only again)
aprr_restore_default()
return success
}

// If kernel code outside PPL does:
*pt_addr = new_entry  // a direct write
// It will fault because APRR mapping for non-PPL domain disallows write to that page

The kernel can do many normal operations, but only through ppl_call_* routines can it change protected mappings or patch code.

Example Un kernel exploit tente d'écraser l'entitlement table, ou de désactiver l'enforcement de code-sign en modifiant un kernel signature blob. Parce que cette page est protégée par PPL, l'écriture est bloquée sauf si elle passe par l'interface PPL. Ainsi, même avec kernel code execution, vous ne pouvez pas contourner les contraintes de code-sign ni modifier arbitrairement les credential data. Sur iOS 17+ certains appareils utilisent SPTM pour isoler davantage les pages gérées par PPL.

PPL → SPTM / Replacements / Future

  • On Apple’s modern SoCs (A15 or later, M2 or later), Apple supports SPTM (Secure Page Table Monitor), which replaces PPL for page table protections.
  • Apple calls out in documentation: “Page Protection Layer (PPL) and Secure Page Table Monitor (SPTM) enforce execution of signed and trusted code … PPL manages the page table permission overrides … Secure Page Table Monitor replaces PPL on supported platforms.”
  • The SPTM architecture likely shifts more policy enforcement into a higher-privileged monitor outside kernel control, further reducing the trust boundary.

MTE | EMTE | MIE

Here’s a higher-level description of how EMTE operates under Apple’s MIE setup:

  1. Tag assignment
  • When memory is allocated (e.g. in kernel or user space via secure allocators), a secret tag is assigned to that block.
  • The pointer returned to the user or kernel includes that tag in its high bits (using TBI / top byte ignore mechanisms).
  1. Tag checking on access
  • Whenever a load or store is executed using a pointer, the hardware checks that the pointer’s tag matches the memory block’s tag (allocation tag). If mismatch, it faults immediately (since synchronous).
  • Because it’s synchronous, there is no “delayed detection” window.
  1. Retagging on free / reuse
  • When memory is freed, the allocator changes the block’s tag (so older pointers with old tags no longer match).
  • A use-after-free pointer would therefore have a stale tag and mismatch when accessed.
  1. Neighbor-tag differentiation to catch overflows
  • Adjacent allocations are given distinct tags. If a buffer overflow spills into neighbor’s memory, tag mismatch causes a fault.
  • This is especially powerful in catching small overflows that cross boundary.
  1. Tag confidentiality enforcement
  • Apple must prevent tag values being leaked (because if attacker learns the tag, they could craft pointers with correct tags).
  • They include protections (microarchitectural / speculative controls) to avoid side-channel leakage of tag bits.
  1. Kernel and user-space integration
  • Apple uses EMTE not just in user-space but also in kernel / OS-critical components (to guard kernel against memory corruption).
  • The hardware/OS ensures tag rules apply even when kernel is executing on behalf of user space.
Example ``` Allocate A = 0x1000, assign tag T1 Allocate B = 0x2000, assign tag T2

// pointer P points into A with tag T1 P = (T1 << 56) | 0x1000

// Valid store *(P + offset) = value // tag T1 matches allocation → allowed

// Overflow attempt: P’ = P + size_of_A (into B region) *(P’ + delta) = value → pointer includes tag T1 but memory block has tag T2 → mismatch → fault

// Free A, allocator retags it to T3 free(A)

// Use-after-free: *(P) = value → pointer still has old tag T1, memory region is now T3 → mismatch → fault

</details>

#### Limitations & challenges

- **Intrablock overflows**: Si l'overflow reste dans la même allocation (ne franchit pas la frontière) et que le tag reste identique, tag mismatch ne le détecte pas.
- **Tag width limitation**: Seuls quelques bits (p.ex. 4 bits, ou un petit domaine) sont disponibles pour le tag — espace de noms limité.
- **Side-channel leaks**: Si les bits de tag peuvent être leaked (via cache / speculative execution), un attaquant peut apprendre les tags valides et bypasser. Apple’s tag confidentiality enforcement est censé atténuer cela.
- **Performance overhead**: Les vérifications de tag à chaque load/store ajoutent un coût ; Apple doit optimiser le hardware pour réduire cet overhead.
- **Compatibility & fallback**: Sur du hardware plus ancien ou des composants qui ne supportent pas EMTE, un fallback doit exister. Apple affirme que MIE n'est activé que sur les appareils disposant du support.
- **Complex allocator logic**: L'allocator doit gérer les tags, le retagging, l'alignement des frontières et éviter les collisions de mis-tag. Des bugs dans la logique de l'allocator pourraient introduire des vulnérabilités.
- **Mixed memory / hybrid areas**: Une partie de la mémoire peut rester untagged (legacy), rendant l'interopérabilité plus délicate.
- **Speculative / transient attacks**: Comme pour beaucoup de protections microarchitecturales, speculative execution ou micro-op fusions pourraient bypasser temporairement les vérifications ou leak des bits de tag.
- **Limited to supported regions**: Apple pourrait n'appliquer EMTE que dans des zones sélectives et à haut risque (kernel, security-critical subsystems), pas universellement.



---

## Key enhancements / differences compared to standard MTE

Voici les améliorations et changements mis en avant par Apple :

| Fonctionnalité | Original MTE | EMTE (Apple’s enhanced) / MIE |
|---|---|---|
| **Check mode** | Prend en charge les modes synchrone et asynchrone. En asynchrone, les tag mismatches sont signalés plus tard (retardés). | Apple insiste sur le **mode synchrone** par défaut — les tag mismatches sont détectés immédiatement, aucune fenêtre de délai/course n'est autorisée. |
| **Coverage of non-tagged memory** | Les accès à la mémoire non-tagged (p.ex. globals) peuvent contourner les vérifications dans certaines implémentations. | EMTE exige que les accès depuis une région taggée vers de la mémoire non-tagged valident aussi la connaissance du tag, rendant plus difficile le contournement via le mélange d'allocations. |
| **Tag confidentiality / secrecy** | Les tags peuvent être observables ou leak via des side channels. | Apple ajoute **Tag Confidentiality Enforcement**, qui tente d'empêcher la leak des valeurs de tag (via speculative side-channels, etc.). |
| **Allocator integration & retagging** | MTE laisse une grande partie de la logique de l'allocator au software. | Les allocators typés sécurisés d'Apple (kalloc_type, xzone malloc, etc.) s'intègrent à EMTE : lorsque la mémoire est allouée ou libérée, les tags sont gérés à granularité fine. |
| **Always-on by default** | Sur de nombreuses plateformes, MTE est optionnel ou désactivé par défaut. | Apple active EMTE / MIE par défaut sur le hardware supporté (p.ex. iPhone 17 / A19) pour le kernel et de nombreux processus utilisateur. |

Parce qu'Apple contrôle à la fois le hardware et la stack software, il peut appliquer EMTE strictement, éviter les pièges de performance et boucher les trous des side-channels.

---

## How EMTE works in practice (Apple / MIE)

Voici une description à haut niveau de la manière dont EMTE opère sous le setup MIE d'Apple :

1. **Tag assignment**
- Quand la mémoire est allouée (p.ex. dans le kernel ou user space via des secure allocators), un **secret tag** est assigné à ce bloc.
- Le pointeur retourné à l'utilisateur ou au kernel inclut ce tag dans ses bits de poids fort (en utilisant TBI / top byte ignore mechanisms).

2. **Tag checking on access**
- Chaque fois qu'un load ou store est exécuté avec un pointeur, le hardware vérifie que le tag du pointeur correspond au tag du bloc mémoire (allocation tag). En cas de mismatch, il génère un fault immédiatement (puisque synchrone).
- Étant synchrone, il n'y a pas de fenêtre de détection retardée.

3. **Retagging on free / reuse**
- Quand la mémoire est libérée, l'allocator change le tag du bloc (donc les anciens pointeurs avec d'anciens tags ne correspondent plus).
- Un use-after-free pointer aura donc un tag stale et provoquera un mismatch lorsqu'il sera accédé.

4. **Neighbor-tag differentiation to catch overflows**
- Les allocations adjacentes reçoivent des tags distincts. Si un buffer overflow déborde dans la mémoire du voisin, le tag mismatch provoque un fault.
- Ceci est particulièrement efficace pour détecter les petits overflows qui franchissent une frontière.

5. **Tag confidentiality enforcement**
- Apple doit empêcher que les valeurs de tag soient leakées (car si un attaquant apprend le tag, il pourrait fabriquer des pointeurs avec les tags corrects).
- Ils incluent des protections (microarchitectural / speculative controls) pour éviter la leak des bits de tag via des side-channels.

6. **Kernel and user-space integration**
- Apple utilise EMTE non seulement en user-space mais aussi dans des composants kernel / critiques pour l'OS (pour protéger le kernel contre la corruption mémoire).
- Le hardware/OS s'assure que les règles de tag s'appliquent même quand le kernel s'exécute pour le compte du user space.

Comme EMTE est intégré à MIE, Apple l'utilise en mode synchrone sur les surfaces d'attaque clés, pas comme optionnel ou mode debug.

---

## Exception handling in XNU

Quand une **exception** survient (p.ex., `EXC_BAD_ACCESS`, `EXC_BAD_INSTRUCTION`, `EXC_CRASH`, `EXC_ARM_PAC`, etc.), la couche **Mach** du kernel XNU est responsable de l'intercepter avant qu'elle ne devienne un **signal** de style UNIX (comme `SIGSEGV`, `SIGBUS`, `SIGILL`, ...).

Ce processus implique plusieurs couches de propagation et de gestion d'exception avant d'atteindre l'espace utilisateur ou d'être converti en signal BSD.


### Exception Flow (High-Level)

1.  **Le CPU déclenche une exception synchrone** (p.ex. déréférencement d'un pointeur invalide, échec PAC, instruction illégale, etc.).

2.  **Le low-level trap handler** s'exécute (`trap.c`, `exception.c` dans le source XNU).

3.  Le trap handler appelle **`exception_triage()`**, le cœur de la gestion des exceptions Mach.

4.  `exception_triage()` décide comment router l'exception :

-   D'abord vers le **thread's exception port**.

-   Ensuite vers le **task's exception port**.

-   Ensuite vers le **host's exception port** (souvent `launchd` ou `ReportCrash`).

Si aucune de ces ports ne gère l'exception, le kernel peut :

-   **La convertir en un BSD signal** (pour les processus user-space).

-   **Panic** (pour les exceptions kernel-space).


### Core Function: `exception_triage()`

La fonction `exception_triage()` route les exceptions Mach le long de la chaîne des handlers possibles jusqu'à ce que l'une d'elles s'en charge ou jusqu'à ce que l'exception soit finalement fatale. Elle est définie dans `osfmk/kern/exception.c`.
```c
void exception_triage(exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt);

Flux d’appel typique :

exception_triage() └── exception_deliver() ├── exception_deliver_thread() ├── exception_deliver_task() └── exception_deliver_host()

Si tout échoue → pris en charge par bsd_exception() → traduit en un signal tel que SIGSEGV.

Ports d’exception

Chaque objet Mach (thread, task, host) peut enregistrer des ports d’exception, vers lesquels sont envoyés les messages d’exception.

Ils sont définis par l’API:

task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()

Chaque exception port possède :

  • Un mask (quelles exceptions il veut recevoir)
  • Un port name (Mach port pour recevoir les messages)
  • Un behavior (comment le kernel envoie le message)
  • Un flavor (quel état de thread inclure)

Debuggers and Exception Handling

Un debugger (par ex., LLDB) définit un exception port sur la task ou le thread cible, généralement en utilisant task_set_exception_ports().

Quand une exception survient :

  • Le Mach message est envoyé au processus debugger.
  • Le debugger peut décider de handle (reprendre, modifier les registres, sauter l’instruction) ou de not handle l’exception.
  • Si le debugger ne la gère pas, l’exception se propage au niveau suivant (task → host).

Flow of EXC_BAD_ACCESS

  1. Le thread déréférence un pointeur invalide → le CPU lève un Data Abort.

  2. Le kernel trap handler appelle exception_triage(EXC_BAD_ACCESS, ...).

  3. Message envoyé à :

  • Thread port → (le debugger peut intercepter le breakpoint).

  • Si le debugger ignore → Task port → (handler au niveau processus).

  • Si ignoré → Host port (généralement ReportCrash).

  1. Si personne ne gère → bsd_exception() traduit en SIGSEGV.

PAC Exceptions

Quand la Pointer Authentication (PAC) échoue (mismatch de signature), une exception Mach spéciale est levée :

  • EXC_ARM_PAC (type)
  • Les codes peuvent inclure des détails (par ex., type de clé, type de pointeur).

Si le binaire a le flag TFRO_PAC_EXC_FATAL, le kernel traite les échecs PAC comme fatals, contournant l’interception par le debugger. C’est pour empêcher des attaquants d’utiliser les debuggers pour contourner les checks PAC et c’est activé pour les platform binaries.

Software Breakpoints

Un breakpoint logiciel (int3 sur x86, brk sur ARM64) est implémenté en provoquant une faute délibérée.
Le debugger attrape cela via l’exception port :

  • Modifie le pointeur d’instruction ou la mémoire.
  • Restaure l’instruction originale.
  • Reprend l’exécution.

Ce même mécanisme permet de “catch” une exception PAC — sauf si TFRO_PAC_EXC_FATAL est défini, auquel cas elle n’atteint jamais le debugger.

Conversion to BSD Signals

Si aucun handler n’accepte l’exception :

  • Le kernel appelle task_exception_notify() → bsd_exception().

  • Cela mappe les Mach exceptions aux signaux :

Mach ExceptionSignal
EXC_BAD_ACCESSSIGSEGV or SIGBUS
EXC_BAD_INSTRUCTIONSIGILL
EXC_ARITHMETICSIGFPE
EXC_SOFTWARESIGTRAP
EXC_BREAKPOINTSIGTRAP
EXC_CRASHSIGKILL
EXC_ARM_PACSIGILL (on non-fatal)

### Key Files in XNU Source

  • osfmk/kern/exception.c → Coeur de exception_triage(), exception_deliver_*().

  • bsd/kern/kern_sig.c → Logique de delivery des signaux.

  • osfmk/arm64/trap.c → Handlers bas-niveau des traps.

  • osfmk/mach/exc.h → Codes d’exception et structures.

  • osfmk/kern/task.c → Setup des task exception ports.


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

Le noyau utilisait un zone allocator (kalloc) divisé en “zones” de tailles fixes.
Chaque zone ne stockait que des allocations d’une seule classe de taille.

D’après la capture d’écran :

Zone NameElement SizeExample Use
default.kalloc.1616 bytesTrès petites structs kernel, pointeurs.
default.kalloc.3232 bytesPetites structs, headers d’objets.
default.kalloc.6464 bytesIPC messages, tout petits buffers kernel.
default.kalloc.128128 bytesObjets moyens comme des parties de OSObject.
default.kalloc.12801280 bytesGrandes structures, métadonnées IOSurface/graphics.

Comment ça fonctionnait :

  • Chaque requête d’allocation était arrondie à la taille de zone la plus proche. (Ex. une requête de 50 octets allait dans la zone kalloc.64).
  • La mémoire dans chaque zone était maintenue dans une freelist — les chunks libérés par le kernel retournaient dans cette zone.
  • Si vous débordiez un buffer de 64 octets, vous écrasiez l’objet suivant dans la même zone.

C’est pour ça que le heap spraying / feng shui était si efficace : on pouvait prédire les voisins d’un objet en sprayant des allocations de la même classe de taille.

The freelist

À l’intérieur de chaque zone kalloc, les objets libérés n’étaient pas retournés directement au système — ils allaient dans une freelist, une liste chaînée de chunks disponibles.

  • Quand un chunk était freed, le kernel écrivait un pointeur au début de ce chunk → l’adresse du chunk libre suivant dans la même zone.

  • La zone gardait un pointeur HEAD vers le premier chunk libre.

  • L’allocation utilisait toujours le HEAD actuel :

  1. Pop HEAD (retourne cette mémoire à l’appelant).

  2. Met à jour HEAD = HEAD->next (stocké dans l’entête du chunk libéré).

  • Le free poussait les chunks de retour :

  • freed_chunk->next = HEAD

  • HEAD = freed_chunk

Donc la freelist n’était qu’une liste chaînée construite à l’intérieur de la mémoire libérée.

État normal :

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)

Exploiting the 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 → overwrite its “next” pointer.

  2. Use-after-free écrire dans un objet freed → overwrite its “next” pointer.

Ensuite, lors de la prochaine allocation de cette taille :

  • L’allocator pop le chunk corrompu.

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

  • Retourne un pointer vers la mémoire arbitraire, permettant fake object primitives ou 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

The goal of heap grooming is to modeler la disposition du heap so that when an attacker triggers an overflow or use-after-free, the target (victim) object sits right next to an attacker-controlled object.
That way, when memory corruption happens, the attacker can reliably overwrite the victim object with controlled data.

Steps:

  1. Spray allocations (fill the holes)
  • Over time, the kernel heap gets fragmented: some zones have holes where old objects were freed.
  • The attacker first makes lots of dummy allocations to fill these gaps, so the heap becomes “packed” and predictable.
  1. Force new pages
  • Once the holes are filled, the next allocations must come from new pages added to the zone.
  • Fresh pages mean objects will be clustered together, not scattered across old fragmented memory.
  • This gives the attacker much better control of neighbors.
  1. Place attacker objects
  • The attacker now sprays again, creating lots of attacker-controlled objects in those new pages.
  • These objects are predictable in size and placement (since they all belong to the same zone).
  1. Free a controlled object (make a gap)
  • The attacker deliberately frees one of their own objects.
  • This creates a “hole” in the heap, which the allocator will later reuse for the next allocation of that size.
  1. Victim object lands in the hole
  • The attacker triggers the kernel to allocate the victim object (the one they want to corrupt).
  • Since the hole is the first available slot in the freelist, the victim is placed exactly where the attacker freed their object.
  1. Overflow / UAF into victim
  • Now the attacker has attacker-controlled objects around the victim.
  • By overflowing from one of their own objects (or reusing a freed one), they can reliably overwrite the victim’s memory fields with chosen values.

Why it works:

  • Zone allocator predictability: allocations of the same size always come from the same zone.
  • Freelist behavior: new allocations reuse the most recently freed chunk first.
  • Heap sprays: attacker fills memory with predictable content and controls layout.
  • End result: attacker controls where the victim object lands and what data sits next to it.

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

Apple hardened the allocator and made heap grooming much harder:

1. From Classic kalloc to kalloc_type

  • Before: a single kalloc.<size> zone existed for each size class (16, 32, 64, … 1280, etc.). Any object of that size was placed there → attacker objects could sit next to privileged kernel objects.
  • Now:
  • Kernel objects are allocated from typed zones (kalloc_type).
  • Each type of object (e.g., ipc_port_t, task_t, OSString, OSData) has its own dedicated zone, even if they’re the same size.
  • The mapping between object type ↔ zone is generated from the kalloc_type system at compile time.

An attacker can no longer guarantee that controlled data (OSData) ends up adjacent to sensitive kernel objects (task_t) of the same size.

2. Slabs and Per-CPU Caches

  • The heap is divided into slabs (pages of memory carved into fixed-size chunks for that zone).
  • Each zone has a per-CPU cache to reduce contention.
  • Allocation path:
  1. Try per-CPU cache.
  2. If empty, pull from the global freelist.
  3. If freelist is empty, allocate a new slab (one or more pages).
  • Benefit: This decentralization makes heap sprays less deterministic, since allocations may be satisfied from different CPUs’ caches.

3. Randomization inside zones

  • Within a zone, freed elements are not handed back in simple FIFO/LIFO order.
  • Modern XNU uses encoded freelist pointers (safe-linking like Linux, introduced ~iOS 14).
  • Each freelist pointer is XOR-encoded with a per-zone secret cookie.
  • This prevents attackers from forging a fake freelist pointer if they gain a write primitive.
  • Some allocations are randomized in their placement within a slab, so spraying doesn’t guarantee adjacency.

4. Guarded Allocations

  • Certain critical kernel objects (e.g., credentials, task structures) are allocated in guarded zones.
  • These zones insert guard pages (unmapped memory) between slabs or use redzones around objects.
  • Any overflow into the guard page triggers a fault → immediate panic instead of silent corruption.

5. Page Protection Layer (PPL) and SPTM

  • Even if you control a freed object, you can’t modify all of kernel memory:
  • PPL (Page Protection Layer) enforces that certain regions (e.g., code signing data, entitlements) are read-only even to the kernel itself.
  • On A15/M2+ devices, this role is replaced/enhanced by SPTM (Secure Page Table Monitor) + TXM (Trusted Execution Monitor).
  • These hardware-enforced layers mean attackers can’t escalate from a single heap corruption to arbitrary patching of critical security structures.
  • (Added / Enhanced): also, PAC (Pointer Authentication Codes) is used in the kernel to protect pointers (especially function pointers, vtables) so that forging or corrupting them becomes harder.
  • (Added / Enhanced): zones may enforce zone_require / zone enforcement, i.e. that an object freed can only be returned through its correct typed zone; invalid cross-zone frees may panic or be rejected. (Apple alludes to this in their memory safety posts)

6. Large Allocations

  • Not all allocations go through kalloc_type.
  • Very large requests (above ~16 KB) bypass typed zones and are served directly from kernel VM (kmem) via page allocations.
  • These are less predictable, but also less exploitable, since they don’t share slabs with other objects.

7. Allocation Patterns Attackers Target

Even with these protections, attackers still look for:

  • Reference count objects: if you can tamper with retain/release counters, you may cause use-after-free.
  • Objects with function pointers (vtables): corrupting one still yields control flow.
  • Shared memory objects (IOSurface, Mach ports): these are still attack targets because they bridge user ↔ kernel.

But — unlike before — you can’t just spray OSData and expect it to neighbor a task_t. You need type-specific bugs or info leaks to succeed.

Example: Allocation Flow in Modern Heap

Suppose userspace calls into IOKit to allocate an OSData object:

  1. Type lookupOSData maps to kalloc_type_osdata zone (size 64 bytes).
  2. Check per-CPU cache for free elements.
  • If found → return one.
  • If empty → go to global freelist.
  • If freelist empty → allocate a new slab (page of 4KB → 64 chunks of 64 bytes).
  1. Return chunk to caller.

Freelist pointer protection:

  • Each freed chunk stores the address of the next free chunk, but encoded with a secret key.
  • Overwriting that field with attacker data won’t work unless you know the key.

Comparison Table

FeatureOld Heap (Pre-iOS 15)Modern Heap (iOS 15+ / A12+)
Allocation granularityFixed size buckets (kalloc.16, kalloc.32, etc.)Size + type-based buckets (kalloc_type)
Placement predictabilityHigh (same-size objects side by side)Low (same-type grouping + randomness)
Freelist managementRaw pointers in freed chunks (easy to corrupt)Encoded pointers (safe-linking style)
Adjacent object controlEasy via sprays/frees (feng shui predictable)Hard — typed zones separate attacker objects
Kernel data/code protectionsFew hardware protectionsPPL / SPTM protect page tables & code pages, and PAC protects pointers
Allocation reuse validationNone (freelist pointers raw)zone_require / zone enforcement
Exploit reliabilityHigh with heap spraysMuch lower, requires logic bugs or info leaks
Large allocations handlingAll small allocations managed equallyLarge ones bypass zones → handled via VM

Modern Userland Heap (iOS, macOS — type-aware / xzone malloc)

In recent Apple OS versions (especially iOS 17+), Apple introduced a more secure userland allocator, xzone malloc (XZM). This is the user-space analog to the kernel’s kalloc_type, applying type awareness, metadata isolation, and memory tagging safeguards.

Goals & Design Principles

  • Type segregation / type awareness: group allocations by type or usage (pointer vs data) to prevent type confusion and cross-type reuse.
  • Metadata isolation: separate heap metadata (e.g. free lists, size/state bits) from object payloads so that out-of-bounds writes are less likely to corrupt metadata.
  • Guard pages / redzones: insert unmapped pages or padding around allocations to catch overflows.
  • Memory tagging (EMTE / MIE): work in conjunction with hardware tagging to detect use-after-free, out-of-bounds, and invalid accesses.
  • Scalable performance: maintain low overhead, avoid excessive fragmentation, and support many allocations per second with low latency.

Architecture & Components

Below are the main elements in the xzone allocator:

Segment Groups & Zones

  • Segment groups partition the address space by usage categories: e.g. data, pointer_xzones, data_large, pointer_large.
  • Each segment group contains segments (VM ranges) that host allocations for that category.
  • Associated with each segment is a metadata slab (separate VM area) that stores metadata (e.g. free/used bits, size classes) for that segment. This out-of-line (OOL) metadata ensures that metadata is not intermingled with object payloads, mitigating corruption from overflows.
  • Segments are carved into chunks (slices) which in turn are subdivided into blocks (allocation units). A chunk is tied to a specific size class and segment group (i.e. all blocks in a chunk share the same size & category).
  • For small / medium allocations, it will use fixed-size chunks; for large/huges, it may map separately.

Chunks & Blocks

  • A chunk is a region (often several pages) dedicated to allocations of one size class within a group.
  • Inside a chunk, blocks are slots available for allocations. Freed blocks are tracked via the metadata slab — e.g. via bitmaps or free lists stored out-of-line.
  • Between chunks (or within), guard slices / guard pages may be inserted (e.g. unmapped slices) to catch out-of-bounds writes.

Type / Type ID

  • Every allocation site (or call to malloc, calloc, etc.) is associated with a type identifier (a malloc_type_id_t) which encodes what kind of object is being allocated. That type ID is passed to the allocator, which uses it to select which zone / segment to serve the allocation.
  • Because of this, even if two allocations have the same size, they may go into entirely different zones if their types differ.
  • In early iOS 17 versions, not all APIs (e.g. CFAllocator) were fully type-aware; Apple addressed some of those weaknesses in iOS 18.

Allocation & Freeing Workflow

Here is a high-level flow of how allocation and deallocation operate in xzone:

  1. malloc / calloc / realloc / typed alloc is invoked with a size and type ID.
  2. The allocator uses the type ID to pick the correct segment group / zone.
  3. Within that zone/segment, it seeks a chunk that has free blocks of the requested size.
  • It may consult local caches / per-thread pools or free block lists from metadata.
  • If no free block is available, it may allocate a new chunk in that zone.
  1. The metadata slab is updated (free bit cleared, bookkeeping).
  2. If memory tagging (EMTE) is in play, the returned block gets a tag assigned, and metadata is updated to reflect its “live” state.
  3. When free() is called:
  • The block is marked as freed in metadata (via OOL slab).
  • The block may be placed into a free list or pooled for reuse.
  • Optionally, block contents may be cleared or poisoned to reduce data leaks or use-after-free exploitation.
  • The hardware tag associated with the block may be invalidated or re-tagged.
  • If an entire chunk becomes free (all blocks freed), the allocator may reclaim that chunk (unmap it or return to OS) under memory pressure.

Security Features & Hardening

These are the defenses built into modern userland xzone:

FeaturePurposeNotes
Metadata decouplingPrevent overflow from corrupting metadataMetadata lives in separate VM region (metadata slab)
Guard pages / unmapped slicesCatch out-of-bounds writesHelps detect buffer overflows rather than silently corrupting adjacent blocks
Type-based segregationPrevent cross-type reuse & type confusionEven same-size allocations from different types go to different zones
Memory Tagging (EMTE / MIE)Detect invalid access, stale references, OOB, UAFxzone works in concert with hardware EMTE in synchronous mode (“Memory Integrity Enforcement”)
Delayed reuse / poisoning / zapReduce chance of use-after-free exploitationFreed blocks may be poisoned, zeroed, or quarantined before reuse
Chunk reclamation / dynamic unmappingReduce memory waste and fragmentationEntire chunks may be unmapped when unused
Randomization / placement variationPrevent deterministic adjacencyBlocks in a chunk and chunk selection may have randomized aspects
Segregation of “data-only” allocationsSeparate allocations that don’t store pointersReduces attacker control over metadata or control fields

Interaction with Memory Integrity Enforcement (MIE / EMTE)

  • Apple’s MIE (Memory Integrity Enforcement) is the hardware + OS framework that brings Enhanced Memory Tagging Extension (EMTE) into always-on, synchronous mode across major attack surfaces.
  • xzone allocator is a fundamental foundation of MIE in user space: allocations done via xzone get tags, and accesses are checked by hardware.
  • In MIE, the allocator, tag assignment, metadata management, and tag confidentiality enforcement are integrated to ensure that memory errors (e.g. stale reads, OOB, UAF) are caught immediately, not exploited later.

If you like, I can also generate a cheat-sheet or diagram of xzone internals for your book. Do you want me to do that next?
:contentReference[oai:20]{index=20}

(Old) Physical Use-After-Free via IOSurface

ios Physical UAF - IOSurface


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 install it even if there is a version mismatch.

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:

macOS Kernel Extensions & Kernelcache

  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.

JSKit-Based Safari Chains and PREYHUNTER Stagers

Renderer RCE abstraction with JSKit

  • Reusable entry: Recent in-the-wild chains abused a WebKit JIT bug (patched as CVE-2023-41993) purely to gain JavaScript-level arbitrary read/write. The exploit immediately pivots into a purchased framework called JSKit, so any future Safari bug only needs to deliver the same primitive.
  • Version abstraction & PAC bypasses: JSKit bundles support for a wide range of iOS releases together with multiple, selectable Pointer Authentication Code bypass modules. The framework fingerprints the target build, selects the appropriate PAC bypass logic, and verifies every step (primitive validation, shellcode launch) before progressing.
  • Manual Mach-O mapping: JSKit parses Mach-O headers directly from memory, resolves the symbols it needs inside dyld-cached images, and can manually map additional Mach-O payloads without writing them to disk. This keeps the renderer process in-memory only and evades code-signature checks tied to filesystem artifacts.
  • Portfolio model: Debug strings such as “exploit number 7” show that the suppliers maintain multiple interchangeable WebKit exploits. Once the JS primitive matches JSKit’s interface, the rest of the chain is unchanged across campaigns.

Kernel bridge: IPC UAF -> code-sign bypass pattern

  • Kernel IPC UAF (CVE-2023-41992): The second stage, still running inside the Safari context, triggers a kernel use-after-free in IPC code, re-allocates the freed object from userland, and abuses the dangling pointers to pivot into arbitrary kernel read/write. The stage also reuses PAC bypass material previously computed by JSKit instead of re-deriving it.
  • Code-signing bypass (CVE-2023-41991): With kernel R/W available, the exploit patches the trust cache / code-signing structures so unsigned payloads execute as system. The stage then exposes a lightweight kernel R/W service to later payloads.
  • Composed pattern: This chain demonstrates a reusable recipe that defenders should expect going forward:
WebKit renderer RCE -> kernel IPC UAF -> kernel arbitrary R/W -> code-sign bypass -> unsigned system stager

PREYHUNTER helper & watcher modules

  • Watcher anti-analysis: Un binaire watcher dédié profile en permanence l’appareil et interrompt la kill-chain lorsqu’un environnement de recherche est détecté. Il inspecte security.mac.amfi.developer_mode_status, la présence d’une console diagnosticd, les locales US ou IL, des traces de jailbreak comme Cydia, des processus tels que bash, tcpdump, frida, sshd ou checkrain, des applications mobile AV (McAfee, AvastMobileSecurity, NortonMobileSecurity), des paramètres proxy HTTP personnalisés et des root CA personnalisés. L’échec de n’importe quel contrôle bloque la livraison ultérieure du payload.

  • Helper surveillance hooks: Le composant helper communique avec d’autres étapes via /tmp/helper.sock, puis charge des jeux de hooks nommés DMHooker et UMHooker. Ces hooks interceptent les chemins audio VOIP (les enregistrements sont stockés sous /private/var/tmp/l/voip_%lu_%u_PART.m4a), implémentent un keylogger système, capturent des photos sans UI et hookent SpringBoard pour supprimer les notifications que ces actions déclencheraient normalement. Le helper joue donc un rôle de validation furtive + couche de light-surveillance avant le déploiement d’implants plus lourds tels que Predator.

  • HiddenDot indicator suppression in SpringBoard: Avec une injection de code au niveau kernel, Predator hooke SBSensorActivityDataProvider._handleNewDomainData: (le point d’agrégation pour l’activité des capteurs). Le hook met à zéro le pointeur Objective-C self (x0) de sorte que l’appel devienne [nil _handleNewDomainData:newData], annulant les mises à jour caméra/micro et supprimant les points vert/orange.

  • Mach exception-based hooking flow (DMHooker): Les hooks sont implémentés via EXC_BREAKPOINT + exception ports, puis thread_set_state modifie les registres et l’exécution reprend. Le code de retour 2 signifie « continue with modified thread state. »

  • PAC-aware redirection for camera access checks: Dans mediaserverd, un pattern-scan (par ex. memmem) localise une routine privée proche de FigVideoCaptureSourceCreateWithSourceInfo à l’intérieur de CMCapture.framework. Le hook retourne 3 pour rediriger en utilisant une adresse de retour PAC pré-signée en cache, satisfaisant PAC tout en contournant le contrôle.

  • VoIP capture pipeline in mediaserverd: Hooke AudioConverterNew et AudioConverterConvertComplexBuffer+52 pour intercepter les buffers, déduire la sample rate à partir des tailles de buffer, convertir float32 PCM → int16 avec NEON, downmix 4-canaux en stéréo et persister via ExtAudioFileWrite(). Le module VoIP lui-même ne supprime pas les indicateurs, donc les opérateurs doivent activer HiddenDot séparément.

WebKit DFG Store-Barrier UAF + ANGLE PBO OOB (iOS 26.1)

Webkit Dfg Store Barrier Uaf Angle Oob

iMessage/Media Parser Zero-Click Chains

Imessage Media Parser Zero Click Coreaudio Pac Bypass

References

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks