PAC -- Pointer Authentication Code
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