2. 系统调用表(sys_call_table)结构

系统调用表是 Linux 内核中用于将系统调用号映射到对应内核函数的核心数据结构。理解其定义、存储位置以及可写性控制机制,是实现系统调用表 Hook 的技术基础。

2.1 sys_call_table 的定义

sys_call_table 本质上是一个函数指针数组,数组的索引对应系统调用号(syscall number),数组元素指向对应的内核服务例程(sys_xxx)。

核心定义(arch/x86/kernel/syscall_64.c):
// 在 x86_64 架构下,系统调用表定义为:
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

关键属性说明:

属性 说明
类型 sys_call_ptr_t —— 函数指针类型,指向返回 long 并接受 struct pt_regs * 参数的函数
限定符 const —— 表本身被标记为只读,防止直接修改
大小 __NR_syscall_max + 1 —— 包含所有系统调用号对应的槽位
初始化 默认填充 sys_ni_syscall(未实现/未定义的系统调用),然后通过 #include 展开具体条目
注意: 不同架构下 sys_call_table 的定义位置不同。x86 在 arch/x86/kernel/syscall_64.c,ARM64 在 arch/arm64/kernel/sys.c,x86_32 则在 arch/x86/kernel/syscall_32.c。

2.2 存储位置与导出

sys_call_table 的存储位置因内核版本和配置而异,这直接影响了 Hook 模块能否直接获取其地址。

  • 内核符号表导出: 早期内核(2.6.x 及之前)中,sys_call_table 通过 EXPORT_SYMBOL 导出,模块可以直接引用。
  • 未导出阶段: 自内核 2.6.18 起,sys_call_table 不再默认导出,模块需要通过 kallsyms_lookup_name 或 kprobes 动态查找。
  • KASLR 影响: 内核地址空间布局随机化(KASLR)使 sys_call_table 的基地址在每次启动时随机化,增加了定位难度。
  • 内存段: 通常位于内核的 .rodata 段(只读数据段),受页级写保护。
安全提示: 由于 sys_call_table 位于只读内存页,直接写入会导致页错误。必须通过修改 CR0 寄存器或临时设置页表属性来绕过写保护。
sys_call_table 存储位置与访问路径 内核内存布局 .text (代码段) .rodata (只读数据) .data (可写数据) .bss (未初始化) 堆/栈/动态区 sys_call_table (函数指针数组) 位于 模块访问路径 EXPORT_SYMBOL kallsyms_lookup_name kprobes 探测 /proc/kallsyms 解析 获取地址

2.3 可写性控制:CR0 寄存器

CR0 是 x86/x86_64 架构中的控制寄存器,其第 16 位(WP,Write Protect)控制着内核是否允许向只读页写入数据。

CR0 寄存器 WP 位的作用:
  • WP = 0: 允许内核态代码向任何线性地址写入,包括标记为只读的页。这是早期内核的默认行为。
  • WP = 1: 禁止内核态代码向只读页写入,违反时触发页错误。现代内核默认启用此位以增强安全性。

由于 sys_call_table 位于 .rodata 段,且 CR0.WP 通常为 1,直接修改会导致内核崩溃。Hook 模块必须临时清除 WP 位,完成写入后立即恢复。

2.4 修改 CR0 的标准操作流程

以下代码展示了在 x86_64 架构下,通过内联汇编临时关闭并恢复 CR0.WP 位的标准模式:

// 关闭写保护(清除 WP 位)
static inline void disable_write_protection(void)
{
    unsigned long cr0;
    asm volatile("mov %%cr0, %0" : "=r" (cr0));
    cr0 &= ~(1UL << 16);   // 清除第 16 位(WP)
    asm volatile("mov %0, %%cr0" : : "r" (cr0));
}

// 恢复写保护(设置 WP 位)
static inline void enable_write_protection(void)
{
    unsigned long cr0;
    asm volatile("mov %%cr0, %0" : "=r" (cr0));
    cr0 |= (1UL << 16);    // 设置第 16 位(WP)
    asm volatile("mov %0, %%cr0" : : "r" (cr0));
}
最佳实践: 修改 CR0 的操作应尽可能短,仅在替换 sys_call_table 中单个条目时关闭写保护,替换完成后立即恢复。避免长时间暴露内核内存的可写性。
CR0 寄存器 WP 位控制流程 CR0 控制寄存器(64 位) WP 位 16 读取 CR0 值 清除/设置 WP 位 写回 CR0 寄存器 结果:临时获得对 .rodata 段的写入能力

2.5 关键对比:不同架构下的可写性控制

架构 控制方式 寄存器/机制 位/标志
x86/x86_64 CR0 寄存器 WP 位(第 16 位) 0 = 允许写入只读页
ARM64 SCTLR_EL1 寄存器 WXN 位(第 19 位) 0 = 允许执行只读页写入
RISC-V satp 页表 + mstatus SUM 位(mstatus 第 18 位) 1 = 允许 Supervisor 访问用户页
安全风险: 修改 CR0.WP 位会暂时禁用内核的写保护机制。如果在多核系统上执行此操作,其他核心可能同时利用这个窗口进行恶意写入。建议在修改期间使用 local_irq_save 禁用本地中断,并确保操作在单核上下文中完成。

2.6 总结要点

  • sys_call_table 是 const 修饰的函数指针数组,位于内核 .rodata 段,默认不可写。
  • 存储位置 受 KASLR 影响,模块需要通过 kallsyms_lookup_name 等动态查找方式获取地址。
  • 可写性控制 通过 CR0 寄存器的 WP 位实现,Hook 模块必须临时清除 WP 位才能修改表项。
  • 操作原则:最小化写保护窗口,修改后立即恢复,避免引入安全漏洞或系统不稳定。