1. Linux 系统调用概述
系统调用(System Call)是用户态程序请求内核服务的唯一合法入口。理解系统调用的完整路径——从用户态 printf 到内核 sys_write——是后续实现系统调用表 Hook 的理论基础。本章从概念、切换机制、系统调用号与 sys_call_table 三个维度建立知识框架。
1.1 系统调用的概念
操作系统将 CPU 指令集划分为两个特权级:用户态(Ring 3) 和 内核态(Ring 0)。用户态程序不能直接访问硬件、修改页表或操作关键数据结构;必须通过系统调用陷入内核,由内核代表用户程序完成特权操作。
| 对比维度 | 用户态(Ring 3) | 内核态(Ring 0) |
|---|---|---|
| 权限范围 | 受限指令集,仅访问用户空间 | 全部指令,直接访问内核空间 |
| 内存访问 | 用户虚拟地址空间 | 内核虚拟地址空间 + 用户空间(通过 copy_from/to_user) |
| 典型操作 | 计算、逻辑判断、内存拷贝 | 文件 I/O、进程调度、网络协议栈 |
| 崩溃影响 | 仅终止当前进程 | 系统 panic 或 oops |
关键认知:系统调用是用户态与内核态之间的唯一受控通道。任何绕过系统调用的内核访问(如直接写 /dev/mem)都是非标准操作,在安全审计中会被标记为异常行为。
1.2 用户态与内核态切换
当用户程序调用 read()、write() 等库函数时,glibc 封装函数会执行以下切换流程:
- 参数准备:将系统调用号放入
rax寄存器,参数依次放入rdi、rsi、rdx等寄存器。 - 陷入指令:执行
syscall(x86_64)或int 0x80(x86),CPU 自动切换到 Ring 0。 - 入口处理:内核入口函数
entry_SYSCALL_64保存寄存器现场,根据rax中的系统调用号索引sys_call_table。 - 执行服务例程:调用对应的内核函数(如
ksys_read)。 - 返回用户态:执行
sysret指令,恢复寄存器,切换回 Ring 3。
性能数据:一次完整的系统调用切换(用户态→内核态→用户态)在 x86_64 上大约需要 50–200 ns,其中上下文保存/恢复占主要开销。这是为什么高性能场景会使用
io_uring 等批量提交机制来减少切换次数。
1.3 系统调用号与 sys_call_table
每个系统调用在 Linux 内核中都有一个唯一的整数编号,称为系统调用号。x86_64 架构下,系统调用号定义在 /usr/include/asm/unistd_64.h 中:
// 常见系统调用号(x86_64)
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
#define __NR_lseek 8
#define __NR_mmap 9
#define __NR_mprotect 10
// ... 共约 450+ 个系统调用
sys_call_table 是内核中一个函数指针数组,数组下标就是系统调用号,数组元素是对应的内核服务函数地址。其定义位于内核源码 arch/x86/kernel/syscall_64.c:
// 内核中的 sys_call_table 定义(简化)
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 ... __NR_syscall_max] = &sys_ni_syscall,
[0] = sys_read,
[1] = sys_write,
[2] = sys_open,
[3] = sys_close,
// ...
};
重要安全约束:在标准内核中,
sys_call_table 被标记为 const 且所在内存页被设置为只读(通过 mark_rodata_ro)。内核模块要修改系统调用表,必须先修改页表属性(如 set_memory_rw),这本身就是安全敏感操作——这也是为什么系统调用表 Hook 技术通常需要关闭内核 CONFIG_STRICT_MEMORY_RWX 或使用 kprobes 等替代方案。
1.4 系统调用表 Hook 的切入点
理解上述机制后,系统调用表 Hook 的核心思路就非常清晰了:
- 定位:获取
sys_call_table在内核中的虚拟地址(通过kallsyms_lookup_name或直接读取/proc/kallsyms)。 - 修改权限:将
sys_call_table所在内存页从只读改为可写(调用set_memory_rw)。 - 替换指针:将目标系统调用号对应的函数指针替换为自定义的 Hook 函数地址。
- 恢复权限(可选):修改完成后将页属性恢复为只读,防止被篡改检测。
实战提示:在 5.7+ 内核中,
kallsyms_lookup_name 不再导出给模块使用,需要借助 kprobes 或直接解析 /proc/kallsyms 来获取 sys_call_table 地址。后续章节会详细演示这些绕过方法。
下表总结了系统调用表 Hook 涉及的关键内核符号及其用途:
| 符号/函数 | 用途 | 内核版本依赖 |
|---|---|---|
sys_call_table |
系统调用函数指针数组 | 所有版本 |
kallsyms_lookup_name |
按名称查找内核符号地址 | 5.7 前导出,之后需 kprobes |
set_memory_rw |
修改内存页为可读写 | 所有版本 |
__NR_xxx |
系统调用号宏定义 | 架构相关,x86_64 稳定 |