iOS Exploiting
Tip
Lernen & üben Sie AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
iOS Exploit-Schutzmaßnahmen
1. Code Signing / Laufzeit-Signaturprüfung
Eingeführt früh (iPhone OS → iOS) Das ist eine der grundlegenden Schutzmaßnahmen: jeglicher ausführbarer Code (Apps, dynamische Bibliotheken, JIT-Code, Extensions, Frameworks, Caches) muss kryptografisch von einer Zertifikatskette signiert sein, die in Apples Trust verwurzelt ist. Zur Laufzeit, bevor ein Binary in den Speicher geladen wird (oder bevor Sprünge über bestimmte Grenzen erfolgen), überprüft das System die Signatur. Ist der Code verändert (Bits umgekippt, gepatcht) oder unsigniert, schlägt das Laden fehl.
- Verhindert: die “klassische payload drop + execute”-Phase in Exploit-Chains; beliebige Code-Injektion; Modifikation eines bestehenden Binaries, um bösartige Logik einzufügen.
- Mechanismus-Details:
- Der Mach-O loader (und dynamic linker) prüft Code-Pages, Segmente, Entitlements, Team-IDs und ob die Signatur den Inhalt der Datei abdeckt.
- Für Speicherbereiche wie JIT-Caches oder dynamisch generierten Code verlangt Apple, dass Pages signiert sind oder über spezielle APIs validiert werden (z. B.
mprotectmit code-sign-Prüfungen). - Die Signatur enthält Entitlements und Identifier; das OS erzwingt, dass bestimmte APIs oder privilegierte Fähigkeiten spezifische Entitlements benötigen, die nicht gefälscht werden können.
Beispiel
Angenommen, ein Exploit erlangt Code-Ausführung in einem Prozess und versucht, Shellcode in den Heap zu schreiben und dorthin zu springen. Auf iOS müsste diese Page sowohl als ausführbar markiert sein **als auch** die Code-Signatur-Bedingungen erfüllen. Da der Shellcode nicht mit Apples Zertifikat signiert ist, schlägt der Sprung fehl oder das System verweigert, diesen Speicherbereich ausführbar zu machen.2. CoreTrust
Eingeführt etwa in der iOS 14+-Ära (oder schrittweise auf neueren Geräten / späteren iOS-Versionen) CoreTrust ist die Subsystem-Komponente, die die Laufzeit-Signaturvalidierung von Binaries (inkl. System- und Nutzer-Binaries) gegen Apples Root-Zertifikat durchführt, anstatt sich auf gecachte Userland-Trust-Stores zu verlassen.
- Verhindert: nachträgliche Manipulationen an Binaries, Jailbreak-Techniken, die versuchen, Systembibliotheken oder User-Apps zu ersetzen oder zu patchen; Täuschung des Systems durch Ersetzen vertrauenswürdiger Binaries durch bösartige Gegenstücke.
- Mechanismus-Details:
- Anstatt einer lokalen Trust-Datenbank oder Zertifikats-Cache zu vertrauen, bezieht sich CoreTrust direkt auf Apples Root oder verifiziert Intermediate-Zertifikate in einer sicheren Kette.
- Es stellt sicher, dass Modifikationen (z. B. im Dateisystem) an bestehenden Binaries erkannt und abgelehnt werden.
- Es bindet Entitlements, Team-IDs, Code-Signing-Flags und andere Metadaten an das Binary zur Ladezeit.
Beispiel
Ein Jailbreak könnte versuchen, `SpringBoard` oder `libsystem` durch eine gepatchte Version zu ersetzen, um Persistenz zu erlangen. Wenn der OS-Loader oder CoreTrust prüft, bemerkt er die Signatur-Abweichung (oder veränderte Entitlements) und verweigert die Ausführung.3. Data Execution Prevention (DEP / NX / W^X)
In vielen OS früher eingeführt; iOS hatte NX-bit / w^x lange Zeit DEP erzwingt, dass als schreibbar markierte Pages (für Daten) nicht ausführbar sind und dass ausführbare Pages nicht schreibbar sind. Man kann nicht einfach Shellcode in Heap- oder Stack-Bereiche schreiben und ausführen.
- Verhindert: direkte Shellcode-Ausführung; klassische Buffer-Overflow → Sprung zum injizierten Shellcode.
- Mechanismus-Details:
- Die MMU / Memory-Protection-Flags (via Page-Tabellen) erzwingen die Trennung.
- Jeder Versuch, eine schreibbare Page ausführbar zu machen, löst eine Systemprüfung aus (und ist entweder verboten oder erfordert Code-Sign-Zustimmung).
- In vielen Fällen erfordert das Setzen von Pages auf ausführbar die Verwendung von OS-APIs, die zusätzliche Constraints oder Prüfungen durchsetzen.
Beispiel
Ein Overflow schreibt Shellcode auf den Heap. Der Angreifer versucht `mprotect(heap_addr, size, PROT_EXEC)`, um ihn ausführbar zu machen. Aber das System verweigert dies oder validiert, dass die neue Page Code-Sign-Bedingungen bestehen muss (was der Shellcode nicht kann).4. Address Space Layout Randomization (ASLR)
Eingeführt in der iOS ~4–5-Ära (grobe iOS 4–5 Zeitspanne) ASLR randomisiert die Basisadressen wichtiger Speicherregionen: Bibliotheken, Heap, Stack usw. bei jedem Prozessstart. Gadget-Adressen verschieben sich zwischen Läufen.
- Verhindert: das Hardcodieren von Gadget-Adressen für ROP/JOP; statische Exploit-Chains; blinde Sprünge zu bekannten Offsets.
- Mechanismus-Details:
- Jede geladene Bibliothek / dynamisches Modul wird an einem randomisierten Offset rebased.
- Stack- und Heap-Basen sind randomisiert (innerhalb bestimmter Entropiegrenzen).
- Manchmal werden andere Regionen (z. B. mmap-Allocations) ebenfalls randomisiert.
- Kombiniert mit Informations-Leak-Maßnahmen zwingt es den Angreifer zuerst, eine Adresse oder einen Pointer zu leaken, um Basisadressen zur Laufzeit zu ermitteln.
Beispiel
Eine ROP-Chain erwartet ein Gadget bei `0x….lib + offset`. Aber da `lib` bei jedem Lauf unterschiedlich relociert wird, scheitert die hardcodierte Chain. Ein Exploit muss zuerst die Basisadresse des Moduls leaken, bevor Gadget-Adressen berechnet werden können.5. Kernel Address Space Layout Randomization (KASLR)
Eingeführt in iOS ~ (iOS 5 / iOS 6 Zeitrahmen) Analog zu User-ASLR randomisiert KASLR die Basis der Kernel-Text-Region und andere Kernel-Strukturen beim Boot.
- Verhindert: Kernel-Level-Exploits, die sich auf feste Positionen von Kernel-Code oder -Daten verlassen; statische Kernel-Exploits.
- Mechanismus-Details:
- Bei jedem Boot wird die Kernel-Base-Adresse randomisiert (innerhalb eines Bereichs).
- Kernel-Datenstrukturen (wie
task_structs,vm_mapetc.) können ebenfalls verschoben oder versetzt werden. - Angreifer müssen zuerst Kernel-Pointer leaken oder Informations-Disclosure-Vulnerabilities nutzen, um Offsets zu berechnen, bevor sie Kernel-Strukturen oder -Code hijacken.
Beispiel
Eine lokale Schwachstelle zielt darauf ab, einen Kernel-Funktionszeiger (z. B. in einem `vtable`) bei `KERN_BASE + offset` zu korrumpieren. Da `KERN_BASE` unbekannt ist, muss der Angreifer es zuerst leaken (z. B. über ein read-Primitive), bevor er die richtige Adresse zur Korruption berechnen kann.6. Kernel Patch Protection (KPP / AMCC)
Eingeführt in neueren iOS / A-series Hardware (post ca. iOS 15–16 Ära oder neuere Chips) KPP (aka AMCC) überwacht kontinuierlich die Integrität der Kernel-Text-Pages (via Hash oder Checksum). Entdeckt es Manipulationen (Patches, inline hooks, Code-Modifikationen) außerhalb erlaubter Fenster, löst es einen Kernel-Panic oder Reboot aus.
- Verhindert: persistentes Kernel-Patching (Modifikation von Kernel-Instruktionen), inline Hooks, statische Funktionsüberschreibungen.
- Mechanismus-Details:
- Ein Hardware- oder Firmware-Modul überwacht den Kernel-Text-Bereich.
- Es re-hasht periodisch oder auf Anforderung die Pages und vergleicht mit erwarteten Werten.
- Bei Abweichungen außerhalb benignen Update-Fenstern panikt das Gerät (um persistente bösartige Patches zu vermeiden).
- Angreifer müssen entweder Erkennungsfenster umgehen oder legitime Patch-Pfade nutzen.
Beispiel
Ein Exploit versucht, eine Kernel-Funktionsprolog (z. B. `memcmp`) zu patchen, um Aufrufe abzufangen. KPP bemerkt, dass die Hashes der Code-Page nicht mehr den erwarteten Werten entsprechen, und löst einen Kernel-Panic aus, wodurch das Gerät abstürzt, bevor der Patch stabil angewendet werden kann.7. Kernel Text Read‐Only Region (KTRR)
Eingeführt in modernen SoCs (post ~A12 / neuere Hardware) KTRR ist ein hardware-erzwungener Mechanismus: sobald der Kernel-Text früh im Boot-Vorgang gesperrt wird, wird er von EL1 (Kernel) aus schreibgeschützt, wodurch weitere Schreibzugriffe auf Code-Pages verhindert werden.
- Verhindert: jegliche Modifikationen am Kernel-Code nach dem Boot (z. B. Patching, in-place Code-Injektion) auf EL1-Privilege-Level.
- Mechanismus-Details:
- Während des Bootens (im secure/bootloader-Stage) markiert der Memory-Controller (oder eine sichere Hardware-Einheit) die physikalischen Pages, die Kernel-Text enthalten, als read-only.
- Selbst wenn ein Exploit volle Kernel-Privilegien erlangt, kann er diese Pages nicht zum Patchen beschreiben.
- Um sie zu ändern, müsste der Angreifer zunächst die Boot-Chain kompromittieren oder KTRR selbst unterwandern.
Beispiel
Ein Privilege-Escalation-Exploit springt nach EL1 und schreibt einen Trampolin-Stub in eine Kernel-Funktion (z. B. im `syscall`-Handler). Weil die Pages durch KTRR als read-only gesperrt sind, schlägt der Schreibversuch fehl (oder erzeugt einen Fault), sodass Patches nicht angewendet werden.8. Pointer Authentication Codes (PAC)
Eingeführt mit ARMv8.3 (Hardware), Apple ab A12 / iOS ~12+
- PAC ist eine Hardware-Funktion, eingeführt in ARMv8.3-A, um Manipulationen an Pointer-Werten (Return-Adressen, Funktionszeiger, bestimmte Datenpointer) zu erkennen, indem eine kleine kryptografische Signatur (ein “MAC”) in ungenutzte hohe Bits des Pointers eingebettet wird.
- Die Signatur (“PAC”) wird über den Pointer-Wert plus einen Modifier (ein Kontextwert, z. B. Stack-Pointer oder differenzierende Daten) berechnet. So erhält derselbe Pointer-Wert in unterschiedlichen Kontexten unterschiedliche PACs.
- Zur Nutzungszeit prüft eine authenticate-Instruktion die PAC. Ist sie gültig, wird die PAC entfernt und der reine Pointer verwendet; ist sie ungültig, wird der Pointer “poisoned” (oder ein Fault ausgelöst).
- Die Keys zur Erzeugung/Überprüfung von PACs liegen in privilegierten Registern (EL1, Kernel) und sind nicht direkt aus dem User-Mode lesbar.
- Da nicht alle 64 Bits eines Pointers in vielen Systemen genutzt werden (z. B. 48-bit Address-Space), sind die oberen Bits “frei” und können den PAC aufnehmen, ohne die effektive Adresse zu ändern.
Architekturbasis & Key-Typen
-
ARMv8.3 führt fünf 128-bit Keys ein (jeweils implementiert über zwei 64-bit Systemregister) für pointer authentication.
-
APIAKey — für Instruction-Pointer (Domain “I”, Key A)
-
APIBKey — zweiter Instruction-Pointer-Key (Domain “I”, Key B)
-
APDAKey — für Data-Pointer (Domain “D”, Key A)
-
APDBKey — für Data-Pointer (Domain “D”, Key B)
-
APGAKey — “generic” Key, für das Signieren von Nicht-Pointer-Daten oder allgemeine Verwendung
-
Diese Keys werden in privilegierten Systemregistern gespeichert (nur in EL1/EL2 etc. zugänglich), nicht aus dem User-Mode.
-
Der PAC wird über eine kryptografische Funktion berechnet (ARM schlägt QARMA als Algorithmus vor) unter Verwendung von:
- Dem Pointer-Wert (kanonischer Teil)
- Einem Modifier (Kontextwert, wie Salt)
- Dem geheimen Key
- Einiger interner Tweak-Logik Wenn der resultierende PAC mit dem in den oberen Bits des Pointers gespeicherten Wert übereinstimmt, schlägt die Authentifizierung fehl oder erfolgreich.
Instruktionsfamilien
Die Namenskonvention ist: PAC / AUT / XPAC, gefolgt von Domain-Buchstaben.
PACxxInstruktionen signieren einen Pointer und fügen einen PAC einAUTxxInstruktionen authentifizieren + strippen (validieren und entfernen des PAC)XPACxxInstruktionen strippen ohne Validierung
Domänen / Suffixe:
| Mnemonic | Bedeutung / Domain | Key / Domain | Beispielhafte Verwendung in Assembly |
|---|---|---|---|
| PACIA | Instruction-Pointer mit APIAKey signieren | “I, A” | PACIA X0, X1 — signiert Pointer in X0 mit APIAKey und Modifier X1 |
| PACIB | Instruction-Pointer mit APIBKey signieren | “I, B” | PACIB X2, X3 |
| PACDA | Data-Pointer mit APDAKey signieren | “D, A” | PACDA X4, X5 |
| PACDB | Data-Pointer mit APDBKey signieren | “D, B” | PACDB X6, X7 |
| PACG / PACGA | Generic (non-pointer) Signierung mit APGAKey | “G” | PACGA X8, X9, X10 (signiert X9 mit Modifier X10 in X8) |
| AUTIA | APIA-signierten Instruction-Pointer authentifizieren & PAC strippen | “I, A” | AUTIA X0, X1 — prüft PAC auf X0 mit Modifier X1, dann strippt |
| AUTIB | APIB-Domain authentifizieren | “I, B” | AUTIB X2, X3 |
| AUTDA | APDA-signierten Data-Pointer authentifizieren | “D, A” | AUTDA X4, X5 |
| AUTDB | APDB-signierten Data-Pointer authentifizieren | “D, B” | AUTDB X6, X7 |
| AUTGA | Generic / Blob authentifizieren (APGA) | “G” | AUTGA X8, X9, X10 (validiert generic) |
| XPACI | PAC strippen (Instruction-Pointer, ohne Validierung) | “I” | XPACI X0 — entfernt PAC aus X0 (Instruction-Domain) |
| XPACD | PAC strippen (Data-Pointer, ohne Validierung) | “D” | XPACD X4 — entfernt PAC aus Data-Pointer in X4 |
Es gibt spezialisierte / Alias-Formen:
PACIASPist Kurzform fürPACIA X30, SP(signiert den Link-Register mit SP als Modifier)AUTIASPistAUTIA X30, SP(authentifiziert den Link-Register mit SP)- Kombinierte Formen wie
RETAA,RETAB(authentifizieren-und-return) oderBLRAA(authentifizieren & branch) existieren in ARM-Erweiterungen / Compiler-Support. - Auch Zero-Modifier-Varianten:
PACIZA/PACIZB, wo der Modifier implizit null ist, etc.
Modifier
Das Hauptziel des Modifiers ist, den PAC an einen spezifischen Kontext zu binden, sodass derselbe signierte Addresswert in unterschiedlichen Kontexten unterschiedliche PACs ergibt. Das verhindert einfaches Reuse von Pointern über Frames oder Objekte hinweg — es ist wie ein Salt für einen Hash.
Daher:
- Der Modifier ist ein Kontextwert (ein anderes Register), der in die PAC-Berechnung einfließt. Typische Wahl: der Stack-Pointer (
SP), ein Frame-Pointer oder eine Objekt-ID. - Die Verwendung von SP als Modifier ist üblich für Return-Address-Signing: der PAC wird an einen bestimmten Stack-Frame gebunden. Versucht man, das LR in einem anderen Frame wiederzuverwenden, ändert sich der Modifier, sodass die PAC-Validation fehlschlägt.
- Derselbe Pointer-Wert, signiert mit unterschiedlichen Modifiers, ergibt unterschiedliche PACs.
- Der Modifier muss nicht geheim sein, idealerweise jedoch nicht vom Angreifer kontrolliert werden.
- Für Instruktionen, die Pointer signieren oder validieren, wenn kein sinnvoller Modifier existiert, verwenden einige Formen null oder eine implizite Konstante.
Apple / iOS / XNU-Anpassungen & Beobachtungen
- Apples PAC-Implementierung enthält per-boot Diversifiers, sodass Keys oder Tweaks bei jedem Boot wechseln und Reuse über Boots hinweg verhindern.
- Sie enthalten auch cross-domain-Mitigations, sodass PACs, die im User-Mode signiert wurden, nicht einfach im Kernel-Mode wiederverwendet werden können, etc.
- Auf Apple M1 / Apple Silicon zeigten Reverse-Engineering-Ergebnisse, dass es neun Modifier-Typen und Apple-spezifische Systemregister zur Key-Kontrolle gibt.
- Apple nutzt PAC in vielen Kernel-Subsystemen: Return-Address-Signing, Pointer-Integrität in Kernel-Daten, signierte Thread-Contexts, etc.
- Google Project Zero zeigte, wie man unter einer mächtigen Memory read/write-Primitive im Kernel PACs (für A-Keys) auf A12-Geräten fälschen konnte, aber Apple hat viele dieser Wege gepatcht.
- In Apples System sind einige Keys kernelweit global, während User-Prozesse prozessspezifische Key-Randomness erhalten können.
PAC-Bypasses
- Kernel-mode PAC: theoretisch vs. reale Bypässe
- Da Kernel-PAC-Keys und -Logik streng kontrolliert sind (privilegierte Register, Diversifiers, Domain-Isolation), ist das Fälschen beliebiger signierter Kernel-Pointer sehr schwierig.
- Azad’s 2020 “iOS Kernel PAC, One Year Later” berichtet, dass in iOS 12–13 einige partielle Bypässe gefunden wurden (signing gadgets, Wiederverwendung signierter Zustände, ungeschützte indirekte Branches), aber kein genereller Full-Bypass. bazad.github.io
- Apples “Dark Magic”-Anpassungen verringern die angreifbaren Oberflächen weiter (Domain-Switching, per-key enabling bits). i.blackhat.com
- Es gibt einen bekannten Kernel-PAC-Bypass CVE-2023-32424 auf Apple Silicon (M1/M2), berichtet von Zecao Cai et al. i.blackhat.com
- Diese Bypässe basieren jedoch oft auf sehr spezifischen Gadgets oder Implementationsfehlern; sie sind keine allgemein einsetzbaren Techniken.
Somit gilt Kernel-PAC als hochrobust, wenn auch nicht perfekt.
- User-mode / Runtime-PAC-Bypass-Techniken
Diese sind häufiger und nutzen Unzulänglichkeiten in der Anwendung von PAC oder in der dynamischen Laufzeit/Linking-Logik. Nachfolgend Klassen mit Beispielen.
2.1 Shared Cache / A-Key-Probleme
- Der dyld shared cache ist ein großes vorverlinktes Blob von System-Frameworks und -Libraries. Da es so weit verbreitet ist, sind Funktionspointer im shared cache bereits signiert und werden von vielen Prozessen verwendet. Angreifer zielen auf diese bereits-signierten Pointer als PAC-Orakel.
- Einige Bypass-Techniken versuchen, A-key-signierte Pointer aus dem shared cache zu extrahieren oder wiederzuverwenden und sie in Gadgets einzubauen.
- Der Vortrag “No Clicks Required” beschreibt, wie man ein Oracle über den shared cache baut, um relative Adressen zu ermitteln und diese mit signierten Pointern zu kombinieren, um PAC zu umgehen. saelo.github.io
- Auch Importe von Funktionszeigern aus Shared Libraries im Userspace wurden als unzureichend PAC-geschützt gefunden, sodass ein Angreifer Funktionspointer erhalten konnte, ohne deren Signatur zu verändern. (Project Zero Bug-Eintrag) bugs.chromium.org
2.2 dlsym(3) / dynamische Symbolauflösung
- Ein bekannter Bypass ist,
dlsym()aufzurufen, um einen bereits signierten Funktionspointer (mit A-key, Diversifier null) zu erhalten und diesen dann zu verwenden. Dadlsymeinen legitim signierten Pointer zurückgibt, umgeht seine Nutzung das Bedürfnis, PAC zu fälschen. - Epsilons Blog beschreibt, wie manche Bypässe dies ausnutzen:
dlsym("someSym")liefert einen signierten Pointer und kann für indirekte Aufrufe benutzt werden. blog.epsilon-sec.com - Synacktivs “iOS 18.4 — dlsym considered harmful” beschreibt einen Bug: einige Symbole, die via
dlsymin iOS 18.4 aufgelöst werden, liefern Pointer, die fehlerhaft signiert sind (oder fehlerhafte Diversifiers haben), was unbeabsichtigte PAC-Bypässe ermöglicht. Synacktiv - Die Logik in dyld für dlsym inkludiert: wenn
result->isCode, signieren sie den zurückgegebenen Pointer mit__builtin_ptrauth_sign_unauthenticated(..., key_asia, 0), d. h. Kontext null. blog.epsilon-sec.com
Daher ist dlsym ein häufig genutzter Vektor für User-mode PAC-Bypässe.
2.3 Andere DYLD / Laufzeit-Relocations
- Der DYLD-Loader und die dynamische Relocation-Logik sind komplex und mappen manchmal temporär Pages als read/write, um Relocations durchzuführen, und schalten sie dann wieder auf read-only. Angreifer nutzen diese Zeitfenster aus. Synacktivs Vortrag beschreibt “Operation Triangulation”, einen timing-basierten PAC-Bypass über dynamische Relocations. Synacktiv
- DYLD-Pages sind jetzt mit SPRR / VM_FLAGS_TPRO geschützt (einige Schutzflags für dyld). Frühere Versionen hatten schwächere Schutzmechanismen. Synacktiv
- In WebKit-Exploit-Chains ist der DYLD-Loader oft Ziel für PAC-Bypässe. Die Slides erwähnen, dass viele PAC-Bypässe den DYLD-Loader angreifen (via Relocation, Interposer-Hooks). Synacktiv
2.4 NSPredicate / NSExpression / ObjC / SLOP
- In Userland-Exploit-Chains werden Objective-C Runtime-Methoden wie
NSPredicate,NSExpressionoderNSInvocationverwendet, um Kontrollaufrufe zu schmuggeln, ohne offensichtliches Pointer-Fälschen. - Auf älteren iOS-Versionen (vor PAC) nutzte ein Exploit fake NSInvocation-Objekte, um beliebige Selector-Aufrufe auf kontrolliertem Speicher zu machen. Mit PAC sind Modifikationen nötig. Die Technik SLOP (SeLector Oriented Programming) wurde jedoch unter PAC weiterentwickelt. Project Zero
- Die ursprüngliche SLOP-Technik erlaubte das Verketteten von ObjC-Aufrufen durch Erzeugen gefälschter Invocations; der Bypass basiert darauf, dass ISA- oder Selector-Pointer manchmal nicht vollständig PAC-geschützt sind. Project Zero
- In Umgebungen, in denen Pointer-Authentication nur partiell angewendet wird, sind Methoden / Selector- / Target-Pointer nicht immer PAC-geschützt, was Raum für Bypässe lässt.
Beispiel-Flow
Beispiel: Signieren & Authentifizieren
``` ; 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>Beispiel</summary>
A buffer overflow überschreibt eine return-Adresse auf dem Stack. Der Angreifer schreibt die Ziel-Gadget-Adresse, kann aber den korrekten PAC nicht berechnen. Wenn die Funktion zurückkehrt, schlägt die CPU-Anweisung `AUTIA` fehl, weil der PAC nicht übereinstimmt. Die Chain bricht zusammen.
Project Zero’s Analyse des A12 (iPhone XS) zeigte, wie Apple’s PAC eingesetzt wird und Methoden zum Fälschen von PACs, wenn ein Angreifer ein Lese/Schreib-Primitiv für Speicher hat.
</details>
### 9. **Branch Target Identification (BTI)**
**Eingeführt mit ARMv8.5 (neuere Hardware)**
BTI ist ein Hardware-Feature, das **indirekte Branch-Ziele** prüft: beim Ausführen von `blr` oder indirekten Calls/Jumps muss das Ziel mit einem **BTI landing pad** (`BTI j` oder `BTI c`) beginnen. Das Springen in Gadget-Adressen, die kein Landing Pad haben, löst eine Ausnahme aus.
LLVMs Implementierung nennt drei Varianten von BTI-Instruktionen und wie sie auf Branch-Typen abgebildet werden.
| BTI Variant | Was es erlaubt (welche Branch-Typen) | Typische Platzierung / Anwendungsfall |
|-------------|----------------------------------------|-------------------------------|
| **BTI C** | Ziele von *call*-artigen indirekten Branches (z. B. `BLR`, oder `BR` mit X16/X17) | Am Eintritt von Funktionen, die indirekt aufgerufen werden können |
| **BTI J** | Ziele von *jump*-artigen Branches (z. B. `BR` für tail calls) | Am Beginn von Blöcken, die über jump tables oder tail-calls erreichbar sind |
| **BTI JC** | Wirkt als sowohl C als auch J | Kann sowohl von Call- als auch von Jump-Branches adressiert werden |
- In Code, der mit branch target enforcement kompiliert wurde, fügen Compiler eine BTI-Instruktion (C, J oder JC) an jedem gültigen indirekten-Branch-Ziel ein (Funktionsanfänge oder Blöcke, die per Jump erreichbar sind), sodass indirekte Branches nur zu diesen Orten erfolgreich sind.
- **Direkte Branches / Calls** (also feste Adressen wie `B`, `BL`) werden **nicht** durch BTI eingeschränkt. Die Annahme ist, dass Code-Seiten vertrauenswürdig sind und ein Angreifer sie nicht ändern kann (daher sind direkte Branches sicher).
- Außerdem sind **RET / return-Instruktionen** im Allgemeinen nicht durch BTI eingeschränkt, weil Return-Adressen über PAC oder return-signing-Mechanismen geschützt sind.
#### Mechanismus und Durchsetzung
- Wenn die CPU einen **indirekten Branch (BLR / BR)** decodiert in einer Seite, die als „guarded / BTI-enabled“ markiert ist, prüft sie, ob die erste Instruktion an der Zieladresse ein gültiges BTI (C, J oder JC, wie erlaubt) ist. Wenn nicht, tritt eine **Branch Target Exception** auf.
- Die Kodierung der BTI-Instruktion wurde so entworfen, dass vorher für NOPs reservierte Opcodes wiederverwendet werden (in früheren ARM-Versionen). Daher bleiben BTI-fähige Binaries abwärtskompatibel: auf Hardware ohne BTI-Unterstützung verhalten sich diese Instruktionen wie NOPs.
- Die Compiler-Passes, die BTIs hinzufügen, fügen sie nur dort ein, wo nötig: Funktionen, die indirekt aufgerufen werden können, oder Basic Blocks, die per Jump adressiert werden.
- Einige Patches und LLVM-Code zeigen, dass BTI nicht für *alle* Basic Blocks eingefügt wird — nur für diejenigen, die potenzielle Branch-Ziele sind (z. B. aus switch / jump tables).
#### BTI + PAC Synergie
PAC schützt den Pointer-Wert (die Quelle) — stellt sicher, dass die Kette indirekter Aufrufe / Returns nicht manipuliert wurde.
BTI stellt sicher, dass selbst ein gültiger Pointer nur auf korrekt markierte Entry-Punkte zielen darf.
Kombiniert benötigt ein Angreifer sowohl einen gültigen Pointer mit korrektem PAC als auch ein Ziel, das dort ein BTI hat. Das erhöht die Schwierigkeit, brauchbare Exploit-Gadgets zu konstruieren.
#### Beispiel
<details>
<summary>Beispiel</summary>
Ein Exploit versucht, in ein Gadget bei `0xABCDEF` zu pivotieren, das nicht mit `BTI c` beginnt. Die CPU prüft beim Ausführen von `blr x0` das Ziel und löst einen Fehler aus, weil die Instruktionsausrichtung kein gültiges Landing Pad enthält. Viele Gadgets werden somit unbrauchbar, sofern sie kein BTI-Prefix enthalten.
</details>
### 10. **Privileged Access Never (PAN) & Privileged Execute Never (PXN)**
**Eingeführt in neueren ARMv8-Erweiterungen / iOS-Unterstützung (für gehärteten Kernel)**
#### PAN (Privileged Access Never)
- **PAN** ist ein Feature, eingeführt in **ARMv8.1-A**, das verhindert, dass **privilegierter Code** (EL1 oder EL2) **lesen oder schreiben** kann auf Speicher, der als **user-accessible (EL0)** markiert ist, es sei denn, PAN ist explizit deaktiviert.
- Die Idee: selbst wenn der Kernel getäuscht oder kompromittiert wird, kann er nicht beliebig User-Pointer dereferenzieren, ohne vorher PAN *zu deaktivieren*, was das Risiko von Exploits im Stil von **ret2usr** oder Missbrauch benutzergesteuerter Buffer reduziert.
- Wenn PAN aktiviert ist (PSTATE.PAN = 1), löst jede privilegierte Load/Store-Instruktion, die auf eine virtuelle Adresse zugreift, die „accessible at EL0“ ist, eine **Permission Fault** aus.
- Der Kernel muss, wenn er legitim auf User-Speicher zugreifen muss (z. B. Daten von/nach User-Buffern kopieren), **temporär PAN deaktivieren** (oder „unprivilegierte“ Load/Store-Instruktionen verwenden), um diesen Zugriff zu ermöglichen.
- In Linux auf ARM64 wurde PAN-Unterstützung etwa 2015 eingeführt: Kernel-Patches erkannten die Feature-Unterstützung und ersetzten `get_user` / `put_user` usw. durch Varianten, die PAN um User-Speicherzugriffe herum löschen.
**Wichtige Nuance / Limitierung / Bug**
- Wie von Siguza und anderen bemerkt, bedeutet ein Spezifikationsfehler (oder uneindeutiges Verhalten) in ARMs Design, dass **execute-only user mappings** (`--x`) **PAN möglicherweise nicht auslösen**. Mit anderen Worten: wenn eine User-Seite ausführbar, aber ohne Lese-Rechte ist, könnte der Kernel-Leseversuch PAN umgehen, weil die Architektur „accessible at EL0“ als lesbare Berechtigung und nicht nur als ausführbar interpretiert. Das führt in bestimmten Konfigurationen zu einem PAN-Bypass.
- Aufgrund dessen könnte ein System, das execute-only User-Seiten zulässt (wie einige JIT- oder Code-Cache-Setups), dazu führen, dass der Kernel versehentlich von ihnen liest, selbst wenn PAN aktiviert ist. Dies ist eine bekannte, subtile Angriffsfläche in manchen ARMv8+-Systemen.
#### PXN (Privileged eXecute Never)
- **PXN** ist ein Page-Table-Flag (in Page-Table-Einträgen, Leaf- oder Block-Einträgen), das anzeigt, dass die Seite **nicht ausführbar ist, wenn in privilegiertem Modus ausgeführt wird** (d. h. wenn EL1 sie ausführt).
- PXN verhindert, dass der Kernel (oder beliebiger privilegierter Code) in User-Seiten springt oder Instruktionen von dort ausführt, selbst wenn die Kontrolle umgelenkt wird. Effektiv blockiert es Kernel-Level-Control-Flow-Redirection in User-Speicher.
- In Kombination mit PAN stellt das sicher:
1. Kernel kann standardmäßig nicht User-Daten lesen oder schreiben (PAN)
2. Kernel kann keinen User-Code ausführen (PXN)
- Im ARMv8 Page-Table-Format haben Leaf-Einträge ein `PXN`-Bit (und auch `UXN` für unprivileged execute-never) in ihren Attribut-Bits.
Somit würde selbst bei einem korrumpierten Funktionspointer im Kernel, der auf User-Speicher zeigt, ein Branch dorthin durch das PXN-Bit einen Fault verursachen.
#### Memory-Permission-Modell & wie PAN und PXN auf Page-Table-Bits abgebildet werden
Um zu verstehen, wie PAN / PXN arbeiten, muss man sehen, wie ARMs Übersetzungs- und Berechtigungsmodell funktioniert (vereinfacht):
- Jeder Page- oder Block-Eintrag hat Attributfelder, einschließlich **AP[2:1]** für Zugriffsrechte (lesen/schreiben, privileged vs unprivileged) und **UXN / PXN**-Bits für execute-never-Einschränkungen.
- Wenn PSTATE.PAN = 1 (aktiviert), erzwingt die Hardware geänderte Semantik: privilegierte Zugriffe auf Seiten, die als „accessible by EL0“ markiert sind (d. h. user-accessible), werden verweigert (Fault).
- Wegen des erwähnten Bugs könnten Seiten, die nur ausführbar sind (keine Leserechte), unter bestimmten Implementierungen nicht als „accessible by EL0“ zählen und somit PAN umgehen.
- Wenn das PXN-Bit einer Seite gesetzt ist, ist Ausführung selbst bei Instruktionsfetches aus einer höheren Privilege-Ebene verboten.
#### Kernel-Nutzung von PAN / PXN in einem gehärteten OS (z. B. iOS / XNU)
In einem gehärteten Kernel-Design (wie Apple es möglicherweise verwendet):
- Der Kernel aktiviert PAN standardmäßig (sodass privilegierter Code eingeschränkt ist).
- In Pfaden, die legitim auf User-Buffer zugreifen müssen (z. B. syscall buffer copy, I/O, read/write user pointer), deaktiviert der Kernel temporär **PAN** oder verwendet spezielle Instruktionen, um den Zugriff zu erlauben.
- Nach Abschluss des User-Datenzugriffs muss PAN wieder aktiviert werden.
- PXN wird über Page-Tables durchgesetzt: User-Seiten haben PXN = 1 (sodass Kernel sie nicht ausführen kann), Kernel-Seiten haben PXN nicht gesetzt (so dass Kernel-Code ausführbar ist).
- Der Kernel muss sicherstellen, dass keine Code-Pfade Ausführung in User-Speicherregionen erlauben (das würde PXN umgehen) — daher sind Exploit-Chains, die auf „jump into user-controlled shellcode“ setzen, blockiert.
Aufgrund des erwähnten PAN-Bypasses über execute-only-Seiten könnte Apple in einem echten System execute-only User-Seiten deaktivieren oder an der Spezifikationsschwäche vorbeipatchen.
#### Angriffsflächen, Bypässe und Mitigationen
- **PAN-Bypass via execute-only pages**: wie besprochen erlaubt die Spezifikation eine Lücke: User-Seiten mit execute-only (keine Lese-Berechtigung) zählen in manchen Implementierungen nicht als „accessible at EL0“, sodass PAN Kernel-Lesezugriffe aus solchen Seiten nicht blockiert. Das verschafft dem Angreifer einen ungewöhnlichen Weg, Daten über execute-only-Abschnitte einzuspeisen.
- **Zeitliches Fenster (Temporal window) Exploit**: wenn der Kernel PAN länger als nötig deaktiviert, könnte ein Race oder ein bösartiger Pfad dieses Fenster ausnutzen, um ungewollte User-Speicherzugriffe durchzuführen.
- **Vergessenes Reaktivieren**: wenn Code-Pfade versäumen, PAN wieder zu aktivieren, könnten nachfolgende Kernel-Operationen fälschlich auf User-Speicher zugreifen.
- **Fehlkonfiguration von PXN**: wenn Page-Tables PXN auf User-Seiten nicht setzen oder User-Code-Seiten falsch mappen, könnte der Kernel dazu verleitet werden, User-Space-Code auszuführen.
- **Spekulation / Side-Channels**: analog zu spekulativen Bypässen kann es mikroarchitektonische Nebenwirkungen geben, die transient PAN / PXN-Prüfungen umgehen (auch wenn solche Angriffe stark CPU-Design-abhängig sind).
- **Komplexe Interaktionen**: bei fortgeschritteneren Features (z. B. JIT, shared memory, just-in-time code regions) benötigt der Kernel feinkörnige Kontrolle, um bestimmte Speicherzugriffe oder Ausführungen in user-gemappten Bereichen zu erlauben; diese sicher unter PAN/PXN-Beschränkungen zu gestalten ist nicht trivial.
#### Beispiel
<details>
<summary>Code-Beispiel</summary>
Hier sind illustrative Pseudo-Assembly-Sequenzen, die das Aktivieren/Deaktivieren von PAN um User-Speicherzugriffe zeigen und wie ein Fault auftreten kann.
</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
Wenn der Kernel das PXN auf dieser Benutzerseite **nicht** gesetzt hätte, könnte der Branch erfolgreich sein — was unsicher wäre.
Wenn der Kernel vergisst, PAN nach dem Zugriff auf Benutzerspeicher wieder zu aktivieren, öffnet das ein Zeitfenster, in dem weitere Kernel-Logik versehentlich beliebigen Benutzerspeicher lesen/schreiben könnte.
Wenn der Benutzerpointer auf eine execute-only-Seite zeigt (Benutzerseite mit nur Execute-Berechtigung, kein Lesen/Schreiben), könnte unter dem PAN-Spezifikations-Bug `ldr W2, [X1]` **nicht** fehlschlagen, selbst wenn PAN aktiviert ist, wodurch — je nach Implementierung — ein Umgehungs-Exploit möglich wird.
</details>
<details>
<summary>Example</summary>
Eine Kernel-Schwachstelle versucht, einen vom Benutzer bereitgestellten Funktionspointer zu nehmen und ihn im Kernel-Kontext aufzurufen (z. B. `call user_buffer`). Unter PAN/PXN ist diese Operation verboten oder führt zu einem Fault.
</details>
---
### 11. **Top Byte Ignore (TBI) / Pointer Tagging**
**Introduced in ARMv8.5 / newer (or optional extension)**
TBI bedeutet, dass das oberste Byte (höchstwertiges Byte) eines 64-Bit-Pointers bei der Adressübersetzung ignoriert wird. Das erlaubt dem OS oder der Hardware, **tag bits** im obersten Byte des Pointers zu speichern, ohne die tatsächliche Adresse zu verändern.
- TBI steht für **Top Byte Ignore** (manchmal auch *Address Tagging* genannt). Es ist ein Hardware-Feature (in vielen ARMv8+-Implementierungen verfügbar), das bei der **Adressübersetzung / Load/Store / Instruction Fetch** die oberen 8 Bits (Bits 63:56) eines 64-Bit-Pointers **ignoriert**.
- Effektiv behandelt die CPU einen Pointer `0xTTxxxx_xxxx_xxxx` (wobei `TT` = oberstes Byte) als `0x00xxxx_xxxx_xxxx` für Zwecke der Adressübersetzung, indem das oberste Byte maskiert wird. Das oberste Byte kann von Software genutzt werden, um **metadata / tag bits** zu speichern.
- Dadurch erhält Software einen „kostenlosen“ in-band Bereich, um ein Tag-Byte in jeden Pointer einzubetten, ohne die angesprochene Speicheradresse zu verändern.
- Die Architektur stellt sicher, dass Loads, Stores und Instruction Fetches den Pointer mit maskiertem obersten Byte (d. h. Tag entfernt) behandeln, bevor der eigentliche Speicherzugriff erfolgt.
Damit entkoppelt TBI den **logischen Pointer** (Pointer + Tag) von der **physischen Adresse**, die für Speicheroperationen verwendet wird.
#### Why TBI: Use cases and motivation
- **Pointer tagging / metadata**: Man kann zusätzliche Metadaten (z. B. Objekt-Typ, Version, Grenzen, Integritäts-Tags) in diesem obersten Byte speichern. Wenn der Pointer später verwendet wird, wird das Tag auf Hardware-Ebene ignoriert, sodass kein manuelles Entfernen vor dem Speicherzugriff nötig ist.
- **Memory tagging / MTE (Memory Tagging Extension)**: TBI ist der grundlegende Hardware-Mechanismus, auf dem MTE aufbaut. In ARMv8.5 verwendet die **Memory Tagging Extension** die Bits 59:56 des Pointers als **logisches Tag** und vergleicht es mit einem **allocation tag**, das im Speicher abgelegt ist.
- **Erhöhte Sicherheit & Integrität**: Durch Kombination von TBI mit Pointer Authentication (PAC) oder Laufzeitprüfungen kann man nicht nur den Pointer-Wert, sondern auch das Tag erzwingen. Ein Angreifer, der einen Pointer ohne korrektes Tag überschreibt, erzeugt einen nicht-matching Tag.
- **Kompatibilität**: Da TBI optional ist und Tag-Bits von der Hardware ignoriert werden, läuft bestehender nicht-getaggter Code normal weiter. Die Tag-Bits werden effektiv zu „Don’t care“-Bits für alten Code.
#### Example
<details>
<summary>Example</summary>
Ein Funktionspointer enthielt ein Tag im obersten Byte (z. B. `0xAA`). Ein Exploit überschreibt die niederwertigen Bits des Pointers, vergisst aber das Tag; wenn der Kernel den Pointer prüft oder sanitisiert, schlägt der Pointer aufgrund des falschen Tags fehl oder wird abgelehnt.
</details>
---
### 12. **Page Protection Layer (PPL)**
**Introduced in late iOS / modern hardware (iOS ~17 / Apple silicon / high-end models)** (einige Berichte zeigen PPL bereits bei macOS / Apple silicon, aber Apple bringt analoge Schutzmaßnahmen auch auf iOS)
- PPL ist als eine **intra-kernel Schutzgrenze** konzipiert: selbst wenn der Kernel (EL1) kompromittiert ist und Lese-/Schreibzugriff hat, **sollte er nicht frei bestimmte sensitive Pages modifizieren können** (insbesondere Page Tables, Code-Signing-Metadaten, Kernel-Code-Pages, Entitlements, Trust Caches usw.).
- Es schafft effektiv ein **„Kernel innerhalb des Kernels“** — eine kleinere vertrauenswürdige Komponente (PPL) mit **erhöhten Privilegien**, die allein geschützte Pages ändern darf. Anderer Kernel-Code muss PPL-Routinen aufrufen, um Änderungen vorzunehmen.
- Das reduziert die Angriffsfläche für Kernel-Exploits: selbst mit vollständigem arbitrary R/W/Execute im Kernelmodus muss Exploit-Code zusätzlich in die PPL-Domäne gelangen (oder PPL umgehen), um kritische Strukturen zu verändern.
- Auf neuerer Apple-Hardware (A15+ / M2+) geht Apple zu **SPTM (Secure Page Table Monitor)** über, das in vielen Fällen PPL für Page-Table-Schutz auf diesen Plattformen ersetzt.
So wird angenommen, dass PPL basierend auf öffentlicher Analyse arbeitet:
#### Use of APRR / permission routing (APRR = Access Permission ReRouting)
- Apple-Hardware verwendet einen Mechanismus namens **APRR (Access Permission ReRouting)**, der es Page-Table-Einträgen (PTEs) erlaubt, kleine Indizes anstelle von vollständigen Berechtigungsbits zu enthalten. Diese Indizes werden über APRR-Register auf effektive Berechtigungen abgebildet. Das erlaubt eine dynamische Remapping der Berechtigungen pro Domäne.
- PPL nutzt APRR, um Privilegien innerhalb des Kernel-Kontexts zu trennen: nur die PPL-Domäne darf die Abbildung zwischen Indizes und effektiven Berechtigungen aktualisieren. Das heißt, wenn Nicht-PPL-Kernel-Code einen PTE schreibt oder versucht, Berechtigungsbits zu ändern, verhindert die APRR-Logik dies (oder erzwingt ein Nur-Lese-Mapping).
- PPL-Code selbst läuft in einem eingeschränkten Bereich (z. B. `__PPLTEXT`), der normalerweise nicht-executable oder nicht-writable ist, bis Eintrittstore ihn temporär zulassen. Der Kernel ruft PPL-Einstiegspunkte („PPL routines“) auf, um sensitive Operationen auszuführen.
#### Gate / Entry & Exit
- Wenn der Kernel eine geschützte Page ändern muss (z. B. die Berechtigungen einer Kernel-Code-Page ändern oder Page-Tables modifizieren), ruft er eine **PPL-Wrapper**-Routine auf, die Validierung durchführt und dann in die PPL-Domäne wechselt. Außerhalb dieser Domäne sind die geschützten Pages effektiv schreibgeschützt oder nicht modifizierbar durch den Haupt-Kernel.
- Während des PPL-Eintritts werden die APRR-Abbildungen so angepasst, dass Speicherseiten im PPL-Bereich innerhalb der PPL-Domäne **ausführbar & schreibbar** sind. Beim Verlassen werden sie wieder auf read-only / non-writable zurückgesetzt. Das stellt sicher, dass nur geprüfte PPL-Routinen geschützte Pages schreiben können.
- Außerhalb von PPL führen Schreibversuche des Kernel-Codes an diesen geschützten Pages zu einem Fault (Permission denied), weil das APRR-Mapping für diese Code-Domäne kein Schreiben erlaubt.
#### Protected page categories
Zu den Pages, die PPL typischerweise schützt, gehören:
- Page-Table-Strukturen (Translation Table Entries, Mapping-Metadaten)
- Kernel-Code-Pages, insbesondere solche mit kritischer Logik
- Code-Sign-Metadaten (Trust Caches, Signatur-Blobs)
- Entitlement-Tabellen, Signature-Enforcement-Tabellen
- Andere hochwichtige Kernel-Strukturen, bei denen ein Patch das Umgehen von Signaturprüfungen oder Manipulation von Credentials ermöglichen würde
Die Idee ist, dass ein Angreifer zwar Kernel-Speicher kontrollieren kann, aber nicht einfach diese Pages patchen oder überschreiben kann, es sei denn, er kompromittiert auch PPL-Routinen oder umgeht PPL.
#### Known Bypasses & Vulnerabilities
1. **Project Zero’s PPL bypass (stale TLB trick)**
- Ein öffentlicher Writeup von Project Zero beschreibt einen Bypass, der **stale TLB entries** ausnutzt.
- Die Idee:
1. Allokiere zwei physische Pages A und B, markiere sie als PPL-Pages (also geschützt).
2. Mappe zwei virtuelle Adressen P und Q, deren L3-Translation-Table-Pages aus A bzw. B stammen.
3. Starte einen Thread, der kontinuierlich Q anläuft, sodass sein TLB-Eintrag am Leben bleibt.
4. Rufe `pmap_remove_options()` auf, um Mappings beginnend bei P zu entfernen; aufgrund eines Bugs entfernt der Code fälschlicherweise die TTEs für sowohl P als auch Q, invalidiert jedoch nur den TLB-Eintrag für P und lässt Q’s stale entry gültig.
5. Reuse B (Page für Q’s Table), um beliebigen Speicher zu mappen (z. B. PPL-geschützte Pages). Weil der stale TLB-Eintrag weiterhin Q’s alte Abbildung enthält, bleibt diese Abbildung für diesen Kontext gültig.
6. Dadurch kann der Angreifer writable mappings für PPL-geschützte Pages platzieren, ohne die PPL-Schnittstelle zu benutzen.
- Dieser Exploit erforderte feine Kontrolle über physische Mappings und TLB-Verhalten. Er zeigt, dass eine Sicherheitsgrenze, die auf TLB / Mapping-Korrektheit beruht, äußerst sorgfältig bei TLB-Invalidierungen und Mapping-Konsistenz sein muss.
- Project Zero kommentierte, dass solche Bypässe subtil und selten sind, aber in komplexen Systemen möglich. Trotzdem halten sie PPL für eine solide Abschwächung.
2. **Other potential hazards & constraints**
- Wenn ein Kernel-Exploit direkt PPL-Routinen betreten kann (durch Aufrufen der PPL-Wrapper), könnte er Beschränkungen umgehen. Daher ist die Argumentvalidierung kritisch.
- Bugs im PPL-Code selbst (z. B. arithmetischer Overflow, Boundary-Checks) können Out-of-Bounds-Änderungen innerhalb von PPL erlauben. Project Zero beobachtete, dass ein solcher Bug in `pmap_remove_options_internal()` in ihrem Bypass ausgenutzt wurde.
- Die PPL-Grenze ist unwiderruflich an Hardware-Enforcement (APRR, Memory Controller) gebunden, daher ist sie nur so stark wie die Hardware-Implementierung.
#### Example
<details>
<summary>Code Example</summary>
Hier ist eine vereinfachte Pseudocode-/Logikdarstellung, die zeigt, wie ein Kernel in PPL aufgerufen werden könnte, um geschützte Pages zu modifizieren:
</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
Ein kernel exploit versucht, die entitlement table zu überschreiben oder die code-sign enforcement zu deaktivieren, indem er ein kernel signature blob modifiziert. Da diese page PPL-protected ist, wird der Schreibzugriff blockiert, sofern nicht über die PPL-Schnittstelle gegangen wird. Selbst mit kernel code execution kann man also code-sign-Einschränkungen nicht umgehen oder credential data beliebig ändern. Auf iOS 17+ nutzen bestimmte Geräte SPTM, um PPL-managed pages weiter zu isolieren.PPL → SPTM / Replacements / Future
- Auf Apples modernen SoCs (A15 oder später, M2 oder später) unterstützt Apple SPTM (Secure Page Table Monitor), welches replaces PPL für 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.”
- Die SPTM-Architektur verlagert wahrscheinlich mehr Policy-Enforcement in einen höher privilegierten Monitor außerhalb der kernel-Kontrolle und reduziert dadurch die trust boundary weiter.
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 muss verhindern, dass Tag-Werte leaked werden (denn wenn ein Angreifer das Tag erfährt, könnte er Pointer mit korrekten Tags konstruieren).
- Sie implementieren Protections (microarchitectural / speculative controls), um side-channel leakage der Tag-Bits zu vermeiden.
- 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>
#### Einschränkungen & Herausforderungen
- **Intrablock overflows**: Wenn ein Overflow innerhalb derselben Allocation bleibt (die Grenze nicht überschreitet) und das Tag gleich bleibt, erkennt tag mismatch das nicht.
- **Tag width limitation**: Nur wenige Bits (z. B. 4 Bits, oder ein kleiner Bereich) stehen für Tags zur Verfügung — begrenzter Namensraum.
- **Side-channel leaks**: Wenn Tag-Bits via Cache / speculative execution leak können, kann ein Angreifer gültige Tags lernen und umgehen. Apples Tag Confidentiality Enforcement soll dem entgegenwirken.
- **Performance overhead**: Tag-Checks bei jedem Load/Store verursachen Kosten; Apple muss die Hardware optimieren, um den Overhead gering zu halten.
- **Compatibility & fallback**: Auf älterer Hardware oder in Bereichen ohne EMTE muss ein Fallback existieren. Apple behauptet, MIE werde nur auf Geräten mit Unterstützung aktiviert.
- **Complex allocator logic**: Der Allocator muss Tags verwalten, retaggen, Grenzen ausrichten und Tag-Kollisionen vermeiden. Bugs in der Allocator-Logik könnten Schwachstellen einführen.
- **Mixed memory / hybrid areas**: Ein Teil des Speichers kann ungetagged (legacy) bleiben, was die Interoperabilität erschwert.
- **Speculative / transient attacks**: Wie bei vielen mikroarchitektonischen Schutzmechanismen könnten speculative Ausführung oder micro-op-Fusionen Checks transient umgehen oder Tag-Bits leak.
- **Limited to supported regions**: Apple könnte EMTE nur in selektiven, risikoreichen Bereichen (Kernel, sicherheitskritische Subsysteme) durchsetzen, nicht flächendeckend.
---
## Key enhancements / differences compared to standard MTE
Hier sind die Verbesserungen und Änderungen, die Apple betont:
| Merkmal | Original MTE | EMTE (Apple’s enhanced) / MIE |
|---|---|---|
| **Check mode** | Unterstützt synchrone und asynchrone Modi. In async werden Tag-Mismatches später gemeldet (verzögert) | Apple besteht standardmäßig auf **synchronem Modus** — Tag-Mismatches werden sofort erkannt, keine Verzögerungs-/Race-Fenster erlaubt. |
| **Coverage of non-tagged memory** | Zugriffe auf nicht-getaggten Speicher (z. B. globals) können in einigen Implementierungen Checks umgehen | EMTE verlangt, dass Zugriffe von einem getaggten Bereich auf nicht-getaggten Speicher ebenfalls Tag-Kenntnis validieren, wodurch das Umgehen durch Misch-Allokationen erschwert wird. |
| **Tag confidentiality / secrecy** | Tags könnten beobachtbar sein oder via Side-Channels geleakt werden | Apple ergänzt **Tag Confidentiality Enforcement**, das versucht, das Leaken von Tag-Werten (z. B. via speculative Side-Channels) zu verhindern. |
| **Allocator integration & retagging** | MTE überlässt viel der Allocator-Logik der Software | Apples secure typed allocators (kalloc_type, xzone malloc, etc.) integrieren sich mit EMTE: Beim Allokieren oder Freigeben werden Tags feingranular verwaltet. |
| **Always-on by default** | Auf vielen Plattformen ist MTE optional oder standardmäßig deaktiviert | Apple aktiviert EMTE / MIE standardmäßig auf unterstützter Hardware (z. B. iPhone 17 / A19) für Kernel und viele User-Prozesse. |
Weil Apple sowohl Hardware als auch Softwarestack kontrolliert, kann es EMTE strikt durchsetzen, Performance-Probleme vermeiden und Side-Channel-Lücken schließen.
---
## Wie EMTE in der Praxis funktioniert (Apple / MIE)
Hier eine High-Level-Beschreibung, wie EMTE unter Apples MIE-Setup arbeitet:
1. **Tag assignment**
- Wenn Speicher alloziert wird (z. B. im Kernel oder im Userspace via secure allocators), wird diesem Block ein **secret tag** zugewiesen.
- Der zurückgegebene Pointer an User oder Kernel enthält dieses Tag in seinen oberen Bits (mittels TBI / top byte ignore mechanisms).
2. **Tag checking on access**
- Wann immer ein Load oder Store mit einem Pointer ausgeführt wird, prüft die Hardware, ob das Pointer-Tag mit dem Tag des Speicherblocks (allocation tag) übereinstimmt. Bei Mismatch fällt es sofort aus (da synchron).
- Da es synchron ist, gibt es kein Fenster für „verzögerte Erkennung“.
3. **Retagging on free / reuse**
- Wenn Speicher freigegeben wird, ändert der Allocator das Tag des Blocks (ältere Pointer mit alten Tags stimmen dann nicht mehr überein).
- Ein use-after-free-Pointer hätte daher ein veraltetes Tag und würde bei Zugriffen mismatchen.
4. **Neighbor-tag differentiation to catch overflows**
- Benachbarte Allokationen erhalten unterschiedliche Tags. Wenn ein Buffer-Overflow in den Speicher des Nachbarn schreibt, führt das zu einem Tag-Mismatch und einem Fault.
- Das ist besonders effektiv, um kleine Overflows zu erkennen, die Grenzen überschreiten.
5. **Tag confidentiality enforcement**
- Apple muss verhindern, dass Tag-Werte geleakt werden (denn wenn ein Angreifer das Tag kennt, könnte er Pointer mit korrektem Tag konstruieren).
- Sie beinhalten Schutzmaßnahmen (mikroarchitektonisch / speculative controls), um das Leaken von Tag-Bits zu vermeiden.
6. **Kernel and user-space integration**
- Apple nutzt EMTE nicht nur im Userspace, sondern auch im Kernel / in OS-kritischen Komponenten (um den Kernel gegen Memory Corruption zu schützen).
- Hardware/OS stellen sicher, dass Tag-Regeln auch gelten, wenn der Kernel im Auftrag von User-Space ausgeführt wird.
Weil EMTE in MIE integriert ist, verwendet Apple EMTE im synchronen Modus über wichtige Angriffsflächen hinweg, nicht nur als optionalen oder Debug-Modus.
---
## Exception handling in XNU
Wenn eine **Exception** auftritt (z. B. `EXC_BAD_ACCESS`, `EXC_BAD_INSTRUCTION`, `EXC_CRASH`, `EXC_ARM_PAC`, etc.), ist die **Mach layer** des XNU-Kernels verantwortlich dafür, sie abzufangen, bevor sie zu einem UNIX-artigen **Signal** (wie `SIGSEGV`, `SIGBUS`, `SIGILL`, ...) wird.
Dieser Prozess umfasst mehrere Ebenen der Exception-Propagation und -Behandlung, bevor er den Userspace erreicht oder in ein BSD-Signal konvertiert wird.
### Exception Flow (High-Level)
1. **CPU triggers a synchronous exception** (z. B. invalid pointer dereference, PAC failure, illegal instruction, etc.).
2. **Low-level trap handler** läuft (`trap.c`, `exception.c` im XNU-Quellcode).
3. Der Trap-Handler ruft **`exception_triage()`** auf, den Kern der Mach-Exception-Behandlung.
4. `exception_triage()` entscheidet, wie die Exception geroutet wird:
- Zuerst an den **thread's exception port**.
- Dann an den **task's exception port**.
- Dann an den **host's exception port** (oft `launchd` oder `ReportCrash`).
Wenn keine dieser Ports die Exception behandelt, kann der Kernel:
- **Sie in ein BSD-Signal umwandeln** (für User-Space-Prozesse).
- **Panic** auslösen (bei Kernel-Space-Exceptions).
### Core Function: `exception_triage()`
Die Funktion `exception_triage()` routet Mach-Exceptions die Kette möglicher Handler hinauf, bis einer sie behandelt oder sie schließlich fatal wird. Sie ist definiert in `osfmk/kern/exception.c`.
```c
void exception_triage(exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt);
Typischer Aufrufablauf:
exception_triage() └── exception_deliver() ├── exception_deliver_thread() ├── exception_deliver_task() └── exception_deliver_host()
Wenn alle fehlschlagen → wird von bsd_exception() behandelt → in ein Signal wie SIGSEGV übersetzt.
Exception-Ports
Jedes Mach-Objekt (thread, task, host) kann exception ports registrieren, an die Exception-Nachrichten gesendet werden.
Sie werden durch die API definiert:
task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()
Jeder exception port hat:
- Eine mask (welche Exceptions er empfangen möchte)
- Einen port name (Mach-Port, der Nachrichten empfängt)
- Ein behavior (wie der Kernel die Nachricht sendet)
- Ein flavor (welcher thread state enthalten ist)
Debugger und Ausnahmebehandlung
Ein debugger (z. B. LLDB) setzt einen exception port auf die Ziel-task oder den Ziel-thread, üblicherweise mittels task_set_exception_ports().
Wenn eine Exception auftritt:
- Die Mach-Nachricht wird an den Debugger-Prozess gesendet.
- Der Debugger kann entscheiden, die Exception zu behandeln (resume, Register ändern, Instruction überspringen) oder nicht zu behandeln.
- Wenn der Debugger sie nicht behandelt, propagiert die Exception zur nächsten Ebene (task → host).
Ablauf von EXC_BAD_ACCESS
-
Thread dereferenziert einen ungültigen Pointer → CPU löst einen Data Abort aus.
-
Der Kernel-Trap-Handler ruft
exception_triage(EXC_BAD_ACCESS, ...)auf. -
Nachricht gesendet an:
-
Thread port → (Debugger kann Breakpoint abfangen).
-
Wenn Debugger ignoriert → Task port → (prozessweiter Handler).
-
Wenn ignoriert → Host port (normalerweise ReportCrash).
- Wenn niemand sie behandelt →
bsd_exception()übersetzt das inSIGSEGV.
PAC-Exceptions
Wenn Pointer Authentication (PAC) fehlschlägt (Signatur stimmt nicht überein), wird eine spezielle Mach-Exception ausgelöst:
EXC_ARM_PAC(Typ)- Codes können Details enthalten (z. B. Key-Typ, Pointer-Typ).
Wenn das Binary das Flag TFRO_PAC_EXC_FATAL gesetzt hat, behandelt der Kernel PAC-Fehler als fatal und umgeht die Debugger-Interception. Das verhindert, dass Angreifer Debugger nutzen, um PAC-Prüfungen zu umgehen, und ist für Plattform-Binaries aktiviert.
Software-Breakpoints
Ein Software-Breakpoint (int3 auf x86, brk auf ARM64) wird durch absichtliches Herbeiführen eines Faults implementiert.
Der Debugger fängt das über den exception port ab:
- Ändert Instruction Pointer oder Speicher.
- Stellt die originale Instruction wieder her.
- Setzt die Ausführung fort.
Dieser Mechanismus erlaubt es auch, eine PAC-Exception „abzufangen“ — es sei denn TFRO_PAC_EXC_FATAL ist gesetzt, in diesem Fall erreicht sie niemals den Debugger.
Konvertierung zu BSD-Signalen
Wenn kein Handler die Exception akzeptiert:
-
Kernel ruft
task_exception_notify() → bsd_exception()auf. -
Das mappt Mach-Exceptions auf Signale:
| 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 (bei nicht-fatalen Fällen) |
Wichtige Dateien im XNU-Source
-
osfmk/kern/exception.c→ Kern vonexception_triage(),exception_deliver_*(). -
bsd/kern/kern_sig.c→ Logik zur Signalzustellung. -
osfmk/arm64/trap.c→ Low-level Trap-Handler. -
osfmk/mach/exc.h→ Exception-Codes und Strukturen. -
osfmk/kern/task.c→ Einrichtung von Task-Exception-Ports.
Alter Kernel-Heap (Pre-iOS 15 / Pre-A12-Ära)
Der Kernel nutzte einen zone allocator (kalloc), aufgeteilt in festgroße “Zones”.
Jede Zone speichert nur Allokationen einer einzigen Größenklasse.
Aus dem Screenshot:
| Zone Name | Element Size | Example Use |
|---|---|---|
default.kalloc.16 | 16 bytes | Sehr kleine Kernel-Strukturen, Pointer. |
default.kalloc.32 | 32 bytes | Kleine Strukturen, Objekt-Header. |
default.kalloc.64 | 64 bytes | IPC-Nachrichten, winzige Kernel-Buffer. |
default.kalloc.128 | 128 bytes | Mittlere Objekte wie Teile von OSObject. |
| … | … | … |
default.kalloc.1280 | 1280 bytes | Große Strukturen, IOSurface-/Grafik-Metadaten. |
Funktionsweise:
- Jede Allokationsanfrage wird auf die nächsthöhere Zone-Größe aufgerundet.
(z. B. landet eine 50-Byte-Anfrage in der
kalloc.64-Zone). - Speicher in jeder Zone wurde in einer freelist gehalten — frei gegebene Chunks gingen zurück in diese Zone.
- Wenn du einen 64-Byte-Buffer übergelaufen hast, überschriebst du das nächste Objekt in derselben Zone.
Deshalb waren heap spraying / feng shui so effektiv: man konnte Nachbarn vorhersagen, indem man Allokationen derselben Größenklasse „gesprayt“ hat.
Die freelist
Innerhalb jeder kalloc-Zone wurden freigegebene Objekte nicht sofort an das System zurückgegeben — sie landeten in einer freelist, einer verketteten Liste verfügbarer Chunks.
-
Wenn ein Chunk freigegeben wurde, schrieb der Kernel einen Pointer an den Anfang dieses Chunks → die Adresse des nächsten freien Chunks in derselben Zone.
-
Die Zone hielt einen HEAD-Pointer auf den ersten freien Chunk.
-
Die Allokation nutzte immer das aktuelle HEAD:
-
Pop HEAD (gibt diesen Speicher an den Aufrufer zurück).
-
Update HEAD = HEAD->next (gespeichert im Header des freigegebenen Chunks).
-
Beim Freigeben wurden Chunks wieder reingeschoben:
-
freed_chunk->next = HEAD -
HEAD = freed_chunk
Die freelist war also einfach eine verkettete Liste, gebaut innerhalb des freigegebenen Speichers.
Normalzustand:
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)
Ausnutzen der freelist
Da die ersten 8 Bytes eines free chunk dem freelist pointer entsprechen, könnte ein Angreifer diesen korrumpieren:
- Heap overflow in einen benachbarten freed chunk → überschreibe dessen „next“ pointer.
- Use-after-free: Schreibe in ein freed object → überschreibe dessen „next“ pointer.
Dann, bei der nächsten Allocation dieser Größe:
- Der allocator poppt den korrumpierten chunk.
- Folgt dem vom Angreifer gelieferten „next“ pointer.
- Gibt einen pointer auf beliebigen Speicher zurück, wodurch fake object primitives oder targeted overwrite möglich werden.
Visuelles Beispiel für 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-Module
-
Watcher Anti-Analyse: Eine dedizierte watcher-Binary profiliert kontinuierlich das Gerät und bricht die kill-chain ab, wenn eine Research-/Analyseumgebung erkannt wird. Sie prüft
security.mac.amfi.developer_mode_status, das Vorhandensein einerdiagnosticd-Konsole, LocalesUSoderIL, Jailbreak-Spuren wie Cydia, Prozesse wiebash,tcpdump,frida,sshdodercheckrain, mobile AV-Apps (McAfee, AvastMobileSecurity, NortonMobileSecurity), benutzerdefinierte HTTP-Proxy-Einstellungen und benutzerdefinierte Root-CAs. Fällt eine dieser Prüfungen durch, wird die weitere Auslieferung von Payloads blockiert. -
Helper Surveillance Hooks: Die Helper-Komponente kommuniziert mit anderen Stages über
/tmp/helper.sockund lädt dann Hook-Sets namens DMHooker und UMHooker. Diese Hooks greifen VOIP-Audiopfade ab (Aufnahmen landen unter/private/var/tmp/l/voip_%lu_%u_PART.m4a), implementieren einen systemweiten Keylogger, nehmen Fotos ohne UI auf und hooken SpringBoard, um Benachrichtigungen zu unterdrücken, die diese Aktionen normalerweise auslösen würden. Der Helper fungiert somit als unauffällige Validierungs- und Leicht-Überwachungs-Schicht, bevor schwerere Implants wie Predator abgesetzt werden. -
HiddenDot-Indikatorunterdrückung in SpringBoard: Mit kernel-level Code-Injection hookt Predator
SBSensorActivityDataProvider._handleNewDomainData:(den Aggregationspunkt für Sensoraktivität). Der Hook nullt den Objective-Cself-Pointer (x0), sodass der Aufruf zu[nil _handleNewDomainData:newData]wird, Kamera-/Mikrofon-Updates verworfen und sowohl grüne als auch orange Punkte unterdrückt werden. -
Mach-exception-basierter Hooking-Ablauf (DMHooker): Hooks werden via
EXC_BREAKPOINT+ Exception-Ports implementiert, anschließend verändertthread_set_stateRegister und die Ausführung wird fortgesetzt. Rückgabecode2bedeutet „continue with modified thread state.“ -
PAC-bewusste Umleitung für Kamera-Zugriffsprüfungen: In
mediaserverdlokalisiert ein Pattern-Scan (z. B.memmem) eine private Routine in der Nähe vonFigVideoCaptureSourceCreateWithSourceInfoinnerhalb vonCMCapture.framework. Der Hook gibt3zurück, um mittels einer vor-signierten, PAC-gecacheten Rücksprungadresse umzuleiten, sodass PAC erfüllt wird und die Prüfung umgangen wird. -
VoIP-Erfassungs-Pipeline in
mediaserverd: HooktAudioConverterNewundAudioConverterConvertComplexBuffer+52, um Buffers abzugreifen, die Samplingrate aus Puffergrößen abzuleiten, float32 PCM → int16 mit NEON umzuwandeln, 4-Kanal auf Stereo downzumischen und viaExtAudioFileWrite()zu persistieren. Das VoIP-Modul selbst unterdrückt die Indikatoren nicht, daher müssen Operatoren HiddenDot separat aktivieren.
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
Referenzen
- https://www.jamf.com/blog/predator-spyware-ios-recording-indicator-bypass-analysis/
- Google Threat Intelligence – Intellexa zero-day exploits continue
Tip
Lernen & üben Sie AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.


