iOS Exploiting

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

iOS Exploit Mitigations

1. Code Signing / Runtime Signature Verification

Introduced early (iPhone OS → iOS)
This is one of the fundamental protections: all executable code (apps, dynamic libraries, JIT-ed code, extensions, frameworks, caches) must be cryptographically signed by a certificate chain rooted in Apple’s trust. At runtime, before loading a binary into memory (or before performing jumps across certain boundaries), the system checks its signature. If the code is modified (bit-flipped, patched) or unsigned, the load fails.

  • Thwarts: the “classic payload drop + execute” stage in exploit chains; arbitrary code injection; modifying an existing binary to insert malicious logic.
  • Mechanism detail:
    • The Mach-O loader (and dynamic linker) checks code pages, segments, entitlements, team IDs, and that the signature covers the file’s contents.
    • For memory regions like JIT caches or dynamically generated code, Apple enforces that pages be signed or validated via special APIs (e.g. mprotect with code-sign checks).
    • The signature includes entitlements and identifiers; the OS enforces that certain APIs or privileged capabilities require specific entitlements that cannot be forged.
Example Suppose an exploit obtains code execution in a process and tries to write shellcode into a heap and jump to it. On iOS, that page would need to be flagged executable **and** satisfy code-signature constraints. Since the shellcode isn’t signed with Apple’s certificate, the jump fails or the system rejects making that memory region executable.

2. CoreTrust

Introduced around iOS 14+ era (or gradually in newer devices / later iOS)
CoreTrust is the subsystem that performs runtime signature validation of binaries (including system and user binaries) against Apple’s root certificate rather than relying on cached userland trust stores.

  • Thwarts: post-install tampering of binaries, jailbreaking techniques that try to swap or patch system libraries or user apps; tricking the system by replacing trusted binaries with malicious counterparts.
  • Mechanism detail:
    • Instead of trusting a local trust database or certificate cache, CoreTrust fetches or refers to Apple’s root directly or verifies intermediate certificates in a secure chain.
    • It ensures that modifications (e.g. in the filesystem) to existing binaries are detected and rejected.
    • It ties entitlements, team IDs, code signing flags, and other metadata to the binary at load time.
Example A jailbreak might try to replace `SpringBoard` or `libsystem` with a patched version to gain persistence. But when the OS’s loader or CoreTrust checks, it notices the signature mismatch (or modified entitlements) and refuses to execute.

3. Data Execution Prevention (DEP / NX / W^X)

Introduced in many OSes earlier; iOS had NX-bit / w^x for a long time
DEP enforces that pages marked writable (for data) are non-executable, and pages marked executable are non-writable. You can’t simply write shellcode into a heap or stack region and execute it.

  • Thwarts: direct shellcode execution; classic buffer-overflow → jump to injected shellcode.
  • Mechanism detail:
    • The MMU / memory protection flags (via page tables) enforce the separation.
    • Any attempt to mark a writable page executable triggers a system check (and is either forbidden or requires code-sign approval).
    • In many cases, making pages executable requires going through OS APIs that enforce additional constraints or checks.
Example An overflow writes shellcode onto the heap. The attacker attempts `mprotect(heap_addr, size, PROT_EXEC)` to make it executable. But the system refuses or validates that the new page must pass code-sign constraints (which the shellcode cannot).

4. Address Space Layout Randomization (ASLR)

Introduced in iOS ~4–5 era (roughly iOS 4–5 timeframe)
ASLR randomizes the base addresses of key memory regions: libraries, heap, stack, etc., on each process launch. Gadgets’ addresses move between runs.

  • Thwarts: hardcoding gadget addresses for ROP/JOP; static exploit chains; blind jumping to known offsets.
  • Mechanism detail:
    • Each loaded library / dynamic module is rebased at a randomized offset.
    • Stack and heap base pointers are randomized (within certain entropy limits).
    • Sometimes other regions (e.g. mmap allocations) are also randomized.
    • Combined with information-leak mitigations, it forces the attacker to first leak an address or pointer to discover base addresses at runtime.
Example A ROP chain expects gadget at `0x….lib + offset`. But since `lib` is relocated differently each run, the hardcoded chain fails. An exploit must first leak the base address of the module before computing gadget addresses.

5. Kernel Address Space Layout Randomization (KASLR)

Introduced in iOS ~ (iOS 5 / iOS 6 timeframe)
Analogous to user ASLR, KASLR randomizes the base of the kernel text and other kernel structures at boot time.

  • Thwarts: kernel-level exploits that rely on fixed location of kernel code or data; static kernel exploits.
  • Mechanism detail:
    • On each boot, the kernel’s base address is randomized (within a range).
    • Kernel data structures (like task_structs, vm_map, etc.) may also be relocated or offset.
    • Attackers must first leak kernel pointers or use information disclosure vulnerabilities to compute offsets before hijacking kernel structures or code.
Example A local vulnerability aims to corrupt a kernel function pointer (e.g. in `vtable`) at `KERN_BASE + offset`. But since `KERN_BASE` is unknown, the attacker must leak it first (e.g. via a read primitive) before computing the correct address for corruption.

6. Kernel Patch Protection (KPP / AMCC)

Introduced in newer iOS / A-series hardware (post around iOS 15–16 era or newer chips)
KPP (aka AMCC) continuously monitors kernel text pages’ integrity (via hash or checksum). If it detects tampering (patches, inline hooks, code modifications) outside allowed windows, it triggers a kernel panic or reboot.

  • Thwarts: persistent kernel patching (modifying kernel instructions), inline hooks, static function overwrites.
  • Mechanism detail:
    • A hardware or firmware module monitors the kernel text region.
    • It periodically or on-demand re-hashes the pages and compares against expected values.
    • If mismatches occur outside benign update windows, it panics the device (to avoid persistent malicious patch).
    • Attackers must either avoid detection windows or use legitimate patch paths.
Example An exploit tries to patch a kernel function prologue (e.g. `memcmp`) to intercept calls. But KPP notices that the code page’s hash no longer matches the expected value and triggers a kernel panic, crashing the device before the patch can stabilize.

7. Kernel Text Read‐Only Region (KTRR)

Introduced in modern SoCs (post ~A12 / newer hardware)
KTRR is a hardware-enforced mechanism: once the kernel text is locked early during boot, it becomes read-only from EL1 (the kernel), preventing further writes to code pages.

  • Thwarts: any modifications to kernel code after boot (e.g. patching, in-place code injection) at EL1 privilege level.
  • Mechanism detail:
    • During boot (in secure/bootloader stage), the memory controller (or a secure hardware unit) marks the physical pages containing kernel text as read-only.
    • Even if an exploit gains full kernel privileges, it cannot write to those pages to patch instructions.
    • To modify them, the attacker must first compromise the boot chain, or subvert KTRR itself.
Example A privilege-escalation exploit jumps into EL1 and writes a trampoline into a kernel function (e.g. in `syscall` handler). But because the pages are locked read-only by KTRR, the write fails (or triggers fault), so patches aren’t applied.

8. Pointer Authentication Codes (PAC)

Introduced with ARMv8.3 (hardware), Apple beginning with A12 / iOS ~12+

  • PAC is a hardware feature introduced in ARMv8.3-A to detect the tampering of pointer values (return addresses, function pointers, certain data pointers) by embedding a small cryptographic signature (a “MAC”) into unused high bits of the pointer.
  • The signature (“PAC”) is computed over the pointer value plus a modifier (a context value, e.g. stack pointer or some distinguishing data). That way the same pointer value in different contexts gets a different PAC.
  • At use time, before dereferencing or branching via that pointer, an authenticate instruction checks the PAC. If valid, the PAC is stripped and the pure pointer is obtained; if invalid, the pointer becomes “poisoned” (or a fault is raised).
  • The keys used for producing/validating PACs live in privileged registers (EL1, kernel) and are not directly readable from user mode.
  • Because not all 64 bits of a pointer are used in many systems (e.g. 48-bit address space), the upper bits are “spare” and can hold the PAC without altering the effective address.

Architectural Basis & Key Types

  • ARMv8.3 introduces five 128-bit keys (each implemented via two 64-bit system registers) for pointer authentication.

    • APIAKey — for instruction pointers (domain “I”, key A)
    • APIBKey — second instruction pointer key (domain “I”, key B)
    • APDAKey — for data pointers (domain “D”, key A)
    • APDBKey — for data pointers (domain “D”, key B)
    • APGAKey — “generic” key, for signing non-pointer data or other generic uses
  • These keys are stored in privileged system registers (accessible only at EL1/EL2 etc.), not accessible from user mode.

  • The PAC is computed via a cryptographic function (ARM suggests QARMA as the algorithm) using:

    1. The pointer value (canonical portion)
    2. A modifier (a context value, like a salt)
    3. The secret key
    4. Some internal tweak logic
      If the resulting PAC matches what is stored in the upper bits of the pointer, authentication succeeds.

Instruction Families

The naming convention is: PAC / AUT / XPAC, then domain letters.

  • PACxx instructions sign a pointer and insert a PAC
  • AUTxx instructions authenticate + strip (validate and remove the PAC)
  • XPACxx instructions strip without validating

Domains / suffixes:

MnemonicMeaning / DomainKey / DomainExample Usage in Assembly
PACIASign instruction pointer with APIAKey“I, A”PACIA X0, X1 — sign pointer in X0 using APIAKey with modifier X1
PACIBSign instruction pointer with APIBKey“I, B”PACIB X2, X3
PACDASign data pointer with APDAKey“D, A”PACDA X4, X5
PACDBSign data pointer with APDBKey“D, B”PACDB X6, X7
PACG / PACGAGeneric (non-pointer) signing with APGAKey“G”PACGA X8, X9, X10 (sign X9 with modifier X10 into X8)
AUTIAAuthenticate APIA-signed instruction pointer & strip PAC“I, A”AUTIA X0, X1 — check PAC on X0 using modifier X1, then strip
AUTIBAuthenticate APIB domain“I, B”AUTIB X2, X3
AUTDAAuthenticate APDA-signed data pointer“D, A”AUTDA X4, X5
AUTDBAuthenticate APDB-signed data pointer“D, B”AUTDB X6, X7
AUTGAAuthenticate generic / blob (APGA)“G”AUTGA X8, X9, X10 (validate generic)
XPACIStrip PAC (instruction pointer, no validation)“I”XPACI X0 — remove PAC from X0 (instruction domain)
XPACDStrip PAC (data pointer, no validation)“D”XPACD X4 — remove PAC from data pointer in X4

There are specialized / alias forms:

  • PACIASP is shorthand for PACIA X30, SP (sign the link register using SP as modifier)
  • AUTIASP is AUTIA X30, SP (authenticate link register with SP)
  • Combined forms like RETAA, RETAB (authenticate-and-return) or BLRAA (authenticate & branch) exist in ARM extensions / compiler support.
  • Also zero-modifier variants: PACIZA / PACIZB where the modifier is implicitly zero, etc.

Modifiers

The main goal of the modifier is to bind the PAC to a specific context so the same address signed in different contexts yields different PACs. This prevents simple pointer reuse across frames or objects. Is like adding a salt to a hash.

Therefore:

  • The modifier is a context value (another register) that is mixed into the PAC computation. Typical choices: the stack pointer (SP), a frame pointer, or some object ID.
  • Using SP as modifier is common for return address signing: the PAC becomes tied to the specific stack frame. If you try to reuse the LR in a different frame, the modifier changes, so PAC validation fails.
  • The same pointer value signed under different modifiers yields different PACs.
  • The modifier does not need to be secret, but ideally it is not attacker-controlled.
  • For instructions that sign or verify pointers where no meaningful modifier exists, some forms use zero or an implicit constant.

Apple / iOS / XNU Customizations & Observations

  • Apple’s PAC implementation includes per-boot diversifiers so that keys or tweaks change each boot, preventing reuse across boots.
  • They also include cross-domain mitigations so that PACs signed in user mode can’t easily be reused in kernel mode, etc.
  • On Apple M1 / Apple Silicon, reverse engineering showed that there are nine modifier types and Apple-specific system registers for key control.
  • Apple uses PAC across many kernel subsystems: return address signing, pointer integrity in kernel data, signed thread contexts, etc.
  • Google Project Zero showed how under a powerful memory read/write primitive in kernel, one could forge kernel PACs (for A keys) on A12-era devices, but Apple patched many of those paths.
  • In Apple’s system, some keys are global across kernel, while user processes may get per-process key randomness.

PAC Bypasses

  1. Kernel-mode PAC: theoretical vs real bypasses
  • Because kernel PAC keys and logic are tightly controlled (privileged registers, diversifiers, domain isolation), forging arbitrary signed kernel pointers is very hard.
  • Azad’s 2020 “iOS Kernel PAC, One Year Later” reports that in iOS 12-13, he found a few partial bypasses (signing gadgets, reuse of signed states, unprotected indirect branches) but no full generic bypass. bazad.github.io
  • Apple’s “Dark Magic” customizations further narrow exploitable surfaces (domain switching, per-key enabling bits). i.blackhat.com
  • There is a known kernel PAC bypass CVE-2023-32424 on Apple silicon (M1/M2) reported by Zecao Cai et al. i.blackhat.com
  • But these bypasses often rely on very specific gadgets or implementation bugs; they’re not general-purpose bypass.

Thus kernel PAC is considered highly robust, though not perfect.

  1. User-mode / runtime PAC bypass techniques

These are more common, and exploit imperfections in how PAC is applied or used in dynamic linking / runtime frameworks. Below are classes, with examples.

2.1 Shared Cache / A key issues

  • The dyld shared cache is a large pre-linked blob of system frameworks and libraries. Because it is so widely shared, function pointers inside the shared cache are “pre-signed” and then used by many processes. Attackers target these already-signed pointers as “PAC oracles”.

  • Some bypass techniques try to extract or reuse A-key signed pointers present in the shared cache and reuse them in gadgets.

  • The “No Clicks Required” talk describes building an oracle over the shared cache to infer relative addresses and combine that with signed pointers to bypass PAC. saelo.github.io

  • Also, imports of function pointers from shared libraries in userspace were found to be insufficiently protected by PAC, letting an attacker get function pointers without changing their signature. (Project Zero bug entry) bugs.chromium.org

2.2 dlsym(3) / dynamic symbol resolution

  • One known bypass is to call dlsym() to get an already signed function pointer (signed with A-key, diversifier zero) and then use it. Because dlsym returns a legitimately signed pointer, using it circumvents the need to forge PAC.

  • Epsilon’s blog details how some bypasses exploit this: calling dlsym("someSym") yields a signed pointer and can be used for indirect calls. blog.epsilon-sec.com

  • Synacktiv’s “iOS 18.4 — dlsym considered harmful” describes a bug: some symbols resolved via dlsym on iOS 18.4 return pointers that are incorrectly signed (or with buggy diversifiers), enabling unintended PAC bypass. Synacktiv

  • The logic in dyld for dlsym includes: when result->isCode, they sign the returned pointer with __builtin_ptrauth_sign_unauthenticated(..., key_asia, 0), i.e. context zero. blog.epsilon-sec.com

Thus, dlsym is a frequent vector in user-mode PAC bypasses.

2.3 Other DYLD / runtime relocations

  • The DYLD loader and dynamic relocation logic is complex and sometimes temporarily maps pages as read/write to perform relocations, then switches them back to read-only. Attackers exploit these windows. Synacktiv’s talk describes “Operation Triangulation”, a timing-based bypass of PAC via dynamic relocations. Synacktiv

  • DYLD pages are now protected with SPRR / VM_FLAGS_TPRO (some protection flags for dyld). But earlier versions had weaker guards. Synacktiv

  • In WebKit exploit chains, the DYLD loader is often a target for PAC bypass. The slides mention that many PAC bypasses have targeted the DYLD loader (via relocation, interposer hooks). Synacktiv

2.4 NSPredicate / NSExpression / ObjC / SLOP

  • In userland exploit chains, Objective-C runtime methods such as NSPredicate, NSExpression or NSInvocation are used to smuggle control calls without obvious pointer forging.

  • On older iOS (before PAC), an exploit used fake NSInvocation objects to call arbitrary selectors on controlled memory. With PAC, modifications are needed. But the technique SLOP (SeLector Oriented Programming) is extended under PAC too. Project Zero

  • The original SLOP technique allowed chaining of ObjC calls by creating fake invocations; the bypass relies on the fact that ISA or selector pointers are sometimes not PAC-protected fully. Project Zero

  • In environments where pointer authentication is applied partially, methods / selectors / target pointers may not always have PAC protection, giving room for bypass.

Example Flow

Example Signing & Authenticating
; Example: function prologue / return address protection  
my_func:  
    stp x29, x30, [sp, #-0x20]!        ; push frame pointer + LR  
    mov x29, sp  
    PACIASP                            ; sign LR (x30) using SP as modifier  
    ; … body …  
    mov sp, x29  
    ldp x29, x30, [sp], #0x20         ; restore  
    AUTIASP                            ; authenticate & strip PAC  
    ret  

; Example: indirect function pointer stored in a struct  
    ; suppose X1 contains a function pointer  
    PACDA X1, X2     ; sign data pointer X1 with context X2  
    STR X1, [X0]      ; store signed pointer  

    ; later retrieval:  
    LDR X1, [X0]  
    AUTDA X1, X2       ; authenticate & strip  
    BLR X1             ; branch to valid target  

; Example: stripping for comparison (unsafe)  
    LDR X1, [X0]  
    XPACI X1           ; strip PAC (instruction domain)  
    CMP X1, #some_label_address  
    BEQ matched_label  
Example A buffer overflow overwrites a return address on the stack. The attacker writes the target gadget address but cannot compute the correct PAC. When the function returns, the CPU’s `AUTIA` instruction faults because the PAC mismatch. The chain fails. Project Zero’s analysis on A12 (iPhone XS) showed how Apple’s PAC is used and methods of forging PACs if an attacker has a memory read/write primitive.

9. Branch Target Identification (BTI)

Introduced with ARMv8.5 (later hardware)
BTI is a hardware feature that checks indirect branch targets: when executing blr or indirect calls/jumps, the target must begin with a BTI landing pad (BTI j or BTI c). Jumping into gadget addresses that lack the landing pad triggers an exception.

LLVM’s implementation notes three variants of BTI instructions and how they map to branch types.

BTI VariantWhat it permits (which branch types)Typical placement / use case
BTI CTargets of call-style indirect branches (e.g. BLR, or BR using X16/X17)Put at entry of functions that may be called indirectly
BTI JTargets of jump-style branches (e.g. BR used for tail calls)Placed at the beginning of blocks reachable by jump tables or tail-calls
BTI JCActs as both C and JCan be targeted by either call or jump branches
  • In code compiled with branch target enforcement, compilers insert a BTI instruction (C, J, or JC) at each valid indirect-branch target (function beginnings or blocks reachable by jumps) so that indirect branches only succeed to those places.
  • Direct branches / calls (i.e. fixed-address B, BL) are not restricted by BTI. The assumption is that code pages are trusted and attacker cannot change them (so direct branches are safe).
  • Also, RET / return instructions generally are not restricted by BTI because return addresses are protected via PAC or return signing mechanisms.

Mechanism and enforcement

  • When the CPU decodes an indirect branch (BLR / BR) in a page marked as “guarded / BTI-enabled,” it checks whether the target address’s first instruction is a valid BTI (C, J, or JC as allowed). If not, a Branch Target Exception occurs.
  • The BTI instruction encoding is designed to reuse opcodes previously reserved for NOPs (in earlier ARM versions). So BTI-enabled binaries remain backward-compatible: on hardware without BTI support, those instructions act as NOPs.
  • The compiler passes that add BTIs insert them only where needed: functions that may be called indirectly, or basic blocks targeted by jumps.
  • Some patches and LLVM code show that BTI is not inserted for all basic blocks — only those that are potential branch targets (e.g. from switch / jump tables).

BTI + PAC synergy

PAC protects the pointer value (the source) — ensures the chain of indirect calls / returns hasn’t been tampered with.

BTI ensures that even a valid pointer must only target properly marked entry points.

Combined, an attacker needs both a valid pointer with correct PAC and that the target must have a BTI placed there. This increases the hardness of constructing exploit gadgets.

Example

Example An exploit tries to pivot into gadget at `0xABCDEF` that doesn’t start with `BTI c`. The CPU, upon executing `blr x0`, checks the target and faults because the instruction alignment doesn’t include a valid landing pad. Thus many gadgets become unusable unless they include BTI prefix.

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 is a feature introduced in ARMv8.1-A that prevents privileged code (EL1 or EL2) from reading or writing memory that is marked as user-accessible (EL0), unless PAN is explicitly disabled.
  • The idea: even if the kernel is tricked or compromised, it cannot arbitrarily dereference user-space pointers without first clearing PAN, thus reducing risks of ret2usr style exploits or misuse of user-controlled buffers.
  • When PAN is enabled (PSTATE.PAN = 1), any privileged load/store instruction accessing a virtual address that is “accessible at EL0” triggers a permission fault.
  • The kernel, when it must legitimately access user-space memory (e.g. copy data to/from user buffers), must temporarily disable PAN (or switch to “unprivileged load/store” instructions) to allow that access.
  • In Linux on ARM64, PAN support was introduced circa 2015: kernel patches added detection of the feature, and replaced get_user / put_user etc. with variants that clear PAN around user memory accesses.

Key nuance / limitation / bug

  • As noted by Siguza and others, a specification bug (or ambiguous behavior) in ARM’s design means that execute-only user mappings (--x) may not trigger PAN. In other words, if a user page is marked executable but without read permission, the kernel’s read attempt might bypass PAN because the architecture considers “accessible at EL0” to require readable permission, not just executable. This leads to a PAN bypass in certain configurations.
  • Because of that, if iOS / XNU allows execute-only user pages (as some JIT or code-cache setups might), the kernel might accidentally read from them even with PAN enabled. This is a known subtle exploitable area in some ARMv8+ systems.

PXN (Privileged eXecute Never)

  • PXN is a page table flag (in the page table entries, leaf or block entries) that indicates that the page is non-executable when running in privileged mode (i.e. when EL1 executes it).
  • PXN prevents the kernel (or any privileged code) from jumping into or executing instructions from user-space pages even if control is diverted. In effect, it stops a kernel-level control-flow redirection into user memory.
  • Combined with PAN, this ensures that:
    1. Kernel cannot (by default) read or write user-space data (PAN)
    2. Kernel cannot execute user-space code (PXN)
  • In the ARMv8 page table format, the leaf entries have a PXN bit (and also UXN for unprivileged execute-never) in their attribute bits.

So even if the kernel has a corrupted function pointer pointing to user memory, and it tried to branch there, the PXN bit would cause a fault.

Memory-permission model & how PAN and PXN map to page table bits

To understand how PAN / PXN work, you need to see how ARM’s translation and permission model works (simplified):

  • Each page or block entry has attribute fields including AP[2:1] for access permissions (read/write, privileged vs unprivileged) and UXN / PXN bits for execute-never restrictions.
  • When PSTATE.PAN is 1 (enabled), the hardware enforces modified semantics: privileged accesses to pages marked as “accessible by EL0” (i.e. user-accessible) are disallowed (fault).
  • Because of the bug mentioned, pages that are marked only executable (no read permission) may not count as “accessible by EL0” under certain implementations, thus bypassing PAN.
  • When a page’s PXN bit is set, even if the instruction fetch comes from a higher privilege level, execution is prohibited.

Kernel usage of PAN / PXN in a hardened OS (e.g. iOS / XNU)

In a hardened kernel design (such as what Apple might use):

  • The kernel enables PAN by default (so privileged code is constrained).
  • In pathways that legitimately need to read or write user buffers (e.g. syscall buffer copy, I/O, read/write user pointer), the kernel temporarily disables PAN or uses special instructions to override.
  • After finishing user data access, it must re-enable PAN.
  • PXN is enforced via page tables: user pages have PXN = 1 (so kernel cannot execute them), kernel pages do not have PXN (so kernel code can execute).
  • The kernel must ensure that no code paths cause execution flow into user memory regions (that would bypass PXN) — so exploit chains relying on “jump into user-controlled shellcode” are blocked.

Because of the noted PAN bypass via execute-only pages, in a real system, Apple might disable or disallow execute-only user pages, or patch around the specification weakness.

Attack surfaces, bypasses, and mitigations

  • PAN bypass via execute-only pages: as discussed, the spec allows a gap: user pages with execute-only (no read perm) might not count as “accessible at EL0,” so PAN won’t block kernel reads from such pages under some implementations. This gives the attacker an unusual path to feed data via “execute-only” sections.
  • Temporal window exploit: if the kernel disables PAN for a window longer than necessary, a race or malicious path might exploit that window to perform unintended user memory access.
  • Forgotten re-enable: if code paths fail to re-enable PAN, subsequent kernel operations might incorrectly access user memory.
  • Misconfiguration of PXN: if page tables do not set PXN on user pages or incorrectly map user code pages, the kernel might be tricked into executing user-space code.
  • Speculation / side-channels: analogous to speculative bypasses, there may be microarchitectural side-effects that cause transient violation of PAN / PXN checks (though such attacks are highly dependent on CPU design).
  • Complex interactions: In more advanced features (e.g. JIT, shared memory, just-in-time code regions), the kernel may need fine-grained control to permit certain memory accesses or execution in user-mapped regions; designing those safely under PAN/PXN constraints is nontrivial.

Example

Code Example Here are illustrative pseudo-assembly sequences showing enabling/disabling PAN around user memory access, and how a fault might occur.
// 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.

Example 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.

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

Example 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.

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.

  1. 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

Code Example Here’s a simplified pseudocode / logic showing how a kernel might call into PPL to modify protected pages:
// 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:

  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).
  1. 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.
  1. 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.
  1. 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.
  1. 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.
  1. 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

Limitations & challenges

  • Intrablock overflows: If overflow stays within the same allocation (doesn’t cross boundary) and the tag remains the same, tag mismatch does not catch it.
  • Tag width limitation: Only a few bits (e.g. 4 bits, or small domain) are available for tag—limited namespace.
  • Side-channel leaks: If tag bits can be leaked (via cache / speculative execution), attacker may learn valid tags and bypass. Apple’s tag confidentiality enforcement is meant to mitigate this.
  • Performance overhead: Tag checks each load/store add cost; Apple must optimize hardware to push overhead low.
  • Compatibility & fallback: On older hardware or parts that don’t support EMTE, fallback must exist. Apple claims MIE is only enabled on devices with support.
  • Complex allocator logic: The allocator must manage tags, retagging, aligning boundaries, and avoid mis-tag collisions. Bugs in allocator logic could introduce vulnerabilities.
  • Mixed memory / hybrid areas: Some memory may remain untagged (legacy), making interoperability trickier.
  • Speculative / transient attacks: As with many microarchitectural protections, speculative execution or micro-op fusions might bypass checks transiently or leak tag bits.
  • Limited to supported regions: Apple might only enforce EMTE in selective, high-risk areas (kernel, security-critical subsystems), not universally.

Key enhancements / differences compared to standard MTE

Here are the improvements and changes Apple emphasizes:

FeatureOriginal MTEEMTE (Apple’s enhanced) / MIE
Check modeSupports 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 memoryAccesses to non-tagged memory (e.g. globals) may bypass checks in some implementationsEMTE 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 / secrecyTags might be observable or leaked via side channelsApple adds Tag Confidentiality Enforcement, which attempts to prevent leakage of tag values (via speculative side-channels etc.).
Allocator integration & retaggingMTE leaves much of allocator logic to softwareApple’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 defaultIn many platforms, MTE is optional or off by defaultApple 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).
  1. 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.
  1. 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.
  1. 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.
  1. 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.
  1. 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.

void exception_triage(exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt);

Typical Call Flow:

exception_triage() └── exception_deliver() ├── exception_deliver_thread() ├── exception_deliver_task() └── exception_deliver_host()

If all fail → handled by bsd_exception() → translated into a signal like SIGSEGV.

Exception Ports

Each Mach object (thread, task, host) can register exception ports, where exception messages are sent.

They are defined by the API:

task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()

Each exception port has:

  • A mask (which exceptions it wants to receive)
  • A port name (Mach port to receive messages)
  • A behavior (how the kernel sends the message)
  • A flavor (which thread state to include)

Debuggers and Exception Handling

A debugger (e.g., LLDB) sets an exception port on the target task or thread, usually using task_set_exception_ports().

When an exception occurs:

  • The Mach message is sent to the debugger process.
  • The debugger can decide to handle (resume, modify registers, skip instruction) or not handle the exception.
  • If the debugger doesn’t handle it, the exception propagates to the next level (task → host).

Flow of EXC_BAD_ACCESS

  1. Thread dereferences invalid pointer → CPU raises Data Abort.

  2. Kernel trap handler calls exception_triage(EXC_BAD_ACCESS, ...).

  3. Message sent to:

    • Thread port → (debugger can intercept breakpoint).

    • If debugger ignores → Task port → (process-level handler).

    • If ignored → Host port (usually ReportCrash).

  4. If no one handles → bsd_exception() translates to SIGSEGV.

PAC Exceptions

When Pointer Authentication (PAC) fails (signature mismatch), a special Mach exception is raised:

  • EXC_ARM_PAC (type)
  • Codes may include details (e.g., key type, pointer type).

If the binary has the flag TFRO_PAC_EXC_FATAL, the kernel treats PAC failures as fatal, bypassing debugger interception. This is to prevent attackers from using debuggers to bypass PAC checks and it’s enabled for platform binaries.

Software Breakpoints

A software breakpoint (int3 on x86, brk on ARM64) is implemented by causing a deliberate fault.
The debugger catches this via the exception port:

  • Modifies instruction pointer or memory.
  • Restores original instruction.
  • Resumes execution.

This same mechanism is what allows you to “catch” a PAC exception — unless TFRO_PAC_EXC_FATAL is set, in which case it never reaches the debugger.

Conversion to BSD Signals

If no handler accepts the exception:

  • Kernel calls task_exception_notify() → bsd_exception().

  • This maps Mach exceptions to signals:

    Mach ExceptionSignal
    EXC_BAD_ACCESSSIGSEGV or SIGBUS
    EXC_BAD_INSTRUCTIONSIGILL
    EXC_ARITHMETICSIGFPE
    EXC_SOFTWARESIGTRAP
    EXC_BREAKPOINTSIGTRAP
    EXC_CRASHSIGKILL
    EXC_ARM_PACSIGILL (on non-fatal)

### Key Files in XNU Source

  • osfmk/kern/exception.c → Core of exception_triage(), exception_deliver_*().

  • bsd/kern/kern_sig.c → Signal delivery logic.

  • osfmk/arm64/trap.c → Low-level trap handlers.

  • osfmk/mach/exc.h → Exception codes and structures.

  • osfmk/kern/task.c → Task exception port setup.


Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)

The kernel used a zone allocator (kalloc) divided into fixed-size “zones.”
Each zone only stores allocations of a single size class.

From the screenshot:

Zone NameElement SizeExample Use
default.kalloc.1616 bytesVery small kernel structs, pointers.
default.kalloc.3232 bytesSmall structs, object headers.
default.kalloc.6464 bytesIPC messages, tiny kernel buffers.
default.kalloc.128128 bytesMedium objects like parts of OSObject.
default.kalloc.12801280 bytesLarge structures, IOSurface/graphics metadata.

How it worked:

  • Each allocation request gets rounded up to the nearest zone size.
    (E.g., a 50-byte request lands in the kalloc.64 zone).
  • Memory in each zone was kept in a free list — chunks freed by the kernel went back into that zone.
  • If you overflowed a 64-byte buffer, you’d overwrite the next object in the same zone.

This is why heap spraying / feng shui was so effective: you could predict object neighbors by spraying allocations of the same size class.

The freelist

Inside each kalloc zone, freed objects weren’t returned directly to the system — they went into a freelist, a linked list of available chunks.

  • When a chunk was freed, the kernel wrote a pointer at the start of that chunk → the address of the next free chunk in the same zone.

  • The zone kept a HEAD pointer to the first free chunk.

  • Allocation always used the current HEAD:

    1. Pop HEAD (return that memory to the caller).

    2. Update HEAD = HEAD->next (stored in the freed chunk’s header).

  • Freeing pushed chunks back:

    • freed_chunk->next = HEAD

    • HEAD = freed_chunk

So the freelist was just a linked list built inside the freed memory itself.

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)

Exploiting the freelist

Because the first 8 bytes of a free chunk = freelist pointer, an attacker could corrupt it:

1. **Heap overflow** into an adjacent freed chunk → overwrite its “next” pointer.

2. **Use-after-free** write into a freed object → overwrite its “next” pointer.

Then, on the next allocation of that size:

- The allocator pops the corrupted chunk.

- Follows the attacker-supplied “next” pointer.

- Returns a pointer to arbitrary memory, enabling fake object primitives or targeted overwrite.

Visual example of freelist poisoning:

Before corruption:
   HEAD ──► [ F1 ] ──► [ F2 ] ──► [ F3 ] ──► NULL

After attacker overwrite of F1->next:
   HEAD ──► [ F1 ]
              (next) ──► 0xDEAD_BEEF_CAFE_BABE  (attacker-chosen)

Next alloc of this zone → kernel hands out memory at attacker-controlled address.

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:

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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.
  6. 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:
    1. Try per-CPU cache.
    2. If empty, pull from the global freelist.
    3. 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:

  1. Type lookupOSData maps to kalloc_type_osdata zone (size 64 bytes).
  2. 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).
  3. 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

FeatureOld Heap (Pre-iOS 15)Modern Heap (iOS 15+ / A12+)
Allocation granularityFixed size buckets (kalloc.16, kalloc.32, etc.)Size + type-based buckets (kalloc_type)
Placement predictabilityHigh (same-size objects side by side)Low (same-type grouping + randomness)
Freelist managementRaw pointers in freed chunks (easy to corrupt)Encoded pointers (safe-linking style)
Adjacent object controlEasy via sprays/frees (feng shui predictable)Hard — typed zones separate attacker objects
Kernel data/code protectionsFew hardware protectionsPPL / SPTM protect page tables & code pages, and PAC protects pointers
Allocation reuse validationNone (freelist pointers raw)zone_require / zone enforcement
Exploit reliabilityHigh with heap spraysMuch lower, requires logic bugs or info leaks
Large allocations handlingAll small allocations managed equallyLarge 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:

  1. malloc / calloc / realloc / typed alloc is invoked with a size and type ID.
  2. The allocator uses the type ID to pick the correct segment group / zone.
  3. 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.
  4. The metadata slab is updated (free bit cleared, bookkeeping).
  5. If memory tagging (EMTE) is in play, the returned block gets a tag assigned, and metadata is updated to reflect its “live” state.
  6. 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:

FeaturePurposeNotes
Metadata decouplingPrevent overflow from corrupting metadataMetadata lives in separate VM region (metadata slab)
Guard pages / unmapped slicesCatch out-of-bounds writesHelps detect buffer overflows rather than silently corrupting adjacent blocks
Type-based segregationPrevent cross-type reuse & type confusionEven same-size allocations from different types go to different zones
Memory Tagging (EMTE / MIE)Detect invalid access, stale references, OOB, UAFxzone works in concert with hardware EMTE in synchronous mode (“Memory Integrity Enforcement”)
Delayed reuse / poisoning / zapReduce chance of use-after-free exploitationFreed blocks may be poisoned, zeroed, or quarantined before reuse
Chunk reclamation / dynamic unmappingReduce memory waste and fragmentationEntire chunks may be unmapped when unused
Randomization / placement variationPrevent deterministic adjacencyBlocks in a chunk and chunk selection may have randomized aspects
Segregation of “data-only” allocationsSeparate allocations that don’t store pointersReduces 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

ios Physical UAF - 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

  1. Go to the page https://ipsw.me/ and download the iOS versions you want to diff. These will be .ipsw files.
  2. 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

  1. Open Ghidra with ghidraRun, create a new project and load the kernelcaches.
  2. Open each kernelcache so they are automatically analyzed by Ghidra.
  3. Then, on the project Window of Ghidra, right click each kernelcache, select Export, select format Binary BinExport (v2) for BinDiff and export them.
  4. 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

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks