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 寄存器或临时设置页表属性来绕过写保护。
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 中单个条目时关闭写保护,替换完成后立即恢复。避免长时间暴露内核内存的可写性。
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 位才能修改表项。
- 操作原则:最小化写保护窗口,修改后立即恢复,避免引入安全漏洞或系统不稳定。