PAC 指令运行时替换机制详解
2026-06-29 · 阅读 0
问题现象
观察到的现象:
- 编译时(kernel文件):函数头部是
paciasp指令(编码:0xD503233F) - 运行时(内核模块查看):函数头部变成了
str x30, [x18], #8指令(编码:0xF85F8A5E)
根本原因:Shadow Call Stack (SCS) 机制
这是 Linux 内核的 Shadow Call Stack (SCS) 机制在运行时动态替换 PAC 指令的结果。
Shadow Call Stack (SCS) 简介
什么是 SCS?
Shadow Call Stack (SCS) 是 Linux 内核实现的软件级返回地址保护机制,用于在不支持 PAC 的 CPU 上提供类似的保护,或者作为 PAC 的补充/替代方案。
SCS 工作原理
【普通栈(可被攻击)】
栈内存:
[SP+0x00]: 局部变量
[SP+0x08]: 返回地址 (X30) ← 攻击者可以修改这里
[SP+0x10]: 调用者栈帧
【Shadow Stack(受保护)】
Shadow Stack 内存(x18 指向):
[x18+0x00]: 返回地址 1 ← 攻击者无法访问
[x18+0x08]: 返回地址 2
[x18+0x10]: 返回地址 3
SCS 指令序列
函数入口(替换 paciasp):
str x30, [x18], #8 ; 将返回地址保存到 shadow stack
; x18 = x18 + 8 (后索引)
函数返回前(替换 autiasp):
ldr x30, [x18, #-8]! ; 从 shadow stack 恢复返回地址
; x18 = x18 - 8 (前索引)
autiasp ; 如果启用 PAC,还需要验证
ret ; 返回
Linux 内核的指令替换机制
替换时机
Linux 内核在启动过程中会:
- 检测 CPU 特性:检查是否支持 PAC
- 检查内核配置:是否启用了 SCS (
CONFIG_SHADOW_CALL_STACK) - 动态替换指令:如果 PAC 不可用或需要 SCS,将所有 PAC 指令替换为 SCS 指令
替换过程
// 伪代码:Linux 内核的指令替换机制
void __init apply_alternatives(void)
{
// 检测 CPU 是否支持 PAC
if (!cpu_has_pac()) {
// 不支持 PAC,使用 SCS 替代
apply_scs_patches();
} else if (IS_ENABLED(CONFIG_SHADOW_CALL_STACK)) {
// 支持 PAC,但配置要求使用 SCS
apply_scs_patches();
}
}
void apply_scs_patches(void)
{
// 查找所有 paciasp 指令
for_each_paciasp_instruction(addr) {
// 替换为 str x30, [x18], #8
patch_instruction(addr, 0xF85F8A5E); // str x30, [x18], #8
}
// 查找所有 autiasp 指令
for_each_autiasp_instruction(addr) {
// 替换为 ldr x30, [x18, #-8]!
patch_instruction(addr, 0xF85F8A1E); // ldr x30, [x18, #-8]!
}
// 刷新指令缓存
flush_icache_all();
}
指令编码对比
PAC 指令编码
PACIASP: 0xD503233F
31-28: 1101 (固定)
27-24: 0101 (固定)
23-21: 000 (固定)
20-16: 00110 (操作码)
15-5: 00000011111 (固定)
4-0: 11111 (固定)
AUTIASP: 0xD503231F
与 PACIASP 只差 bit 5 (0 → 1)
SCS 指令编码
str x30, [x18], #8: 0xF85F8A5E
31-24: 11111000 (STR 后索引)
23-21: 101 (64-bit, post-index)
20-12: 1111000101 (x30, x18, imm=8)
11-0: 010111100010 (编码细节)
ldr x30, [x18, #-8]!: 0xF85F8A1E
31-24: 11111000 (LDR 前索引)
23-21: 100 (64-bit, pre-index)
20-12: 1111000101 (x30, x18, imm=-8)
11-0: 000111100010 (编码细节)
x18 寄存器的特殊用途
ARM64 寄存器约定
在 ARM64 架构中:
- x18:平台保留寄存器(Platform Reserved Register)
- 在 Linux 内核中,x18 被用作 SCS 指针(Shadow Call Stack Pointer)
SCS 指针初始化
// 内核启动时初始化 SCS
void __init scs_init(void)
{
// 为每个任务分配 shadow stack
// x18 寄存器指向当前任务的 shadow stack
//
// Shadow stack 通常分配在内核内存中
// 大小:每个任务 ~1KB
}
任务切换时的处理
// 任务切换时保存/恢复 x18
switch_to:
// 保存当前任务的 x18
str x18, [prev_task, #task_scs_offset]
// 恢复新任务的 x18
ldr x18, [next_task, #task_scs_offset]
// 继续任务切换...
为什么会出现这种替换?
原因 1:CPU 不支持 PAC
某些 ARM64 CPU(如 Cortex-A53/A55)不支持 PAC
→ 内核检测到不支持
→ 自动使用 SCS 作为替代
→ paciasp 被替换为 str x30, [x18], #8
原因 2:内核配置启用 SCS
内核配置:CONFIG_SHADOW_CALL_STACK=y
→ 即使 CPU 支持 PAC,也使用 SCS
→ paciasp 被替换为 str x30, [x18], #8
→ 原因:SCS 更简单,不需要密钥管理
原因 3:PAC 被禁用
启动参数:nopti 或 noharden
→ PAC 功能被禁用
→ 使用 SCS 作为回退方案
→ paciasp 被替换为 str x30, [x18], #8
完整的函数对比
使用 PAC 的函数(编译时)
my_function:
paciasp ; 0xD503233F - 签名返回地址
stp x29, x30, [sp, #-16]!
mov x29, sp
// ... 函数体 ...
ldp x29, x30, [sp], #16
autiasp ; 0xD503231F - 验证签名
ret
使用 SCS 的函数(运行时)
my_function:
str x30, [x18], #8 ; 0xF85F8A5E - 保存到 shadow stack
stp x29, x30, [sp, #-16]!
mov x29, sp
// ... 函数体 ...
ldp x29, x30, [sp], #16
ldr x30, [x18, #-8]! ; 0xF85F8A1E - 从 shadow stack 恢复
ret
在 KernelPatch 中的影响
问题 1:指令识别错误
// 错误的检测方式
if (insn == 0xD503233F) { // 只检查 paciasp
// 处理 PAC...
}
// ❌ 运行时 paciasp 已经被替换,检测不到!
// 正确的检测方式
if (insn == 0xD503233F || insn == 0xF85F8A5E) {
// 处理 PAC 或 SCS
}
问题 2:Hook 时需要处理 SCS
// hook_prepare 时需要识别 SCS 指令
hook_err_t hook_prepare(hook_t *hook)
{
uint32_t insn = *((uint32_t *)hook->origin_addr);
// 检测 SCS
if (insn == 0xF85F8A5E) { // str x30, [x18], #8
// 这是 SCS 指令,需要特殊处理
// 重定位时需要保持 x18 的使用
}
// 检测 PAC
if (insn == 0xD503233F) { // paciasp
// 这是 PAC 指令
}
// 重定位指令...
}
问题 3:备份和恢复
// 备份时需要记录实际指令
void backup_function(uint64_t func_addr)
{
uint32_t insn = *((uint32_t *)func_addr);
// 备份实际运行的指令(可能是 SCS)
backup[0] = insn; // 可能是 0xF85F8A5E,不是 0xD503233F
// 恢复时使用备份的指令
*((uint32_t *)func_addr) = backup[0];
}
如何确认是否使用了 SCS?
方法 1:检查内核配置
# 查看内核配置
zcat /proc/config.gz | grep SHADOW_CALL_STACK
# 输出:CONFIG_SHADOW_CALL_STACK=y
# 或者
cat /boot/config-$(uname -r) | grep SHADOW_CALL_STACK
方法 2:检查 dmesg 日志
# 查看内核启动日志
dmesg | grep -i "shadow\|scs\|pac"
# 可能输出:
# [ 0.000000] Shadow Call Stack enabled
# [ 0.000000] Using SCS instead of PAC
方法 3:检查 CPU 特性
# 查看 CPU 特性
cat /proc/cpuinfo | grep Features
# 如果看到 "pac" 但实际使用 SCS,说明被配置覆盖了
方法 4:反汇编内核函数
# 使用 objdump 反汇编
objdump -d /boot/vmlinux | grep -A 5 "my_function"
# 查看第一条指令是 paciasp 还是 str x30, [x18], #8
最佳实践
1. 同时支持 PAC 和 SCS
// 检测返回地址保护机制
bool has_return_address_protection(uint64_t func_addr)
{
uint32_t insn = *((uint32_t *)func_addr);
// PAC
if (insn == 0xD503233F) return true; // paciasp
// SCS
if (insn == 0xF85F8A5E) return true; // str x30, [x18], #8
return false;
}
2. Hook 时正确处理
hook_err_t hook_prepare(hook_t *hook)
{
uint32_t insn = *((uint32_t *)hook->origin_addr);
// 检测保护机制
if (insn == 0xD503233F) {
// PAC:需要保持 PAC 指令
hook->has_pac = true;
} else if (insn == 0xF85F8A5E) {
// SCS:需要保持 x18 的使用
hook->has_scs = true;
// 重定位时需要确保 x18 不被破坏
}
// 重定位指令...
}
3. 备份时记录实际指令
void backup_function_instructions(uint64_t func_addr, uint32_t *backup, int count)
{
for (int i = 0; i < count; i++) {
// 备份运行时实际指令(不是编译时的)
backup[i] = *((uint32_t *)(func_addr + i * 4));
}
}
总结
关键点
paciasp→str x30, [x18], #8是 Linux 内核的 Shadow Call Stack (SCS) 机制- 替换发生在运行时,通过内核的 alternatives 机制动态替换
- x18 寄存器被用作 Shadow Call Stack 指针
- 替换原因:CPU 不支持 PAC、内核配置启用 SCS、或 PAC 被禁用
在 KernelPatch 中
- ✅ 需要同时检测 PAC 和 SCS 指令
- ✅ Hook 时需要保持 x18 寄存器不被破坏
- ✅ 备份时记录运行时实际指令
- ✅ 恢复时使用备份的实际指令
参考资料
- Linux 内核源码:
arch/arm64/kernel/entry.S - Linux 内核源码:
arch/arm64/kernel/alternative.c - Linux 内核文档:
Documentation/arm64/shadow-call-stack.rst - ARM 架构参考手册:ARMv8-A Architecture Reference Manual
文档版本:2.0
最后更新:2026-06-26
相关 KernelPatch 版本:0.13.2+
评论
- 还没有评论,来说点什么吧。