ARM64 安全特性:PAC 与 BTI 详解
概述
PAC (Pointer Authentication Code) 和 BTI (Branch Target Identification) 是 ARM64 架构引入的两大硬件安全特性,用于防御控制流劫持攻击。
引入版本:
- PAC:ARMv8.3-A(2016)
- BTI:ARMv8.5-A(2019)
内核支持:
- Linux 5.7+ 开始支持 BTI
- Linux 5.8+ 开始支持 PAC
- Android 11+ 默认启用
在 KernelPatch 中的影响:
- setup1.S 中的 BTI 检测代码
- paging_init 函数可能使用 PAC/BTI
- Hook 机制需要处理这些指令
🔐 PAC (Pointer Authentication Code)
什么是 PAC?
PAC 通过在指针的未使用高位添加签名,防止指针被篡改。
核心思想:
64 位指针在用户空间和内核空间都有大量未使用的高位:
用户空间(48 位 VA):
63 48 47 0
┌──────────┬──────────────────────┐
│ unused │ actual address │
└──────────┴──────────────────────┘
内核空间(48 位 VA):
63 48 47 0
┌──────────┬──────────────────────┐
│ unused │ actual address │
└──────────┴──────────────────────┘
PAC 使用高 16 位存储签名:
63 48 47 0
┌──────────┬──────────────────────┐
│ PAC │ actual address │
└──────────┴──────────────────────┘
PAC 指令集
签名指令(PACIA/PACIB 系列)
PACIASP ; 使用 SP 签名 X30 (返回地址)
PACIASPPC ; 使用 SP 和 PC 签名 X30
PACIBSP ; 使用 SP 签名 X30(使用 key B)
PACIA X0, X1 ; 使用 X1 签名 X0
PACIB X0, X1 ; 使用 X1 签名 X0(使用 key B)
PACDA X0, X1 ; 签名数据指针
PACDB X0, X1
编码:
PACIASP: 0xD503233F
31 30 29 28 ... 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
1 1 0 1 ... 0 0 0 0 1 1 0 0 1 1 1 1 1 1
验证指令(AUTIA/AUTIB 系列)
AUTIASP ; 验证并移除 X30 的签名
AUTIASPPC
AUTIBSP
AUTIA X0, X1 ; 验证 X0 的签名
AUTIB X0, X1
AUTDA X0, X1 ; 验证数据指针
AUTDB X0, X1
编码:
AUTIASP: 0xD503231F
与 PACIASP 只差一位(bit 5)
PAC 工作原理
签名过程
// 伪代码
uint64_t pac_sign(uint64_t pointer, uint64_t context, uint64_t key)
{
// 1. 提取地址的有效部分
uint64_t address = pointer & 0x0000FFFFFFFFFFFF; // 48 位
// 2. 计算签名(使用硬件加密算法 QARMA)
uint16_t pac = qarma_encrypt(address, context, key);
// 3. 将签名填充到高位
uint64_t signed_pointer = address | ((uint64_t)pac << 48);
return signed_pointer;
}
验证过程
// 伪代码
uint64_t pac_verify(uint64_t signed_pointer, uint64_t context, uint64_t key)
{
// 1. 分离地址和签名
uint64_t address = signed_pointer & 0x0000FFFFFFFFFFFF;
uint16_t pac = (signed_pointer >> 48) & 0xFFFF;
// 2. 重新计算签名
uint16_t expected_pac = qarma_encrypt(address, context, key);
// 3. 验证
if (pac != expected_pac) {
// ❌ 签名不匹配!
// 修改高位为错误标记(全 1 或全 0)
return address | 0xFFFF000000000000; // 非法地址
}
// ✅ 验证成功,返回原始地址
return address;
}
PAC 密钥
ARM64 提供 5 个 128 位密钥:
APIAKey (A key for instruction addresses)
APIBKey (B key for instruction addresses)
APDAKey (A key for data addresses)
APDBKey (B key for data addresses)
APGAKey (Generic key)
每个密钥在系统寄存器中:
APIAKey_EL1: bits[127:64] (high), bits[63:0] (low)
密钥特点:
- 每个进程/上下文可以有不同的密钥
- 密钥随机生成(启动时或进程创建时)
- 无法直接读取(安全)
典型使用场景
场景 1:保护返回地址
函数入口:
PACIASP ; 签名 X30 (返回地址)
; 使用 SP 作为上下文
; X30[63:48] = PAC
STP X29, X30, [SP, #-16]!
; ... 函数体 ...
; 如果攻击者修改栈上的返回地址:
; [SP] = 0xDEADBEEF ← 尝试劫持
函数返回:
LDP X29, X30, [SP], #16
; X30 = 0xDEADBEEF (被篡改)
AUTIASP ; 验证签名
; PAC 不匹配!
; X30 = 0xFFFFDEADBEEF (非法地址)
RET ; 跳转到 0xFFFFDEADBEEF
; → MMU 异常 → 崩溃(阻止了攻击)
场景 2:保护函数指针
// C 代码
void (*callback)(void) = my_function;
// 攻击者尝试修改
callback = evil_function; // ❌ 希望劫持
// 使用 PAC 保护
void (**signed_callback)(void) = &callback;
// 编译器生成
PACIA X0, X1 ; 签名函数指针
STR X0, [X1] ; 存储签名后的指针
// 调用时
LDR X0, [X1] ; 加载(可能被篡改)
AUTIA X0, X1 ; 验证签名
BLR X0 ; 如果验证失败,跳转到非法地址
PAC 失败的表现
正常指针:
0x0000FFFFFFE12345 (未签名)
签名后:
0x1234FFFFFFE12345 (签名 = 0x1234)
篡改后:
0x1234FFFFFFE99999 (地址被改)
验证失败后:
0xFFFFFFFFE99999 (高位被填充为 0xFFFF)
或
0x0002FFFFFE99999 (高位被填充为错误值)
访问此地址:
→ Level 1/2 translation fault
→ 内核 panic
在 panic.txt 中看到的情况:
- 这不是 PAC 失败(崩溃地址是 0xfffffffffffffff8)
- 而是 NULL 指针解引用
🎯 BTI (Branch Target Identification)
什么是 BTI?
BTI 标记合法的跳转目标,防止跳转到函数中间。
攻击场景(无 BTI):
func:
0x000: STP X29, X30, [SP, #-16]!
0x004: MOV X29, SP
0x008: BL other_func
0x00C: ...
0x010: LDP X29, X30, [SP], #16 ← 攻击者跳转到这里
0x014: RET ; 跳过函数入口检查
攻击者构造:
X17 = 0x010 (跳过入口,直接到返回)
BR X17 ; 绕过函数序言
有 BTI 保护:
func:
0x000: BTI C ← 标记:只能从 CALL 跳转到
0x004: STP X29, X30, [SP, #-16]!
0x008: MOV X29, SP
...
0x010: LDP X29, X30, [SP], #16
0x014: RET
攻击者构造:
X17 = 0x010
BR X17 ; ❌ 跳转到非 BTI 位置
; → BTI 异常 → 崩溃
BTI 指令
BTI C ; 只能从 CALL (BL/BLR) 跳转到
BTI J ; 只能从 JUMP (B/BR) 跳转到
BTI JC ; 两者都可以
BTI ; 等同于 BTI JC
编码:
BTI C: 0xD503245F
31 ... 11 10 9 8 7 6 5 4 ... 0
1 ... 1 0 1 0 0 1 1 1 ... 1
│ └─┴─ op2 = 0b011 (BTI)
└─ CRm[0] = 1 (C)
BTI J: 0xD503249F
CRm[1] = 1 (J)
BTI JC: 0xD50324DF
CRm[1:0] = 0b11 (JC)
本质:BTI 是 HINT 指令的一种
HINT #0x14 (BTI C)
HINT #0x16 (BTI J)
HINT #0x17 (BTI JC)
BTI 工作原理
CPU 状态机
CPU 维护一个"分支状态":
初始状态:GUARDED
↓ 执行 BL/BLR (CALL)
GUARDED_CALL
↓ 下一条指令
├─ 如果是 BTI C 或 BTI JC → OK,状态恢复
└─ 如果不是 → BTI 异常!
初始状态:GUARDED
↓ 执行 B/BR (JUMP)
GUARDED_JUMP
↓ 下一条指令
├─ 如果是 BTI J 或 BTI JC → OK
└─ 如果不是 → BTI 异常!
示例
正常调用流程:
BL func ; CPU 进入 GUARDED_CALL 状态
func:
BTI C ; ✅ 匹配,状态恢复为 GUARDED
STP X29, X30, [SP, #-16]!
...
攻击流程:
X17 = func + 0x10
BR X17 ; CPU 进入 GUARDED_JUMP 状态
func+0x10:
LDP X29, X30, [SP], #16 ; ❌ 不是 BTI J
; → BTI 异常!
BTI 与 HINT 的关系
HINT 指令:
HINT #imm ; 提示指令,可以被 NOP
NOP = HINT #0 = 0xD503201F
YIELD = HINT #1 = 0xD503203F
WFE = HINT #2 = 0xD503205F
WFI = HINT #3 = 0xD503207F
SEV = HINT #4 = 0xD503209F
SEVL = HINT #5 = 0xD50320BF
...
BTI C = HINT #0x14 = 0xD503245F
BTI J = HINT #0x16 = 0xD503249F
BTI JC = HINT #0x17 = 0xD50324DF
向后兼容:
- 在不支持 BTI 的 CPU 上,BTI 被当作 NOP
- 代码可以在新旧 CPU 上运行
- 但只有新 CPU 提供安全保护
🔗 PAC 与 BTI 的配合使用
完整的函数保护
secure_function:
BTI C ; 1️⃣ 只能从 CALL 跳转到
PACIASP ; 2️⃣ 签名返回地址 X30
STP X29, X30, [SP, #-16]!
MOV X29, SP
; ... 函数体 ...
; 即使攻击者修改了栈上的返回地址
; 也无法绕过 AUTIASP 的验证
LDP X29, X30, [SP], #16
AUTIASP ; 3️⃣ 验证签名,移除签名值
RET ; 4️⃣ 安全返回
攻击者的困境:
1. 无法跳转到函数中间(BTI 阻止)
2. 无法劫持返回地址(PAC 阻止)
编译器生成
GCC/Clang 选项:
# 启用 PAC
-mbranch-protection=pac-ret
# 启用 BTI
-mbranch-protection=bti
# 同时启用
-mbranch-protection=standard # pac-ret+bti
# 完整示例
aarch64-linux-gnu-gcc -mbranch-protection=standard \
-march=armv8.5-a \
-o secure_program program.c
生成的代码:
my_function:
BTI C
PACIASP
STP X29, X30, [SP, #-16]!
; ...
LDP X29, X30, [SP], #16
AUTIASP
RET
🐛 在 KernelPatch 中的问题
问题 1:BTI 指令被错误处理
setup1.S 中的 bug:
// kernel/base/setup1.S:227-246
ldr w12, [x13] ; w12 = paging_init[0]
mov w3, #0x201F
movk w3, #0xD503, lsl#16 ; w3 = NOP (0xD503201F)
orr w1, w3, #0x100 ; w1 = 0xD503211F
mov w2, #0xFFFFFD1F ; w2 = 掩码
and w0, w12, w2
cmp w0, w1
b.ne .backup
; 如果检测到 BTI C:
mov w12, w3 ; ❌ 备份改为 NOP (0xD503201F)
; 应该备份原始的 BTI C (0xD503245F)
问题:
paging_init 原始:
0x000: D503245F BTI C
备份:
backup = D503201F NOP ❌ 错误
恢复后:
0x000: D503201F NOP ❌ 缺少 BTI
后果:
如果有代码通过 BL 跳转到 paging_init
期望看到 BTI C
但实际是 NOP
→ BTI 异常?(取决于 CPU 实现)
问题 2:掩码错误匹配其他指令
掩码 0xFFFFFD1F 的问题:
BTI C (0xD503245F):
D503245F & FFFFFE1F = D503201F ❌ 结果不是 D503211F
实际上应该检查的掩码:
D503245F & FFFFFFDF = D503245F ✅
但当前掩码会匹配:
PACIASP (0xD503233F) & FFFFFE1F = D503231F
AUTIASP (0xD503231F) & FFFFFE1F = D503231F
BTI C (0xD503245F) & FFFFFE1F = D503245F
都不等于 D503211F,所以逻辑有问题!
让我重新分析 setup1.S 的代码:
orr w1, w3, #0x100 ; w1 = 0xD503201F | 0x100 = 0xD503211F
mov w2, #0xFFFFFD1F ; w2 = 0xFFFFFD1F
and w0, w12, w2 ; w0 = insn & 0xFFFFFD1F
cmp w0, w1 ; 比较 w0 == 0xD503211F
如果 insn = BTI C (0xD503245F):
w0 = 0xD503245F & 0xFFFFFD1F = 0xD503245F ≠ 0xD503211F
如果 insn = AUTIASP (0xD503231F):
w0 = 0xD503231F & 0xFFFFFD1F = 0xD503211F ✅ 匹配!
结论:这段代码实际上在查找 AUTIASP,不是 BTI C!
问题 3:AUTIASP 被 NOP 化
setup1.S 的 .cmp_auti 循环:
add x11, x13, #4 ; 从第二条指令开始
.cmp_auti:
ldr w0, [x11], #4
and w0, w0, w2
cmp w0, w1 ; 查找 0xD503211F
b.ne .cmp_auti
stur w3, [x11, #-4] ; ❌ 找到后 NOP 化
这个循环在找 AUTIASP 并将其改为 NOP!
后果:
paging_init 原始:
0x000: BTI C (或其他)
0x004: PACIASP
0x008: STP X29, X30, [SP, #-16]!
...
0x0XX: LDP X29, X30, [SP], #16
0x0YY: AUTIASP ← 被找到
0x0ZZ: RET
被修改后:
0x000: B _paging_init (临时)
0x004: PACIASP
...
0x0YY: NOP ❌ 原本是 AUTIASP
0x0ZZ: RET
恢复后:
0x000: BTI C (或 NOP,如果被错误备份)
0x004: PACIASP
...
0x0YY: NOP ❌ 仍然是 NOP
0x0ZZ: RET
执行:
PACIASP ; 签名 X30
...
(没有 AUTIASP) ; ❌ 签名未移除
RET ; X30 仍带签名
; → PAC 验证失败
; → X30 被修改为非法值
; → 崩溃!
🔧 正确的处理方式
方案 1:完全不处理(推荐)
; kernel/base/setup1.S(修复版)
map_prepare:
; 简单备份第一条指令
add x13, x15, x19
ldr w12, [x13]
str w12, [x9, #map_paging_init_backup_offset]
; ❌ 删除所有 BTI 检测代码
; ❌ 删除 .cmp_auti 循环
; 写入 B 指令
; ... (保持原有代码)
为什么可以不处理?
- ✅ B 指令直接跳转,不经过 BTI 检查
- ✅ 恢复原始指令后,BTI 自然生效
- ✅ 不修改函数内部的 PAC 指令,不会破坏验证
方案 2:跳过 BTI(如果必要)
; 如果确实需要 hook 第二条指令
map_prepare:
add x13, x15, x19
ldr w12, [x13]
; 检测 BTI C
mov w1, #0x245F
movk w1, #0xD503, lsl#16 ; w1 = 0xD503245F (精确匹配)
cmp w12, w1
b.ne .not_bti_c
; 是 BTI C,hook 第二条指令
add x13, x13, #4
ldr w12, [x13]
.not_bti_c:
str w12, [x9, ...] ; 备份
; 写入 B 指令到 x13 位置
问题:
- 恢复时需要特殊处理
- 增加复杂度
- 不如方案 1 简单
📚 PAC 与 BTI 的检测
检测 CPU 是否支持
// kernel code
#include <asm/cpufeature.h>
bool cpu_has_pac()
{
// 读取 ID_AA64ISAR1_EL1 寄存器
uint64_t isar1;
asm volatile("mrs %0, ID_AA64ISAR1_EL1" : "=r"(isar1));
// APA (Address PAC, using QARMA5)
int apa = (isar1 >> 4) & 0xF;
// API (Address PAC, using QARMA3)
int api = (isar1 >> 8) & 0xF;
return (apa != 0) || (api != 0);
}
bool cpu_has_bti()
{
uint64_t pfr1;
asm volatile("mrs %0, ID_AA64PFR1_EL1" : "=r"(pfr1));
// BT field (bits [3:0])
int bt = pfr1 & 0xF;
return (bt != 0);
}
检测指令是否使用 PAC/BTI
bool is_pac_instruction(uint32_t insn)
{
// PACIASP family
if ((insn & 0xFFFFFFE0) == 0xD5033220) return true;
// AUTIASP family
if ((insn & 0xFFFFFFE0) == 0xD5033200) return true;
// PACIA/AUTIA
if ((insn & 0xFFE0FC00) == 0xDAC10000) return true;
return false;
}
bool is_bti_instruction(uint32_t insn)
{
// BTI C/J/JC
if ((insn & 0xFFFFFF3F) == 0xD503245F) return true;
return false;
}
🎓 实际案例分析
案例 1:paging_init 的正确形式
; 可能的 paging_init 函数(内核 6.1)
paging_init:
0x000: D503245F BTI C ; ARMv8.5+
0x004: D503233F PACIASP ; ARMv8.3+
0x008: A9BF7BFD STP X29, X30, [SP, #-16]!
0x00C: 910003FD MOV X29, SP
; ... 函数体 ...
0x0XX: A8C17BFD LDP X29, X30, [SP], #16
0x0YY: D503231F AUTIASP ; ❌ setup1.S 会 NOP 化这条
0x0ZZ: D65F03C0 RET
; 或者(不同编译选项)
paging_init:
0x000: A9BF7BFD STP X29, X30, [SP, #-16]! ; 无 PAC/BTI
0x004: 910003FD MOV X29, SP
; ...
案例 2:setup1.S 的错误逻辑分析
当前代码试图做什么:
目的(推测):
1. 检测 paging_init 是否使用 PAC
2. 如果使用,移除 PAC 保护(通过 NOP 化 AUTIASP)
3. 避免 PAC 验证失败
实际效果:
1. 掩码 0xFFFFFD1F 匹配 AUTIASP 而非 BTI
2. 找到并 NOP 化 AUTIASP
3. 导致 PAC 验证失败 → 崩溃
为什么要移除 PAC?(错误的想法)
担心:
paging_init 被劫持(B _paging_init)
↓
_paging_init 执行
↓
恢复 paging_init 并调用
↓
paging_init 返回时 PAC 验证
↓
担心验证失败?
实际上:
调用原始 paging_init 时,LR 是正确的
PACIASP 会重新签名 LR
AUTIASP 会正确验证
完全不需要移除!
🔍 识别 PAC/BTI 的技巧
通过特征码识别
# 反汇编工具
def identify_security_feature(instructions):
has_pac = False
has_bti = False
# 检查前几条指令
for i, insn in enumerate(instructions[:10]):
if insn == 0xD503245F: # BTI C
has_bti = True
elif insn == 0xD503249F: # BTI J
has_bti = True
elif insn == 0xD50324DF: # BTI JC
has_bti = True
elif insn == 0xD503233F: # PACIASP
has_pac = True
elif insn == 0xD503237F: # PACIBSP
has_pac = True
# 检查函数末尾
for insn in instructions[-10:]:
if insn == 0xD503231F: # AUTIASP
has_pac = True
elif insn == 0xD503237F: # AUTIBSP
has_pac = True
return has_pac, has_bti
# 使用
instructions = [0xD503245F, 0xD503233F, 0xA9BF7BFD, ...]
pac, bti = identify_security_feature(instructions)
print(f"PAC: {pac}, BTI: {bti}")
🛡️ PAC/BTI 绕过技术(安全研究)
PAC 绕过方法
方法 1:泄漏 PAC 密钥
如果能读取 APIAKey_EL1 等寄存器
→ 可以自己计算正确的 PAC
→ 伪造签名指针
防御:密钥存储在安全寄存器,无法直接读取
方法 2:重用已签名的指针
// 如果有多个相同上下文的签名指针
void *ptr1 = sign_pointer(0x12345, SP); // 签名 A
void *ptr2 = sign_pointer(0x12345, SP); // 签名 A(相同)
// 可以互相替换
// 但无法跨上下文使用
方法 3:NOP 化验证指令(KernelPatch 的方法)
// 直接修改代码,移除 AUTIASP
// ❌ 危险:会导致崩溃
// ✅ 更好的方法:不要劫持使用 PAC 的函数
BTI 绕过方法
方法 1:禁用 BTI(内核级)
// 清除 SCTLR_EL1.BT 位
uint64_t sctlr_el1;
asm volatile("mrs %0, sctlr_el1" : "=r"(sctlr_el1));
sctlr_el1 &= ~(1UL << 50); // 清除 BT 位
asm volatile("msr sctlr_el1, %0" : : "r"(sctlr_el1));
// 现在 BTI 不再生效
方法 2:在 BTI 之前插入 Hook
func:
BTI C ← 不 hook 这里
STP X29, X30, [SP, #-16]! ← hook 这里
KernelPatch 的做法:
; 直接用 B 指令覆盖第一条指令
; B 指令跳转不触发 BTI 检查
; 所以不需要特殊处理
🎯 在 Hook 中正确处理 PAC/BTI
Hook 函数入口的正确方法
// kernel/base/hook.c
hook_err_t hook_prepare(hook_t *hook)
{
// 1. 读取前几条指令
for (int i = 0; i < TRAMPOLINE_NUM; i++) {
hook->origin_insts[i] = *((uint32_t *)hook->origin_addr + i);
}
// 2. 检测 BTI
if (hook->origin_insts[0] == ARM64_BTI_C ||
hook->origin_insts[0] == ARM64_BTI_J ||
hook->origin_insts[0] == ARM64_BTI_JC) {
// ✅ 没问题,B 指令会跳过 BTI
}
// 3. 在重定位代码开头插入 BTI JC
uint32_t *bti = hook->relo_insts + hook->relo_insts_num;
bti[0] = ARM64_BTI_JC; // 0xD50324DF
bti[1] = ARM64_NOP;
hook->relo_insts_num += 2;
// 4. 重定位指令(包括 PACIASP/AUTIASP)
for (int i = 0; i < hook->tramp_insts_num; i++) {
// ✅ PACIASP/AUTIASP 会被当作普通指令直接复制
// 因为它们不是 PC 相对寻址
relocate_inst(hook, ...);
}
return HOOK_NO_ERR;
}
📖 总结
PAC (Pointer Authentication Code)
目的:防止指针篡改
机制:在指针高位添加签名
关键指令:PACIA*/AUTIA*, PACIB*/AUTIB*
保护对象:返回地址、函数指针、数据指针
失败表现:指针被修改为非法地址 → 访问异常
特点:
- ✅ 硬件加速,性能开销小
- ✅ 透明保护,应用无需修改
- ❌ 只保护指针,不保护数据
- ❌ 密钥泄漏会失效
BTI (Branch Target Identification)
目的:防止跳转到函数中间
机制:标记合法的跳转目标
关键指令:BTI C, BTI J, BTI JC
保护对象:函数入口点
失败表现:BTI 异常 → 崩溃
特点:
- ✅ 防御 ROP/JOP 攻击
- ✅ 编译器自动插入
- ❌ 只保护跳转,不保护顺序执行
- ❌ 可以通过禁用绕过
在 KernelPatch 中的正确处理
- ✅ 不要 NOP 化 PAC 指令(PACIASP/AUTIASP)
- ✅ 不要修改 BTI 的备份(直接备份原始值)
- ✅ 删除 setup1.S 中的 BTI 检测代码
- ✅ 在 relo_insts 开头添加 BTI JC
- ✅ 使用 B 指令劫持(跳过 BTI 检查)
🔗 相关文档
- 06-启动劫持流程.md - setup1.S 详解
- 15-问题排查与修复.md - BTI bug 修复
- 05-ARM64重定位机制.md - 指令处理
- 09-hook机制详解.md - Hook 实现
📚 参考资料
ARM 官方文档
- ARM Architecture Reference Manual ARMv8
- ARMv8.3-A Pointer Authentication
- ARMv8.5-A Branch Target Identification
Linux 内核文档
- Documentation/arm64/pointer-authentication.rst
- Documentation/arm64/branch-protection.rst
研究论文
- "PAC it up: Towards Pointer Integrity using ARM Pointer Authentication"
- "Examining Pointer Authentication on the iPhone XS"
文档版本:2.0
最后更新:2026-06-26
相关 KernelPatch 版本:0.13.2+
评论
- 还没有评论,来说点什么吧。