题目分析
题目代码很简单,仅注册了ioctl函数,里面包含了两个case,在参数为0x6666时,可以泄露出bss段flag的地址。
在参数为0x1337时,用户输入是一个结构体,而传入驱动的是一个指针。
这个结构体应该是这样的:
1 | struct _input |
对这个结构体指针进行一系列判断以后,会比较结构体中的flag指针指向的内容和长度,当比较的用户输入长度和内容都是flag时,使用printk打印flag内容。
仔细查看一下_chk_range_not_ok,看类C代码看不出什么意思,查看一下汇编代码:
1 | .text:0000000000000000 __chk_range_not_ok proc near ; CODE XREF: baby_ioctl+7B↓p |
可以看出,是将第一个参数和第二个参数相加,判断是否小于第三个参数,如果不小于将al置为1。
在动态调试的时候,发现第三个参数是一个常量: 0x7ffffffff000
这样的话,题目就很清楚了。判断的限制是传入的结构体+16,也就是整个结构体要在用户态中,并且结构体中第一个成员所指向的内存也要在用户态中。并且第二个参数要和bss段上的flag长度相等。这样会逐字节比较输入的flag是否等于bss保存的flag。
环境搭建
和libc的pwn题不太一样,内核驱动的pwn需要驱动在一个版本的内核代码下编译,否则无法运行。
在程序代码中可以看到一个内核版本号——4.15.0-22-generic SMP mod_unload,我从官网源代码下载的版本只有4.15.0而没有-22的小版本。
此时,可以通过下载相应的内核deb包。
1 | apt download linux-image-4.15.0-22-generic |
下载得到的linux-image-4.15.0-22-generic_4.15.0-22.24~16.04.1_amd64.deb中data.tar.xz下的boot中可以找到一个叫vmlinuz-4.15.0-22-generic的文件,这就是需要的内核映像。
此处需要明确文件的区别 //一定是我太菜才不知道
vmlinux:这个是编译出来原始的内核,未经过压缩,不能直接通过qemu启动,是一个ELF文件,里面包括了符号表等等一系列内核相关的指令,可以用IDA Pro查看,可以找gadget等等等等。类比于libc pwn中的libc-2.23.so之类的,给了这个东西就可以找gadget、找地址偏移等等。
bzImage:这个是vmlinux压缩以后,并且加上一段解压启动代码得到。这个东西可以放到QEMU中跑,但是不能用IDA打开。
*.cpio:这个东西一般都是打包生成的,最开始是从busybox中导出来的,类似于启动的文件系统吧(?)一般kernel pwn会在这里放*.ko。甚至放vmlinux…
漏洞利用
环境搭建起来了,就可以看一下漏洞如何利用了。比赛中出现了两种解题方法,一种是预期解,另外一种是侧信道攻击。
侧信道攻击
所谓侧信道攻击就是用能标明flag内容的方法,通过其他表现形式把flag爆破出来。
在程序中存在一个问题,就是在比较之前没有判断用户输入的flag段是否可读的,当输入的flag处于不可读段的时候,在比较时会触发段错误,从而造成kernel panic,利用这种现象可以用一下方法每次爆破一个字节的flag。
方法原理如下,利用mmap新建3个段,第一个、第三个权限设为000,第二可读写,并且每次将已有的flag防止在第二个段的最后,每次最后一个字节时爆破的字节,当这个字节和flag不符合时,内核驱动会退出,因此不触发错误。而当最后一个字节正确时,程序比较会下移一个字节,触发错误,引起kernel panic,从而可以判断出单字节的flag,原理如下:
这样测试33次就可以得到flag了。
EXP
1 | // start.c |
预期解
预期解的漏洞叫做double fetch漏洞,应该算是一种竞争条件漏洞
Serna[ Serna, F. J. MS08-61:thecaseofthekernelmodedoublefetch,2008.https://blogs.technet.microsoft.com/srd/2008/10/14/ms08-061-the-case-of-the-kernel-mode-double-fetch/.]
用户通常会通过调用内核函数完成特定功能,当内核函数两次从同一用户内存地址读取同一数据时,通常第一次读取用来验证数据或建立联系,第二次则用来使用该数据。与此同时,用户空间并发运行的恶意线程可以在两次内核读取操作之间,利用竞争条件对该数据进行篡改,从而造成内核使用数据的不一致。Double fetch漏洞可造成包括缓冲区溢出、信息泄露、空指针引用等后果,最终造成内核崩溃或者恶意提权。
也就是说,参数是从用户态传进来的,当用户态对传入的结构体改变时,内核读到的数据也会被改变。
因此,在用户态新建一个线程不断的修改传入的结构体中flag指针为内核flag的值。当驱动运行时,恰好通过地址验证后,在数据判断之前内核数据flag地址被改掉的话,则可以做到通过内容验证,从而打印出flag内容。
printk输出的内容可以用dmesg来查看。
此处有个坑点,是QEMU默认启动时会使用当个core、单个thread,这样内核相当于是单进程的。
而单进程的内核很难触发这个漏洞,因此需要在QEMU启动时设置好内核和进程数如:
1 | -m 256M -smp 2,cores=2,threads=1 \ |
这个问题坑了我好久,此处非常感谢Veritas501师傅。
具体EXP,Veritas501师傅的文章写的很好了,我没有什么创新点就不写了。
reference
https://veritas501.space/2018/06/04/0CTF%20final%20baby%20kernel/
https://www.secspace.com/view-ff3bbe863b544a929f96110e7b8992c8-e5cf621eacdb49b3b35b71a20e0ce9be.html