PAC and BTI: ARM's Answer to Code Reuse
Pointer authentication signs return addresses so a forged one faults; BTI forces indirect branches onto landing pads. How both work, how to enable and verify them, and their limits.
This is the third walkthrough in the ARM64 Exploitation area, and it is written from the defender's side. The ret2win and ROP guides both ended at the same two mitigations. Here we explain what they actually do, how to turn them on and confirm they took, and where their limits are. PAC and BTI are the most important thing to understand about modern ARM security, because they attack the code-reuse class in hardware.
PAC: signing the return address
Pointer authentication (ARMv8.3) uses a secret, per-process key to compute a short signature — a PAC — over a pointer plus a context value, and stores that signature in the pointer's otherwise-unused high bits. For return addresses, -mbranch-protection=pac-ret makes the compiler:
- in the prologue,
paciasp— signx30(lr) using the key andspas context, then save it; - in the epilogue,
autiasp— authenticatex30beforeret(or the combinedretaa).
paciasp (prologue)sign x30 with key + spautiasp (epilogue)authenticate x30 before retret / faulta forged lr fails auth and faultsIf an attacker overwrites the saved x30 via a stack overflow, the high-bit signature no longer matches the pointer. autiasp corrupts the value further (it flips the top bits on failure), so the subsequent ret jumps to a non-canonical address and the process faults. The attacker cannot supply a correct PAC because they do not have the key.
BTI: landing pads for indirect branches
Branch-target identification (ARMv8.5) protects the forward edge. With -mbranch-protection=bti, indirect branches (br, blr) may only transfer control to an instruction that is a valid landing pad — a bti (with c/j/jc modes) that the compiler emits at legitimate entry points.
A ROP or JOP gadget sitting in the middle of a function does not begin with a bti, so using it as an indirect-branch target raises a Branch Target exception. This does not restrict which valid function you jump to (it is coarse-grained), but it removes the vast supply of mid-function gadgets that code reuse relies on.
| Feature | Edge protected | Instruction | What it blocks |
|---|---|---|---|
PAC (pac-ret) | Backward (returns) | paciasp/autiasp/retaa | Forged saved link register |
| BTI | Forward (indirect branches) | bti c/j | Jumps into mid-function gadgets |
-mbranch-protection=standard enables both.
Enabling and verifying
# Build with both PAC (return addresses) and BTI:
aarch64-linux-gnu-gcc -mbranch-protection=standard -O2 -o app app.c
Then confirm the binary really carries them — a flag that is silently dropped protects nothing:
$ readelf -n ./app | grep -A2 'GNU'
Properties: AArch64 feature: BTI, PAC
$ objdump -d ./app | grep -m1 -E 'paciasp|bti c'
4007c0: d503233f paciasp ; return-address signing in the prologue
If the .note.gnu.property feature line and the paciasp/bti instructions are present, the protection is live. On real systems, every library in the process must also be built with these features for the protection to be complete.
The limits — an honest accounting
PAC and BTI raise the cost of code reuse; they are not an end to it:
- Signing gadgets. If the program contains code that signs an attacker-influenced pointer with the right key and context, an attacker can obtain a validly signed pointer without the key.
- Context reuse. PAC ties a signature to a context (e.g.
sp). Signed pointers can sometimes be replayed where the same context recurs. - Leaks. A disclosure bug that leaks a validly signed pointer can hand the attacker a usable authenticated value — the same leak-then-act pattern from x86.
- Speculation (PACMAN). Speculative-execution techniques have been shown to test PAC guesses without triggering faults, chipping at the brute-force cost.
- BTI is coarse. It permits any valid landing pad, so whole-function reuse (ret2libc) into a
bti-marked entry can still be possible.
None of this makes PAC/BTI optional — it makes them one strong layer among several. Combine them with a stack canary, PIE/ASLR, and memory-safety work.
What this teaches a defender
- Turn on
-mbranch-protection=standardand verify the note. It is the highest-leverage single measure against ARM code reuse, and it is easy to enable but also easy to drop silently (a prebuilt dependency, a wrong flag). Checkreadelf -n. - All-or-nothing across the process. PAC/BTI protect only the objects built with them. One legacy library without the feature is a gap. Audit the whole loaded set.
- It is a cost multiplier, not a cure. Treat PAC/BTI like ASLR: they make exploitation expensive and unreliable, which is valuable, but the durable fix remains not having the memory-corruption bug — see memory-safe languages.
Key takeaways
- PAC signs the return address with a secret key and a context; the epilogue authenticates it, so a forged saved
lrfaults instead of returning. - BTI forces indirect branches onto
btilanding pads, invalidating mid-function gadgets on the forward edge. -mbranch-protection=standardenables both; verify withreadelf -n(BTI, PAC) and by findingpaciasp/bti c.- They are a strong layer with real limits (signing gadgets, leaks, PACMAN, coarse BTI) — combine with the classic mitigations and memory safety.
This completes the ARM64 Exploitation arc: the link register and ret2win, ROP through the link register, and the PAC/BTI defences that break it. For the cross-architecture picture, compare with the x86-64 Exploitation Techniques area.