Eksploatacja iOS
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
iOS Exploit Mitigations
1. Code Signing / Walidacja podpisu w czasie działania
Wprowadzone wcześnie (iPhone OS → iOS) To jedna z podstawowych ochron: wszystkiemu wykonywalnemu kodowi (aplikacje, dynamiczne biblioteki, JIT-ed code, rozszerzenia, frameworks, caches) musi towarzyszyć podpis kryptograficzny wystawiony przez łańcuch certyfikatów zaufany przez Apple. W czasie działania, przed załadowaniem binarki do pamięci (lub przed wykonaniem skoków przez niektóre granice), system sprawdza podpis. Jeśli kod został zmodyfikowany (bit-flip, patch) lub nie jest podpisany, załadunek kończy się niepowodzeniem.
- Przeciwdziała: etapowi „classic payload drop + execute” w łańcuchach exploitów; dowolnemu wstrzykiwaniu kodu; modyfikowaniu istniejącej binarki w celu wstawienia złośliwej logiki.
- Szczegóły mechanizmu:
- Mach-O loader (i dynamic linker) sprawdzają strony kodu, segmenty, entitlements, team IDs oraz że podpis obejmuje zawartość pliku.
- Dla regionów pamięci jak JIT caches lub dynamicznie generowany kod, Apple wymusza, aby strony były podpisane lub weryfikowane przez specjalne API (np.
mprotectz kontrolami code-sign). - Podpis zawiera entitlements i identyfikatory; OS egzekwuje, że niektóre API lub uprawnienia wymagają konkretnych entitlements, których nie da się sfałszować.
Przykład
Załóżmy, że exploit uzyska wykonanie kodu w procesie i próbuje zapisać shellcode na heapie i skoczyć do niego. Na iOS ta strona musiałaby być oznaczona jako executable **i** spełniać ograniczenia code-sign. Ponieważ shellcode nie jest podpisany certyfikatem Apple, skok się nie powiedzie lub system odrzuci ustawienie tego regionu pamięci jako wykonywalnego.2. CoreTrust
Wprowadzone mniej więcej w erze iOS 14+ (lub stopniowo na nowszych urządzeniach / późniejszych wersjach iOS) CoreTrust to subsystem, który wykonuje runtime signature validation binarek (w tym systemowych i użytkownika) względem Apple’s root certificate, zamiast polegać na lokalnych cache’ach zaufania w userlandzie.
- Przeciwdziała: modyfikacjom binarek po instalacji, technikom jailbreaking próbującym podmienić lub zpatchować systemowe biblioteki lub aplikacje użytkownika; oszukiwaniu systemu przez zastąpienie zaufanych binarek złośliwymi odpowiednikami.
- Szczegóły mechanizmu:
- Zamiast ufać lokalnej bazie zaufania czy cache’owi certyfikatów, CoreTrust odwołuje się do rootu Apple lub weryfikuje certyfikaty pośrednie w bezpiecznym łańcuchu.
- Zapewnia wykrycie i odrzucenie modyfikacji (np. w filesystemie) istniejących binarek.
- Wiąże entitlements, team IDs, flagi code signing i inne metadane z binarką w czasie ładowania.
Przykład
Jailbreak mógłby próbować podmienić `SpringBoard` albo `libsystem` na zmodyfikowaną wersję, by uzyskać perystalencję. Ale gdy loader lub CoreTrust systemu sprawdzą plik, zauważą mismatch podpisu (lub zmienione entitlements) i odmówią wykonania.3. Data Execution Prevention (DEP / NX / W^X)
Wprowadzone w wielu OSach wcześniej; iOS miał NX-bit / w^x od dawna DEP wymusza, że strony oznaczone jako writable (dla danych) są non-executable, a strony oznaczone jako executable są non-writable. Nie można po prostu zapisać shellcode’a na heap/stack i go wykonać.
- Przeciwdziała: bezpośredniemu wykonaniu shellcode; klasycznemu buffer-overflow → skok do wstrzykniętego shellcode.
- Szczegóły mechanizmu:
- MMU / flagi ochrony pamięci (przez page tables) egzekwują separację.
- Każda próba oznaczenia writable strony jako executable wywołuje kontrolę systemową (i jest zabroniona lub wymaga zatwierdzenia przez code-sign).
- W wielu przypadkach nadanie stronie executable wymaga użycia API systemowego, które narzuca dodatkowe ograniczenia lub sprawdzenia.
Przykład
Overflow zapisuje shellcode na heapie. Atakujący próbuje `mprotect(heap_addr, size, PROT_EXEC)` by uczynić ją wykonywalną. System jednak odmawia lub weryfikuje, że nowa strona musi przejść ograniczenia code-sign (których shellcode nie spełnia).4. Address Space Layout Randomization (ASLR)
Wprowadzone w era iOS ~4–5 (około iOS 4–5) ASLR losowo przesuwa bazowe adresy kluczowych regionów pamięci: biblioteki, heap, stack itp. Adresy gadgetów zmieniają się pomiędzy uruchomieniami.
- Przeciwdziała: hardcodowaniu adresów gadgetów dla ROP/JOP; statycznym łańcuchom exploitów; ślepym skokom do znanych offsetów.
- Szczegóły mechanizmu:
- Każda załadowana biblioteka / moduł dynamiczny jest rebased na losowym offsetcie.
- Bazy stacka i heapu są losowane (w pewnych granicach entropii).
- Czasami inne regiony (np. mmap allocations) również są randomizowane.
- W połączeniu z information-leak mitigations, zmusza to atakującego do najpierw wycieku adresu lub wskaźnika, by poznać base addresses w czasie działania.
Przykład
Łańcuch ROP spodziewa się gadgetu w `0x….lib + offset`. Ale ponieważ `lib` jest relokowany inaczej przy każdym uruchomieniu, hardcodowany łańcuch zawodzi. Exploit musi najpierw wypuścić leak base modułu przed obliczeniem adresów gadgetów.5. Kernel Address Space Layout Randomization (KASLR)
Wprowadzone w iOS ~ (czas iOS 5 / iOS 6) Analogicznie do user ASLR, KASLR losuje bazę tekstu kernela i inne struktury kernela podczas bootu.
- Przeciwdziała: exploitom na poziomie jądra polegającym na stałych lokalizacjach kodu/danych kernela; statycznym exploitom kernela.
- Szczegóły mechanizmu:
- Przy każdym bootcie baza kernela jest losowana (w pewnym zakresie).
- Struktury danych kernela (jak task_structs, vm_map itd.) mogą być też relokowane lub offsetowane.
- Atakujący muszą najpierw wyciec kernel pointers lub użyć information disclosure bugs, by obliczyć offsety przed przejęciem struktur kernela lub kodu.
Przykład
Lokalna luka próbuje uszkodzić wskaźnik funkcji kernela (np. w vtable) na `KERN_BASE + offset`. Ale ponieważ `KERN_BASE` jest nieznany, atakujący musi najpierw wypuścić leak (np. przez read primitive), zanim obliczy poprawny adres do korupcji.6. Kernel Patch Protection (KPP / AMCC)
Wprowadzone w nowszych iOS / A-series hardware (po około iOS 15–16 lub na nowszych chipach) KPP (aka AMCC) ciągle monitoruje integralność stron z tekstem kernela (przez hash lub checksum). Jeśli wykryje modyfikację (patche, inline hooks, zmiany kodu) poza dozwolonymi oknami, wywołuje kernel panic lub reboot.
- Przeciwdziała: trwałym patchom kernela (modyfikacjom instrukcji), inline hookom, nadpisaniu funkcji statycznie.
- Szczegóły mechanizmu:
- Moduł hardware/firmware monitoruje region tekstu kernela.
- Okresowo lub na żądanie przelicza hash stron i porównuje z oczekiwanymi wartościami.
- Jeśli wykryje niezgodności poza benign update windows, powoduje panic urządzenia (by zapobiec trwałym, złośliwym patchom).
- Atakujący muszą albo unikać okien detekcji, albo użyć legalnych ścieżek patchowania.
Przykład
Exploit próbuje przełatać prolog funkcji kernela (np. `memcmp`) by przechwycić wywołania. KPP zauważa, że hash strony kodu nie zgadza się z oczekiwaną wartością i wywołuje kernel panic, rozbijając urządzenie zanim patch się utrwali.7. Kernel Text Read‐Only Region (KTRR)
Wprowadzone w nowoczesnych SoC (po ~A12 / nowsze hardware) KTRR to mechanizm wymuszany przez hardware: raz zablokowany wczesnym etapem bootu tekst kernela staje się read-only z poziomu EL1 (kernela), uniemożliwiając dalsze zapisy do stron kodu.
- Przeciwdziała: wszelkim modyfikacjom kodu kernela po boot (np. patchowanie, in-place code injection) z poziomu EL1.
- Szczegóły mechanizmu:
- Podczas bootu (w secure/bootloader stage) memory controller (lub bezpieczny moduł hardware) oznacza fizyczne strony zawierające kernel text jako read-only.
- Nawet jeśli exploit uzyska pełne uprawnienia kernela, nie może zapisać do tych stron, by patchować instrukcje.
- By je zmodyfikować, atakujący musiałby najpierw skompromitować chain bootu, lub podważyć sam KTRR.
Przykład
Exploit eskalujący uprawnienia skacze do EL1 i próbuje zapisać trampolinę w funkcji kernela (np. w syscall handler). Ale ponieważ strony są zablokowane jako read-only przez KTRR, zapis się nie powiedzie (lub spowoduje fault), więc patche nie zostaną zastosowane.8. Pointer Authentication Codes (PAC)
Wprowadzone z ARMv8.3 (hardware), Apple zaczęło używać od A12 / iOS ~12+
- PAC to funkcja hardware wprowadzona w ARMv8.3-A, która wykrywa manipulację wartościami wskaźników (adresy powrotu, wskaźniki funkcji, niektóre wskaźniki danych) poprzez osadzenie małego podpisu kryptograficznego („MAC”) w nieużywanych wysokich bitach wskaźnika.
- Podpis („PAC”) jest obliczany na podstawie wartości wskaźnika plus modifier (wartość kontekstowa, np. stack pointer lub inne dane rozróżniające). Dzięki temu ta sama wartość wskaźnika w różnych kontekstach ma inny PAC.
- W czasie użycia, przed dereferencją lub skokiem przez ten wskaźnik, instrukcja authenticate sprawdza PAC. Jeśli ważny, PAC jest usuwany i otrzymuje się czysty wskaźnik; jeśli nie, wskaźnik zostaje „poisoned” (lub podniesiony jest fault).
- Klucze używane do tworzenia/walidacji PAC przechowywane są w uprzywilejowanych rejestrach (EL1, kernel) i nie są dostępne z user mode.
- Ponieważ nie wszystkie 64 bity wskaźnika są wykorzystywane w wielu systemach (np. 48-bit address space), górne bity są „wolne” i mogą pomieścić PAC bez zmiany efektywnego adresu.
Architektoniczne podstawy i typy kluczy
-
ARMv8.3 wprowadza pięć 128-bitowych kluczy (każdy zrealizowany przez dwa 64-bitowe rejestry systemowe) do pointer authentication.
-
APIAKey — dla instruction pointers (domena „I”, klucz A)
-
APIBKey — drugi klucz dla instruction pointers (domena „I”, klucz B)
-
APDAKey — dla data pointers (domena „D”, klucz A)
-
APDBKey — dla data pointers (domena „D”, klucz B)
-
APGAKey — „generic” key, do podpisywania nie-wskaźnikowych danych lub innych zastosowań
-
Te klucze są przechowywane w uprzywilejowanych rejestrach systemowych (dostępnych tylko na EL1/EL2 itp.), niedostępnych z user mode.
-
PAC jest obliczany przez funkcję kryptograficzną (ARM sugeruje QARMA jako algorytm) używając:
- wartości wskaźnika (część kanoniczna)
- modifier (wartość kontekstowa, jak salt)
- sekretnego klucza
- pewnej wewnętrznej logiki tweak Jeśli wynikowy PAC zgadza się z tym, co jest przechowane w górnych bitach wskaźnika, uwierzytelnianie kończy się sukcesem.
Rodziny instrukcji
Konwencja nazewnictwa: PAC / AUT / XPAC, potem litery domen.
PACxxinstrukcje podpisują wskaźnik i wstawiają PACAUTxxinstrukcje uwierzytelniają + usuwają (walidują i zdejmują PAC)XPACxxinstrukcje usuwają bez walidacji
Domeny / sufiksy:
| Mnemonic | Znaczenie / Domena | Klucz / Domena | Przykładowe użycie w assemblerze |
|---|---|---|---|
| PACIA | Podpisz instruction pointer z APIAKey | „I, A” | PACIA X0, X1 — sign pointer w X0 używając APIAKey z modifier X1 |
| PACIB | Podpisz instruction pointer z APIBKey | „I, B” | PACIB X2, X3 |
| PACDA | Podpisz data pointer z APDAKey | „D, A” | PACDA X4, X5 |
| PACDB | Podpisz data pointer z APDBKey | „D, B” | PACDB X6, X7 |
| PACG / PACGA | Generic (nie-wskaźnikowe) podpisanie z APGAKey | „G” | PACGA X8, X9, X10 (podpisz X9 z modifier X10 do X8) |
| AUTIA | Uwierzytelnij instruction pointer podpisany przez APIA & usuń PAC | „I, A” | AUTIA X0, X1 — sprawdź PAC w X0 używając modifier X1, potem usuń |
| AUTIB | Uwierzytelnij domenę APIB | „I, B” | AUTIB X2, X3 |
| AUTDA | Uwierzytelnij data pointer podpisany APDA | „D, A” | AUTDA X4, X5 |
| AUTDB | Uwierzytelnij data pointer podpisany APDB | „D, B” | AUTDB X6, X7 |
| AUTGA | Uwierzytelnij generic / blob (APGA) | „G” | AUTGA X8, X9, X10 (waliduj generic) |
| XPACI | Usuń PAC (instruction pointer, bez walidacji) | „I” | XPACI X0 — usuń PAC z X0 (domena instrukcji) |
| XPACD | Usuń PAC (data pointer, bez walidacji) | „D” | XPACD X4 — usuń PAC z data pointer w X4 |
Istnieją wyspecjalizowane / aliasowe formy:
PACIASPto skrót dlaPACIA X30, SP(podpisz link register używając SP jako modifier)AUTIASPtoAUTIA X30, SP(uwierzytelnij link register względem SP)- Formy łączone jak
RETAA,RETAB(uwierzytelnij-i-zwróć) lubBLRAA(uwierzytelnij & branch) istnieją w rozszerzeniach ARM / wsparciu kompilatora. - Są też warianty z zerowym modifierem:
PACIZA/PACIZBgdzie modifier jest implicite zero, itd.
Modifiers
Głównym celem modifiera jest wiązanie PAC z konkretnym kontekstem, tak aby ta sama adresowana wartość podpisana w różnych kontekstach dawała różne PAC. To zapobiega prostemu ponownemu użyciu wskaźnika między ramkami/obiektami. Jest to jak dodanie soli do hasha.
W związku z tym:
- modifier to wartość kontekstowa (inny rejestr) mieszana w obliczeniach PAC. Typowe wybory: stack pointer (
SP), frame pointer, albo jakiś identyfikator obiektu. - Użycie SP jako modifiera jest powszechne dla podpisywania adresów powrotu: PAC jest powiązany z konkretną ramką stosu. Jeśli spróbujesz ponownie użyć LR w innej ramce, modifier się zmieni i walidacja PAC zawiedzie.
- Ta sama wartość wskaźnika podpisana przy różnych modifierach da różne PAC.
- Modifier nie musi być tajny, ale najlepiej, żeby nie był kontrolowany przez atakującego.
- Dla instrukcji, które podpisują lub weryfikują wskaźniki, gdy nie ma sensownego modifiera, niektóre formy używają zera lub implicite stałej.
Apple / iOS / XNU customizacje i obserwacje
- Implementacja PAC w Apple zawiera per-boot diversifiers, tak że klucze lub tweak’y zmieniają się przy każdym boocie, uniemożliwiając reuse między bootami.
- Dodano również cross-domain mitigations, więc PACy podpisane w user mode nie są łatwo reuse’owalne w kernel mode, itd.
- Na Apple M1 / Apple Silicon, inżynieria wsteczna wykazała, że jest dziewięć typów modifierów i Apple-specyficzne rejestry systemowe do kontroli kluczy.
- Apple używa PAC w wielu subsystemach kernela: podpisywanie adresów powrotu, integralność wskaźników w danych kernela, podpisane konteksty wątków, itd.
- Google Project Zero pokazał, że przy potężnym primitive do read/write w kernelu można było sfałszować kernel PACy (dla A keys) na urządzeniach z A12, ale Apple załatało wiele takich ścieżek.
- W systemie Apple niektóre klucze są globalne dla kernela, podczas gdy procesy użytkownika mogą otrzymywać per-process randomness kluczy.
PAC Bypasses
- Kernel-mode PAC: teoretyczne vs realne bypasses
- Ponieważ kernel PAC keys i logika są ściśle kontrolowane (uprzywilejowane rejestry, diversifiery, izolacja domen), fałszowanie dowolnych podpisanych wskaźników kernela jest bardzo trudne.
- Azad w 2020 „iOS Kernel PAC, One Year Later” raportował, że w iOS 12-13 znalazł kilka częściowych bypassów (signing gadgets, reuse signed states, niechronione indirect branches) ale nie pełnego, uniwersalnego bypassu. bazad.github.io
- Apple’owskie „Dark Magic” customizacje jeszcze bardziej ograniczają powierzchnie do exploitów (przełączanie domen, per-key enabling bits). i.blackhat.com
- Istnieje znany kernel PAC bypass CVE-2023-32424 na Apple silicon (M1/M2) zgłoszony przez Zecao Cai et al. i.blackhat.com
- Jednak te bypasses często polegają na bardzo specyficznych gadgetach lub błędach implementacyjnych; nie są ogólnymi metodami obejścia.
Stąd kernel PAC jest uważany za wysoce odporny, choć nie idealny.
- User-mode / runtime PAC bypass techniques
Są one bardziej powszechne i wykorzystują niedoskonałości w tym, jak PAC jest stosowany lub używany w dynamicznym linkingu / runtime frameworks. Poniżej klasy z przykładami.
2.1 Shared Cache / A key issues
-
dyld shared cache to duży pre-linked blob systemowych frameworks i bibliotek. Ponieważ jest szeroko współdzielony, wskaźniki funkcji wewnątrz shared cache są „pre-signed” i potem używane przez wiele procesów. Atakujący celują w te już-podpisane wskaźniki jako „PAC oracles”.
-
Niektóre techniki obejścia próbują wydobyć lub ponownie użyć A-key podpisanych wskaźników obecnych w shared cache i reuse’ować je w gadgetach.
-
Talk „No Clicks Required” opisuje budowanie oracla nad shared cache w celu wnioskowania względnych adresów i łączenia tego z podpisanymi wskaźnikami by obejść PAC. saelo.github.io
-
Również importowane wskaźniki funkcji z shared libraries w userspace okazywały się czasem niewystarczająco chronione przez PAC, pozwalając atakującemu uzyskać wskaźniki bez zmiany ich podpisu. (project zero bug entry) bugs.chromium.org
2.2 dlsym(3) / dynamic symbol resolution
-
Jeden znany bypass polega na wywołaniu
dlsym()w celu uzyskania już podpisanego wskaźnika funkcji (signed with A-key, diversifier zero) i jego użycia. Ponieważdlsymzwraca legalnie podpisany wskaźnik, użycie go omija konieczność fałszowania PAC. -
Blog Epsilon opisuje, jak niektóre bypasses wykorzystują to: wywołanie
dlsym("someSym")zwraca podpisany wskaźnik i może być użyty do wywołań pośrednich. blog.epsilon-sec.com -
Synacktiv w „iOS 18.4 — dlsym considered harmful” opisuje błąd: niektóre symbole rozwiązywane przez
dlsymna iOS 18.4 zwracają wskaźniki, które są niepoprawnie podpisane (lub z błędnymi diversifierami), umożliwiając niezamierzony PAC bypass. Synacktiv -
Logika w dyld dla dlsym zawiera: kiedy
result->isCode, podpisują zwracany wskaźnik przez__builtin_ptrauth_sign_unauthenticated(..., key_asia, 0), tzn. kontekst zero. blog.epsilon-sec.com
Dlatego dlsym jest częstym wektorem w user-mode PAC bypassach.
2.3 Inne DYLD / runtime relocations
-
Loader DYLD i logika relokacji dynamicznej są złożone i czasem tymczasowo mapują strony jako read/write, by przeprowadzić relokacje, potem przełączają je z powrotem na read-only. Atakujący wykorzystują te okna. Talk Synacktiv opisuje „Operation Triangulation”, timing-based bypass PAC przez dynamic relocations. Synacktiv
-
Strony DYLD są teraz chronione przez SPRR / VM_FLAGS_TPRO (pewne flagi ochrony dla dyld). Ale wcześniejsze wersje miały słabsze zabezpieczenia. Synacktiv
-
W łańcuchach exploitów WebKit loader DYLD często jest celem PAC bypassów. Slajdy wspominają, że wiele PAC bypassów celowało w DYLD loader (przez relokację, interposer hooks). Synacktiv
2.4 NSPredicate / NSExpression / ObjC / SLOP
-
W userland exploit chains, runtime Objective-C jak
NSPredicate,NSExpressionczyNSInvocationsą używane do przemytu wywołań kontrolowanych bez oczywistego fałszowania wskaźników. -
Na starszych iOS (przed PAC) exploit używał fake NSInvocation obiektów by wywołać dowolne selektory na kontrolowanej pamięci. Z PAC wymagane były modyfikacje. Technika SLOP (SeLector Oriented Programming) została rozszerzona pod PAC także. Project Zero
-
Oryginalna technika SLOP pozwalała na łańcuchowanie ObjC wywołań przez tworzenie fałszywych invocations; bypass polegał na tym, że ISA lub selector pointers czasami nie były w pełni chronione PAC. Project Zero
-
W środowiskach, gdzie pointer authentication jest stosowane częściowo, metody / selectory / target pointers mogą nie być zawsze chronione PAC, dając pole do obejścia.
Przykładowy przebieg
Przykład Signing & Authenticating
``` ; Example: function prologue / return address protection my_func: stp x29, x30, [sp, #-0x20]! ; push frame pointer + LR mov x29, sp PACIASP ; sign LR (x30) using SP as modifier ; … body … mov sp, x29 ldp x29, x30, [sp], #0x20 ; restore AUTIASP ; authenticate & strip PAC ret; Example: indirect function pointer stored in a struct ; suppose X1 contains a function pointer PACDA X1, X2 ; sign data pointer X1 with context X2 STR X1, [X0] ; store signed pointer
; later retrieval: LDR X1, [X0] AUTDA X1, X2 ; authenticate & strip BLR X1 ; branch to valid target
; Example: stripping for comparison (unsafe) LDR X1, [X0] XPACI X1 ; strip PAC (instruction domain) CMP X1, #some_label_address BEQ matched_label
</details>
<details>
<summary>Example</summary>
Przepełnienie bufora nadpisuje adres powrotu na stosie. Atakujący zapisuje adres docelowego gadgetu, ale nie może obliczyć poprawnego PAC. Gdy funkcja zwraca, instrukcja CPU `AUTIA` generuje fault z powodu niezgodności PAC. Łańcuch zawodzi.
Analiza Project Zero dotycząca A12 (iPhone XS) pokazała, jak Apple używa PAC i metody fałszowania PAC, jeśli atakujący ma prymityw do odczytu/zapisu pamięci.
</details>
### 9. **Branch Target Identification (BTI)**
**Introduced with ARMv8.5 (later hardware)**
BTI to funkcja sprzętowa, która sprawdza **indirect branch targets**: przy wykonywaniu `blr` lub pośrednich wywołań/skoków, cel musi zaczynać się od **BTI landing pad** (`BTI j` lub `BTI c`). Skakanie do adresów gadgetów, które nie mają landing padu, powoduje wyjątek.
Implementacja LLVM notuje trzy warianty instrukcji BTI i jak są mapowane na typy branchy.
| BTI Variant | What it permits (which branch types) | Typical placement / use case |
|-------------|----------------------------------------|-------------------------------|
| **BTI C** | Targets of *call*-style indirect branches (e.g. `BLR`, or `BR` using X16/X17) | Umieszczany na wejściu funkcji, które mogą być wywoływane pośrednio |
| **BTI J** | Targets of *jump*-style branches (e.g. `BR` used for tail calls) | Umieszczany na początku bloków osiągalnych przez jump table lub tail-call |
| **BTI JC** | Acts as both C and J | Może być celem zarówno call- jak i jump-branchy |
- W kodzie skompilowanym z branch target enforcement, kompilatory wstawiają instrukcję BTI (C, J lub JC) w każdym poprawnym celu pośredniego skoku (początki funkcji lub bloki osiągalne przez skoki), tak aby pośrednie skoki udawały się tylko do tych miejsc.
- **Direct branches / calls** (tj. stałoadresowe `B`, `BL`) **nie są ograniczone** przez BTI. Zakłada się, że strony z kodem są zaufane i atakujący nie może ich zmienić (więc bezpośrednie skoki są bezpieczne).
- Również instrukcje **RET / return** na ogół nie są ograniczane przez BTI, ponieważ adresy powrotu są chronione przez PAC lub mechanizmy podpisywania powrotu.
#### Mechanism and enforcement
- Gdy CPU dekoduje **indirect branch (BLR / BR)** na stronie oznaczonej jako „guarded / BTI-enabled”, sprawdza, czy pierwsza instrukcja pod adresem celu jest poprawnym BTI (C, J lub JC, jeśli dozwolone). Jeśli nie, następuje **Branch Target Exception**.
- Kodowanie instrukcji BTI zostało zaprojektowane tak, by ponownie użyć opcode’ów wcześniej zarezerwowanych dla NOPów (w wcześniejszych wersjach ARM). Dzięki temu binaria z BTI są wstecznie kompatybilne: na sprzęcie bez wsparcia BTI te instrukcje działają jak NOP.
- Przepustki kompilatora, które dodają BTI, wstawiają je tylko tam, gdzie są potrzebne: funkcje, które mogą być wywołane pośrednio, lub bloki podstawowe będące celami skoków.
- Niektóre poprawki i fragmenty kodu LLVM pokazują, że BTI nie jest wstawiany do *wszystkich* bloków podstawowych — tylko do tych, które są potencjalnymi celami branchy (np. z switch / jump tables).
#### BTI + PAC synergy
PAC chroni wartość wskaźnika (źródło) — zapewnia, że łańcuch pośrednich wywołań / powrotów nie został zmodyfikowany.
BTI zapewnia, że nawet poprawny wskaźnik może wskazywać jedynie na poprawnie oznaczone punkty wejścia.
W połączeniu, atakujący potrzebuje zarówno poprawnego wskaźnika z właściwym PAC, jak i celu z wstawionym BTI. To zwiększa trudność konstruowania exploitów.
#### Example
<details>
<summary>Example</summary>
Exploit próbuje przełączyć się do gadgetu pod `0xABCDEF`, który nie zaczyna się od `BTI c`. CPU przy wykonaniu `blr x0` sprawdza cel i generuje fault, ponieważ wyrównanie instrukcji nie zawiera poprawnego landing padu. Wiele gadgetów staje się więc nieużytecznych, chyba że mają prefiks BTI.
</details>
### 10. **Privileged Access Never (PAN) & Privileged Execute Never (PXN)**
**Introduced in more recent ARMv8 extensions / iOS support (for hardened kernel)**
#### PAN (Privileged Access Never)
- **PAN** to funkcja wprowadzona w **ARMv8.1-A**, która zapobiega temu, by **kod uprzywilejowany** (EL1 lub EL2) **czytał lub zapisywał** pamięć oznaczoną jako **dostępną dla użytkownika (EL0)**, chyba że PAN jest jawnie wyłączone.
- Idea: nawet jeśli jądro zostanie oszukane lub przejęte, nie może dowolnie dereferencjonować wskaźników użytkownika bez wcześniejszego *wyłączenia* PAN, co zmniejsza ryzyko exploitów typu **`ret2usr`** lub nadużyć buforów kontrolowanych przez użytkownika.
- Gdy PAN jest włączone (PSTATE.PAN = 1), każda uprzywilejowana instrukcja load/store odwołująca się do wirtualnego adresu, który jest „dostępny w EL0”, powoduje **permission fault**.
- Jądro, gdy musi legalnie uzyskać dostęp do pamięci użytkownika (np. kopiować dane do/z buforów użytkownika), musi **tymczasowo wyłączyć PAN** (lub użyć „unprivileged load/store” instrukcji), aby umożliwić ten dostęp.
- W Linuxie na ARM64 wsparcie PAN zostało wprowadzone około 2015: poprawki do jądra dodały wykrywanie tej funkcji i zastąpiły `get_user` / `put_user` itp. wariantami, które wokół dostępu do pamięci użytkownika wyłączają PAN.
**Key nuance / limitation / bug**
- Jak zauważyli Siguza i inni, błąd specyfikacji (lub niejednoznaczność) w projekcie ARM powoduje, że **execute-only user mappings** (`--x`) mogą **nie wywoływać PAN**. Innymi słowy, jeśli strona użytkownika jest oznaczona jako wykonywalna lecz bez prawa do odczytu, próba odczytu przez jądro może obejść PAN, ponieważ architektura traktuje „accessible at EL0” jako wymagające prawa do odczytu, a nie tylko wykonania. To prowadzi do obejścia PAN w pewnych konfiguracjach.
- Z tego powodu, jeśli iOS / XNU pozwala na strony użytkownika tylko do wykonywania (jak w niektórych JITach lub code-cache), jądro może przypadkowo odczytywać z nich dane nawet przy włączonym PAN. To znane, subtelne miejsce podatne na wykorzystanie w niektórych systemach ARMv8+.
#### PXN (Privileged eXecute Never)
- **PXN** to flaga w pagetable (w wpisach pagetable, leaf lub block), która wskazuje, że strona jest **niewykonywalna w trybie uprzywilejowanym** (tj. gdy EL1 wykonuje instrukcje).
- PXN zapobiega temu, by jądro (lub inny kod uprzywilejowany) skakało do lub wykonywało instrukcje z pamięci użytkownika nawet jeśli kontrola zostanie przekierowana. W praktyce uniemożliwia wykonanie kodu na poziomie jądra pochodzącego z przestrzeni użytkownika.
- W połączeniu z PAN zapewnia to:
1. Jądro standardowo nie może czytać ani pisać danych użytkownika (PAN)
2. Jądro nie może wykonywać kodu użytkownika (PXN)
- W formacie tabeli stron ARM, wpisy leaf mają bit `PXN` (oraz `UXN` dla execute-never w trybie nieuprzywilejowanym) w swoich bitach atrybutów.
Nawet jeśli jądro ma uszkodzony wskaźnik funkcji wskazujący na pamięć użytkownika i próbuje tam skoczyć, bit PXN spowoduje fault.
#### Memory-permission model & how PAN and PXN map to page table bits
Aby zrozumieć, jak działają PAN / PXN, trzeba zobaczyć model translacji i uprawnień ARM (uproszczone):
- Każdy wpis strony lub bloku ma pola atrybutów, w tym **AP[2:1]** dla uprawnień dostępu (odczyt/zapis, uprzywilejowany vs nieuprzywilejowany) oraz bity **UXN / PXN** dla ograniczeń execute-never.
- Gdy PSTATE.PAN = 1 (włączone), sprzęt egzekwuje zmodyfikowaną semantykę: uprzywilejowane dostępy do stron oznaczonych jako „dostępne przez EL0” (tj. dostępne dla użytkownika) są zabronione (fault).
- Z powodu wspomnianego buga, strony oznaczone jedynie jako wykonywalne (bez prawa do odczytu) mogą nie być traktowane jako „dostępne przez EL0” w pewnych implementacjach, co umożliwia obejście PAN.
- Gdy dla strony ustawiony jest bit PXN, nawet jeśli fetch instrukcji pochodzi z wyższego poziomu uprzywilejowania, wykonanie jest zabronione.
#### Kernel usage of PAN / PXN in a hardened OS (e.g. iOS / XNU)
W zaostrzonym projekcie jądra (takim, jakiego używa Apple):
- Jądro włącza PAN domyślnie (tak by kod uprzywilejowany był ograniczony).
- W ścieżkach, które legalnie muszą czytać lub pisać buffory użytkownika (np. kopiowanie w syscallach, I/O, read/write user pointer), jądro tymczasowo **wyłącza PAN** lub używa specjalnych instrukcji, aby to umożliwić.
- Po zakończeniu dostępu do danych użytkownika musi ponownie włączyć PAN.
- PXN jest wymuszone przez tabelę stron: strony użytkownika mają PXN = 1 (więc jądro nie może ich wykonywać), strony jądra nie mają PXN (więc kod jądra może być wykonywany).
- Jądro musi zapewnić, że żadne ścieżki kodu nie prowadzą do wykonania kodu w regionach pamięci użytkownika (co obejść PXN) — stąd łańcuchy exploitów polegające na „skoku do shellcode’u użytkownika” są zablokowane.
Z powodu wspomnianego obejścia PAN przez execute-only strony, w rzeczywistym systemie Apple może wyłączyć lub zabronić stron execute-only dla użytkownika albo załatać tę niejednoznaczność specyfikacji.
#### Attack surfaces, bypasses, and mitigations
- **PAN bypass via execute-only pages**: jak omówiono, spec pozwala na lukę: strony użytkownika z execute-only (bez prawa do odczytu) mogą nie być traktowane jako „accessible at EL0”, więc PAN nie zablokuje odczytów jądra z takich stron w niektórych implementacjach. To daje atakującemu nietypową drogę do dostarczenia danych przez sekcje execute-only.
- **Temporal window exploit**: jeśli jądro wyłącza PAN na dłużej niż to konieczne, może się pojawić wyścig lub złośliwa ścieżka, która wykorzysta ten przedział czasu do wykonania niezamierzonych dostępów do pamięci użytkownika.
- **Forgotten re-enable**: jeśli ścieżki kodu zapomną o ponownym włączeniu PAN, kolejne operacje jądra mogą niewłaściwie uzyskać dostęp do pamięci użytkownika.
- **Misconfiguration of PXN**: jeśli tabela stron nie ustawia PXN dla stron użytkownika lub błędnie mapuje strony kodu użytkownika, jądro może zostać oszukane, by wykonać kod użytkownika.
- **Speculation / side-channels**: podobnie do spekulacyjnych obejść, mogą istnieć mikroarchitektoniczne efekty uboczne powodujące przejściowe naruszenie sprawdzeń PAN / PXN (choć takie ataki silnie zależą od projektu CPU).
- **Complex interactions**: w bardziej zaawansowanych funkcjach (np. JIT, shared memory, regiony kodu just-in-time) jądro może potrzebować drobiazgowej kontroli, by zezwolić na pewne dostępy lub wykonanie w regionach mapowanych dla użytkownika; zaprojektowanie tego bezpiecznie w ramach ograniczeń PAN/PXN jest niebanalne.
#### Example
<details>
<summary>Code Example</summary>
Oto ilustracyjne pseudo-assembly pokazujące włączanie/wyłączanie PAN wokół dostępu do pamięci użytkownika oraz jak może wystąpić fault.
// 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
The kernel can do many normal operations, but only through ppl_call_* routines can it change protected mappings or patch code.
Example
A kernel exploit tries to overwrite the entitlement table, or disable code-sign enforcement by modifying a kernel signature blob. Because that page is PPL-protected, the write is blocked unless going through the PPL interface. So even with kernel code execution, you cannot bypass code-sign constraints or modify credential data arbitrarily. On iOS 17+ certain devices use SPTM to further isolate PPL-managed pages.PPL → SPTM / Replacements / Future
- On Apple’s modern SoCs (A15 or later, M2 or later), Apple supports SPTM (Secure Page Table Monitor), which replaces PPL for page table protections.
- Apple calls out in documentation: “Page Protection Layer (PPL) and Secure Page Table Monitor (SPTM) enforce execution of signed and trusted code … PPL manages the page table permission overrides … Secure Page Table Monitor replaces PPL on supported platforms.”
- The SPTM architecture likely shifts more policy enforcement into a higher-privileged monitor outside kernel control, further reducing the trust boundary.
MTE | EMTE | MIE
Here’s a higher-level description of how EMTE operates under Apple’s MIE setup:
- Tag assignment
- When memory is allocated (e.g. in kernel or user space via secure allocators), a secret tag is assigned to that block.
- The pointer returned to the user or kernel includes that tag in its high bits (using TBI / top byte ignore mechanisms).
- Tag checking on access
- Whenever a load or store is executed using a pointer, the hardware checks that the pointer’s tag matches the memory block’s tag (allocation tag). If mismatch, it faults immediately (since synchronous).
- Because it’s synchronous, there is no “delayed detection” window.
- Retagging on free / reuse
- When memory is freed, the allocator changes the block’s tag (so older pointers with old tags no longer match).
- A use-after-free pointer would therefore have a stale tag and mismatch when accessed.
- Neighbor-tag differentiation to catch overflows
- Adjacent allocations are given distinct tags. If a buffer overflow spills into neighbor’s memory, tag mismatch causes a fault.
- This is especially powerful in catching small overflows that cross boundary.
- Tag confidentiality enforcement
- Apple must prevent tag values being leaked (because if attacker learns the tag, they could craft pointers with correct tags).
- They include protections (microarchitectural / speculative controls) to avoid side-channel leakage of tag bits.
- Kernel and user-space integration
- Apple uses EMTE not just in user-space but also in kernel / OS-critical components (to guard kernel against memory corruption).
- The hardware/OS ensures tag rules apply even when kernel is executing on behalf of user space.
Example
``` Allocate A = 0x1000, assign tag T1 Allocate B = 0x2000, assign tag T2// pointer P points into A with tag T1 P = (T1 << 56) | 0x1000
// Valid store *(P + offset) = value // tag T1 matches allocation → allowed
// Overflow attempt: P’ = P + size_of_A (into B region) *(P’ + delta) = value → pointer includes tag T1 but memory block has tag T2 → mismatch → fault
// Free A, allocator retags it to T3 free(A)
// Use-after-free: *(P) = value → pointer still has old tag T1, memory region is now T3 → mismatch → fault
</details>
#### Ograniczenia i wyzwania
- **Intrablock overflows**: Jeśli overflow pozostaje w tej samej alokacji (nie przekracza granicy) i tag pozostaje taki sam, tag mismatch go nie wykryje.
- **Tag width limitation**: Dostępne są tylko nieliczne bity dla taga (np. 4 bity, lub mała domena) — ograniczona przestrzeń nazw.
- **Side-channel leaks**: Jeśli bity taga mogą być leaked (via cache / speculative execution), atakujący może poznać prawidłowe tagi i obejść ochronę. Apple’s tag confidentiality enforcement ma to złagodzić.
- **Performance overhead**: Sprawdzenia tagu przy każdym load/store dodają koszt; Apple musi zoptymalizować hardware, aby zredukować overhead.
- **Compatibility & fallback**: Na starszym hardware lub w częściach, które nie wspierają EMTE, musi istnieć fallback. Apple twierdzi, że MIE jest włączone tylko na urządzeniach z odpowiednim wsparciem.
- **Complex allocator logic**: Allocator musi zarządzać tagami, retaggingiem, wyrównywaniem granic i unikać kolizji tagów. Błędy w logice allocatora mogą wprowadzić podatności.
- **Mixed memory / hybrid areas**: Część pamięci może pozostać untagged (legacy), co utrudnia interoperacyjność.
- **Speculative / transient attacks**: Jak w przypadku wielu mikrarchitektonicznych protekcji, speculative execution lub micro-op fusions mogą transientnie ominąć checki lub leak tag bits.
- **Limited to supported regions**: Apple może egzekwować EMTE jedynie w wybranych, wysokiego ryzyka obszarach (kernel, security-critical subsystems), a nie globalnie.
---
## Key enhancements / differences compared to standard MTE
Oto usprawnienia i zmiany, które Apple podkreśla:
| 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.|
Ponieważ Apple kontroluje zarówno hardware, jak i stack software’owy, może rygorystycznie egzekwować EMTE, unikać problemów z wydajnością i zamykać luki side-channel.
---
## How EMTE works in practice (Apple / MIE)
Poniżej opis na wyższym poziomie, jak EMTE działa w konfiguracji Apple / MIE:
1. **Tag assignment**
- Kiedy pamięć jest alokowana (np. w kernelu lub w user space via secure allocators), blokowi przypisywany jest **secret tag**.
- Wskaźnik zwrócony do użytkownika lub kernela zawiera ten tag w high bits (używając TBI / top byte ignore mechanisms).
2. **Tag checking on access**
- Kiedy wykonywany jest load lub store z użyciem wskaźnika, hardware sprawdza, czy tag wskaźnika pasuje do taga bloku pamięci (allocation tag). Jeśli mismatch, następuje fault natychmiast (ponieważ synchronous).
- Ponieważ jest synchronous, nie ma okienka “delayed detection”.
3. **Retagging on free / reuse**
- Gdy pamięć jest freed, allocator zmienia tag bloku (stare wskaźniki z poprzednim tagiem przestają pasować).
- Wskaźnik use-after-free będzie więc miał przestarzały tag i spowoduje mismatch podczas dostępu.
4. **Neighbor-tag differentiation to catch overflows**
- Sąsiednim alokacjom przypisywane są różne tagi. Jeśli buffer overflow przeleje się do pamięci sąsiada, tag mismatch spowoduje fault.
- To jest szczególnie skuteczne przy wykrywaniu małych overflowów, które przekraczają granicę alokacji.
5. **Tag confidentiality enforcement**
- Apple musi uniemożliwić odkrycie tagów (ponieważ jeśli atakujący pozna tag, mógłby skonstruować wskaźniki z poprawnymi tagami).
- Wprowadza zabezpieczenia (mikroarchitektoniczne / spekulacyjne), aby zapobiec side-channel leakage of tag values.
6. **Kernel and user-space integration**
- Apple używa EMTE nie tylko w user-space, ale także w kernelu / OS-critical komponentach (aby chronić kernel przed memory corruption).
- Hardware/OS zapewnia, że reguły tagów obowiązują nawet gdy kernel wykonuje operacje w imieniu user space.
Ponieważ EMTE jest zintegrowane z MIE, Apple używa EMTE w synchronous mode na kluczowych powierzchniach ataku, a nie jako opcję czy tryb debugowania.
---
## Exception handling in XNU
Kiedy występuje **exception** (np. `EXC_BAD_ACCESS`, `EXC_BAD_INSTRUCTION`, `EXC_CRASH`, `EXC_ARM_PAC`, itd.), warstwa **Mach** jądra XNU jest odpowiedzialna za jej przechwycenie zanim zostanie ona przekształcona w UNIX-style **signal** (jak `SIGSEGV`, `SIGBUS`, `SIGILL`, ...).
Proces ten obejmuje wiele warstw propagacji i obsługi wyjątków zanim dotrze do user space lub zostanie skonwertowany do BSD signal.
### Przepływ wyjątków (High-Level)
1. **CPU triggers a synchronous exception** (np. nieprawidłowe odwołanie do wskaźnika, PAC failure, nielegalna instrukcja itp.).
2. **Low-level trap handler** runs (`trap.c`, `exception.c` in XNU source).
3. The trap handler calls **`exception_triage()`**, rdzeń obsługi Mach exceptions.
4. `exception_triage()` decyduje, jak skierować exception:
- Najpierw do **thread's exception port**.
- Następnie do **task's exception port**.
- Następnie do **host's exception port** (często `launchd` lub `ReportCrash`).
Jeśli żaden z tych portów nie obsłuży wyjątku, kernel może:
- **Przekształcić go w BSD signal** (dla procesów user-space).
- **Panic** (dla wyjątków w kernel-space).
### Główna funkcja: `exception_triage()`
Funkcja `exception_triage()` kieruje Mach exceptions w górę łańcucha możliwych handlerów aż któryś z nich obsłuży wyjątek lub aż stanie się on ostatecznie fatalny. Jest zdefiniowana w `osfmk/kern/exception.c`.
```c
void exception_triage(exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt);
Typowy przebieg wywołań:
exception_triage() └── exception_deliver() ├── exception_deliver_thread() ├── exception_deliver_task() └── exception_deliver_host()
Jeśli wszystkie zawiodą → obsługiwane przez bsd_exception() → zamieniane na sygnał, np. SIGSEGV.
Porty wyjątków
Każdy obiekt Mach (thread, task, host) może zarejestrować exception ports, na które wysyłane są komunikaty wyjątków.
Są one zdefiniowane przez API:
task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()
Każdy port wyjątków ma:
- A mask (które wyjątki chce otrzymywać)
- A port name (Mach port do odbioru wiadomości)
- A behavior (w jaki sposób kernel wysyła wiadomość)
- A flavor (który thread state ma być dołączony)
Debuggery i obsługa wyjątków
Debugger (np. LLDB) ustawia exception port na docelowym zadaniu lub wątku, zwykle używając task_set_exception_ports().
Gdy wystąpi wyjątek:
- Mach message jest wysyłany do procesu debuggera.
- Debugger może zdecydować się obsłużyć (wznowić, zmodyfikować rejestry, pominąć instrukcję) lub nie obsłużyć wyjątku.
- Jeśli debugger go nie obsłuży, wyjątek propaguje się do następnego poziomu (task → host).
Przebieg EXC_BAD_ACCESS
-
Wątek dereferencjuje nieprawidłowy pointer → CPU zgłasza Data Abort.
-
Kernel trap handler wywołuje
exception_triage(EXC_BAD_ACCESS, ...). -
Wiadomość wysyłana jest do:
-
Thread port → (debugger może przechwycić breakpoint).
-
Jeśli debugger zignoruje → Task port → (handler na poziomie procesu).
-
Jeśli zignorowano → Host port (zwykle ReportCrash).
- Jeśli nikt nie obsłuży →
bsd_exception()tłumaczy to naSIGSEGV.
PAC Exceptions
Gdy Pointer Authentication (PAC) zawiedzie (niezgodność sygnatury), podnoszony jest specjalny Mach exception:
EXC_ARM_PAC(typ)- Kody mogą zawierać szczegóły (np. typ klucza, typ pointera).
Jeżeli binarka ma flagę TFRO_PAC_EXC_FATAL, kernel traktuje błędy PAC jako fatalne, omijając przechwycenie przez debuggera. Ma to zapobiec wykorzystaniu debuggerów do obejścia kontroli PAC i jest włączone dla platform binaries.
Software Breakpoints
Software breakpoint (int3 na x86, brk na ARM64) jest implementowany poprzez celowe wywołanie faultu.
Debugger przechwytuje to przez exception port:
- Modyfikuje instruction pointer lub pamięć.
- Przywraca oryginalną instrukcję.
- Wznawia wykonywanie.
Ten sam mechanizm pozwala “przechwycić” wyjątek PAC — chyba że TFRO_PAC_EXC_FATAL jest ustawione, wtedy nigdy nie trafia do debuggera.
Konwersja na sygnały BSD
Jeśli żaden handler nie zaakceptuje wyjątku:
-
Kernel wywołuje
task_exception_notify() → bsd_exception(). -
To mapuje Mach exceptions na sygnały:
| Zone Name | Element Size | Example Use |
|---|---|---|
default.kalloc.16 | 16 bytes | Bardzo małe struktury jądra, wskaźniki. |
default.kalloc.32 | 32 bytes | Małe struktury, nagłówki obiektów. |
default.kalloc.64 | 64 bytes | IPC messages, tiny kernel buffers. |
default.kalloc.128 | 128 bytes | Obiekty średniej wielkości, części OSObject. |
| … | … | … |
default.kalloc.1280 | 1280 bytes | Duże struktury, IOSurface/graphics metadata. |
(Nawias: tabela mapowania Mach Exception → Signal została zachowana poniżej.)
| 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) |
Kluczowe pliki w źródłach XNU
osfmk/kern/exception.c→ Rdzeńexception_triage(),exception_deliver_*().bsd/kern/kern_sig.c→ Logika dostarczania sygnałów.osfmk/arm64/trap.c→ Niskopoziomowe trap handlery.osfmk/mach/exc.h→ Kody wyjątków i struktury.osfmk/kern/task.c→ Konfiguracja exception port dla tasków.
Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)
Kernel używał zone allocator (kalloc) podzielonego na strefy o stałym rozmiarze (“zones”).
Każda strefa przechowywała alokacje tylko jednej klasy rozmiaru.
Z ekranu (screenshot):
Tabela powyżej opisuje przykładowe strefy i rozmiary.
Jak to działało:
- Każde żądanie alokacji było zaokrąglane w górę do najbliższego rozmiaru strefy.
(Przykładowo, żądanie 50 bajtów trafiało do strefy
kalloc.64). - Pamięć w każdej strefie była utrzymywana w freelist — kawałki zwolnione przez kernel wracały do tej strefy.
- Jeśli przepełniłeś bufor 64-bajtowy, nadpisywałeś następny obiekt w tej samej strefie.
Dlatego właśnie heap spraying / feng shui był tak skuteczny: można było przewidzieć sąsiadów obiektu poprzez rozpylanie alokacji z tej samej klasy rozmiaru.
The freelist
Wewnątrz każdej strefy kalloc, zwolnione obiekty nie były od razu zwracane systemowi — trafiały do freelist, listy połączonej dostępnych kawałków.
-
Gdy chunk był zwalniany, kernel zapisywał wskaźnik na początku tego chunka → adres następnego wolnego chunka w tej samej strefie.
-
Strefa trzymała wskaźnik HEAD na pierwszy wolny chunk.
-
Alokacja zawsze używała bieżącego HEAD:
-
Pop HEAD (zwróć tę pamięć wywołującemu).
-
Update HEAD = HEAD->next (przechowywane w nagłówku zwolnionego chunka).
-
Zwalnianie wkładało chunk z powrotem:
-
freed_chunk->next = HEAD
-
HEAD = freed_chunk
Więc freelist był po prostu listą połączoną zbudowaną wewnątrz samej zwolnionej pamięci.
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)
Wykorzystanie freelist
Ponieważ pierwsze 8 bajtów free chunk = freelist pointer, atakujący może go skorygować:
-
Heap overflow do sąsiedniego zwolnionego chunku → nadpisanie jego „next” pointer.
-
Use-after-free zapis do zwolnionego obiektu → nadpisanie jego „next” pointer.
Następnie, przy kolejnej alokacji tego rozmiaru:
-
Alokator pobiera (pops) skorygowany chunk.
-
Podąża za dostarczonym przez atakującego „next” pointer.
-
Zwraca pointer do dowolnej pamięci, co umożliwia fake object primitives lub targeted overwrite.
Wizualny przykład 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: Dedykowany binarny watcher nieustannie profiluje urządzenie i przerywa kill-chain, gdy wykryte zostanie środowisko badawcze. Sprawdza
security.mac.amfi.developer_mode_status, obecność konsolidiagnosticd, lokalizacjeUSlubIL, ślady jailbreaka takie jak Cydia, procesy takie jakbash,tcpdump,frida,sshd, lubcheckrain, mobilne AV (McAfee, AvastMobileSecurity, NortonMobileSecurity), niestandardowe ustawienia HTTP proxy oraz niestandardowe root CA. Niepowodzenie któregokolwiek sprawdzenia blokuje dalsze dostarczanie payloadów. - Helper surveillance hooks: Komponent helper komunikuje się z innymi etapami przez
/tmp/helper.sock, następnie ładuje zestawy hooków o nazwach DMHooker i UMHooker. Te hooki przechwytują ścieżki audio VOIP (nagrania trafiają do/private/var/tmp/l/voip_%lu_%u_PART.m4a), implementują systemowy keylogger, wykonują zdjęcia bez UI oraz hookują SpringBoard, aby stłumić powiadomienia, które normalnie by się pojawiły. W efekcie helper działa jako ukryta warstwa walidacji i lekkiego nadzoru zanim zostaną wdrożone cięższe implanty, takie jak 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
References
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
HackTricks

