溺的文档
KernelPatch · 第 16 篇 / 共 22 篇

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 内核在启动过程中会:

  1. 检测 CPU 特性:检查是否支持 PAC
  2. 检查内核配置:是否启用了 SCS (CONFIG_SHADOW_CALL_STACK)
  3. 动态替换指令:如果 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));
    }
}

总结

关键点

  1. paciaspstr x30, [x18], #8 是 Linux 内核的 Shadow Call Stack (SCS) 机制
  2. 替换发生在运行时,通过内核的 alternatives 机制动态替换
  3. x18 寄存器被用作 Shadow Call Stack 指针
  4. 替换原因: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+

评论

  • 还没有评论,来说点什么吧。

无需注册或登录,填个昵称即可评论。