iOS Exploiting
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.
iOS Exploit Mitigations
1. Code Signing / Runtime Signature Verification
Introduced early (iPhone OS → iOS) C’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 limites), le système vérifie sa signature. Si le code est modifié (bit-flip, patché) ou non signé, le chargement échoue.
- Thwarts : l’étape classique « payload drop + execute » dans les chaînes d’exploit ; injection de code arbitraire ; modification d’un binaire existant pour y insérer de la logique malveillante.
- Mechanism detail :
- Le loader 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 des régions mémoire comme les caches JIT ou le code généré dynamiquement, Apple impose que les pages soient signées ou validées via des APIs spéciales (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 pas être falsifiés.
Example
Supposons qu’un exploit obtienne 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 les 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
Introduced around iOS 14+ era (or gradually in newer devices / later iOS) CoreTrust est le sous-système qui effectue la validation de signature à l’exécution des binaires (y compris system et user binaries) contre le certificat racine d’Apple plutôt que de se reposer sur des stores de confiance en userland mis en cache.
- Thwarts : la modification post-install des binaires, les techniques de jailbreak qui tentent d’échanger ou de patcher des bibliothèques système ou des apps utilisateur ; tromper le système en remplaçant des binaires de confiance par des équivalents malveillants.
- Mechanism detail :
- Plutôt que de faire confiance à une base de confiance locale ou à un cache de certificats, CoreTrust consulte ou se réfère directement au root d’Apple ou vérifie les certificats intermédiaires dans une chaîne sécurisée.
- Il s’assure que les modifications (ex. dans le filesystem) aux binaires existants 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.
Example
Un jailbreak pourrait essayer de remplacer `SpringBoard` ou `libsystem` par une version patchée pour obtenir la persistance. Mais quand le loader de l’OS ou CoreTrust vérifie, il détecte la discordance de signature (ou des entitlements modifiés) et refuse d’exécuter.3. Data Execution Prevention (DEP / NX / W^X)
Introduced in many OSes earlier; iOS had NX-bit / w^x for a long time DEP impose que les pages marquées écrites (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.
- Thwarts : exécution directe de shellcode ; buffer-overflow classique → saut vers du shellcode injecté.
- Mechanism detail :
- Le MMU / les flags de protection mémoire (via les tables de pages) imposent la séparation.
- Toute tentative de marquer une page writable comme executable 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 nécessite de passer par des APIs OS qui appliquent des contraintes ou des contrôles supplémentaires.
Example
Un overflow écrit du shellcode sur le heap. L’attaquant tente `mprotect(heap_addr, size, PROT_EXEC)` pour rendre la zone exécutable. Mais le système refuse ou valide que la nouvelle page doit passer des contraintes de code-sign (ce que le shellcode ne peut pas).4. Address Space Layout Randomization (ASLR)
Introduced in iOS ~4–5 era (roughly iOS 4–5 timeframe) ASLR randomise les adresses de base des régions mémoire clés : libraries, heap, stack, etc., à chaque lancement de processus. Les adresses des gadgets bougent entre les exécutions.
- Thwarts : hardcoding des adresses de gadgets pour ROP/JOP ; chaînes d’exploit statiques ; sauter aveuglément vers des offsets connus.
- Mechanism detail :
- Chaque library / module dynamique chargé est rebasé à un offset randomisé.
- Les pointeurs de base du stack et du heap sont randomisés (dans certaines limites d’entropie).
- Parfois d’autres régions (ex. allocations mmap) sont également 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.
Example
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 codée en dur échoue. Un exploit doit d’abord leak l’adresse de base du module avant de calculer les adresses des gadgets.5. Kernel Address Space Layout Randomization (KASLR)
Introduced in iOS ~ (iOS 5 / iOS 6 timeframe) Analogique à l’ASLR utilisateur, KASLR randomise la base du kernel text et d’autres structures kernel au démarrage.
- Thwarts : exploits kernel qui comptent sur des emplacements fixes du code ou des données kernel ; exploits kernel statiques.
- Mechanism detail :
- À chaque boot, l’adresse de base du kernel est randomisée (dans une plage).
- Des structures de données kernel (comme
task_structs,vm_map, etc.) peuvent également être relocées ou décalées. - Les attaquants doivent d’abord leak des pointeurs kernel ou exploiter des vulnérabilités d’information disclosure pour calculer les offsets avant de détourner des structures ou du code kernel.
Example
Une vuln locale vise à corrompre un pointeur de fonction kernel (ex. dans un `vtable`) à `KERN_BASE + offset`. Mais comme `KERN_BASE` est inconnu, l’attaquant doit d’abord le leak (ex. via un primitive de lecture) avant de calculer l’adresse correcte pour la corruption.6. Kernel Patch Protection (KPP / AMCC)
Introduced in newer iOS / A-series hardware (post around iOS 15–16 era or newer chips) KPP (aka AMCC) surveille en continu l’intégrité des pages de kernel text (via hash ou checksum). Si elle détecte des altérations (patchs, hooks inline, modifications de code) en dehors de fenêtres autorisées, elle déclenche un kernel panic ou un reboot.
- Thwarts : patching persistant du kernel (modification des instructions kernel), hooks inline, overwrites statiques de fonctions.
- Mechanism detail :
- 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 discordances surviennent hors des fenêtres de mise à jour bénignes, il panique l’appareil (pour éviter une persistence malveillante).
- Les attaquants doivent soit éviter les fenêtres de détection soit utiliser des chemins de patchs légitimes.
Example
Un exploit tente de patcher le prologue d’une fonction kernel (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, plantant l’appareil avant que le patch ne se stabilise.7. Kernel Text Read‐Only Region (KTRR)
Introduced in modern SoCs (post ~A12 / newer hardware) KTRR est un mécanisme appliqué par hardware : une fois que le kernel text est verrouillé tôt pendant le boot, il devient en lecture seule depuis EL1 (le kernel), empêchant toute écriture ultérieure sur les pages de code.
- Thwarts : toute modification du code kernel après le boot (ex. patching, injection de code en place) au niveau de privilège EL1.
- Mechanism detail :
- Pendant le boot (dans la phase secure/bootloader), le contrôleur mémoire (ou une unité hardware sécurisée) marque les pages physiques contenant le kernel text comme lecture seule.
- Même si un exploit obtient des 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.
Example
Un exploit d’escalade de privilèges saute en EL1 et écrit un trampoline dans une fonction kernel (ex. dans le handler `syscall`). Mais comme 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)
Introduced with ARMv8.3 (hardware), Apple beginning with A12 / iOS ~12+
- PAC est une fonctionnalité hardware introduite dans ARMv8.3-A pour détecter la manipulation de valeurs de pointeurs (return addresses, function pointers, certains data pointers) en encodant une petite signature cryptographique (un “MAC”) dans les 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, ex. le stack pointer ou une donnée distinctrice). Ainsi, la même valeur de pointeur dans des contextes différents obtient une PAC différente.
- Au moment de l’utilisation, avant de déréférencer ou de brancher via ce pointeur, une instruction authenticate vérifie la PAC. Si elle est valide, la PAC est retirée et le pointeur pur est obtenu ; si elle est 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 toutes les 64 bits d’un pointeur ne sont pas utilisées dans beaucoup de systèmes (ex. espace d’adressage 48-bit), les bits supérieurs sont “libres” et peuvent contenir la PAC sans altérer l’adresse effective.
Architectural Basis & Key Types
-
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 données non-pointer ou d’autres usages génériques
-
Ces clés sont stockées dans des registres système privilégiés (accessibles uniquement en EL1/EL2 etc.), non accessibles depuis le mode user.
-
La PAC est calculée via une fonction cryptographique (ARM suggère QARMA comme algorithme) en utilisant :
- La valeur du pointeur (portion canonique)
- Un modifier (une valeur de contexte, comme un salt)
- La clé secrète
- De la logique de tweak interne Si la PAC résultante correspond à ce qui est stocké dans les bits supérieurs du pointeur, l’authentification réussit.
Instruction Families
La convention de nommage est : PAC / AUT / XPAC, puis les lettres de domaine.
PACxxinstructions signent un pointeur et insèrent une PACAUTxxinstructions authentifient + strip (valident et retirent la PAC)XPACxxinstructions retirent sans valider
Domains / suffixes:
| Mnémonique | Signification / Domaine | Clé / Domaine | Exemple d’utilisation en assembleur |
|---|---|---|---|
| PACIA | Signer un instruction pointer avec APIAKey | “I, A” | PACIA X0, X1 — signer le pointeur dans X0 en utilisant APIAKey avec le 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 | Signature générique (non-pointer) avec APGAKey | “G” | PACGA X8, X9, X10 (signer X9 avec le modifier X10 dans X8) |
| AUTIA | Authentifier un instruction pointer signé APIA & retirer la PAC | “I, A” | AUTIA X0, X1 — vérifier la PAC sur X0 avec le modifier X1, puis retirer |
| AUTIB | Authentifier domaine APIB | “I, B” | AUTIB X2, X3 |
| AUTDA | Authentifier un data pointer signé APDA | “D, A” | AUTDA X4, X5 |
| AUTDB | Authentifier un data pointer signé APDB | “D, B” | AUTDB X6, X7 |
| AUTGA | Authentifier générique / blob (APGA) | “G” | AUTGA X8, X9, X10 (valider générique) |
| XPACI | Retirer la PAC (instruction pointer, sans validation) | “I” | XPACI X0 — enlever la PAC de X0 (domaine instruction) |
| XPACD | Retirer la PAC (data pointer, sans validation) | “D” | XPACD X4 — enlever la PAC du data pointer en X4 |
Il existe des formes spécialisées / alias :
PACIASPest une abréviation pourPACIA X30, SP(signer le link register en utilisant SP comme modifier)AUTIASPestAUTIA X30, SP(authentifier le link register avec SP)- Des formes combinées comme
RETAA,RETAB(authentifier-et-return) ouBLRAA(authentifier & branch) existent dans les extensions ARM / le 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 la PAC à un contexte spécifique de sorte que la même adresse signée dans des contextes différents produise des PAC différentes. Cela empêche la réutilisation simple de pointeurs 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 de la PAC. Choix typiques : le stack pointer (
SP), un frame pointer, ou un identifiant d’objet. - Utiliser SP comme modifier est courant pour le signing des return addresses : la PAC est liée à la frame de stack spécifique. Si vous essayez de réutiliser le LR dans une frame différente, le modifier change, donc la validation PAC échoue.
- La même valeur de pointeur signée sous des modifiers différents produit des PAC différentes.
- Le modifier n’a pas besoin d’être secret, mais idéalement il n’est pas contrôlé par l’attaquant.
- Pour les instructions qui signent ou vérifient des pointeurs où aucun modifier significatif n’existe, certaines formes utilisent zéro ou une constante implicite.
Apple / iOS / XNU Customizations & Observations
- L’implémentation PAC d’Apple inclut des diversifiers 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 afin que des PAC signées en user mode ne puissent pas facilement être réutilisées en kernel mode, etc.
- Sur Apple M1 / Apple Silicon, le reverse engineering a montré qu’il existe neuf types de modifiers et des registres système Apple-specific pour le contrôle des clés.
- Apple utilise PAC dans de nombreux sous-systèmes kernel : signing des return addresses, intégrité des pointeurs dans les données kernel, signed thread contexts, etc.
- Google Project Zero a montré que, sous un puissant primitive 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 user peuvent obtenir une randomisation de clé par processus.
PAC Bypasses
- Kernel-mode PAC: theoretical vs real bypasses
- Parce que les clés et la logique PAC kernel sont strictement contrôlées (registres privilégiés, diversifiers, isolation des domaines), forger arbitrairement des pointeurs kernel signés est très difficile.
- Azad’s 2020 “iOS Kernel PAC, One Year Later” rapporte que dans 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 customizations “Dark Magic” d’Apple réduisent encore les surfaces exploitables (domain switching, bits d’activation par clé). i.blackhat.com
- Il existe un kernel PAC bypass CVE-2023-32424 connu sur Apple silicon (M1/M2) rapporté par Zecao Cai et al. i.blackhat.com
- Mais ces bypass 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 kernel PAC est considéré comme hautement robuste, bien que pas parfait.
- User-mode / runtime PAC bypass techniques
Ceux-ci sont plus courants, et exploitent des imperfections dans la manière dont PAC est appliqué ou utilisé dans le dynamic linking / runtime frameworks. Ci‑dessous des classes, avec exemples.
2.1 Shared Cache / A key issues
-
Le dyld shared cache est un grand blob pré-lié de frameworks et libraries système. Parce qu’il est largement partagé, des function pointers à l’intérieur du shared cache sont « pre-signed » puis utilisés par de nombreux processus. Les attaquants ciblent ces pointeurs déjà signés comme des « PAC oracles ».
-
Certaines techniques de bypass tentent d’extraire ou de réutiliser des pointeurs signés 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 inférer des adresses relatives et combiner cela avec des pointeurs signés pour bypasser PAC. saelo.github.io
-
Aussi, les imports de function pointers depuis des libraries userspace se sont avérés parfois insuffisamment protégés par PAC, permettant à un attaquant d’obtenir des function pointers sans changer leur signature. (Project Zero bug entry) bugs.chromium.org
2.2 dlsym(3) / dynamic symbol resolution
-
Un bypass connu est d’appeler
dlsym()pour obtenir un function pointer déjà signé (signed with A-key, diversifier zero) puis de l’utiliser. Parce quedlsymretourne un pointeur légitimement signé, l’utiliser contourne le besoin de forger la 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’s “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 buggés), permettant un bypass PAC involontaire. Synacktiv -
La logique dans dyld pour dlsym inclut : quand
result->isCode, ils signent le pointeur retourné avec__builtin_ptrauth_sign_unauthenticated(..., key_asia, 0), c.-à-d. contexte zéro. blog.epsilon-sec.com
Ainsi, dlsym est un vecteur fréquent dans les bypass PAC en user-mode.
2.3 Other DYLD / runtime relocations
-
Le loader DYLD et la logique de relocation dynamique sont complexes et mappent parfois temporairement des pages en read/write pour effectuer des relocations, puis les remettent en read-only. Les attaquants exploitent ces fenêtres. Le talk de Synacktiv décrit “Operation Triangulation”, un bypass temporel de PAC via des 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 des chaînes d’exploit WebKit, le loader DYLD est souvent une cible pour 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 des chaînes d’exploit userland, des méthodes du runtime Objective-C comme
NSPredicate,NSExpressionouNSInvocationsont utilisées pour dissimuler des appels contrôlés sans pointer forging évident. -
Sur d’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 SLOP originale permettait d’enchaîner des appels ObjC en créant des invocations factices ; le bypass repose sur le fait que ISA ou des pointeurs de selector ne sont parfois pas entièrement protégés par PAC. Project Zero
-
Dans des environnements où pointer authentication est appliquée partiellement, les méthodes / selectors / pointeurs target peuvent ne pas toujours bénéficier de la protection PAC, laissant une marge pour le bypass.
Example Flow
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>Exemple</summary>
A buffer overflow overwrites a return address on the stack. The attacker writes the target gadget address but cannot compute the correct PAC. When the function returns, the CPU’s `AUTIA` instruction faults because the PAC mismatch. The chain fails.
Project Zero’s analysis on A12 (iPhone XS) showed how Apple’s PAC is used and methods of forging PACs if an attacker has a memory read/write primitive.
</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 branchements indirects** : 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 ce 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 branchements.
| BTI Variant | Ce que cela permet (quels types de branchements) | Placement typique / cas d'utilisation |
|-------------|--------------------------------------------------|----------------------------------------|
| **BTI C** | Cibles des branchements indirects de type *call* (ex. `BLR`, ou `BR` utilisant X16/X17) | Placé à l’entrée des fonctions pouvant être appelées indirectement |
| **BTI J** | Cibles des branchements de type *jump* (ex. `BR` utilisé pour des tail calls) | Placé au début des blocs atteignables via des tables de saut ou des tail-calls |
| **BTI JC** | Agit comme C et J | Peut être ciblé par des branchements de type call ou jump |
- Dans du code compilé avec enforcement des branch targets, les compilateurs insèrent une instruction BTI (C, J, ou JC) à chaque cible valide de branchement indirect (début de fonction ou blocs atteignables par des sauts) afin que les branchements indirects ne réussissent que vers ces emplacements.
- Les **branchements / appels directs** (c.-à-d. 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 branchements directs sont sûrs).
- 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 signature de retour.
#### Mécanisme et application
- Quand le CPU décode un **branchements indirect (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 précédemment réservés pour des NOPs (dans les versions ARM antérieures). Ainsi les binaires BTI-enabled restent rétrocompatibles : sur du matériel 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 blocs de base ciblés par des sauts.
- Certains patchs et le code LLVM montrent que BTI n’est pas inséré pour *tous* les blocs de base — seulement pour ceux qui sont des cibles potentielles de branchement (ex. issus de switch / tables de saut).
#### Synergie BTI + PAC
PAC protège la valeur du pointeur (la source) — garantit que la chaîne d’appels/retours indirects n’a pas été altérée.
BTI garantit que même un pointeur valide doit viser 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 placé là. Cela augmente la difficulté de construire des gadgets exploitables.
#### Exemple
<details>
<summary>Exemple</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 plante parce que l’alignement d’instruction n’inclut pas un landing pad valide. Ainsi beaucoup de gadgets deviennent inutilisables à moins qu’ils n’incluent le préfixe BTI.
</details>
### 10. **PAN & 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 kernel est trompé ou compromis, il ne peut pas déréférencer arbitrairement des pointeurs utilisateur sans d’abord *désactiver* PAN, réduisant ainsi les risques d’exploits de type **`ret2usr`** ou de mauvaise utilisation des 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 un **fault de permission**.
- Le kernel, lorsqu’il doit légitimement accéder à la mémoire utilisateur (ex. copier des données vers/depuis des buffers utilisateur), doit **désactiver temporairement PAN** (ou utiliser des instructions de load/store non privilégiées) pour permettre cet accès.
- Dans Linux sur ARM64, le support de PAN a été introduit vers 2015 : des patches du 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 / bogue**
- Comme noté par Siguza et d’autres, un bug de spécification (ou un comportement ambigu) dans la conception ARM signifie que les mappings utilisateur execute-only (`--x`) peuvent **ne pas déclencher PAN**. Autrement dit, si une page utilisateur est marquée exécutable mais sans permission de lecture, la tentative de lecture du kernel peut contourner PAN parce que l’architecture considère qu’“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 autorise des pages utilisateur execute-only (comme certains setups JIT ou code-cache), le kernel pourrait accidentellement lire depuis elles même avec PAN activé. C’est une zone subtile connue comme exploitable sur certains systèmes ARMv8+.
#### PXN (Privileged eXecute Never)
- **PXN** est un bit dans la table de pages (dans les entrées leaf ou block) qui indique que la page est **non exécutable en mode privilégié** (c.-à-d. 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 utilisateur même si le contrôle est détourné. En pratique, cela bloque une redirection de flux d’exécution au niveau noyau vers la mémoire utilisateur.
- Combiné avec PAN, cela assure que :
1. Le kernel ne peut pas (par défaut) lire ou écrire des données utilisateur (PAN)
2. Le kernel ne peut pas exécuter du code utilisateur (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’attributs.
Ainsi, même si le kernel a un pointeur de fonction corrompu pointant vers la mémoire utilisateur et essaie de brancher là-bas, le bit PXN provoquerait un fault.
#### Modèle de permissions mémoire & comment PAN et PXN se mappent aux bits des tables de pages
Pour comprendre comment PAN / PXN fonctionnent, il faut voir comment la translation et le modèle de permissions ARM fonctionnent (simplifié) :
- Chaque entrée de page ou de block a des champs d’attributs incluant **AP[2:1]** pour les permissions d’accès (lect/écr, privilégié vs non privilégié) et les bits **UXN / PXN** pour les restrictions d’exécution.
- Quand PSTATE.PAN = 1 (activé), le hardware applique une sémantique modifiée : les accès privilégiés aux pages marquées “accessibles par EL0” (c.-à-d. accessibles par l’utilisateur) sont interdits (fault).
- À cause du bug mentionné, les pages marquées uniquement exécutables (sans permission de lecture) peuvent ne pas être considérées comme “accessibles par EL0” sous certaines implémentations, contournant ainsi PAN.
- Quand le bit PXN d’une page est mis, même si la fetch d’instruction vient d’un niveau de privilège supérieur, l’exécution est interdite.
#### Usage du kernel de PAN / PXN dans un OS durci (ex. iOS / XNU)
Dans un design de noyau durci (comme ce qu’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 (ex. copie de syscall, 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.
- Le PXN est appliqué via les tables de pages : les pages utilisateur 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 cause un flux vers des régions mémoire utilisateur (ce qui contournerait PXN) — ainsi les chaînes d’exploit visant à “sauter dans du shellcode contrôlé par l’utilisateur” sont bloquées.
À cause du contournement PAN via des pages execute-only noté ci-dessus, 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 mitigations
- **Contournement PAN via des pages execute-only** : comme discuté, la spec autorise un écart : les pages utilisateur execute-only (sans permission de lecture) peuvent ne pas être considérées comme “accessibles à EL0”, donc PAN ne bloquera pas les lectures kernel depuis ces pages sous certaines implémentations. Cela offre à l’attaquant une voie peu commune pour fournir des données via des sections “execute-only”.
- **Exploit de fenêtre temporelle** : si le kernel désactive PAN pour une fenêtre plus longue que nécessaire, une race ou un chemin malveillant pourrait exploiter cette fenêtre pour effectuer des accès mémoire utilisateur non souhaités.
- **Oubli de réactivation** : si des chemins de code n'ont pas réactivé PAN, des opérations kernel ultérieures pourraient accéder incorrectement la mémoire utilisateur.
- **Mauvaise configuration de PXN** : si les tables de pages ne positionnent pas PXN sur les pages utilisateur ou mappent incorrectement des pages de code utilisateur, le kernel pourrait être trompé pour exécuter du code utilisateur.
- **Spéculation / side-channels** : analogue aux contournements spéculatifs, il peut exister des effets microarchitecturaux transitoires qui provoquent une violation temporaire des vérifications PAN / PXN (bien que de telles attaques dépendent fortement du design du CPU).
- **Interactions complexes** : dans des fonctionnalités avancées (ex. JIT, mémoire partagée, zones de code just-in-time), le kernel peut avoir besoin d’un contrôle fin pour permettre certains accès mémoire ou exécutions dans des régions mappées utilisateur ; concevoir cela en sécurité avec PAN/PXN est non trivial.
#### Exemple
<details>
<summary>Exemple de code</summary>
Voici des séquences pseudo-assembly illustratives montrant l’activation/désactivation de PAN autour d’un accès mémoire utilisateur, et comment un fault peut survenir.
</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.
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.
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.
</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.
</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
Le kernel peut effectuer de nombreuses opérations normales, mais ce n’est qu’à travers les routines ppl_call_* qu’il peut modifier des mappings protégés ou patcher du code.
Exemple
Un kernel exploit essaie d'écraser la entitlement table, ou de désactiver l'enforcement du code-sign en modifiant un kernel signature blob. Parce que cette page est PPL-protected, l'écriture est bloquée sauf si elle passe par l'interface PPL. Donc 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 encore davantage les pages gérées par PPL.PPL → SPTM / Remplacements / Avenir
- Sur les SoC modernes d’Apple (A15 ou ultérieur, M2 ou ultérieur), Apple prend en charge SPTM (Secure Page Table Monitor), qui remplace PPL pour les protections de la table de pages.
- Apple précise dans la 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. »
- L’architecture SPTM déplace probablement davantage l’application des politiques vers un monitor de plus haut privilège hors du contrôle du kernel, réduisant encore la frontière de confiance.
MTE | EMTE | MIE
Voici une description de haut niveau du fonctionnement de EMTE dans le cadre du setup MIE d’Apple :
- Tag assignment
- Lorsqu’une mémoire est allouée (par ex. dans le kernel ou user space via des secure allocators), un secret tag est assigné à ce bloc.
- Le pointer retourné à l’utilisateur ou au kernel inclut ce tag dans ses bits hauts (en utilisant TBI / top byte ignore mechanisms).
- Tag checking on access
- Chaque fois qu’une load ou store est exécutée en utilisant un pointer, le hardware vérifie que le tag du pointer correspond au tag du bloc mémoire (allocation tag). En cas de mismatch, ça fault immédiatement (puisque synchrone).
- Parce que c’est synchrone, il n’y a pas de fenêtre de « delayed detection ».
- Retagging on free / reuse
- Quand la mémoire est freed, l’allocator change le tag du bloc (donc les anciens pointers avec de vieux tags ne correspondent plus).
- Un pointer use-after-free aura donc un tag périmé et un mismatch lors de l’accès.
- 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 mismatch de tag provoque un fault.
- C’est particulièrement efficace pour détecter les petits overflows qui franchissent la frontière.
- Tag confidentiality enforcement
- Apple doit empêcher que les valeurs de tag soient leaked (car si un attacker apprend le tag, il pourrait fabriquer des pointers avec les bons tags).
- Ils incluent des protections (microarchitectural / speculative controls) pour éviter la side-channel leakage des bits de tag.
- Kernel and user-space integration
- Apple utilise EMTE non seulement en user-space mais aussi dans les composants kernel / critiques pour l’OS (pour protéger le kernel contre la corruption mémoire).
- Le hardware/OS assure que les règles de tag s’appliquent même lorsque le kernel s’exécute pour le compte du user space.
Exemple
``` 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 un overflow reste dans la même allocation (ne franchit pas la frontière) et que le tag reste identique, le 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 — namespace limité.
- **Side-channel leaks** : Si les bits de tag peuvent être leaked (via cache / speculative execution), l'attaquant peut apprendre des tags valides et contourner la protection. La Tag Confidentiality Enforcement d’Apple vise à 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 cette overhead.
- **Compatibility & fallback** : Sur du hardware plus ancien ou des composants qui ne supportent pas EMTE, il doit exister un mécanisme de fallback. 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 tags. 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 complexe.
- **Speculative / transient attacks** : Comme pour beaucoup de protections microarchitecturales, l’exécution spéculative ou des fusions de micro-op pourraient bypasser les vérifications de manière transitoire ou leak des bits de tag.
- **Limited to supported regions** : Apple peut n’appliquer EMTE que sur des zones sélectionnées et à 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:
| Feature | 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.|
Because Apple controls both the hardware and software stack, it can enforce EMTE tightly, avoid performance pitfalls, and close side-channel holes.
---
## How EMTE works in practice (Apple / 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).
2. **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.
3. **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.
4. **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.
5. **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.
6. **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.
Because EMTE is built into MIE, Apple uses EMTE in synchronous mode across key attack surfaces, not as opt-in or debugging mode.
---
## Exception handling in XNU
When an **exception** occurs (e.g., `EXC_BAD_ACCESS`, `EXC_BAD_INSTRUCTION`, `EXC_CRASH`, `EXC_ARM_PAC`, etc.), the **Mach layer** of the XNU kernel is responsible for intercepting it before it becomes a UNIX-style **signal** (like `SIGSEGV`, `SIGBUS`, `SIGILL`, ...).
This process involves multiple layers of exception propagation and handling before reaching user space or being converted to a BSD signal.
### Exception Flow (High-Level)
1. **CPU triggers a synchronous exception** (e.g., invalid pointer dereference, PAC failure, illegal instruction, etc.).
2. **Low-level trap handler** runs (`trap.c`, `exception.c` in XNU source).
3. The trap handler calls **`exception_triage()`**, the core of the Mach exception handling.
4. `exception_triage()` decides how to route the exception:
- First to the **thread's exception port**.
- Then to the **task's exception port**.
- Then to the **host's exception port** (often `launchd` or `ReportCrash`).
If none of these ports handle the exception, the kernel may:
- **Convert it into a BSD signal** (for user-space processes).
- **Panic** (for kernel-space exceptions).
### Core Function: `exception_triage()`
The function `exception_triage()` routes Mach exceptions up the chain of possible handlers until one handles it or until it's finally fatal. It's defined in `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’appels typique :
exception_triage() └── exception_deliver() ├── exception_deliver_thread() ├── exception_deliver_task() └── exception_deliver_host()
Si toutes les tentatives échouent → géré 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 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 exception port a:
- 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 (p.ex., LLDB) définit un exception port sur la tâche ou le thread cible, généralement via task_set_exception_ports().
Quand une exception se produit :
- Le message Mach est envoyé au processus debugger.
- Le debugger peut décider de gérer (reprendre, modifier les registres, sauter l’instruction) ou de ne pas gérer l’exception.
- Si le debugger ne la gère pas, l’exception se propage au niveau suivant (task → host).
Flow of EXC_BAD_ACCESS
-
Le thread déréférence un pointeur invalide → le CPU lève un Data Abort.
-
Le gestionnaire de trap du kernel appelle
exception_triage(EXC_BAD_ACCESS, ...). -
Message envoyé à :
-
Thread port → (le debugger peut intercepter le breakpoint).
-
Si le debugger ignore → Task port → (gestionnaire au niveau process).
-
Si ignoré → Host port (généralement ReportCrash).
- Si personne ne gère →
bsd_exception()traduit enSIGSEGV.
PAC Exceptions
Quand Pointer Authentication (PAC) échoue (mismatch de signature), une Mach exception spéciale est levée :
EXC_ARM_PAC(type)- Les codes peuvent inclure des détails (p.ex., type de key, type de pointer).
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 les attaquants d’utiliser des debuggers pour contourner les vérifs PAC et c’est activé pour les platform binaries.
Software Breakpoints
Un software breakpoint (int3 on x86, brk on ARM64) est implémenté en provoquant volontairement une faute.
Le debugger le capte via l’exception port :
- Modifie le instruction pointer 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 vers des signaux :
| Mach Exception | 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) |
### Key Files in XNU Source
-
osfmk/kern/exception.c→ Coeur deexception_triage(),exception_deliver_*(). -
bsd/kern/kern_sig.c→ Logique de delivery des signaux. -
osfmk/arm64/trap.c→ Handlers de trap bas-niveau. -
osfmk/mach/exc.h→ Codes d’exception et structures. -
osfmk/kern/task.c→ Configuration des task exception ports.
Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)
Le kernel utilisait un zone allocator (kalloc) divisé en “zones” de tailles fixes.
Chaque zone stockait uniquement des allocations d’une seule classe de taille.
D’après la capture d’écran :
| 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 ça fonctionnait :
- Chaque requête d’allocation est arrondie vers le haut à la taille de zone la plus proche.
(P.ex., une requête de 50 bytes tombe dans la zone
kalloc.64). - La mémoire dans chaque zone était gardée dans une freelist — les chunks libérés par le kernel retournaient dans cette zone.
- Si vous débordiez un buffer de 64 bytes, vous écrasiez l’objet suivant dans la même zone.
C’est pourquoi le heap spraying / feng shui était si efficace : vous pouviez 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 rendus 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 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 actuel :
-
Pop HEAD (retourne cette mémoire à l’appelant).
-
Met à jour HEAD = HEAD->next (stocké dans l’en-tê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 elle-même.
Normal state:
Zone page (64-byte chunks for example):
[ A ] [ F ] [ F ] [ A ] [ F ] [ A ] [ F ]
Freelist view:
HEAD ──► [ F ] ──► [ F ] ──► [ F ] ──► [ F ] ──► NULL
(next ptrs stored at start of freed chunks)
Exploitation du freelist
Parce que les 8 premiers octets d’un chunk libre = freelist pointer, un attaquant pourrait le corrompre :
-
Heap overflow dans un chunk libéré adjacent → écraser son pointeur “next”.
-
Use-after-free écriture dans un objet libéré → écraser son pointeur “next”.
Ensuite, lors de la prochaine allocation de cette taille :
- L’allocateur retire le chunk corrompu.
- Suit le pointeur “next” fourni par l’attaquant.
- Retourne un pointeur vers une zone mémoire arbitraire, permettant des fake object primitives ou un targeted overwrite.
Exemple visuel de freelist poisoning:
Before corruption:
HEAD ──► [ F1 ] ──► [ F2 ] ──► [ F3 ] ──► NULL
After attacker overwrite of F1->next:
HEAD ──► [ F1 ]
(next) ──► 0xDEAD_BEEF_CAFE_BABE (attacker-chosen)
Next alloc of this zone → kernel hands out memory at attacker-controlled address.
This freelist design made exploitation highly effective pre-hardening: predictable neighbors from heap sprays, raw pointer freelist links, and no type separation allowed attackers to escalate UAF/overflow bugs into arbitrary kernel memory control.
Heap Grooming / Feng Shui
The goal of heap grooming is to shape the heap layout 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:
- 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.
- 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.
- 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).
- 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.
- 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.
- 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:
- Try per-CPU cache.
- If empty, pull from the global freelist.
- 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:
- Type lookup →
OSDatamaps tokalloc_type_osdatazone (size 64 bytes). - 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).
- 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
| 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)
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:
- malloc / calloc / realloc / typed alloc is invoked with a size and type ID.
- The allocator uses the type ID to pick the correct segment group / zone.
- 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.
- The metadata slab is updated (free bit cleared, bookkeeping).
- If memory tagging (EMTE) is in play, the returned block gets a tag assigned, and metadata is updated to reflect its “live” state.
- 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:
| 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)
- 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
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
PREYHUNTER helper & watcher modules
- 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 applications mobile AV (McAfee, AvastMobileSecurity, NortonMobileSecurity), des paramètres proxy HTTP personnalisés, et des root CAs personnalisés. Si une vérification échoue, toute livraison de payload ultérieure est bloquée. - Helper surveillance hooks: Le composant helper communique avec les 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 à l’échelle du système, capturent des photos sans interface utilisateur, et hookent SpringBoard pour supprimer les notifications que ces actions génèreraient normalement. Le helper agit donc comme une couche de validation discrète et de surveillance légère avant le déploiement d’implants plus lourds tels que Predator.
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
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.


