4. 常见系统调用漏洞案例:缓冲区溢出、整数溢出、空指针解引用、格式字符串漏洞
系统调用是用户空间与内核空间交互的唯一入口,也是攻击者最常利用的攻击面。即使系统调用处理函数逻辑正确,若参数校验不严,仍可能引发严重的安全漏洞。本章通过四个经典漏洞案例,深入分析其原理、触发条件与防护策略。
核心观点: 系统调用参数校验的缺失或缺陷,是内核漏洞的主要成因。理解这些漏洞的底层机制,是编写安全内核模块的基础。
4.1 缓冲区溢出漏洞
缓冲区溢出是最经典的内存破坏漏洞。当系统调用从用户空间拷贝数据到内核缓冲区时,若未严格校验数据长度,超出缓冲区容量的数据会覆盖相邻内存区域,可能导致内核崩溃或权限提升。
4.1.1 漏洞原理
内核模块在处理 copy_from_user() 或 strncpy_from_user() 等操作时,如果目标缓冲区大小固定,而用户提供的长度参数大于缓冲区实际容量,就会发生溢出。
4.1.2 典型漏洞代码
// 漏洞示例:未校验长度的系统调用处理函数
static long vulnerable_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
char kernel_buf[64]; // 固定大小缓冲区
char __user *user_buf = (char __user *)arg;
// 危险:直接拷贝用户数据,未检查长度
if (copy_from_user(kernel_buf, user_buf, sizeof(kernel_buf) + 64)) {
return -EFAULT;
}
// 后续处理...
return 0;
}
4.1.3 攻击效果
- 覆盖栈返回地址: 攻击者可控制内核执行流,跳转到任意地址。
- 覆盖关键数据结构: 如修改进程凭证结构体,实现提权。
- 内核崩溃(DoS): 破坏内核栈或堆结构,导致系统重启。
4.1.4 防护策略
| 防护措施 | 说明 |
|---|---|
| 严格长度校验 | 在拷贝前比较用户提供的长度与缓冲区实际容量 |
| 使用安全函数 | 优先使用 strscpy()、memcpy_s() 等边界检查函数 |
| 栈保护机制 | 启用 CONFIG_STACKPROTECTOR 内核选项 |
| 地址空间布局随机化 | KASLR 增加攻击者预测地址的难度 |
最佳实践: 始终使用
copy_from_user() 的返回值判断实际拷贝的字节数,并确保目标缓冲区大小由 sizeof() 或宏定义,而非硬编码。
4.2 整数溢出漏洞
整数溢出发生在算术运算结果超出数据类型所能表示的范围时。在内核系统调用中,整数溢出常被用于绕过长度检查,进而触发缓冲区溢出或其他内存破坏。
4.2.1 漏洞原理
当系统调用参数中包含长度、大小或索引等整数值时,若内核模块对这些值进行加法、乘法或类型转换操作,可能产生意想不到的小值或负值,导致后续内存操作越界。
4.2.2 典型漏洞代码
// 漏洞示例:整数溢出导致缓冲区分配不足
static long vulnerable_write(struct file *filp, const char __user *buf, size_t count)
{
unsigned int total_size;
char *kernel_buf;
// 漏洞:count 为 0xFFFFFFFF 时,+1 溢出为 0
total_size = count + 1;
kernel_buf = kmalloc(total_size, GFP_KERNEL);
if (!kernel_buf)
return -ENOMEM;
// 实际拷贝 count 字节,但缓冲区只有 0 字节
if (copy_from_user(kernel_buf, buf, count)) {
kfree(kernel_buf);
return -EFAULT;
}
// ...
}
4.2.3 常见整数溢出类型
| 溢出类型 | 示例 | 后果 |
|---|---|---|
| 加法溢出 | size = a + b,a+b > UINT_MAX |
分配过小缓冲区 |
| 乘法溢出 | size = n * elem_size,乘积溢出 |
堆缓冲区溢出 |
| 符号转换溢出 | int → unsigned int,负数变极大正数 |
绕过长度检查 |
| 截断溢出 | size_t → unsigned short,高位被截断 |
索引越界 |
4.2.4 防护策略
- 使用安全算术函数: 内核提供
check_add_overflow()、check_mul_overflow()等宏。 - 前置校验: 在算术操作前检查操作数是否会导致溢出。
- 使用无符号类型: 避免符号转换带来的意外。
- 静态分析工具: 使用
sparse或Coverity检测整数溢出。
注意: 整数溢出漏洞往往不是独立存在,而是作为触发缓冲区溢出的“跳板”。攻击者通过整数溢出绕过长度检查,再通过缓冲区溢出实现代码执行。
4.3 空指针解引用漏洞
空指针解引用是指程序试图访问地址为 0 的内存区域。在内核中,这通常会导致内核崩溃(Oops),但在某些架构或配置下,攻击者可利用此漏洞实现任意地址读写。
4.3.1 漏洞原理
系统调用参数中可能包含指针类型,如用户空间地址、内核对象指针等。若内核模块未检查指针是否为 NULL 就直接解引用,就会触发空指针漏洞。
4.3.2 典型漏洞代码
// 漏洞示例:未检查指针是否为 NULL
static long vulnerable_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct user_data __user *udata = (struct user_data __user *)arg;
struct kernel_data *kdata;
// 漏洞:未检查 udata 是否为 NULL
if (copy_from_user(&kdata, &udata->ptr, sizeof(kdata))) {
return -EFAULT;
}
// 如果 udata 为 NULL,udata->ptr 解引用会崩溃
// 或者 kdata 为 NULL 时,后续操作也会崩溃
kdata->value = 42; // 空指针解引用
return 0;
}
4.3.3 攻击场景
| 场景 | 触发方式 | 影响 |
|---|---|---|
| 用户传入 NULL 指针 | ioctl(fd, CMD, NULL) |
内核崩溃(DoS) |
| 内核对象分配失败 | kmalloc 返回 NULL 未检查 |
内核崩溃或信息泄露 |
| 释放后重用(UAF) | 对象被释放后指针未置 NULL | 任意代码执行 |
| mmap 映射失败 | 映射地址为 0 的页面 | 低权限用户提权 |
4.3.4 防护策略
- 显式 NULL 检查: 在解引用前使用
if (!ptr) return -EINVAL;。 - 使用 IS_ERR() 宏: 检查内核函数返回的指针是否有效。
- 禁用 mmap_min_addr: 防止用户空间映射零页(但需谨慎)。
- 使用内核地址消毒剂(KASAN): 检测内存错误访问。
提示: 在 Linux 内核中,
mmap_min_addr 默认值为 65536,防止用户空间映射低地址区域,从而降低空指针解引用的可利用性。
4.4 格式字符串漏洞
格式字符串漏洞在用户空间已较为罕见,但在内核模块中仍时有发生。当系统调用参数中的字符串被直接传递给 printk()、sprintf() 等格式化函数时,攻击者可通过精心构造的格式说明符读取或写入内核内存。
4.4.1 漏洞原理
如果内核模块将用户提供的字符串作为格式化函数的 格式字符串参数(而非 格式化参数),攻击者可以使用 %s、%x、%n 等格式说明符来操纵栈内存。
4.4.2 典型漏洞代码
// 漏洞示例:将用户输入直接作为格式字符串
static long vulnerable_print(struct file *filp, const char __user *user_str, size_t len)
{
char kernel_buf[256];
if (copy_from_user(kernel_buf, user_str, min(len, sizeof(kernel_buf)))) {
return -EFAULT;
}
kernel_buf[255] = '\0';
// 漏洞:用户输入作为格式字符串
printk(kernel_buf); // 应使用 printk("%s", kernel_buf)
return 0;
}
4.4.3 攻击向量
| 格式说明符 | 攻击效果 |
|---|---|
%s |
读取栈上的指针指向的字符串,导致信息泄露 |
%x / %p |
泄露栈上的十六进制值,如返回地址、canary 值 |
%n |
将已输出的字符数写入指定地址,实现任意内存写入 |
%*n |
更精细的内存写入控制 |
4.4.4 防护策略
- 固定格式字符串: 永远不要将用户输入作为
printk()的第一个参数。 - 使用
%s传递字符串: 正确写法为printk("%s", user_data)。 - 启用内核选项:
CONFIG_DEBUG_FS和CONFIG_SECURITY_DMESG_RESTRICT限制日志访问。 - 代码审查: 重点关注所有
printk()、sprintf()、snprintf()的调用。
严重性: 内核格式字符串漏洞通常可直接导致权限提升,因为
%n 允许攻击者向任意内核地址写入数据,覆盖进程凭证或函数指针。
4.5 漏洞对比与总结
| 漏洞类型 | 根本原因 | 典型后果 | 核心防护 |
|---|---|---|---|
| 缓冲区溢出 | 未校验数据长度 | 代码执行、提权 | 边界检查 + 安全函数 |
| 整数溢出 | 算术运算结果超出范围 | 绕过检查、堆溢出 | 安全算术宏 + 前置校验 |
| 空指针解引用 | 未检查指针有效性 | 内核崩溃、提权 | 显式 NULL 检查 |
| 格式字符串漏洞 | 用户输入作为格式字符串 | 信息泄露、任意写入 | 固定格式字符串 |
核心原则: 所有来自用户空间的参数都是不可信的。系统调用处理函数必须对每个参数进行严格的类型、范围、长度和有效性校验。使用内核提供的安全辅助函数(如
check_add_overflow、strscpy、IS_ERR)可以显著降低漏洞风险。