PAC adds a cryptographic signature to pointers. If an attacker corrupts a pointer, the signature check fails, causing a crash instead of code execution. Applies from A12 (iPhone XS) onwards.


How It Works

Signing & Verification

Original pointer:  0x0000FFFFF1234000
                    │              │
         PAC bits   │   Address    │
  (upper bits, normally 0)

Signed pointer:    0xAB12FFFFF1234000
                    │              │
         PAC value  │   Address    │
  (cryptographic hash)

Sign:   PACIA X0, X1    →  X0 = sign(X0, key_A, X1_context)
Verify: AUTIA X0, X1    →  if valid: strip PAC, return original pointer
                            if invalid: corrupt pointer → crash on dereference
Strip:  XPACI X0        →  strip PAC bits without verification

Keys

Key Instruction prefix Used for
IA (Instruction A) PACI/AUTI Return addresses, function pointers
IB (Instruction B) PACIB/AUTIB Alternate instruction key
DA (Data A) PACDA/AUTDA Data pointers
DB (Data B) PACDB/AUTDB Alternate data key
GA (Generic) PACGA Generic authentication

Each process has its own set of keys (set by the kernel at process creation).

Context (Modifier)

PAC value = f(pointer, key, context):

PACIA X0, X1    ← X1 is the context (modifier)
                   Typically: SP (stack pointer), or 0, or address of storage

Same pointer + same key + DIFFERENT context = DIFFERENT PAC value
→ A pointer signed for context A is not valid for context B
→ Prevents reuse of signed pointers across different call sites

PAC in the XNU Kernel

What Is Protected

1. Return addresses (LR/X30):
   Function prologue: PACIBSP     ; Sign LR with key B, context SP
   Function epilogue: RETAB       ; Auth LR with key B, return

2. Function pointers in kernel structures:
   Signed with key A + context (usually the address of the pointer storage)

3. vtable pointers (C++ virtual dispatch):
   IOKit object vtables signed with PAC

4. Thread state:
   Exception frames contain PAC'd return addresses

arm64e ABI

arm64e = ARM64 with PAC extensions. Mach-O binaries compiled for arm64e:

CPU_SUBTYPE_ARM64E = 0x02

arm64e binaries:
  - Compiler generates PAC instructions automatically
  - vtable entries signed at compile time
  - JIT code requires PAC signing

PAC Bypass Techniques

1. Signing Gadgets

Find code sequences in system frameworks that sign arbitrary pointers:

; Gadget example (conceptual):
  LDR X0, [X1]      ; Load attacker-controlled value
  PACIA X0, X2       ; Sign it with key A, context X2
  STR X0, [X3]       ; Store signed pointer
  RET

If the attacker controls X1 (source) and X3 (destination): Forge a signed pointer for an arbitrary address.

Real-world: Predator spyware hunted signing gadgets in JavaScriptCore (20-byte ARM64 sequence).

2. PAC Forgery via Context Mismatch

If a pointer is signed with context C1 but verified with context C2:
  - Bug in kernel code uses the wrong context
  - Attacker controls the context value
  → Sign pointer with the expected verify context

3. PACMAN Attack (Speculative Execution)

Brute-force the PAC value via speculative execution:
  1. Guess PAC value
  2. Speculatively execute AUTIA
  3. If correct: speculative load succeeds (measurable side effect)
  4. If wrong: speculative load fails (different timing)
  → Timing oracle reveals correct PAC value
  → 2^16 attempts max (PAC is typically 16 bits)

4. Kernel Memory R/W → PAC Bypass

If you already have kread/kwrite:
  1. Read PAC key values from kernel memory
  2. Compute valid PAC offline
  3. Write the signed pointer directly
  
Or:
  1. Find a signing gadget address
  2. Setup registers + call the gadget to sign an arbitrary pointer

5. Non-PAC Code Paths

Not every pointer is PAC'd:
  - Legacy code not yet migrated to arm64e
  - Data pointers (not always signed)
  - Certain kernel objects miss PAC coverage
  → Target unprotected pointers

Resources