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
- Vérifiez les plans d’abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
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, bibliothèques dynamiques, code JIT, 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-flip, patché) ou non signé, le chargement échoue.
- Contre : l’étape classique « déposer le payload + exécuter » dans les chaînes d’exploit ; injection de code arbitraire ; modifier un binaire existant pour y insérer une logique malveillante.
- Détails du mécanisme :
- Le chargeur Mach-O (et le 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 caches JIT ou le code généré dynamiquement, Apple exige que les pages soient signées ou validées via des APIs spéciales (par ex.
mprotectavec vérifications de code-sign). - La signature inclut les entitlements et identifiants ; l’OS impose que certaines APIs ou capacités privilégiées requièrent des entitlements spécifiques qui ne peuvent être falsifiés.
Exemple
Supposons qu’un exploit obtienne de l’exécution de code dans un processus 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 aux contraintes de code-signature. Comme 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 appareils récents / versions iOS ultérieures) CoreTrust est le sous-système qui effectue la validation de signature à l’exécution des binaires (y compris système et binaires utilisateur) contre le certificat racine d’Apple plutôt que de se fier à des magasins de confiance userland mis en cache.
- Contre : la falsification post-installation de binaires, les techniques de jailbreak qui tentent d’échanger ou patcher des bibliothèques système ou des apps utilisateur ; tromper le système en remplaçant des binaires de confiance par des homologues malveillants.
- Détails du mécanisme :
- Au lieu de faire confiance à une base locale de certificats ou un cache, CoreTrust consulte directement la racine d’Apple ou vérifie les certificats intermédiaires dans une chaîne sécurisée.
- Il garantit que les modifications (p.ex. dans le filesystem) des binaires existants sont détectées et rejetées.
- Il lie les entitlements, team IDs, flags de signature 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 quand le chargeur de l’OS ou CoreTrust vérifie, il remarque le mismatch de signature (ou les entitlements modifiés) et refuse d’exécuter.3. Data Execution Prevention (DEP / NX / W^X)
Introduit dans beaucoup d’OS plus tôt ; iOS a eu le NX-bit / w^x depuis longtemps DEP impose que les pages marquées en écriture (pour les données) soient non-exécutables, et que les pages marquées exécutables soient non-écrivables. On ne peut pas simplement écrire du shellcode dans un heap ou une stack et l’exécuter.
- Contre : exécution directe de shellcode ; overflow classique → saut vers du shellcode injecté.
- Détails du mécanisme :
- L’UMM / les flags de protection mémoire (via les tables de pages) font respecter la séparation.
- Toute tentative de marquer une page écrivable comme exécutable déclenche une vérification système (et est soit interdite soit requiert l’approbation de code-sign).
- Dans de nombreux cas, rendre des pages exécutables exige de passer par des APIs OS qui imposent des contraintes ou des vérifications supplémentaires.
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 des contraintes de code-sign (choses que le shellcode ne peut pas satisfaire).4. Address Space Layout Randomization (ASLR)
Introduit vers l’ère iOS ~4–5 ASLR randomise les adresses de base des régions mémoire clés : bibliothèques, heap, stack, etc., à chaque lancement de processus. Les adresses des gadgets bougent entre les exécutions.
- Contre : hardcoding d’adresses de gadgets pour ROP/JOP ; chaînes d’exploit statiques ; sauts « aveugles » vers des offsets connus.
- Détails du mécanisme :
- Chaque bibliothèque / module dynamique chargé est rebasé à un offset aléatoire.
- Les pointeurs de base de stack et de heap sont randomisés (avec certaines limites d’entropie).
- Parfois d’autres régions (p.ex. mmap allocations) sont aussi randomisées.
- Combiné aux mitigations d’information-disclosure, 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 s’attend à un gadget à `0x….lib + offset`. Mais comme `lib` est relocée différemment à chaque exécution, la chaîne hardcodée é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 vers iOS ~ (iOS 5 / iOS 6) Analogue à l’ASLR utilisateur, KASLR randomise la base du texte du kernel et d’autres structures kernel au démarrage.
- Contre : exploits kernel qui reposent sur des adresses fixes de code ou de données kernel ; exploits kernel statiques.
- Détails du mécanisme :
- À chaque boot, l’adresse de 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 offseté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 (p.ex. dans un `vtable`) à `KERN_BASE + offset`. Mais comme `KERN_BASE` est inconnu, l’attaquant doit d’abord le leak (p.ex. via une primitive de read) avant de calculer l’adresse correcte à corrompre.6. Kernel Patch Protection (KPP / AMCC)
Introduit sur iOS plus récents / hardware A-series (post environ iOS 15–16 ou puces plus récentes) KPP (alias AMCC) surveille en continu l’intégrité des pages de texte du kernel (via hash ou checksum). Si elle détecte une altération (patchs, hooks inline, modifications de code) en dehors des fenêtres autorisées, elle déclenche un kernel panic ou un reboot.
- Contre : patching persistant du kernel (modification d’instructions), hooks inline, overwrites de fonctions statiques.
- Détails du mécanisme :
- Un module hardware ou firmware surveille la région de texte du kernel.
- Il re-hashe périodiquement ou à la demande les pages et compare aux valeurs attendues.
- Si des mismatches surviennent hors fenêtres de mise à jour bénignes, il panique l’appareil (pour éviter une persistance malveillante).
- Les attaquants doivent soit éviter les fenêtres de détection soit utiliser des chemins de patch légitimes.
Exemple
Un exploit tente de patcher le prologue d’une fonction kernel (p.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 panic kernel, plantant l’appareil avant que le patch ne se stabilise.7. Kernel Text Read‐Only Region (KTRR)
Introduit sur les SoC modernes (post ~A12 / hardware plus récent) KTRR est un mécanisme imposé par le hardware : une fois le texte du kernel verrouillé tôt au boot, il devient en lecture seule depuis EL1 (le kernel), empêchant toute écriture ultérieure sur les pages de code.
- Contre : toute modification du code kernel après le boot (p.ex. patching, injection de code in-place) au niveau de privilège EL1.
- Détails du mécanisme :
- Pendant le boot (dans la phase secure/bootloader), le contrôleur mémoire (ou une unité matérielle sécurisée) marque les pages physiques contenant le texte du kernel en lecture seule.
- Même si un exploit obtient les privilèges kernel complets, il ne peut pas écrire sur ces pages pour patcher les 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 (p.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 patchs ne sont pas appliqués.8. Pointer Authentication Codes (PAC)
Introduit avec ARMv8.3 (hardware), Apple commençant avec A12 / iOS ~12+
- PAC est une fonctionnalité hardware introduite dans ARMv8.3-A pour détecter la falsification de valeurs de pointeurs (adresses de retour, pointeurs de fonction, certains pointeurs de données) en incorporant une petite signature cryptographique (un « MAC ») dans des bits supérieurs inutilisés du pointeur.
- La signature (« PAC ») est calculée sur la valeur du pointeur plus un modifier (une valeur de contexte, p.ex. stack pointer ou une donnée distincte). Ainsi la même valeur de pointeur dans des contextes différents a un PAC différent.
- Au moment de l’utilisation, avant de déréférencer ou de faire un branchement via ce pointeur, une instruction authenticate vérifie le PAC. Si valide, le PAC est retiré et on obtient le pointeur pur ; si invalide, le pointeur devient « poison » (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 directement lisibles depuis le mode utilisateur.
- Parce que tous les 64 bits d’un pointeur ne sont pas utilisés dans beaucoup de systèmes (p.ex. espace d’adressage 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 system 64-bit) pour la pointer authentication.
-
APIAKey — pour les instruction pointers (domaine “I”, clé A)
-
APIBKey — seconde clé d’instruction (domaine “I”, clé B)
-
APDAKey — pour les data pointers (domaine “D”, clé A)
-
APDBKey — pour les data pointers (domaine “D”, clé B)
-
APGAKey — clé « generic », pour signer des données non-pointeur ou usages génériques
-
Ces clés sont stockées dans des registres système privilégiés (accessibles seulement à EL1/EL2 etc.), non accessibles depuis le mode utilisateur.
-
Le PAC est calculé via une fonction cryptographique (ARM suggère QARMA comme algorithme) en utilisant :
- La valeur du pointeur (portion canonique)
- Un modifier (valeur de contexte, comme un salt)
- La clé secrète
- Un certain logic de tweak interne 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 les lettres de domaine.
PACxxinstructions signent un pointeur et insèrent un PACAUTxxinstructions authentifient + retirent (valident et enlèvent le PAC)XPACxxinstructions retirent sans valider
Domaines / suffixes :
| Mnémonique | Signification / Domaine | Clé / Domaine | Exemple d’utilisation en assembleur |
|---|---|---|---|
| PACIA | Signer un instruction pointer avec APIAKey | “I, A” | PACIA X0, X1 — sign pointer in X0 using APIAKey with modifier X1 |
| PACIB | Signer un instruction pointer avec APIBKey | “I, B” | PACIB X2, X3 |
| PACDA | Signer un data pointer avec APDAKey | “D, A” | PACDA X4, X5 |
| PACDB | Signer un data pointer avec APDBKey | “D, B” | PACDB X6, X7 |
| PACG / PACGA | Generic (non-pointer) signing avec APGAKey | “G” | PACGA X8, X9, X10 (sign X9 with modifier X10 into X8) |
| AUTIA | Authentifier un instruction pointer signé par APIA & retirer PAC | “I, A” | AUTIA X0, X1 — check PAC on X0 using modifier X1, then strip |
| AUTIB | Authentifier domaine APIB | “I, B” | AUTIB X2, X3 |
| AUTDA | Authentifier un data pointer signé par APDA | “D, A” | AUTDA X4, X5 |
| AUTDB | Authentifier un data pointer signé par APDB | “D, B” | AUTDB X6, X7 |
| AUTGA | Authentifier generic / blob (APGA) | “G” | AUTGA X8, X9, X10 (validate generic) |
| XPACI | Retirer PAC (instruction pointer, sans validation) | “I” | XPACI X0 — remove PAC from X0 (instruction domain) |
| XPACD | Retirer PAC (data pointer, sans validation) | “D” | XPACD X4 — remove PAC from data pointer in X4 |
Il existe des formes spécialisées / alias :
PACIASPest l’abréviation dePACIA X30, SP(signer le link register en utilisant SP comme modifier)AUTIASPestAUTIA X30, SP(authentifier le link register avec SP)- Formes combinées comme
RETAA,RETAB(authentifier-et-retourner) ouBLRAA(authentifier & branch) existent dans les extensions ARM / support compilateur. - Aussi des variantes à modifier implicite zéro :
PACIZA/PACIZBoù le modifier est implicitement zéro, etc.
Modifiers
Le but principal du modifier est de lier le PAC à un contexte spécifique afin que la même adresse signée dans différents contextes produise des PAC différents. Cela empêche la réutilisation simple d’un pointeur entre frames ou objets — 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 la signature d’adresses de retour : le PAC est lié à la frame de stack spécifique. Si on tente de réutiliser LR dans une autre frame, le modifier change et la validation PAC échoue.
- La même valeur de pointeur signée sous des modifiers différents produit des PAC distincts.
- Le modifier n’a pas besoin d’être secret, mais idéalement il n’est pas contrôlable par l’attaquant.
- Pour des instructions qui signent ou vérifient 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 cross-boot.
- Ils incluent aussi des mitigations cross-domain pour que les PAC signés en user mode ne puissent pas être réutilisés facilement en kernel mode, etc.
- Sur Apple M1 / Apple Silicon, le reverse engineering a montré qu’il existe neuf types de modifier et des registres système Apple-spécifiques pour le contrôle des clés.
- Apple utilise PAC dans de nombreux sous-systèmes kernel : signature des adresses de retour, intégrité des pointeurs dans les données kernel, contextes de thread signés, etc.
- Google Project Zero a montré comment, sous une primitive puissante de lecture/écriture mémoire en kernel, on pouvait forger des PAC kernel (pour les clés A) sur des appareils A12, mais Apple a patché beaucoup de ces chemins.
- Dans le système d’Apple, certaines clés sont globales au kernel, tandis que les processus utilisateur peuvent obtenir des randomness de clé par-processus.
Contournements de PAC
- PAC kernel : 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 dans « iOS Kernel PAC, One Year Later » (2020) rapporte qu’en iOS 12-13 il a trouvé quelques contournements partiels (gadgets de signing, réutilisation d’états signés, branches indirectes non protégées) mais pas de contournement générique complet. bazad.github.io
- Les personnalisations « Dark Magic » d’Apple réduisent encore les surfaces exploitables (changement de domaine, bits d’activation par clé). i.blackhat.com
- Il existe un contournement kernel PAC connu CVE-2023-32424 sur Apple silicon (M1/M2) rapporté par Zecao Cai et al. i.blackhat.com
- Mais ces contournements reposent souvent sur des gadgets très spécifiques ou des bugs d’implémentation ; ils ne sont pas génériques.
Ainsi le PAC kernel est considéré comme très robuste, bien que pas parfait.
- Techniques de bypass en user-mode / runtime PAC
Celles-ci sont plus courantes, et exploitent des imperfections dans l’application de PAC ou son usage dans le linking dynamique / frameworks runtime. Ci-dessous des classes avec exemples.
2.1 Shared Cache / problèmes de clé A
-
Le dyld shared cache est un gros blob pré-linké de frameworks et librairies système. Parce qu’il est tellement partagé, les pointeurs de fonction à 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 réutiliser des pointeurs signés par A-key présents dans le shared cache et de les réutiliser dans des gadgets.
-
Le talk “No Clicks Required” décrit la construction d’un oracle sur le shared cache pour déduire des adresses relatives et combiner cela avec des pointeurs signés afin de contourner PAC. saelo.github.io
-
Aussi, l’import de pointeurs de fonction depuis les librairies partagées en userspace a été trouvé insuffisamment protégé par PAC, permettant à un attaquant d’obtenir des pointeurs de fonction sans changer leur signature. (entrée bug Project Zero) bugs.chromium.org
2.2 dlsym(3) / résolution dynamique de symboles
-
Un contournement connu est d’appeler
dlsym()pour obtenir un pointeur de fonction déjà signé (signé avec A-key, diversifier zero) puis de l’utiliser. Puisquedlsymrenvoie un pointeur légitime signé, l’utiliser évite de devoir forger un PAC. -
Le blog d’Epsilon détaille comment certains bypass exploitent ceci : 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
dlsymsur iOS 18.4 renvoient des pointeurs incorrectement signés (ou avec des diversifiers bogués), permettant un contournement involontaire de PAC. 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 bypass 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 les relocations, puis les repassent en read-only. Les attaquants exploitent ces fenêtres. La présentation de Synacktiv décrit “Operation Triangulation”, un bypass timing-based de PAC via les relocations dynamiques. Synacktiv
-
Les pages DYLD sont maintenant protégées avec SPRR / VM_FLAGS_TPRO (quelques 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 bypass PAC. Les slides mentionnent que beaucoup de bypass PAC ont ciblé 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,NSExpressionouNSInvocationsont utilisées pour dissimuler des appels de contrôle sans forcer la forge de pointeurs. -
Sur de vieux 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) est étendue sous PAC aussi. Project Zero
-
La technique SLOP originale permettait d’enchaîner des appels ObjC en créant de fausses invocations ; le bypass repose sur le fait que ISA ou les selectors sont parfois pas entièrement protégés par PAC. Project Zero
-
Dans des environnements où la pointer authentication est appliquée partiellement, les méthodes / selectors / pointeurs cibles peuvent ne pas toujours avoir de protection PAC, laissant une marge de manœuvre pour un contournement.
Exemple de flux
Exemple de signature & d'authentification
``` ; 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 débordement de tampon écrase une adresse de retour sur la pile. L'attaquant écrit l'adresse du gadget cible mais ne peut pas calculer le PAC correct. Lorsque la fonction retourne, l'instruction `AUTIA` du CPU déclenche une faute à cause de l'incompatibilité du PAC. La chaîne échoue.
L'analyse de Project Zero sur A12 (iPhone XS) a montré comment Apple utilise PAC et les méthodes pour forger des PACs si un attaquant dispose d'un primitive de lecture/écriture mémoire.
</details>
### 9. **Identification de la cible de branchement (BTI)**
**Introduit avec ARMv8.5 (matériel plus récent)**
BTI est une fonctionnalité matérielle qui vérifie les cibles de branches indirectes : lors de l'exécution de `blr` ou d'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 ce landing pad déclenche une exception.
L'implémentation d'LLVM note trois variantes d'instructions BTI et comment elles se mappent aux types de branchement.
| BTI Variant | Ce que cela permet (quels types de branchements) | Emplacement / cas d'utilisation typique |
|-------------|----------------------------------------|-------------------------------|
| **BTI C** | Cibles des branches indirectes de type *call* (par ex. `BLR`, ou `BR` utilisant X16/X17) | Placée à l'entrée des fonctions qui peuvent être appelées indirectement |
| **BTI J** | Cibles des branches de type *jump* (par ex. `BR` utilisé pour des tail-calls) | Placée au début des blocs accessibles via des tables de saut ou des tail-calls |
| **BTI JC** | Agit à la fois comme C et J | Peut être ciblée par des branches de type call ou jump |
- Dans le code compilé avec enforcement des branch targets, les compilateurs insèrent une instruction BTI (C, J, ou JC) à chaque cible valide de branche indirecte (début de fonctions ou blocs accessibles par saut) de sorte que les branches indirectes ne réussissent que vers ces emplacements.
- **Branches / appels directs** (i.e. adresses fixes `B`, `BL`) **ne sont pas restreints** 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 branches directes sont considérées sûres).
- De plus, les instructions **RET / return** ne sont généralement pas restreintes par BTI car les adresses de retour sont protégées via PAC ou des mécanismes de signature de retour.
#### Mécanisme et application
- Lorsque le CPU décode une **branche indirecte (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). Sinon, une **Branch Target Exception** se produit.
- L'encodage de l'instruction BTI est conçu pour réutiliser des opcodes auparavant réservés pour des NOPs (dans des versions ARM antérieures). Ainsi, les binaires BTI-enabled restent rétrocompatibles : sur du matériel sans support BTI, ces instructions se comportent comme des NOPs.
- Les passes du compilateur qui ajoutent des BTI les insèrent seulement là où c'est nécessaire : fonctions susceptibles d'être appelées indirectement, ou blocs basiques ciblés par des sauts.
- Certains patchs et le code LLVM montrent que BTI n'est pas inséré pour *tous* les blocs basiques — seulement pour ceux qui sont des cibles potentielles de branchement (par ex. issus de switch / jump tables).
#### Synergie BTI + PAC
PAC protège la valeur du pointeur (la source) — il garantit que la chaîne d'appels/retours indirects n'a pas été altérée.
BTI garantit que, même si le pointeur est valide, il ne peut cibler que des points d'entrée correctement marqués.
Combinés, un attaquant a besoin à la fois d'un pointeur valide avec le PAC correct et que la cible possède un BTI à cet endroit. Cela augmente la difficulté de construire des gadgets exploitables.
#### Example
<details>
<summary>Example</summary>
Un exploit tente de basculer 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 génère une faute car l'alignement d'instruction n'inclut pas de landing pad valide. Ainsi, de nombreux gadgets deviennent inutilisables à moins qu'ils n'incluent le préfixe BTI.
</details>
### 10. **Privileged Access Never (PAN) & Privileged Execute Never (PXN)**
**Introduit dans des extensions ARMv8 plus récentes / support iOS (pour noyau durci)**
#### PAN (Privileged Access Never)
- **PAN** est une fonctionnalité introduite dans **ARMv8.1-A** qui empêche le **code privilégié** (EL1 ou EL2) de **lire ou écrire** la mémoire marquée comme **accessible par l'utilisateur (EL0)**, sauf si PAN est explicitement désactivé.
- L'idée : même si le noyau est trompé ou compromis, il ne peut pas déréférencer arbitrairement des pointeurs user-space sans d'abord *désactiver* PAN, réduisant ainsi les risques d'exploits de style **`ret2usr`** ou l'abus de buffers contrôlés par l'utilisateur.
- Quand PAN est activé (PSTATE.PAN = 1), toute instruction privilégiée de load/store accédant à une adresse virtuelle « accessible à EL0 » déclenche une **permission fault**.
- Le noyau, quand il doit légitimement accéder à la mémoire user (p.ex. copier des données depuis/vers des buffers utilisateur), doit **désactiver temporairement PAN** (ou utiliser des instructions “unprivileged load/store”) pour permettre cet accès.
- Dans Linux sur ARM64, le support PAN a été introduit vers 2015 : des patchs noyau ont ajouté la détection de la fonctionnalité, et remplacé `get_user` / `put_user` etc. par des variantes qui effacent 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 d'ARM signifie que les mappings utilisateur **execute-only** (`--x`) peuvent **ne pas déclencher PAN**. En d'autres termes, si une page user est marquée exécutable mais sans permission de lecture, la tentative de lecture du noyau pourrait contourner PAN parce que l'architecture considère « accessible à EL0 » comme nécessitant la permission de lecture, pas seulement d'exécution. Cela mène à un contournement de PAN dans certaines configurations.
- À cause de cela, si iOS / XNU autorise des pages utilisateur execute-only (comme cela peut arriver pour certains JIT ou caches de code), le noyau pourrait lire accidentellement depuis elles même avec PAN activé. C'est une zone subtilement exploitable connue sur certains systèmes ARMv8+.
#### PXN (Privileged eXecute Never)
- **PXN** est un bit des tables de pages (dans les entrées leaf ou block) qui indique que la page est **non exécutable lorsqu'on s'exécute en mode privilégié** (i.e. lorsque EL1 l'exécute).
- PXN empêche le noyau (ou tout code privilégié) de sauter vers ou d'exécuter des instructions provenant de pages user même si le contrôle est détourné. En pratique, cela bloque une redirection du contrôle du noyau vers la mémoire utilisateur.
- Combiné avec PAN, cela garantit que :
1. Le noyau ne peut pas (par défaut) lire ou écrire les données user (PAN)
2. Le noyau ne peut pas exécuter le code user (PXN)
- Dans le format de table de pages 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 noyau a un pointeur de fonction corrompu pointant vers la mémoire user et tente de brancher là-bas, le bit PXN provoquera une faute.
#### Modèle de permissions mémoire et comment PAN et PXN se mappent aux bits des tables de pages
Pour comprendre comment PAN / PXN fonctionnent, il faut voir comment le modèle de traduction et de permissions d'ARM fonctionne (simplifié) :
- Chaque entrée de page ou de block a des champs d'attributs incluant **AP[2:1]** pour les permissions d'accès (lecture/écriture, privilégié vs non-privilégié) et les bits **UXN / PXN** pour les restrictions execute-never.
- Quand PSTATE.PAN vaut 1 (activé), le matériel applique une sémantique modifiée : les accès privilégiés aux pages marquées comme « accessibles par EL0 » (i.e. accessible par l'utilisateur) sont interdits (fault).
- À cause du bug mentionné, les pages marquées seulement exécutables (sans permission de lecture) peuvent ne pas compter comme « accessibles par EL0 » sous certaines implémentations, contournant ainsi PAN.
- Quand le bit PXN d'une page est positionné, même si le fetch d'instruction provient d'un niveau de privilège supérieur, l'exécution est interdite.
#### Utilisation de PAN / PXN par le noyau dans un OS durci (ex. iOS / XNU)
- Le noyau active PAN par défaut (ainsi le code privilégié est contraint).
- Dans les chemins qui doivent légitimement lire ou écrire des buffers utilisateur (p.ex. copie du buffer de syscall, I/O, read/write user pointer), le noyau désactive temporairement **PAN** ou utilise des instructions spéciales pour outrepasser.
- Après avoir terminé l'accès aux données utilisateur, il doit réactiver PAN.
- PXN est appliqué via les tables de pages : les pages utilisateur ont PXN = 1 (donc le noyau ne peut pas les exécuter), les pages noyau n'ont pas PXN (donc le code noyau peut s'exécuter).
- Le noyau doit s'assurer qu'aucun chemin d'exécution ne provoque un flux d'exécution vers des régions mémoire utilisateur (ce qui contournerait PXN) — ainsi les chaînes d'exploitation reposant sur un “jump into user-controlled shellcode” sont bloquées.
À cause du contournement PAN via les pages execute-only mentionné, dans un système réel Apple pourrait désactiver ou interdire les pages utilisateur execute-only, ou patcher autour de la faiblesse de la spécification.
#### Surfaces d'attaque, contournements et atténuations
- **PAN bypass via execute-only pages** : comme discuté, la spec laisse un écart : les pages user execute-only (sans permission de lecture) peuvent ne pas être considérées comme « accessibles à EL0 », donc PAN n'empêchera pas les lectures du noyau depuis ces pages sous certaines implémentations. Cela donne à l'attaquant un chemin inhabituel pour fournir des données via des sections “execute-only”.
- **Temporal window exploit** : si le noyau désactive PAN pendant une fenêtre plus longue que nécessaire, une course ou un chemin malveillant pourrait exploiter cette fenêtre pour effectuer un accès mémoire user non intentionnel.
- **Forgotten re-enable** : si des chemins de code oublient de réactiver PAN, des opérations noyau ultérieures pourraient accéder incorrectement à la mémoire user.
- **Misconfiguration of PXN** : si les tables de pages ne définissent pas PXN sur les pages user ou mappent incorrectement des pages de code user, le noyau pourrait être trompé pour exécuter du code user.
- **Speculation / side-channels** : analogue aux contournements par spéculation, il peut exister des effets microarchitecturaux qui causent des violations transitoires des contrôles PAN / PXN (bien que de telles attaques dépendent fortement du design du CPU).
- **Complex interactions** : dans des fonctionnalités plus avancées (p.ex. JIT, shared memory, just-in-time code regions), le noyau 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 n'est pas trivial.
#### Example
<details>
<summary>Code Example</summary>
Voici des séquences pseudo-assembly illustratives montrant l'activation/désactivation de PAN autour d'un accès mémoire user, et comment une faute pourrait se produire.
</details>
// 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
If the kernel had **not** set PXN on that user page, then the branch might succeed — which would be insecure.
Si le kernel n'avait **pas** défini PXN sur cette page utilisateur, la branche pourrait réussir — ce qui serait insecure.
If the kernel forgets to re-enable PAN after user memory access, it opens a window where further kernel logic might accidentally read/write arbitrary user memory.
Si le kernel oublie de réactiver PAN après un accès mémoire utilisateur, cela crée une fenêtre pendant laquelle d'autres logiques du kernel pourraient accidentellement lire/écrire de la mémoire utilisateur arbitraire.
If the user pointer is into an execute-only page (user page with only execute permission, no read/write), under the PAN spec bug, `ldr W2, [X1]` might **not** fault even with PAN enabled, enabling a bypass exploit, depending on implementation.
Si le pointeur utilisateur cible une page execute-only (page utilisateur avec seulement la permission d'exécution, pas de lecture/écriture), en présence du bug de spécification PAN, `ldr W2, [X1]` pourrait **ne pas** provoquer de fault même avec PAN activé, ce qui permettrait un exploit de contournement, selon l'implémentation.
</details>
<details>
<summary>Example</summary>
A kernel vulnerability tries to take a user-provided function pointer and call it in kernel context (i.e. `call user_buffer`). Under PAN/PXN, that operation is disallowed or faults.
Une vulnérabilité du kernel tente de récupérer un pointeur de fonction fourni par l'utilisateur et de l'appeler dans le contexte kernel (i.e. `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.
Un pointeur de fonction contient un tag dans son octet supérieur (par exemple `0xAA`). Un exploit écrase les bits faibles du pointeur mais néglige le tag ; lorsque le kernel vérifie ou sanitize le pointeur, celui-ci échoue ou est rejeté.
</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:
Voici un pseudocode simplifié montrant comment un kernel pourrait appeler PPL pour modifier des pages protégées :
</details>
```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
A kernel exploit tries to overwrite the entitlement table, or disable code-sign enforcement by modifying a kernel signature blob. Because that page is PPL-protected, the write is blocked unless going through the PPL interface. So even with kernel code execution, you cannot bypass code-sign constraints or modify credential data arbitrarily. On iOS 17+ certain devices use SPTM to further isolate PPL-managed pages.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:
- 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).
- 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.
- 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.
- 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.
- 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.
- 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 dépasse pas la frontière) et que le tag reste identique, le tag mismatch ne le détecte pas.
- **Tag width limitation** : Seuls quelques bits (par 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 des tags valides et contourner la protection. Apple’s tag confidentiality enforcement est destinée à atténuer cela.
- **Performance overhead** : Les vérifications de tag à chaque load/store ajoutent du coût ; Apple doit optimiser le hardware pour réduire l'overhead.
- **Compatibility & fallback** : Sur du hardware plus ancien ou des parties qui ne supportent pas EMTE, il doit exister un fallback. Apple affirme que MIE n'est activé que sur les appareils avec 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 avec de nombreuses protections microarchitecturales, la speculative execution ou les micro-op fusions pourraient contourner les vérifications de façon transitoire ou leak des bits de tag.
- **Limited to supported regions** : Apple peut n'appliquer EMTE que sur des zones sélectionnées à haut risque (kernel, sous-systèmes critiques pour la sécurité), pas universellement.
---
## Key enhancements / differences compared to standard MTE
Here are the improvements and changes Apple emphasizes:
| Fonctionnalité | Original MTE | EMTE (Apple’s enhanced) / MIE |
|---|---|---|
| **Check mode** | Supports synchronous and asynchronous modes. In async, tag mismatches are reported later (delayed)| Apple insists on **synchronous mode** by default—tag mismatches are caught immediately, no delay/race windows allowed.|
| **Coverage of non-tagged memory** | Accesses to non-tagged memory (e.g. globals) may bypass checks in some implementations | EMTE requires that accesses from a tagged region to non-tagged memory also validate tag knowledge, making it harder to bypass by mixing allocations.|
| **Tag confidentiality / secrecy** | Tags might be observable or leaked via side channels | Apple adds **Tag Confidentiality Enforcement**, which attempts to prevent leakage of tag values (via speculative side-channels etc.).|
| **Allocator integration & retagging** | MTE leaves much of allocator logic to software | Apple’s secure typed allocators (kalloc_type, xzone malloc, etc.) integrate with EMTE: when memory is allocated or freed, tags are managed at fine granularity.|
| **Always-on by default** | In many platforms, MTE is optional or off by default | Apple enables EMTE / MIE by default on supported hardware (e.g. iPhone 17 / A19) for kernel and many user processes.|
Parce qu'Apple contrôle à la fois le hardware et la stack logicielle, elle peut appliquer EMTE de manière stricte, éviter les problèmes de performance et combler les failles de side-channel.
---
## How EMTE works in practice (Apple / MIE)
Voici une description à haut niveau du fonctionnement d'EMTE sous la configuration MIE d'Apple :
1. **Tag assignment**
- Lorsqu'une zone mémoire est allouée (par ex. dans le kernel ou en user space via des secure allocators), un **secret tag** est attribué à ce bloc.
- Le pointeur retourné à l'utilisateur ou au kernel inclut ce tag dans ses bits hauts (en utilisant les mécanismes TBI / top byte ignore).
2. **Tag checking on access**
- Chaque fois qu'un load ou store est exécuté à l'aide d'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).
- Parce que c'est en mode synchrone, il n'y a pas de fenêtre de détection différée.
3. **Retagging on free / reuse**
- Lorsqu'une zone mémoire est libérée, l'allocator change le tag du bloc (ainsi les anciens pointeurs avec l'ancien tag ne correspondent plus).
- Un use-after-free aura donc un tag obsolète et provoquera un mismatch lors d'un accès.
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.
- Cela est particulièrement efficace pour détecter les petits débordements qui franchissent la frontière.
5. **Tag confidentiality enforcement**
- Apple doit empêcher que les valeurs de tag soient leaked (parce que si un attaquant apprend le tag, il pourrait construire des pointeurs avec les bons tags).
- Ils incluent des protections (contrôles microarchitecturaux / spéculatifs) pour éviter les fuites des bits de tag.
6. **Kernel and user-space integration**
- Apple utilise EMTE non seulement en user-space mais aussi dans le kernel / composants critiques de l'OS (pour protéger le kernel contre la corruption mémoire).
- Le hardware/OS garantit que les règles de tag s'appliquent même lorsque le kernel s'exécute au nom d'un espace utilisateur.
Parce qu'EMTE est intégré dans MIE, Apple l'utilise en mode synchrone sur les surfaces d'attaque clés, et non comme option ou mode de debug.
---
## Exception handling in XNU
Quand une **exception** survient (par 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 type UNIX (comme `SIGSEGV`, `SIGBUS`, `SIGILL`, ...).
Ce processus implique plusieurs couches de propagation et de gestion des exceptions avant d'atteindre l'espace utilisateur ou d'être converti en signal BSD.
### Exception Flow (High-Level)
1. **CPU triggers a synchronous exception** (par ex. dereference de pointeur invalide, PAC failure, instruction illégale, etc.).
2. **Low-level trap handler** s'exécute (`trap.c`, `exception.c` dans les sources 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 aucun de ces ports ne gère l'exception, le kernel peut :
- **Convert it into a BSD signal** (pour les processus user-space).
- **Panic** (pour les exceptions kernel-space).
### Core Function: `exception_triage()`
La fonction `exception_triage()` achemine les exceptions Mach le long de la chaîne de handlers possibles jusqu'à ce qu'un handler la prenne en charge ou jusqu'à ce qu'elle 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 → géré par bsd_exception() → traduit en un signal comme SIGSEGV.
Ports d’exception
Chaque objet Mach (thread, task, host) peut enregistrer des ports d’exception, vers lesquels les messages d’exception sont envoyés.
Ils sont définis par l’API :
task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()
Chaque port d’exception possède :
- Un masque (quelles exceptions il souhaite recevoir)
- Un nom de port (Mach port pour recevoir les messages)
- Un comportement (comment le noyau envoie le message)
- Un flavor (quel état du thread inclure)
Débogueurs et gestion des exceptions
Un débogueur (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 message Mach est envoyé au processus débogueur.
- Le débogueur peut décider de gérer (reprendre, modifier les registres, sauter l’instruction) ou de ne pas gérer l’exception.
- Si le débogueur ne la gère pas, l’exception se propage au niveau suivant (task → host).
Flux de EXC_BAD_ACCESS
-
Le thread déréférence un pointeur invalide → le CPU déclenche un Data Abort.
-
Le gestionnaire de trap du noyau appelle
exception_triage(EXC_BAD_ACCESS, ...). -
Message envoyé à :
-
Thread port → (le débogueur peut intercepter le breakpoint).
-
Si le débogueur ignore → Task port → (gestionnaire au niveau process).
-
Si ignoré → Host port (généralement ReportCrash).
- Si personne ne gère →
bsd_exception()traduit enSIGSEGV.
Exceptions PAC
Quand la Pointer Authentication (PAC) échoue (signature non conforme), 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 drapeau TFRO_PAC_EXC_FATAL, le noyau considère les échecs PAC comme fatals, contournant l’interception par le débogueur. Ceci empêche les attaquants d’utiliser des débogueurs pour contourner les vérifications PAC et c’est activé pour les binaires de plateforme.
Breakpoints logiciels
Un breakpoint logiciel (int3 sur x86, brk sur ARM64) est implémenté en provoquant volontairement une faute.
Le débogueur l’attrape 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 débogueur.
Conversion vers les signaux BSD
Si aucun gestionnaire n’accepte l’exception :
-
Le noyau appelle
task_exception_notify() → bsd_exception(). -
Cela mappe les exceptions Mach en signaux :
| Exception Mach | Signal |
|---|---|
| EXC_BAD_ACCESS | SIGSEGV or SIGBUS |
| EXC_BAD_INSTRUCTION | SIGILL |
| EXC_ARITHMETIC | SIGFPE |
| EXC_SOFTWARE | SIGTRAP |
| EXC_BREAKPOINT | SIGTRAP |
| EXC_CRASH | SIGKILL |
| EXC_ARM_PAC | SIGILL (on non-fatal) |
Fichiers clés dans le code source XNU
-
osfmk/kern/exception.c→ Cœur deexception_triage(),exception_deliver_*(). -
bsd/kern/kern_sig.c→ Logique de livraison des signaux. -
osfmk/arm64/trap.c→ Gestionnaires de trap bas niveau. -
osfmk/mach/exc.h→ Codes d’exception et structures. -
osfmk/kern/task.c→ Mise en place des task exception port.
Ancien heap du kernel (avant iOS 15 / ère pré-A12)
Le noyau utilisait un zone allocator (kalloc) divisé en “zones” de tailles fixes.
Chaque zone ne stocke que des allocations d’une seule classe de taille.
From the screenshot:
| Zone Name | Element Size | Example Use |
|---|---|---|
default.kalloc.16 | 16 bytes | Very small kernel structs, pointers. |
default.kalloc.32 | 32 bytes | Small structs, object headers. |
default.kalloc.64 | 64 bytes | IPC messages, tiny kernel buffers. |
default.kalloc.128 | 128 bytes | Medium objects like parts of OSObject. |
| … | … | … |
default.kalloc.1280 | 1280 bytes | Large structures, IOSurface/graphics metadata. |
Comment cela fonctionnait :
- Chaque requête d’allocation est arrondie à la taille de zone la plus proche.
(Par ex., une requête de 50 octets ira dans la zone
kalloc.64). - La mémoire de chaque zone était conservée dans une freelist — les chunks freed par le noyau 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 pourquoi heap spraying / feng shui était si efficace : vous pouviez prévoir les voisins des objets en remplissant avec des allocations de la même classe de taille.
La 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 noyau écrivait un pointeur au début de ce chunk → l’adresse du prochain chunk libre dans la même zone.
-
La zone gardait un pointeur HEAD vers le premier chunk libre.
-
L’allocation utilisait toujours le HEAD courant :
-
Pop HEAD (retourne cette mémoire à l’appelant).
-
Met à jour HEAD = HEAD->next (stocké dans l’en-tête du chunk freed).
-
Le free poussait les chunks en 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 freed elle-même.
É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 peut le corrompre :
-
Heap overflow dans un freed chunk adjacent → écraser son “next” pointer.
-
Use-after-free en écrivant dans un objet freed → écraser son “next” pointer.
Puis, lors de la prochaine allocation de cette taille :
-
L’allocator retire le chunk corrompu.
-
Suit le “next” pointer fourni par l’attaquant.
-
Retourne un pointer vers une zone mémoire arbitraire, permettant des fake object primitives ou targeted overwrite.
Visual example of 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.
Cette conception de freelist rendait l’exploitation très efficace avant les hardenings : voisins prévisibles via heap sprays, liens bruts de pointeurs dans les freelist, et absence de séparation par type permettaient aux attaquants d’escalader des bugs UAF/overflow en contrôle arbitraire de la mémoire kernel.
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 (victime) se retrouve juste à côté d’un objet contrôlé par l’attaquant.
De cette façon, quand la corruption mémoire se produit, l’attaquant peut de manière fiable écraser l’objet victime avec des données contrôlées.
Étapes :
- Spray allocations (fill the holes)
- Avec le temps, le kernel heap se fragmente : certaines zones contiennent des trous où d’anciens objets ont été freed.
- L’attaquant commence par faire beaucoup d’allocations factices pour boucher ces gaps, de sorte que le heap devienne “packé” et prévisible.
- Force new pages
- Une fois les trous remplis, les allocations suivantes doivent provenir de nouvelles pages ajoutées à la zone.
- Des pages fraîches signifient que les objets seront groupés ensemble, pas dispersés dans une mémoire fragmentée ancienne.
- Cela donne à l’attaquant un bien meilleur contrôle des voisins.
- Place attacker objects
- L’attaquant recommence le spray, créant beaucoup d’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).
- Free a controlled object (make a gap)
- L’attaquant free délibérément un de ses propres objets.
- Cela crée un “trou” dans le heap, que l’allocator réutilisera plus tard pour la prochaine allocation de cette taille.
- Victim object lands in the hole
- L’attaquant déclenche le kernel pour allouer l’objet victime (celui qu’il veut corrompre).
- Puisque le trou est le premier slot disponible dans la freelist, la victime est placée exactement là où l’attaquant a freed son objet.
- Overflow / UAF into victim
- Maintenant l’attaquant a des objets contrôlés autour de la victime.
- 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 de la victime avec des valeurs choisies.
Pourquoi ça marche :
- Zone allocator predictability : les allocations de la même taille proviennent toujours de la même zone.
- Freelist behavior : les nouvelles allocations réutilisent d’abord le chunk freed le plus récent.
- 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 victime atterrit et quelles données se trouvent à côté.
Modern Kernel Heap (iOS 15+/A12+ SoCs)
Apple a durci l’allocator 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) a sa propre zone dédiée, même s’ils ont la même taille. - La correspondance entre type d’objet ↔ zone est générée par le kalloc_type system à 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 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 per-CPU cache pour réduire la contention.
- Chemin d’allocation :
- Tenter le per-CPU cache.
- Si vide, puiser dans la global freelist.
- Si la 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 depuis les caches de différents CPU.
3. Randomization inside zones
- Dans une zone, les éléments freed ne sont pas rendus dans un simple ordre FIFO/LIFO.
- XNU moderne utilise des encoded freelist pointers (safe-linking style comme Linux, introduit ~iOS 14).
- Chaque pointeur de freelist est XOR-encodé avec un cookie secret par zone.
- Cela empêche les attaquants de forger un faux pointeur de freelist s’ils obtiennent une primitive d’écriture.
- Certaines allocations sont randomized dans leur placement au sein d’un slab, donc le spraying ne garantit pas l’adjacence.
4. Guarded Allocations
- Certains objets kernel critiques (par ex. credentials, structures task) 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 une fault → panic immédiat au lieu d’une corruption silencieuse.
5. Page Protection Layer (PPL) et SPTM
- Même si vous contrôlez un objet freed, vous ne pouvez pas modifier toute la mémoire kernel :
- PPL (Page Protection Layer) fait en sorte que certaines régions (par ex. les données de code signing, les entitlements) soient read-only même pour le kernel lui-même.
- Sur les devices A15/M2+, ce rôle est remplacé/renforcé par SPTM (Secure Page Table Monitor) + TXM (Trusted Execution Monitor).
- Ces couches imposées par le hardware empêchent les attaquants de passer d’une simple corruption de heap à un patching arbitraire de structures de sécurité critiques.
- (Added / Enhanced) : aussi, PAC (Pointer Authentication Codes) est utilisé dans le kernel pour protéger les pointeurs (notamment les function pointers, vtables) afin de rendre leur falsification ou corruption plus difficile.
- (Added / Enhanced) : les zones peuvent appliquer zone_require / zone enforcement, c.-à-d. qu’un objet freed ne peut être retourné que via sa typed zone correcte ; des frees inter-zone invalides peuvent panicker ou être rejetées. (Apple fait allusion à cela dans leurs posts sur la memory safety)
6. Large Allocations
- Toutes les allocations ne passent pas par
kalloc_type. - Les requêtes très grandes (au-dessus d’environ 16 KB) contournent les typed zones et sont servies directement via kernel VM (kmem) par 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 falsifier des compteurs retain/release, vous pouvez provoquer un use-after-free.
- Objects with function pointers (vtables) : corrompre l’un d’eux donne encore du control flow.
- Shared memory objects (IOSurface, Mach ports) : ce sont toujours des cibles car ils font le pont user ↔ kernel.
Mais — contrairement à avant — vous ne pouvez plus simplement spray OSData et espérer qu’il soit adjacent à un task_t. Il vous 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 :
- Type lookup →
OSDatamappe vers la zonekalloc_type_osdata(taille 64 bytes). - Check per-CPU cache for free elements.
- Si trouvé → retourne un élément.
- Si vide → aller à la global freelist.
- Si la freelist est vide → allouer un nouveau slab (page de 4KB → 64 chunks de 64 bytes).
- Retourner le chunk au caller.
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, and PAC protects pointers |
| Allocation reuse validation | None (freelist pointers raw) | zone_require / zone enforcement |
| Exploit reliability | High with heap sprays | Much lower, requires logic bugs or info leaks |
| Large allocations handling | All small allocations managed equally | Large ones bypass zones → handled via VM |
Modern Userland Heap (iOS, macOS — type-aware / xzone malloc)
Dans les récentes versions d’Apple OS (surtout iOS 17+), Apple a introduit un allocator userland plus sécurisé, xzone malloc (XZM). C’est l’équivalent user-space de kalloc_type, appliquant la conscience de type, l’isolation des métadonnées et des protections de memory tagging.
Goals & Design Principles
- Type segregation / type awareness : grouper les allocations par type ou usage (pointer vs data) pour empêcher la type confusion et la réutilisation cross-type.
- Metadata isolation : séparer les metadata du heap (par ex. freelists, bits de taille/état) du payload des objets pour que les écritures hors-borne corruptant le moins possible les metadata.
- Guard pages / redzones : insérer des pages non mappées ou du padding autour des allocations pour détecter les overflows.
- Memory tagging (EMTE / MIE) : fonctionner avec le tagging hardware pour détecter les use-after-free, out-of-bounds et accès invalides.
- Scalable performance : garder une faible overhead, éviter une fragmentation excessive, et supporter beaucoup d’allocations par seconde avec faible latence.
Architecture & Components
Ci‑dessous les éléments principaux de l’allocator xzone :
Segment Groups & Zones
- Segment groups partitionnent l’espace d’adressage par catégories d’usage : ex.
data,pointer_xzones,data_large,pointer_large. - Chaque segment groupe contient des segments (ranges VM) qui hébergent les allocations pour cette catégorie.
- Associé à chaque segment se trouve une metadata slab (zone VM séparée) qui stocke les metadata (par ex. free/used bits, classes de taille) pour ce segment. Cette out-of-line (OOL) metadata garantit que les metadata ne sont pas mélangées avec les payloads des objets, ce qui réduit la possibilité de corruption via overflow.
- Les segments sont découpés en chunks (slices) qui sont à leur tour subdivisés en blocks (unités d’allocation). Un chunk est lié à une classe de taille et à un segment group spécifique (i.e. tous les blocks d’un chunk partagent la même taille & catégorie).
- Pour les allocations small/medium, on utilisera des chunks de taille fixe ; pour les large/huge, on peut mapper séparément.
Chunks & Blocks
- Un chunk est une région (souvent plusieurs pages) dédiée aux allocations d’une classe de taille au sein d’un group.
- À l’intérieur d’un chunk, les blocks sont des slots disponibles pour les allocations. Les blocks freed sont suivis via la metadata slab — par ex. via des bitmaps ou des freelists stockés out-of-line.
- Entre les chunks (ou à l’intérieur), des guard slices / guard pages peuvent être insérés (ex. slices non mappées) pour attraper les écritures hors-borne.
Type / Type ID
- Chaque site d’allocation (ou appel à malloc, calloc, etc.) est associé à un type identifier (un
malloc_type_id_t) qui encode quel type d’objet est alloué. Ce type ID est passé à l’allocator, qui l’utilise pour sélectionner quel zone/segment servira l’allocation. - Ainsi, même si deux allocations ont la même taille, elles peuvent aller dans des zones complètement différentes si leurs types diffèrent.
- Dans les premières versions d’iOS 17, toutes les APIs (ex. CFAllocator) n’étaient pas entièrement type-aware ; Apple a corrigé certaines de ces faiblesses dans iOS 18.
Allocation & Freeing Workflow
Voici un flux haut niveau de l’allocation et de la désallocation dans xzone :
- malloc / calloc / realloc / typed alloc est invoqué avec une taille et un type ID.
- L’allocator utilise le type ID pour choisir le segment group / zone correcte.
- Dans cette zone/segment, il cherche un chunk qui a des blocks libres de la taille demandée.
- Il peut consulter des local caches / per-thread pools ou les free block lists dans la metadata.
- Si aucun block libre n’est disponible, il peut allouer un nouveau chunk dans cette zone.
- La metadata slab est mise à jour (bit free effacé, bookkeeping).
- Si le memory tagging (EMTE) est activé, le block retourné reçoit un tag, et la metadata est mise à jour pour refléter son état “live”.
- Quand
free()est appelé :
- Le block est marqué comme freed dans la metadata (via OOL slab).
- Le block peut être placé dans une free list ou poolé pour réutilisation.
- Optionnellement, le contenu du block peut être effacé ou poisonné pour réduire les data leaks ou l’exploitation de UAF.
- Le tag hardware associé au block peut être invalidé ou re-taggué.
- Si un chunk entier devient libre (tous les blocks freed), l’allocator peut reclaim ce chunk (le unmapper ou le retourner à l’OS) sous pression mémoire.
Security Features & Hardening
Voici les défenses intégrées dans xzone userland :
| Feature | Purpose | Notes |
|---|---|---|
| Metadata decoupling | Prevent overflow from corrupting metadata | Metadata lives in separate VM region (metadata slab) |
| Guard pages / unmapped slices | Catch out-of-bounds writes | Helps detect buffer overflows rather than silently corrupting adjacent blocks |
| Type-based segregation | Prevent cross-type reuse & type confusion | Even same-size allocations from different types go to different zones |
| Memory Tagging (EMTE / MIE) | Detect invalid access, stale references, OOB, UAF | xzone works in concert with hardware EMTE in synchronous mode (“Memory Integrity Enforcement”) |
| Delayed reuse / poisoning / zap | Reduce chance of use-after-free exploitation | Freed blocks may be poisoned, zeroed, or quarantined before reuse |
| Chunk reclamation / dynamic unmapping | Reduce memory waste and fragmentation | Entire chunks may be unmapped when unused |
| Randomization / placement variation | Prevent deterministic adjacency | Blocks in a chunk and chunk selection may have randomized aspects |
| Segregation of “data-only” allocations | Separate allocations that don’t store pointers | Reduces attacker control over metadata or control fields |
Interaction with Memory Integrity Enforcement (MIE / EMTE)
- MIE (Memory Integrity Enforcement) d’Apple est le framework hardware + OS qui met l’Enhanced Memory Tagging Extension (EMTE) en mode always-on, synchrone, sur les surfaces d’attaque majeures.
- L’allocator xzone est une fondation essentielle de MIE en user space : les allocations faites via xzone reçoivent des tags, et les accès sont vérifiés par le hardware.
- Dans MIE, l’allocator, l’assignation des tags, la gestion des metadata, et l’enforcement de la confidentialité des tags sont intégrés pour s’assurer que les erreurs mémoire (ex. lectures stales, OOB, UAF) sont détectées immédiatement, pas exploitées plus tard.
- Si vous voulez, je peux aussi générer une cheat-sheet ou un diagramme des internals de xzone pour votre livre. Voulez-vous que je fasse ça ensuite ?
- :contentReference[oai:20]{index=20}
(Old) Physical Use-After-Free via 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 isntall it even if there is a version mismatch.
Using BinDiff with Kernel versions
- Go to the page https://ipsw.me/ and download the iOS versions you want to diff. These will be
.ipswfiles. - Decompress until you get the bin format of the kernelcache of both
.ipswfiles. You have information on how to do this on:
macOS Kernel Extensions & Kernelcache
- Open Ghidra with
ghidraRun, create a new project and load the kernelcaches. - Open each kernelcache so they are automatically analyzed by Ghidra.
- Then, on the project Window of Ghidra, right click each kernelcache, select
Export, select formatBinary BinExport (v2) for BinDiffand export them. - 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
Modules helper & watcher de PREYHUNTER
- Watcher anti-analysis: Un binaire watcher dédié profile en continu 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 consolediagnosticd, les localesUSouIL, des traces de jailbreak telles que Cydia, des processus commebash,tcpdump,frida,sshd, oucheckrain, des apps AV mobiles (McAfee, AvastMobileSecurity, NortonMobileSecurity), des réglages de proxy HTTP personnalisés, et des CA racine personnalisés. Si une vérification échoue, la livraison ultérieure des payloads est bloquée. - 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 se trouvent 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 agit donc comme une couche discrète de validation et de surveillance légère avant le déploiement d’implants plus lourds tels que Predator.
iMessage/Media Parser Zero-Click Chains
Imessage Media Parser Zero Click Coreaudio Pac Bypass
Références
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
- Vérifiez les plans d’abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
HackTricks

