iOS Exploiting
Reading time: 53 minutes
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 / Weryfikacja podpisu w czasie wykonywania
Wprowadzone wcześnie (iPhone OS → iOS)
To jedna z podstawowych ochron: wszystkie wykonywalne kody (apps, dynamic libraries, JIT-ed code, extensions, frameworks, caches) muszą być kryptograficznie podpisane przez łańcuch certyfikatów zaufany przez Apple. W czasie wykonywania, przed załadowaniem binarki do pamięci (lub przed wykonaniem skoków przez niektóre granice), system sprawdza jej podpis. Jeśli kod został zmodyfikowany (bit-flipped, patched) lub nie jest podpisany, załadunek kończy się niepowodzeniem.
- Uniemożliwia: etap „classic payload drop + execute” w łańcuchach exploitów; arbitralne wstrzykiwanie kodu; modyfikowanie istniejącej binarki w celu wstawienia złośliwej logiki.
- Szczegóły mechanizmu:
- Loader Mach-O (oraz dynamiczny linker) sprawdza strony kodu, segmenty, entitlements, team ID oraz czy podpis obejmuje zawartość pliku.
- Dla regionów pamięci takich jak JIT caches czy dynamicznie generowany kod, Apple wymusza, aby strony były podpisane lub walidowane przez specjalne API (np.
mprotect
with code-sign checks). - Podpis zawiera entitlements i identyfikatory; OS egzekwuje, że niektóre API lub uprzywilejowane zdolności wymagają konkretnych entitlements, których nie da się podrobić.
Example
Załóżmy, że exploit uzyskuje wykonanie kodu w procesie i próbuje zapisać shellcode na heapie i skoczyć do niego. Na iOS taka strona musiałaby być oznaczona jako executable **i** spełniać ograniczenia code-signature. Ponieważ shellcode nie jest podpisany certyfikatem Apple, skok kończy się niepowodzeniem lub system odmawia nadania tej stronie prawa do wykonywania.2. CoreTrust
Wprowadzone około ery iOS 14+ (lub stopniowo na nowszych urządzeniach / późniejszych wersjach iOS)
CoreTrust to podsystem wykonujący weryfikację podpisu w czasie wykonywania binarek (w tym systemowych i użytkownika) względem rootowego certyfikatu Apple zamiast polegać na lokalnych, cache’owanych store’ach zaufania w userlandzie.
- Uniemożliwia: post-install tampering binarek, techniki jailbreaka próbujące podmienić lub patchować systemowe biblioteki lub aplikacje; oszukiwanie systemu przez zastąpienie zaufanych binarek złośliwymi odpowiednikami.
- Szczegóły mechanizmu:
- Zamiast ufać lokalnej bazie zaufania lub cache’owi certyfikatów, CoreTrust odwołuje się bezpośrednio do rootu Apple lub weryfikuje certyfikaty pośrednie w bezpiecznym łańcuchu.
- Zapewnia wykrywanie i odrzucanie modyfikacji (np. w filesystemie) istniejących binarek.
- Wiąże entitlements, team ID, flagi code signing i inne metadane z binarką w czasie ładowania.
Example
Jailbreak mógłby próbować podmienić `SpringBoard` lub `libsystem` na zmodyfikowaną wersję, żeby uzyskać persystencję. Ale kiedy loader OS lub CoreTrust sprawdza, zauważa niezgodność podpisu (lub zmienione entitlements) i odmawia wykonania.3. Data Execution Prevention (DEP / NX / W^X)
Wprowadzone w wielu OSach wcześniej; iOS miał NX-bit / w^x od długiego czasu
DEP wymusza, że strony oznaczone jako writable (dla danych) są nie-wykonywalne, a strony oznaczone jako executable są nie-zapisywalne. Nie można po prostu zapisać shellcode’u na heapie lub stosie i wykonać go.
- Uniemożliwia: bezpośrednie wykonanie shellcode’u; klasyczny buffer-overflow → skok do wstrzykniętego shellcode’u.
- Szczegóły mechanizmu:
- MMU / flagi ochrony pamięci (przez page tables) egzekwują tę separację.
- Każda próba oznaczenia zapisywalnej strony jako executable uruchamia systemową kontrolę (i jest albo zabroniona, albo wymaga akceptacji code-sign).
- W wielu przypadkach nadanie stronom praw do wykonywania wymaga użycia API OS, które narzucają dodatkowe ograniczenia lub sprawdzenia.
Example
Overflow zapisuje shellcode na heapie. Atakujący wywołuje `mprotect(heap_addr, size, PROT_EXEC)` by uczynić go wykonywalnym. System jednak odmawia albo weryfikuje, że nowa strona musi przejść constraints code-sign (których shellcode nie spełnia).4. Address Space Layout Randomization (ASLR)
Wprowadzone w erze iOS ~4–5 (w przybliżeniu iOS 4–5)
ASLR losowo przekształca bazy kluczowych regionów pamięci: biblioteki, heap, stos itd. za każdym uruchomieniem procesu. Adresy gadgetów zmieniają się między uruchomieniami.
- Uniemożliwia: hardkodowanie adresów gadgetów dla ROP/JOP; statyczne łańcuchy exploitów; ślepe skakanie do znanych offsetów.
- Szczegóły mechanizmu:
- Każda załadowana biblioteka / moduł dynamiczny jest rebazowany pod losowy offset.
- Bazy stosu i heapu są losowane (w pewnych granicach entropii).
- Czasem inne regiony (np. mmap allocations) są również losowane.
- W połączeniu z mitigacjami informacji-leak wymusza, by atakujący najpierw wyciekł adres lub wskaźnik, by poznać bazy adresów w czasie działania.
Example
ROP chain oczekuje gadgetu pod `0x….lib + offset`. Ale ponieważ `lib` jest relokowany inaczej przy każdym uruchomieniu, hardkodowany łańcuch zawodzi. Exploit musi najpierw leakować base address modułu zanim obliczy adresy gadgetów.5. Kernel Address Space Layout Randomization (KASLR)
Wprowadzone około iOS ~ (iOS 5 / iOS 6)
Analogicznie do user ASLR, KASLR losowo ustawia bazę kernel text i innych struktur jądra podczas rozruchu.
- Uniemożliwia: exploitom na poziomie jądra polegającym na stałych lokalizacjach kodu lub danych jądra; statyczne eksploity kernelowe.
- Szczegóły mechanizmu:
- Przy każdym rozruchu baza kernela jest losowana (w pewnym zakresie).
- Struktury danych kernela (jak task_structs, vm_map itp.) mogą być również relokowane lub offsetowane.
- Atakujący musi najpierw leakować wskaźniki kernela lub użyć informacji disclosure, by obliczyć offsety przed przejęciem struktur lub kodu kernela.
Example
Lokalna luka ma na celu uszkodzenie wskaźnika funkcji kernela (np. w `vtable`) w `KERN_BASE + offset`. Ale skoro `KERN_BASE` jest nieznany, atakujący musi najpierw leakować go (np. przez primitive read) zanim obliczy poprawny adres do korupcji.6. Kernel Patch Protection (KPP / AMCC)
Wprowadzone w nowszych iOS / A-series hardware (około iOS 15–16 i nowsze chipy)
KPP (aka AMCC) ciągle monitoruje integralność stron z kodem jądra (przez hash lub checksum). Jeśli wykryje tampering (patche, inline hooks, modyfikacje kodu) poza dozwolonymi oknami, powoduje kernel panic lub reboot.
- Uniemożliwia: trwałe patchowanie jądra (modyfikowanie instrukcji kernela), inline hooks, statyczne nadpisania funkcji.
- Szczegóły mechanizmu:
- Moduł sprzętowy lub firmware monitoruje regiony tekstu kernela.
- Okresowo lub na żądanie ponownie hashuje strony i porównuje z oczekiwanymi wartościami.
- Jeśli nastąpią niezgodności poza benign update windows, urządzenie wywołuje panic (by uniknąć trwałego zainfekowania).
- Atakujący musi albo unikać okien detekcji albo użyć legalnych ścieżek patchowania.
Example
Exploit próbuje zpatchować prolog funkcji kernela (np. `memcmp`) by przechwytywać wywołania. KPP jednak zauważa, że hash strony kodu nie pasuje do oczekiwanej wartości i wywołuje kernel panic, crashując urządzenie zanim patch się ustabilizuje.7. Kernel Text Read‐Only Region (KTRR)
Wprowadzone w nowoczesnych SoC (od ~A12 / nowszy hardware)
KTRR to mechanizm egzekwowany przez sprzęt: gdy kernel text zostaje zablokowany wcześnie podczas bootu, staje się read-only z poziomu EL1 (kernel), zapobiegając dalszym zapisom do stron kodu.
- Uniemożliwia: wszelkie modyfikacje kodu kernela po boot (np. patchowanie, in-place code injection) na poziomie uprzywilejowania EL1.
- Szczegóły mechanizmu:
- Podczas boot (w secure/bootloader stage) memory controller (lub bezpieczna jednostka sprzętowa) oznacza fizyczne strony zawierające kernel text jako read-only.
- Nawet jeśli exploit zyskuje pełne uprawnienia kernela, nie może zapisać na tych stronach, by zpatchować instrukcje.
- Aby je zmodyfikować, atakujący musi najpierw skompromitować chain boota lub podważyć sam KTRR.
Example
Exploit eskalujący uprawnienia wskakuje do EL1 i zapisuje trampoline w funkcji kernela (np. w obsłudze syscall). Ale ponieważ strony są zablokowane jako read-only przez KTRR, zapis kończy się błędem (lub powoduje fault), więc patche nie są zastosowane.8. Pointer Authentication Codes (PAC)
Wprowadzone z ARMv8.3 (sprzętowo), Apple zaczęło od A12 / iOS ~12+
- PAC to funkcja sprzętowa wprowadzona w ARMv8.3-A do wykrywania manipulacji wartościami wskaźników (adresy powrotów, wskaźniki funkcji, niektóre wskaźniki danych) przez osadzanie małego kryptograficznego podpisu („MAC”) w niewykorzystywanych wysokich bitach wskaźnika.
- Podpis („PAC”) jest obliczany nad wartością wskaźnika plus modifier (wartość kontekstowa, np. stack pointer lub inne rozróżniające dane). Dzięki temu ta sama wartość wskaźnika w różnych kontekstach ma inny PAC.
- W momencie użycia, przed dereferencją lub branchem przez ten wskaźnik, instrukcja authenticate sprawdza PAC. Jeśli poprawny, PAC jest usuwany i zostaje czysty wskaźnik; jeśli nie, wskaźnik staje się „poisoned” (lub podniesiony jest fault).
- Klucze używane do tworzenia/validacji PAC są w rejestrach uprzywilejowanych (EL1, kernel) i nie są dostępne z poziomu user mode.
- Ponieważ nie wszystkie 64 bity wskaźnika są używane w wielu systemach (np. 48-bitowy adres 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 implementowany 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ń ogólnych
-
Klucze te są przechowywane w uprzywilejowanych rejestrach systemowych (dostępnych tylko na EL1/EL2 itd.), 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)
- modifiera (wartość kontekstowa, jak salt)
- Sekretnego klucza
- Pewnej wewnętrznej logiki tweak Jeśli wynikowy PAC pasuje do tego, co jest zapisane w górnych bitach wskaźnika, uwierzytelnianie przechodzi pomyślnie.
Rodziny instrukcji
Konwencja nazewnictwa: PAC / AUT / XPAC, potem litery domen.
PACxx
instrukcje podpisują wskaźnik i wstawiają PACAUTxx
instrukcje uwierzytelniają + usuwają (walidują i usuwają PAC)XPACxx
instrukcje usuwają bez walidacji
Domeny / sufiksy:
Mnemonic | Meaning / Domain | Key / Domain | Example Usage in Assembly |
---|---|---|---|
PACIA | Sign instruction pointer with APIAKey | “I, A” | PACIA X0, X1 — sign pointer in X0 using APIAKey with modifier X1 |
PACIB | Sign instruction pointer with APIBKey | “I, B” | PACIB X2, X3 |
PACDA | Sign data pointer with APDAKey | “D, A” | PACDA X4, X5 |
PACDB | Sign data pointer with APDBKey | “D, B” | PACDB X6, X7 |
PACG / PACGA | Generic (non-pointer) signing with APGAKey | “G” | PACGA X8, X9, X10 (sign X9 with modifier X10 into X8) |
AUTIA | Authenticate APIA-signed instruction pointer & strip PAC | “I, A” | AUTIA X0, X1 — check PAC on X0 using modifier X1, then strip |
AUTIB | Authenticate APIB domain | “I, B” | AUTIB X2, X3 |
AUTDA | Authenticate APDA-signed data pointer | “D, A” | AUTDA X4, X5 |
AUTDB | Authenticate APDB-signed data pointer | “D, B” | AUTDB X6, X7 |
AUTGA | Authenticate generic / blob (APGA) | “G” | AUTGA X8, X9, X10 (validate generic) |
XPACI | Strip PAC (instruction pointer, no validation) | “I” | XPACI X0 — remove PAC from X0 (instruction domain) |
XPACD | Strip PAC (data pointer, no validation) | “D” | XPACD X4 — remove PAC from data pointer in X4 |
Istnieją specjalizowane / aliasowane formy:
PACIASP
to skrót dlaPACIA X30, SP
(podpisz link register używając SP jako modifier)AUTIASP
toAUTIA X30, SP
(uwierzytelnij link register z SP)- Formy łączone jak
RETAA
,RETAB
(authenticate-and-return) lubBLRAA
(authenticate & branch) istnieją w rozszerzeniach ARM / wsparciu kompilatora. - Są też warianty z zerowym modifierem:
PACIZA
/PACIZB
gdzie modifier jest implicitnie zero, itd.
Modifiery
Głównym celem modifiera jest wiązanie PAC z konkretnym kontekstem, tak by ta sama wartość podpisana w różnych kontekstach dawała różne PACy. To zapobiega prostemu reuse wskaźników między framami lub obiektami. Jest to jak dodanie soli do hasha.
Dlatego:
- modifier to wartość kontekstowa (inny rejestr) użyta w obliczeniu PAC. Typowe wybory: stack pointer (
SP
), frame pointer lub jakiś identyfikator obiektu. - Używanie SP jako modifiera jest powszechne dla podpisywania adresów powrotu: PAC jest wtedy powiązany z konkretną ramką stosu. Jeśli spróbujesz reuse’ować LR w innej ramce, modifier się zmieni i walidacja PAC zakończy się niepowodzeniem.
- Ta sama wartość wskaźnika podpisana pod różnymi modifierami 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, gdzie nie ma sensownego modifiera, niektóre formy używają zera lub implicitnego stałego.
Apple / iOS / XNU Customizations & Observations
- Implementacja PAC przez Apple zawiera per-boot diversifiers, więc klucze lub tweakery zmieniają się przy każdym rozruchu, co uniemożliwia reuse across boots.
- Zawierają też cross-domain mitigations, tak że PACy podpisane w user mode nie mogą być łatwo reuse’owane w kernel mode itd.
- Na Apple M1 / Apple Silicon, reverse engineering pokazał, że istnieje dziewięć typów modifierów i specyficzne rejestry systemowe Apple do kontroli kluczy.
- Apple używa PAC w wielu podsystemach kernela: podpisanie adresów powrotu, integralność wskaźników w danych kernela, podpisywanie kontekstów wątków itp.
- Google Project Zero pokazał, że przy potężnym primitive do odczytu/zapisu pamięci kernela można było sfałszować kernel PACy (dla kluczy A) na urządzeniach z A12, ale Apple załatało wiele z tych ścieżek.
- W systemie Apple niektóre klucze są globalne dla kernela, podczas gdy procesy użytkownika mogą dostać per-process key randomness.
PAC Bypasses
- Kernel-mode PAC: teoretyczne vs real bypasses
- Ponieważ klucze PAC kernela i logika są ściśle kontrolowane (uprzywilejowane rejestry, diversifiers, izolacja domen), sfałszowanie dowolnych podpisanych pointeró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 indirekt branche) ale nie pełny, uniwersalny bypass. bazad.github.io
- Apple’s „Dark Magic” customizations further ograniczają exploitable surfaces (domain switching, per-key enabling bits). i.blackhat.com
- Jest znany kernel PAC bypass CVE-2023-32424 na Apple silicon (M1/M2) zgłoszony przez Zecao Cai i in. i.blackhat.com
- Lecz te bypasses często opierają się na bardzo specyficznych gadgetach lub bugach implementacyjnych; nie są to ogólne, uniwersalne obejścia.
W efekcie kernel PAC jest uważane za wysoce odporne, choć nie idealne.
- User-mode / runtime PAC bypass techniques
Są one częstsze i wykorzystują niedoskonałości w stosowaniu PAC w dynamicznym ładowaniu / 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 frameworków i bibliotek. Ponieważ jest szeroko współdzielony, wskaźniki funkcji wewnątrz shared cache są „pre-signed” i używane przez wiele procesów. Atakujący celują w te już podpisane wskaźniki jako „PAC oracles”.
- Niektóre techniki bypassu próbują wydobyć lub reuse’ować A-key podpisane wskaźniki obecne w shared cache i użyć ich w gadgetach.
- Prezentacja "No Clicks Required" opisuje budowę oracla nad shared cache do inferencji względnych adresów i łączenia tego z podpisanymi wskaźnikami by obejść PAC. saelo.github.io
- Również importy wskaźników funkcji z shared libraries w userspace okazały się czasem niewystarczająco chronione przez PAC, pozwalając atakującemu na uzyskanie wskaźników bez zmiany ich podpisu. (Project Zero bug entry) bugs.chromium.org
2.2 dlsym(3) / dynamic symbol resolution
- Jedno znane obejście polega na wywołaniu
dlsym()
aby otrzymać już podpisany wskaźnik funkcji (signed with A-key, diversifier zero) i następnie go użyć. Ponieważdlsym
zwraca legalnie podpisany pointer, użycie go omija potrzebę fałszowania PAC. - Blog Epsilon szczegółowo opisuje jak niektóre bypasses wykorzystują to: wywołanie
dlsym("someSym")
zwraca podpisany pointer, który można użyć do indirect calls. blog.epsilon-sec.com - Synacktiv w „iOS 18.4 --- dlsym considered harmful” opisuje błąd: pewne symbole rozwiązane przez
dlsym
na iOS 18.4 zwracają pointery niepoprawnie podpisane (lub z błędnymi diversifierami), umożliwiając niezamierzony PAC bypass. Synacktiv - Logika w dyld dla dlsym zawiera: gdy
result->isCode
, podpisują zwrócony pointer z__builtin_ptrauth_sign_unauthenticated(..., key_asia, 0)
, tzn. kontekst zero. blog.epsilon-sec.com
Z tego powodu dlsym
jest częstym wektorem w user-mode PAC bypassach.
2.3 Other DYLD / runtime relocations
- Loader DYLD i logika relokacji dynamicznej są złożone i czasem tymczasowo mapują strony jako read/write żeby wykonać relokacje, po czym przełączają z powrotem na read-only. Atakujący wykorzystują te okna czasowe. Prelekcja Synacktiv opisuje „Operation Triangulation”, timing-based bypass PAC poprzez 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 guardy. Synacktiv
- W łańcuchach exploitów WebKit, loader DYLD jest często celem PAC bypass. Slajdy wspominają, że wiele PAC bypassów atakowało DYLD loader (przez relokacje, interposer hooks). Synacktiv
2.4 NSPredicate / NSExpression / ObjC / SLOP
- W userlandowych łańcuchach exploitów, runtime Objective-C takie jak
NSPredicate
,NSExpression
czyNSInvocation
są używane do przemycania wywołań kontroli bez oczywistego fałszowania wskaźników. - Na starszych iOS (przed PAC), exploit używał fake NSInvocation obiektów do wywoływania arbitralnych selectorów na kontrolowanej pamięci. Z PAC technika wymaga modyfikacji. Ale technika SLOP (SeLector Oriented Programming) została rozszerzona pod PAC również. Project Zero
- Oryginalna technika SLOP pozwalała łączyć wywołania ObjC przez tworzenie fałszywych invocations; obejście polega na tym, że ISA lub pointery selectorów czasem nie są w pełni chronione przez PAC. Project Zero
- W środowiskach, gdzie pointer authentication jest stosowane tylko częściowo, metody / selectory / target pointers nie zawsze mają ochronę PAC, co daje pole do obejścia.
Example Flow
Przykład podpisywania i uwierzytelniania
``` ; 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>Przykład</summary>
Przepełnienie bufora nadpisuje adres powrotu na stosie. Atakujący zapisuje adres docelowego gadgetu, ale nie potrafi obliczyć prawidłowego PAC. Kiedy funkcja zwraca, instrukcja procesora `AUTIA` powoduje błąd z powodu niezgodności PAC. Łańcuch się nie udaje.
Analiza Project Zero na A12 (iPhone XS) pokazała, jak Apple używa PAC i metody fałszowania PAC, jeśli atakujący ma prymityw do czytania/zapisu pamięci.
</details>
### 9. **Branch Target Identification (BTI)**
**Wprowadzone w ARMv8.5 (późniejszy sprzęt)**
BTI to funkcja sprzętowa sprawdzająca **pośrednie cele skoków**: przy wykonywaniu `blr` lub pośrednich wywołań/skoków, cel musi zaczynać się od **BTI landing pad** (`BTI j` lub `BTI c`). Skok do adresów gadgetów, które nie mają landing padu, wywołuje wyjątek.
Implementacja w LLVM opisuje trzy warianty instrukcji BTI i jak mapują się one na typy rozkazów skoku.
| BTI Variant | Co umożliwia (które typy skoków) | Typowe umiejscowienie / przypadek użycia |
|-------------|----------------------------------------|-------------------------------|
| **BTI C** | Cele pośrednich rozkazów w stylu *call* (np. `BLR`, lub `BR` używając X16/X17) | Umieszczany na wejściu funkcji, które mogą być wywoływane pośrednio |
| **BTI J** | Cele pośrednich rozkazów w stylu *jump* (np. `BR` używany dla tail calls) | Umieszczany na początku bloków osiągalnych z tabel skoków lub tail-calli |
| **BTI JC** | Działa jako oba: C i J | Może być celem zarówno skoków typu call, jak i jump |
- W kodzie skompilowanym z wymuszonym sprawdzaniem branch target, kompilatory wstawiają instrukcję BTI (C, J lub JC) w każdym prawidłowym celu pośredniego skoku (początki funkcji lub bloki osiągalne przez skoki), tak aby pośrednie skoki powiodły się tylko do tych miejsc.
- **Bezpośrednie skoki / wywołania** (tj. stałe-adresowe `B`, `BL`) **nie są ograniczone** przez BTI. Założenie jest takie, że strony kodu są zaufane i atakujący nie może ich zmienić (więc bezpośrednie skoki są bezpieczne).
- Również, **RET / return** zwykle nie są ograniczane przez BTI, ponieważ adresy powrotu są chronione przez PAC lub mechanizmy podpisywania powrotów.
#### Mechanizm i egzekwowanie
- Kiedy CPU dekoduje **pośredni skok (BLR / BR)** na stronie oznaczonej jako „guarded / BTI-enabled”, sprawdza, czy pierwsza instrukcja pod adresem celu jest prawidłowym BTI (C, J lub JC, w zależności od dozwolonych). Jeśli nie, następuje **Branch Target Exception**.
- Kodowanie instrukcji BTI zaprojektowano tak, by ponownie wykorzystać opcode’y wcześniej zarezerwowane dla NOPów (w starszych wersjach ARM). Dzięki temu binaria z BTI są wstecznie kompatybilne: na sprzęcie bez wsparcia BTI te instrukcje działają jak NOP.
- Przechodzące przez kompilator fazy, które dodają BTI, wstawiają je tylko tam, gdzie trzeba: do funkcji, które mogą być wywoływane pośrednio, albo do podstawowych bloków będących celami skoków.
- Niektóre poprawki i fragmenty kodu LLVM pokazują, że BTI nie jest wstawiane dla *wszystkich* bloków podstawowych — tylko dla tych, które są potencjalnymi celami skoków (np. z switch / jump tables).
#### Synergia BTI + PAC
PAC chroni wartość wskaźnika (źródło) — zapewnia, że łańcuch pośrednich wywołań / powrotów nie został sfałszowany.
BTI zapewnia, że nawet prawidłowy wskaźnik musi wskazywać na odpowiednio oznakowany punkt wejścia.
Połączenie obu mechanizmów oznacza, że atakujący potrzebuje zarówno prawidłowego wskaźnika z poprawnym PAC, jak i tego, żeby cel miał wstawiony BTI. To zwiększa trudność konstruowania użytecznych gadgetów.
#### Przykład
<details>
<summary>Przykład</summary>
Exploit próbuje pivotować do gadgetu na `0xABCDEF`, który nie zaczyna się od `BTI c`. CPU, przy wykonywaniu `blr x0`, sprawdza cel i wyrzuca błąd, ponieważ wyrównanie/instrukcja nie zawiera prawidłowego 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)**
**Wprowadzone w nowszych rozszerzeniach ARMv8 / wsparcie iOS (dla utwardzonego jądra)**
#### PAN (Privileged Access Never)
- **PAN** to funkcja wprowadzona w **ARMv8.1-A**, która uniemożliwia **uprzywilejowanemu kodowi** (EL1 lub EL2) **czytanie lub zapisywanie** pamięci oznaczonej jako dostępna dla użytkownika (EL0), chyba że PAN jest wyraźnie wyłączone.
- Idea: nawet jeśli kernel zostanie oszukany lub skompromitowany, nie może dowolnie dereferencjonować wskaźników użytkownika bez uprzedniego *wyczyszczenia* PAN, co zmniejsza ryzyko exploitów w stylu **ret2usr** lub niewłaściwego użycia buforów kontrolowanych przez użytkownika.
- Gdy PAN jest włączone (PSTATE.PAN = 1), każde uprzywilejowane polecenie load/store odwołujące się do wirtualnego adresu, który jest „dostępny na EL0”, wywołuje **fault uprawnień**.
- Kernel, gdy musi legalnie uzyskać dostęp do pamięci użytkownika (np. kopiowanie danych do/z buforów użytkownika), musi **tymczasowo wyłączyć PAN** (lub użyć „unprivileged load/store” instrukcji), żeby umożliwić ten dostęp.
- W Linux na ARM64 wsparcie PAN zostało wprowadzone około 2015: poprawki jądra dodały wykrywanie tej funkcji i zastąpiły `get_user` / `put_user` itd. wariantami, które czyszczą PAN wokół dostępu do pamięci użytkownika.
**Kluczowy niuans / ograniczenie / bug**
- Jak zauważyli Siguza i inni, błąd specyfikacji (lub niejednoznaczne zachowanie) w projekcie ARM powoduje, że **execute-only user mappings** (`--x`) mogą **nie wywołać PAN**. Innymi słowy, jeśli strona użytkownika jest oznaczona jako wykonywalna, ale bez uprawnień do odczytu, próba odczytu przez kernel może ominąć PAN, ponieważ architektura może uznać „dostępne na EL0” za wymagające uprawnień do odczytu, a nie tylko wykonywania. To prowadzi do obejścia PAN w pewnych konfiguracjach.
- Z tego powodu, jeśli iOS / XNU pozwala na execute-only user pages (jak w niektórych JIT lub code-cache), kernel może przypadkowo czytać z nich nawet przy włączonym PAN. To jest znany, subtelny obszar potencjalnie wykorzystywalny w niektórych systemach ARMv8+.
#### PXN (Privileged eXecute Never)
- **PXN** to bit w tabeli stron (w wpisach leaf lub block), który wskazuje, że strona nie jest **wykonywalna w trybie uprzywilejowanym** (tj. kiedy EL1 wykonuje kod).
- PXN zapobiega sytuacji, w której kernel (lub jakikolwiek uprzywilejowany kod) wskoczy i wykona instrukcje z pamięci użytkownika, nawet jeśli sterowanie zostało przekierowane. W praktyce blokuje przekierowanie przepływu sterowania jądra do pamięci użytkownika.
- W połączeniu z PAN zapewnia to, że:
1. Kernel nie może (domyślnie) czytać ani zapisywać danych użytkownika (PAN)
2. Kernel nie może wykonywać kodu użytkownika (PXN)
- W formacie tablicy stron ARMv8, wpisy leaf mają bit `PXN` (oraz `UXN` dla execute-never w trybie unprivileged) w polach atrybutów.
Nawet jeśli kernel ma uszkodzony wskaźnik funkcji wskazujący na pamięć użytkownika i spróbuje do niej skoczyć, bit PXN spowoduje fault.
#### Model uprawnień pamięci i jak PAN / PXN mapują się na bity tabeli stron
Aby zrozumieć działanie PAN / PXN, warto spojrzeć na uproszczony model translacji i uprawnień ARM:
- 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ń wykonania.
- Gdy PSTATE.PAN = 1 (włączone), sprzęt egzekwuje zmodyfikowaną semantykę: uprzywilejowane dostępy do stron oznaczonych jako „dostępne na EL0” (tj. dostępnych dla użytkownika) są zabronione (fault).
- Z powodu wspomnianego buga, strony oznaczone tylko jako wykonywalne (bez uprawnień do odczytu) mogą nie być traktowane jako „dostępne na EL0” w pewnych implementacjach, co pozwala obejść PAN.
- Gdy bit PXN jest ustawiony dla strony, nawet jeśli fetch instrukcji pochodzi z wyższego poziomu przywilejów, wykonanie jest zablokowane.
#### Użycie PAN / PXN przez kernel w utwardzonym OS (np. iOS / XNU)
W projekcie utwardzonego jądra (jakim może być używany przez Apple):
- Kernel włącza PAN domyślnie (tak by uprzywilejowany kod był ograniczony).
- W ścieżkach, które legalnie muszą odczytywać lub zapisywać bufory użytkownika (np. kopiowanie w syscallach, I/O, read/write user pointer), kernel tymczasowo **wyłącza PAN** lub używa specjalnych instrukcji by to obejść.
- Po zakończeniu dostępu do danych użytkownika, musi ponownie włączyć PAN.
- PXN jest egzekwowane przez tablice stron: strony użytkownika mają PXN = 1 (więc kernel nie może ich wykonywać), strony kernela nie mają PXN (więc kod kernela może być wykonywany).
- Kernel musi zapewnić, że żadne ścieżki wykonania nie prowadzą do pamięci użytkownika (co omijałoby PXN) — więc łańcuchy exploitów polegające na „skoku do shellcode’u w pamięci użytkownika” są zablokowane.
Z powodu wspomnianego obejścia PAN przez execute-only pages, w rzeczywistym systemie Apple może wyłączyć lub zabronić execute-only user pages albo załatać tę słabość specyfikacji.
#### Powierzchnie ataku, obejścia i łagodzenia
- **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ć uznane za „dostępne na EL0”, więc PAN nie zablokuje odczytu jądra z takich stron w niektórych implementacjach. To daje atakującemu nietypową drogę do przekazania danych przez sekcje execute-only.
- **Temporal window exploit**: jeśli kernel wyłącza PAN na okno dłuższe niż to konieczne, wyścig lub złośliwa ścieżka może wykorzystać to okno do wykonania niezamierzonych dostępów do pamięci użytkownika.
- **Forgotten re-enable**: jeśli ścieżki kodu zapomną ponownie włączyć PAN, kolejne operacje kernela mogą nieprawidłowo uzyskiwać dostęp do pamięci użytkownika.
- **Misconfiguration of PXN**: jeśli tablice stron nie ustawiają PXN na stronach użytkownika lub błędnie mapują strony kodu użytkownika, kernel może zostać oszukany by wykonać kod użytkownika.
- **Speculation / side-channels**: analogicznie do obejść spekulacyjnych, mogą istnieć efektów mikroarchitektoniczne pozwalające na przejściowe naruszenie PAN / PXN (choć takie ataki są silnie zależne od projektu CPU).
- **Complex interactions**: w bardziej zaawansowanych funkcjach (np. JIT, shared memory, regiony kodu tworzonych w locie), kernel może potrzebować drobnej kontroli by pozwolić na pewne dostępy czy wykonanie w obszarach mapowanych dla użytkownika; projektowanie tego bezpiecznie przy PAN/PXN jest niebanalne.
#### Przykład
<details>
<summary>Przykład kodu</summary>
Poniżej ilustracyjne pseudo-assembly pokazujące włączanie/wyłączanie PAN wokół dostępu do pamięci użytkownika i jak może wystąpić fault.
</details>
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs"> </span></div>
// 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
Jeśli kernel **nie** ustawił PXN na tej stronie użytkownika, to skok mógłby się wykonać — co byłoby niebezpieczne.
Jeśli kernel zapomni ponownie włączyć PAN po dostępie do pamięci użytkownika, otwiera się okno, w którym dalsza logika jądra może przypadkowo odczytać/zapisać dowolną pamięć użytkownika.
Jeśli wskaźnik użytkownika wskazuje na stronę execute-only (strona użytkownika z tylko uprawnieniem do wykonania, bez odczytu/zapisu), w wyniku błędu w specyfikacji PAN `ldr W2, [X1]` może **nie** spowodować faultu nawet przy włączonym PAN, co umożliwia obejście zabezpieczeń, zależnie od implementacji.
</details>
<details>
<summary>Example</summary>
Wadliwość jądra próbuje pobrać wskaźnik do funkcji dostarczony przez użytkownika i wywołać go w kontekście jądra (tj. `call user_buffer`). Pod PAN/PXN taka operacja jest zabroniona lub wywołuje fault.
</details>
---
### 11. **Top Byte Ignore (TBI) / Pointer Tagging**
**Introduced in ARMv8.5 / newer (or optional extension)**
TBI oznacza, że najwyższy bajt (najbardziej znaczący bajt) 64-bitowego wskaźnika jest ignorowany przy translacji adresu. Pozwala to systemowi operacyjnemu lub sprzętowi osadzić **bity tagu** w najwyższym bajcie wskaźnika bez wpływu na rzeczywisty adres.
- TBI oznacza **Top Byte Ignore** (czasami nazywane *Address Tagging*). To funkcja sprzętowa (dostępna w wielu implementacjach ARMv8+), która **ignoruje najwyższe 8 bitów** (bity 63:56) 64-bitowego wskaźnika podczas wykonywania **translacji adresu / load/store / fetch instrukcji**.
- W praktyce CPU traktuje wskaźnik `0xTTxxxx_xxxx_xxxx` (gdzie `TT` = najwyższy bajt) jako `0x00xxxx_xxxx_xxxx` do celów translacji adresu, ignorując (maskując) najwyższy bajt. Najwyższy bajt może być użyty przez oprogramowanie do przechowywania **metadanych / bitów tagu**.
- Daje to oprogramowaniu „darmowe” miejsce w paśmie, aby osadzić bajt tagu w każdym wskaźniku bez zmiany wskazywanego adresu pamięci.
- Architektura zapewnia, że loady, store’y i fetch instrukcji traktują wskaźnik z zamaskowanym najwyższym bajtem (tj. bez tagu) przed wykonaniem właściwego dostępu do pamięci.
W ten sposób TBI oddziela **wskaźnik logiczny** (pointer + tag) od **fizycznego adresu** używanego do operacji pamięci.
#### Why TBI: Use cases and motivation
- **Pointer tagging / metadata**: Można przechowywać dodatkowe metadane (np. typ obiektu, wersję, granice, tagi integralności) w tym najwyższym bajcie. Gdy później użyjesz wskaźnika, tag jest ignorowany na poziomie sprzętowym, więc nie musisz go ręcznie usuwać przed dostępem do pamięci.
- **Memory tagging / MTE (Memory Tagging Extension)**: TBI jest podstawowym mechanizmem sprzętowym, na którym opiera się MTE. W ARMv8.5 **Memory Tagging Extension** używa bitów 59:56 wskaźnika jako **logicznego tagu** i porównuje go z **allocation tag** przechowywanym w pamięci.
- **Zwiększone bezpieczeństwo i integralność**: Łącząc TBI z pointer authentication (PAC) lub sprawdzeniami w czasie wykonania, można wymusić poprawność nie tylko wartości wskaźnika, ale też tagu. Atakujący nadpisujący wskaźnik bez prawidłowego tagu spowoduje niezgodność tagów.
- **Kompatybilność**: Ponieważ TBI jest opcjonalne i bity tagu są ignorowane przez sprzęt, istniejący nieoznakowany kod działa dalej normalnie. Bity tagu stają się de facto „nieistotnymi” bitami dla starszego kodu.
#### Example
<details>
<summary>Example</summary>
Wskaźnik do funkcji zawierał tag w najwyższym bajcie (np. `0xAA`). Exploit nadpisuje niższe bity wskaźnika, ale pomija tag, więc gdy kernel weryfikuje lub sanitizuje wskaźnik, trafia na niezgodność i odrzuca go.
</details>
---
### 12. **Page Protection Layer (PPL)**
**Introduced in late iOS / modern hardware (iOS ~17 / Apple silicon / high-end models)** (niektóre raporty wskazują PPL w macOS / Apple silicon, ale Apple wprowadza analogiczne zabezpieczenia do iOS)
- PPL został zaprojektowany jako **granica ochronna wewnątrz jądra**: nawet jeśli kernel (EL1) zostanie przejęty i ma możliwości R/W, **nie powinien móc swobodnie modyfikować** pewnych **wrażliwych stron** (szczególnie page table, metadane podpisów kodu, strony z kodem jądra, entitlements, trust caches itp.).
- Efektywnie tworzy to **„jądro w jądrze”** — mniejszy zaufany komponent (PPL) z **podwyższonymi uprawnieniami**, który samodzielnie może modyfikować chronione strony. Inny kod jądra musi wywoływać rutyny PPL, aby wprowadzić zmiany.
- To zmniejsza powierzchnię ataku dla exploitów jądra: nawet przy pełnym arbitralnym R/W/execute w trybie jądra, kod atakujący musi jeszcze jakoś wejść w domenę PPL (lub obejść PPL), aby zmodyfikować krytyczne struktury.
- Na nowszym Apple silicon (A15+ / M2+) Apple przechodzi do **SPTM (Secure Page Table Monitor)**, które w wielu przypadkach zastępuje PPL dla ochrony page-table na tych platformach.
Oto jak, według publicznej analizy, PPL prawdopodobnie działa:
#### Use of APRR / permission routing (APRR = Access Permission ReRouting)
- Hardware Apple używa mechanizmu zwanego **APRR (Access Permission ReRouting)**, który pozwala wpisom w page table (PTE) zawierać małe indeksy zamiast pełnych bitów uprawnień. Te indeksy są mapowane przez rejestry APRR na faktyczne uprawnienia. Pozwala to na dynamiczne remapowanie uprawnień per domena.
- PPL wykorzystuje APRR do segregacji przywilejów w kontekście jądra: tylko domena PPL ma pozwolenie na aktualizację mapowania między indeksami a efektywnymi uprawnieniami. To znaczy, gdy nie‑PPL kod jądra zapisuje PTE lub próbuje zmienić bity uprawnień, logika APRR zabrania tego (lub wymusza mapowanie tylko do odczytu).
- Kod PPL sam działa w ograniczonym obszarze (np. `__PPLTEXT`), który normalnie jest nie‑wykonywalny lub nie‑zapisowalny aż do chwilowych bramek wejścia. Kernel wywołuje punkty wejścia PPL („PPL routines”), aby wykonać operacje wrażliwe.
#### Gate / Entry & Exit
- Gdy kernel potrzebuje zmodyfikować chronioną stronę (np. zmienić uprawnienia strony z kodem jądra, lub zmodyfikować page tables), wywołuje **funkcję‑opakowanie PPL**, która przeprowadza walidację, a następnie przełącza się do domeny PPL. Poza tą domeną chronione strony są w praktyce tylko do odczytu lub niemodyfikowalne przez główne jądro.
- Podczas wejścia do PPL mapowania APRR są regulowane tak, aby strony pamięci w regionie PPL były ustawiane jako **wykonywalne i zapisywalne** w kontekście PPL. Po wyjściu zwracane są do trybu tylko do odczytu / nie‑zapisowalnego. To zapewnia, że tylko audytowane rutyny PPL mogą pisać do chronionych stron.
- Poza PPL próby przez kod jądra, by zapisać do tych chronionych stron, będą powodować fault (brak uprawnień), ponieważ mapowanie APRR dla tej domeny kodu nie pozwala na zapis.
#### Protected page categories
Strony, które PPL typowo chroni, to:
- Struktury page table (translation table entries, metadata mapowania)
- Strony kodu jądra, zwłaszcza te zawierające krytyczną logikę
- Metadane code‑sign (trust caches, bloby podpisów)
- Tabele entitlements, tabele wymuszania podpisów
- Inne wysokowartościowe struktury jądra, których patchowanie pozwoliłoby obejść sprawdzanie podpisów lub manipulować poświadczeniami
Idea jest taka, że nawet jeśli pamięć jądra jest w pełni kontrolowana, atakujący nie może po prostu załatać lub przepisać tych stron, chyba że również przełamie rutyny PPL lub obejdzie PPL.
#### Known Bypasses & Vulnerabilities
1. **Project Zero’s PPL bypass (stale TLB trick)**
- Publiczny opis przez Project Zero przedstawia obejście wykorzystujące **stare (stale) wpisy TLB**.
- Koncepcja:
1. Alokuj dwie strony fizyczne A i B, oznacz je jako strony PPL (czyli chronione).
2. Zmapuj dwa wirtualne adresy P i Q, których strony tabeli L3 pochodzą z A i B.
3. Uruchom wątek, który ciągle odwiedza Q, utrzymując aktywny wpis TLB dla Q.
4. Wywołaj `pmap_remove_options()` aby usunąć mapowania zaczynające się od P; z powodu buga kod błędnie usuwa TTE zarówno dla P jak i Q, ale unieważnia tylko wpis TLB dla P, pozostawiając „stary” wpis TLB dla Q aktywny.
5. Ponownie użyj B (strony tabeli Q) do zmapowania dowolnej pamięci (np. stron chronionych przez PPL). Ponieważ stary wpis TLB nadal mapuje poprzednie mapowanie dla Q, to mapowanie pozostaje ważne dla tego kontekstu.
6. Dzięki temu atakujący może wstawić zapisywalne mapowanie stron chronionych przez PPL bez przechodzenia przez interfejs PPL.
- Ten exploit wymagał precyzyjnej kontroli mapowania fizycznego i zachowania TLB. Pokazuje, że granica bezpieczeństwa oparta na poprawności TLB / mapowania musi być bardzo ostrożna wobec unieważnień TLB i spójności mapowań.
- Project Zero skomentowało, że obejścia tego typu są subtelne i rzadkie, ale możliwe w złożonych systemach. Mimo to oceniają PPL jako solidne zabezpieczenie.
2. **Other potential hazards & constraints**
- Jeśli exploit jądra może bezpośrednio wejść do rutyn PPL (poprzez wywołanie wrapperów PPL), może obejść ograniczenia. Dlatego walidacja argumentów jest krytyczna.
- Błędy w samym kodzie PPL (np. overflow arytmetyczny, błędy w sprawdzeniach granic) mogą pozwolić na modyfikacje poza zakresem wewnątrz PPL. Project Zero zaobserwowało, że taki błąd w `pmap_remove_options_internal()` został wykorzystany w ich obejściu.
- Granica PPL jest nieodwracalnie związana z egzekucją sprzętową (APRR, kontroler pamięci), więc jest silna tylko tyle, ile silna jest implementacja sprzętowa.
#### Example
<details>
<summary>Code Example</summary>
Poniżej uproszczony pseudokod / logika pokazująca, jak kernel mógłby wywołać PPL, by zmodyfikować chronione strony:
</details>
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs">c</span></div>
```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
Exploit kernela próbuje nadpisać entitlement table lub wyłączyć egzekwowanie code-sign przez modyfikację kernel signature blob. Ponieważ ta strona jest chroniona przez PPL, zapis jest zablokowany, chyba że przeprowadzony przez interfejs PPL. Tak więc nawet przy wykonaniu kodu w kernelu nie można obejść ograniczeń code-sign ani dowolnie modyfikować danych poświadczeń. Na iOS 17+ niektóre urządzenia używają SPTM, by dodatkowo izolować strony zarządzane przez PPL.PPL → SPTM / Zastąpienia / Przyszłość
- Na nowoczesnych SoC Apple (A15 lub nowsze, M2 lub nowsze), Apple wspiera SPTM (Secure Page Table Monitor), które zastępuje PPL w ochronie page table.
- Apple wskazuje w dokumentacji: “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.”
- Architektura SPTM prawdopodobnie przesuwa więcej egzekwowania polityk do bardziej uprzywilejowanego monitora poza kontrolą kernelu, jeszcze bardziej redukując granicę zaufania.
MTE | EMTE | MIE
Oto opis na wyższym poziomie, jak EMTE działa w konfiguracji MIE Apple:
- Przypisanie taga
- Gdy pamięć jest alokowana (np. w kernelu lub user space przez secure allocators), do bloku przypisywany jest secret tag.
- Pointer zwracany użytkownikowi lub kernelowi zawiera ten tag w swoich górnych bitach (używając TBI / top byte ignore mechanisms).
- Sprawdzanie taga przy dostępie
- Kiedy wykonywany jest load lub store z użyciem pointera, hardware sprawdza, czy tag w pointerze pasuje do taga bloku pamięci (allocation tag). Jeśli nie pasuje, następuje fault natychmiast (ponieważ jest to synchronous).
- Ponieważ jest to synchronous, nie ma okna „delayed detection”.
- Retagowanie przy free / ponownym użyciu
- Gdy pamięć jest zwalniana, allocator zmienia tag bloku (więc starsze pointery ze starymi tagami przestają pasować).
- Wskaźnik będący use-after-free będzie więc miał przestarzały tag i spowoduje mismatch przy dostępie.
- Rozróżnianie tagów sąsiednich bloków, aby wykryć overflows
- Sąsiednim alokacjom przypisywane są różne tagi. Jeśli buffer overflow przelije się do pamięci sąsiada, mismatch tagów spowoduje fault.
- To jest szczególnie skuteczne w wykrywaniu małych overflowów, które przekraczają granicę.
- Egzekwowanie poufności tagów
- Apple musi zapobiegać leak wartości tagów (ponieważ jeśli attacker pozna tag, mógłby stworzyć pointery z poprawnymi tagami).
- Wprowadzono zabezpieczenia (microarchitectural / speculative controls), aby unikać side-channel leakage bitów tagu.
- Integracja kernelu i user-space
- Apple używa EMTE nie tylko w user-space, ale też w kernel / komponentach krytycznych dla OS (aby chronić kernel przed memory corruption).
- Hardware/OS zapewniają, że reguły tagów obowiązują nawet gdy kernel wykonuje kod w imieniu 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 ramach tej samej alokacji (nie przekracza granicy) i tag pozostaje taki sam, tag mismatch tego nie wykryje.
- **Tag width limitation**: Dostępnych jest tylko kilka bitów (np. 4 bity, lub mała domena) dla tagów — ograniczona przestrzeń nazw.
- **Side-channel leaks**: Jeśli bitów tagu można leakować (via cache / speculative execution), atakujący może poznać poprawne tagi i obejść mechanizm. Apple’s tag confidentiality enforcement ma to ograniczyć.
- **Performance overhead**: Sprawdzanie tagu przy każdym load/store dodaje koszt; Apple musi zoptymalizować hardware, aby ograniczyć narzut.
- **Compatibility & fallback**: Na starszym sprzęcie lub częściach, które nie wspierają EMTE, musi istnieć fallback. Apple twierdzi, że MIE jest włączone tylko na urządzeniach z obsługą.
- **Complex allocator logic**: Allocator musi zarządzać tagami, retagging, wyrównywaniem granic i unikać kolizji mis-tag. Błędy w logice allocator-a 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 zabezpieczeń mikroarchitektonicznych, speculative execution lub micro-op fusions mogą transientnie obejść checks lub leakować bity tagu.
- **Limited to supported regions**: Apple może egzekwować EMTE tylko w wybranych, wysokiego ryzyka obszarach (kernel, security-critical subsystems), nie globalnie.
---
## Kluczowe usprawnienia / różnice w porównaniu do standardowego MTE
Here are the improvements and changes Apple emphasizes:
| Feature | Original MTE | EMTE (Apple’s enhanced) / MIE |
|---|---|---|
| **Check mode** | Supports synchronous and asynchronous modes. In async, tag mismatches are reported later (delayed)| Apple insists on **synchronous mode** by default—tag mismatches are caught immediately, no delay/race windows allowed.|
| **Coverage of non-tagged memory** | Accesses to non-tagged memory (e.g. globals) may bypass checks in some implementations | EMTE requires that accesses from a tagged region to non-tagged memory also validate tag knowledge, making it harder to bypass by mixing allocations.|
| **Tag confidentiality / secrecy** | Tags might be observable or leaked via side channels | Apple adds **Tag Confidentiality Enforcement**, which attempts to prevent leakage of tag values (via speculative side-channels etc.).|
| **Allocator integration & retagging** | MTE leaves much of allocator logic to software | Apple’s secure typed allocators (kalloc_type, xzone malloc, etc.) integrate with EMTE: when memory is allocated or freed, tags are managed at fine granularity.|
| **Always-on by default** | In many platforms, MTE is optional or off by default | Apple enables EMTE / MIE by default on supported hardware (e.g. iPhone 17 / A19) for kernel and many user processes.|
Because Apple controls both the hardware and software stack, it can enforce EMTE tightly, avoid performance pitfalls, and close side-channel holes.
---
## How EMTE works in practice (Apple / MIE)
Here’s a higher-level description of how EMTE operates under Apple’s MIE setup:
1. **Tag assignment**
- When memory is allocated (e.g. in kernel or user space via secure allocators), a **secret tag** is assigned to that block.
- The pointer returned to the user or kernel includes that tag in its high bits (using TBI / top byte ignore mechanisms).
2. **Tag checking on access**
- Whenever a load or store is executed using a pointer, the hardware checks that the pointer’s tag matches the memory block’s tag (allocation tag). If mismatch, it faults immediately (since synchronous).
- Because it's synchronous, there is no “delayed detection” window.
3. **Retagging on free / reuse**
- When memory is freed, the allocator changes the block’s tag (so older pointers with old tags no longer match).
- A use-after-free pointer would therefore have a stale tag and mismatch when accessed.
4. **Neighbor-tag differentiation to catch overflows**
- Adjacent allocations are given distinct tags. If a buffer overflow spills into neighbor’s memory, tag mismatch causes a fault.
- This is especially powerful in catching small overflows that cross boundary.
5. **Tag confidentiality enforcement**
- Apple must prevent tag values being leaked (because if attacker learns the tag, they could craft pointers with correct tags).
- They include protections (microarchitectural / speculative controls) to avoid side-channel leakage of tag bits.
6. **Kernel and user-space integration**
- Apple uses EMTE not just in user-space but also in kernel / OS-critical components (to guard kernel against memory corruption).
- The hardware/OS ensures tag rules apply even when kernel is executing on behalf of user space.
Because EMTE is built into MIE, Apple uses EMTE in synchronous mode across key attack surfaces, not as opt-in or debugging mode.
---
## Exception handling in XNU
When an **exception** occurs (e.g., `EXC_BAD_ACCESS`, `EXC_BAD_INSTRUCTION`, `EXC_CRASH`, `EXC_ARM_PAC`, etc.), the **Mach layer** of the XNU kernel is responsible for intercepting it before it becomes a UNIX-style **signal** (like `SIGSEGV`, `SIGBUS`, `SIGILL`, ...).
This process involves multiple layers of exception propagation and handling before reaching user space or being converted to a BSD signal.
### Exception Flow (High-Level)
1. **CPU triggers a synchronous exception** (e.g., invalid pointer dereference, PAC failure, illegal instruction, etc.).
2. **Low-level trap handler** runs (`trap.c`, `exception.c` in XNU source).
3. The trap handler calls **`exception_triage()`**, the core of the Mach exception handling.
4. `exception_triage()` decides how to route the exception:
- First to the **thread's exception port**.
- Then to the **task's exception port**.
- Then to the **host's exception port** (often `launchd` or `ReportCrash`).
If none of these ports handle the exception, the kernel may:
- **Convert it into a BSD signal** (for user-space processes).
- **Panic** (for kernel-space exceptions).
### Core Function: `exception_triage()`
The function `exception_triage()` routes Mach exceptions up the chain of possible handlers until one handles it or until it's finally fatal. It's defined in `osfmk/kern/exception.c`.
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs">c</span></div>
```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()
→ przetłumaczone 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 o wyjątkach.
task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()
Każdy exception port ma:
- maskę (które wyjątki chce otrzymywać)
- port name (Mach port odbierający wiadomości)
- behavior (sposób, w jaki kernel wysyła wiadomość)
- flavor (który thread state ma zostać dołączony)
Debuggers and Exception Handling
A 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 nie obsłuży wyjątku, propaguje się on na następny poziom (task → host).
Flow of EXC_BAD_ACCESS
-
Wątek dereferencuje nieprawidłowy wskaźnik → CPU zgłasza Data Abort.
-
Kernel trap handler wywołuje
exception_triage(EXC_BAD_ACCESS, ...)
. -
Wiadomość wysyłana do:
-
Thread port → (debugger może przechwycić breakpoint).
-
Jeśli debugger zignoruje → Task port → (handler na poziomie procesu).
-
Jeśli zignorowane → Host port (zwykle ReportCrash).
- Jeśli nikt nie obsłuży →
bsd_exception()
tłumaczy naSIGSEGV
.
PAC Exceptions
Kiedy Pointer Authentication (PAC) zawiedzie (niezgodność podpisu), podnoszony jest specjalny Mach exception:
EXC_ARM_PAC
(typ)- Kody mogą zawierać szczegóły (np. typ klucza, typ wskaźnika).
Jeśli binarium ma flagę TFRO_PAC_EXC_FATAL
, kernel traktuje błędy PAC jako fatalne, pomijając przechwycenie przez debugger. Ma to zapobiegać użyciu debuggerów do obchodzenia kontroli PAC i jest włączone dla platform binaries.
Software Breakpoints
Software breakpoint (int3
na x86, brk
na ARM64) jest implementowany przez wywołanie celowego błędu.
Debugger łapie to przez exception port:
- Modyfikuje instruction pointer lub pamięć.
- Przywraca oryginalną instrukcję.
- Wznawia wykonanie.
To ten sam mechanizm, który pozwala „złapać” wyjątek PAC — chyba że TFRO_PAC_EXC_FATAL
jest ustawione, wtedy nigdy nie dociera on do debuggera.
Conversion to BSD Signals
Jeśli żaden handler nie zaakceptuje wyjątku:
-
Kernel wywołuje
task_exception_notify() → bsd_exception()
. -
To mapuje Mach exceptions na sygnały:
Mach Exception | Signal |
---|---|
EXC_BAD_ACCESS | SIGSEGV or SIGBUS |
EXC_BAD_INSTRUCTION | SIGILL |
EXC_ARITHMETIC | SIGFPE |
EXC_SOFTWARE | SIGTRAP |
EXC_BREAKPOINT | SIGTRAP |
EXC_CRASH | SIGKILL |
EXC_ARM_PAC | SIGILL (on non-fatal) |
### Key Files in XNU Source
-
osfmk/kern/exception.c
→ Rdzeńexception_triage()
,exception_deliver_*()
. -
bsd/kern/kern_sig.c
→ Logika dostarczania sygnałów. -
osfmk/arm64/trap.c
→ Niskopoziomowe handlers pułapek. -
osfmk/mach/exc.h
→ Kody wyjątków i struktury. -
osfmk/kern/task.c
→ Konfiguracja task exception port.
Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)
Kernel używał zone allocator (kalloc
) podzielonego na strefy o stałych rozmiarach ("zones").
Każda zone przechowywała alokacje tylko jednej klasy rozmiaru.
Ze zrzutu ekranu:
Zone Name | Element Size | Example Use |
---|---|---|
default.kalloc.16 | 16 bytes | Very small kernel structs, pointers. |
default.kalloc.32 | 32 bytes | Small structs, object headers. |
default.kalloc.64 | 64 bytes | IPC messages, tiny kernel buffers. |
default.kalloc.128 | 128 bytes | Medium objects like parts of OSObject . |
… | … | … |
default.kalloc.1280 | 1280 bytes | Large structures, IOSurface/graphics metadata. |
Jak to działało:
- Każde żądanie alokacji było zaokrąglane w górę do najbliższego rozmiaru zone.
(Np. żądanie 50 bajtów trafiało do
kalloc.64
). - Pamięć w każdej zone była utrzymywana w freelisting — kawałki zwolnione przez kernel wracały do tej zone.
- Jeśli przepełniłeś bufor 64-bajtowy, nadpisywałeś następny obiekt w tej samej zone.
Dlatego heap spraying / feng shui było tak skuteczne: można było przewidzieć sąsiadów obiektów, rozrzucając alokacje tej samej klasy rozmiaru.
The freelist
W każdej strefie kalloc zwolnione obiekty nie były zwracane bezpośrednio do systemu — 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 zone.
-
Zone trzymała HEAD wskaźnik na pierwszy wolny chunk.
-
Alokacja zawsze używała aktualnego HEAD:
-
Pop HEAD (zwróć tę pamięć wywołującemu).
-
Zaktualizuj HEAD = HEAD->next (przechowywane w nagłówku zwolnionego chunka).
-
Zwalnianie wkładało chunki z powrotem:
-
freed_chunk->next = HEAD
-
HEAD = freed_chunk
Freelist był więc zwykłą listą połączoną zbudowaną wewnątrz samej zwolnionej pamięci.
Normalny stan:
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)
Wykorzystywanie freelist
Ponieważ pierwsze 8 bajtów free chunk = freelist pointer, atakujący może go uszkodzić:
-
Heap overflow into an adjacent freed chunk → nadpisać jego “next” pointer.
-
Use-after-free write into a freed object → nadpisać jego “next” pointer.
Następnie, przy następnym alokowaniu tej wielkości:
-
Allocator pobiera (pops) skorumpowany chunk.
-
Podąża za dostarczonym przez atakującego “next” pointerem.
-
Zwraca wskaźnik do arbitralnej pamięci, umożliwiając 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 →
OSData
maps tokalloc_type_osdata
zone (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
.ipsw
files. - Decompress until you get the bin format of the kernelcache of both
.ipsw
files. You have information on how to do this on:
macOS Kernel Extensions & Kernelcache
- 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 BinDiff
and 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
.
iMessage/Media Parser Zero-Click Chains
Imessage Media Parser Zero Click Coreaudio Pac Bypass
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.