利用 pt_regs 构造通用内核 ROP

系统调用 与 pt_regs 结构体

系统调用的本质是什么?或许不少人都能够答得上来是由我们在用户态布置好相应的参数后执行 syscall 这一汇编指令,通过门结构进入到内核中的 entry_SYSCALL_64这一函数,随后通过系统调用表跳转到对应的函数。

现在让我们将目光放到 entry_SYSCALL_64 这一用汇编写的函数内部,注意到当程序进入到内核太时,该函数会将所有的寄存器压入内核栈上,形成一个 pt_regs 结构体,该结构体实质上位于内核栈底,定义arrow-up-right如下:

struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long rbp;
    unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long rax;
    unsigned long rcx;
    unsigned long rdx;
    unsigned long rsi;
    unsigned long rdi;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_rax;
/* Return frame for iretq */
    unsigned long rip;
    unsigned long cs;
    unsigned long eflags;
    unsigned long rsp;
    unsigned long ss;
/* top of stack page */
};

内核栈 与通用 ROP

我们都知道,内核栈只有一个页面的大小,而 pt_regs 结构体则固定位于内核栈栈底,当我们劫持内核结构体中的某个函数指针时(例如 seq_operations->start),在我们通过该函数指针劫持内核执行流时 rsp 与 栈底的相对偏移通常是不变的

而在系统调用当中过程有很多的寄存器其实是不一定能用上的,比如 r8 ~ r15,这些寄存器为我们布置 ROP 链提供了可能,我们不难想到:

  • 只需要寻找到一条形如 "add rsp, val ; ret" 的 gadget 便能够完成 ROP

这里笔者给出一个通用的 ROP 板子,方便调试时观察:

新版本内核对抗利用 pt_regs 进行攻击的办法

正所谓魔高一尺道高一丈,内核主线在 这个 commitarrow-up-right 中为系统调用栈添加了一个偏移值,这意味着 pt_regs 与我们触发劫持内核执行流时的栈间偏移值不再是固定值

当然,若是在这个随机偏移值较小且我们仍有足够多的寄存器可用的情况下,仍然可以通过布置一些 slide gadget 来继续完成利用,不过稳定性也大幅下降了。

例题:西湖论剑2021线上初赛 - easykernel

分析

首先查看启动脚本,可以发现开启了 SMEP 和 KASLR:

进入题目环境,查看 /sys/devices/system/cpu/vulnerabilities/*,可以发现开启了 PTI (页表隔离):

题目给了个 test.ko,拖入 IDA 进行分析,发现只定义了 ioctl,可以看出是常见的“菜单堆”题目,给出了分配、释放、读、写 object 的功能。对于分配 object,我们需要传入如下形式结构体:

对于释放、读、写 object,则需要传入如下形式结构体:

分配:0x20

比较常规的 kmalloc,没有限制size,最多可以分配 0x20 个 chunk:

释放:0x30

kfree 以后没有清空指针,直接就有一个裸的 UAF 糊脸

读:0x40

会调用 show 函数:

其实就是套了一层皮的读 object 内容,加了 hardened usercopy 检查:

写:0x50

常规的写入 object:

解法:UAF + seq_operations + pt_regs + ROP

题目没有说明,那笔者默认应该是没开 Hardened Freelist,现在又有 UAF,那么解法就是多种多样的了,笔者这里选择用 seq_operations + pt_regs 构造 ROP 进行提权:

exp 如下:

Last updated