2. Linux 系统调用基础:系统调用概念、系统调用号与表、用户态到内核态的切换流程
2.1 系统调用概念
系统调用(System Call)是用户空间程序请求内核服务的唯一合法入口。它定义了用户态与内核态之间的安全边界,确保任何对硬件资源、进程管理、文件系统、网络通信等核心功能的访问都必须经过内核的授权与校验。
在 Linux 中,系统调用具有以下关键特征:
- 特权级切换:从 Ring 3(用户态)切换到 Ring 0(内核态),获得执行特权指令的能力。
- 固定接口:每个系统调用由唯一的编号标识,参数传递遵循特定约定(寄存器传递)。
- 安全屏障:内核在系统调用入口处进行参数校验、权限检查、地址空间合法性验证。
- 可抢占性:系统调用执行过程中可能被更高优先级的任务或中断抢占。
安全视角: 系统调用是攻击者从用户态尝试提权或破坏内核的关键入口。内核模块开发者必须理解系统调用的完整路径,才能正确地在钩子函数或自定义系统调用中实施参数校验。
2.2 系统调用号与系统调用表
每个系统调用在 Linux 内核中都有一个唯一的 系统调用号(syscall number)。该编号是用户态程序与内核态处理函数之间的索引映射。
2.2.1 系统调用号分配
系统调用号定义在架构相关的头文件中,例如 x86_64 架构的定义位于 arch/x86/include/asm/unistd_64.h。以下为部分常见系统调用号示例:
| 系统调用号 | 系统调用名称 | 内核处理函数 | 功能简述 |
|---|---|---|---|
| 0 | read | sys_read | 从文件描述符读取数据 |
| 1 | write | sys_write | 向文件描述符写入数据 |
| 2 | open | sys_open | 打开或创建文件 |
| 39 | getpid | sys_getpid | 获取当前进程 ID |
| 59 | execve | sys_execve | 执行新程序 |
| 62 | kill | sys_kill | 向进程发送信号 |
2.2.2 系统调用表结构
内核维护一个全局的系统调用表 sys_call_table,该表是一个函数指针数组,数组下标即为系统调用号,数组元素指向对应的内核处理函数。
在 x86_64 架构中,系统调用表的声明类似于:
// arch/x86/kernel/syscall_64.c
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,
// ... 其他系统调用
};
系统调用表的关键安全属性:
- 只读保护:现代内核将
sys_call_table放置在只读内存区域,防止恶意模块篡改。 - 索引边界:内核在分发系统调用前会校验系统调用号是否在有效范围内(
nr < NR_syscalls)。 - 空指针防护:未实现的系统调用号指向
sys_ni_syscall,该函数直接返回-ENOSYS。
内核模块安全提示: 如果内核模块试图通过修改
1. 内核可能已启用
2. 即使成功修改,也必须在钩子函数中执行严格的参数校验,否则会引入安全漏洞。
sys_call_table 来钩取系统调用,必须注意:1. 内核可能已启用
CONFIG_HAVE_ARCH_SECCOMP_FILTER 或 CONFIG_ARCH_HAS_SYSCALL_WRAPPER,导致直接修改失效。2. 即使成功修改,也必须在钩子函数中执行严格的参数校验,否则会引入安全漏洞。
2.3 用户态到内核态的切换流程
当用户态程序发起系统调用时,CPU 需要完成从用户态到内核态的上下文切换。x86_64 架构下,主要使用 syscall 指令(快速系统调用)实现切换。
2.3.1 切换流程步骤
- 用户态准备:应用程序将系统调用号放入
rax寄存器,参数依次放入rdi、rsi、rdx、r10、r8、r9寄存器。 - 执行 syscall 指令:CPU 将
RIP保存到RCX,将RFLAGS保存到R11,然后跳转到IA32_LSTARMSR 指定的内核入口点(entry_SYSCALL_64)。 - 内核态入口处理:保存用户态寄存器上下文,切换到内核栈,设置
FS/GS段基址。 - 系统调用分发:调用
do_syscall_64()函数,该函数根据rax中的系统调用号索引sys_call_table,调用对应的处理函数。 - 内核处理与返回:系统调用处理函数执行完毕后,将返回值放入
rax,执行sysretq指令返回用户态,恢复RIP和RFLAGS。
2.3.2 切换流程 SVG 架构图
2.4 系统调用参数传递与校验时机
理解系统调用的切换流程后,内核模块开发者需要重点关注 参数校验的时机:
| 校验阶段 | 位置 | 校验内容 | 安全意义 |
|---|---|---|---|
| 入口校验 | do_syscall_64() 之前 |
系统调用号范围、seccomp 过滤 |
防止非法系统调用号、沙箱逃逸 |
| 参数预检 | 系统调用处理函数开头 | 用户态指针有效性、长度范围、权限检查 | 防止空指针解引用、缓冲区溢出 |
| 数据拷贝 | copy_from_user/copy_to_user |
地址空间合法性、页边界检查 | 防止内核内存泄露、任意地址读写 |
| 逻辑校验 | 系统调用执行过程中 | 参数语义合法性、状态一致性 | 防止逻辑漏洞、条件竞争 |
内核模块开发要点: 当编写自定义系统调用或钩取现有系统调用时,必须在 参数预检 阶段使用
access_ok() 宏验证用户态指针,使用 copy_from_user() 安全拷贝数据,切勿直接解引用用户态指针。
2.5 系统调用与内核模块安全增强的关系
系统调用是内核模块安全增强的核心战场。内核模块可以通过以下方式介入系统调用路径:
- 系统调用钩子:替换
sys_call_table中的函数指针,在原始调用前后插入参数校验逻辑。 - 自定义系统调用:通过
register_syscall或修改内核源码添加新的系统调用,实现专用安全接口。 - seccomp 过滤器:利用内核的 seccomp 机制限制进程可用的系统调用,减少攻击面。
- LSM 钩子:通过 Linux Security Module 框架在系统调用关键路径上实施访问控制。