BlizzardCTF2017-Strng
1.打开虚拟环境,然后都说flag在/root/flag,给的也不是vmlinux,那么就应该是qemu逃逸。
2.由于只有文件,大佬们都直接告诉用户名密码,也没说怎么找,那就当作本来题目给了用户名和密码。用户名是ubuntu,密码是passw0rd。
3.将qemu-system-x86_64拖到IDA中开始分析,会分析很长一段时间,先看看启动参数,launch.sh:
1 | ./qemu-system-x86_64 \ |
没啥好注意的,显示加载了设备strng,那么这应该就是需要分析的PCI设备。然后最后一行netdev user,id=net0,hostfwd=tcp::5555-:22,把22端口重定向到了宿主机的5555端口,所以使用ssh ubuntu@127.0.0.1 -p 5555进去。同时这里注意加载内存要1G,为了防止崩溃,我改成了128M。
4.然后进入qemu中看看设备信息,好找到mmio和pmio的地址:
(1)首先输入lspci,可以看到有一个Unclassified device
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
这个就应该是strng设备了。
(2)加载IDA完成之后验证一下,函数栏搜索strng,查看相关函数。先查看设备初始化函数:strng_class_init。(这里需要将变量k的类型设置为PCIDeviceClass*)
可以看到加载了strng设备,然后设备号device_id是0x11e9,vendor_id是0x1234,对应在qemu中查看一下刚才猜测的strng设备,输入:lspci -v -s 00:03.0
可以看到猜测没错。同时可以看到对应的mmio地址为0xfebf1000,大小256。pmio的地址为0xc050,大小8。
▲有时候这些命令可能不好使,判断完设备号之后,可以输入:
hexdump /sys/devices/pci0000:00/0000:00:03.0/config来查看
5.之后从read和write函数中找漏洞:
这里将第一个参数opaque修改下类型为:struct STRNGState,至于为什么是这个,打开read和write的汇编代码,很明显发现有STRNGState,然后再跳转到结构体界面中找,虽然不太好找。找到之后双击,可以显示出结构体的所有成员,发现就是需要的那个:
(1)先看strng_mmio_read函数,读入addr并按二进制将其右移两位,相当于除以4,之后将结果作为regs数组的索引,返回该regs[add>>2]的值。同时还需要注意的是addr的低两位只能为0,否则过不了if ( size == 4 && !(addr & 3) )的检查。
(2)再看strng_mmio_write函数:
当size等于4时,将addr右移两位得到寄存器的索引idx,并提供4个功能:
①当idx为0时,调用srand函数但并不给赋值给内存。当i为1时,调用rand得到随机数并赋值给regs[1]。
②当idx为3时,调用rand_r函数,使用regs[2]的地址作为参数,最后将返回值赋值regs[3],但后续仍然会将val值覆盖到regs[3]中,就是迷惑用的,实际功能也就是将传入的value赋值给regs[3]。
▲但是这里的传regs[2]的地址也是一个关键,如果我们能将rand_r函数劫持为system函数,然后在regs[2]中放入”cat /root/flag”字符串,那不就可以调用system(“cat /root/flag”)从而读取flag了吗。
其余则直接将传入的value赋值给regs[idx]。
那么通过控制addr,进而控制idx>=2,就可以逐次将4个字节数据写入到regs[idx]上。
▲按理说如果将idx超出regs数组范围,64之后,那么不就可以任意越界写了吗,但是这里不行,因为传入的addr是不能大于mmio的大小,pci设备内部会进行检查,而刚好regs的大小为256,所以无法通过mmio进行越界读写。
(3)接着看strng_pmio_read函数:当传入的端口地址addr为0时,直接返回opaque->addr,否则将opaque->addr右移两位作为索引idx,返回regs[idx]的值。这个opaque->addr在strng_pmio_write中被赋值。
(4)然后再看strng_pmio_write函数:
当size等于4时,以传入的端口地址为判断提供4个功能:
①当传入的端口地址addr为0时,直接将传入的value赋值给opaque->addr。
②当传入的端口地址addr不为0时,将opaque->addr右移两位得到索引idx,分为三个功能:
A.idx为0时,执行srand,返回值不存储。
B.idx为1时,执行rand并将返回结果存储到regs[1]中。
C.idx为3时,调用rand_r并将regs[2]的地址作为第一个参数,返回值存储到regs[3]中。
否则直接将value存储到regs[idx]中。
▲这里就可以调用strng_pmio_write函数,形成任意地址写漏洞。
A.通过将addr设置为0,然后使得传入的value直接赋值给opaque->addr,使得opaque->addr形成的索引idx大于64,将reg[idx]越界指向rand_r函数指针。
B.然后再次调用strng_pmio_write函数,传入不为0的addr。通过opaque->addr形成的索引idx,使得regs[idx]指向rand_r函数指针,将value越界写入rand_r,劫持rand_r函数。
6.那么总的利用过程就清楚了:
(1)通过strng_mmio_write函数,将regs[2]赋值为”cat /root/flag”。
(2)通过strng_pmio_write函数,将rand_r函数劫持为system函数。
之后再调用strng_mmio_write函数,使得idx为3,然后将regs[2]的地址作为参数,调用rand_r函数,从而调用system(“cat /root/flag”)获取flag。
7.但是现在还需要system函数的地址,通过上面分析,可以发现有一个越界读漏洞:
(1)通过strng_pmio_write函数设置opaque->addr,使得opaque->addr形成的索引idx大于64,进而使得regs[idx]指向srand函数。
(2)通过strng_pmio_read函数,借助修改后的opaque->addr,读取idx索引regs[idx]指向的内容,也就是srand函数指针中的内容,对应的就是srand函数地址。
(这里大多数的exp都是针对srandom函数来泄露libc的,但rand或者rand_r应该也都可以)
8.那么现在总的利用过程就是泄露libc地址,然后改写rand_r函数为system函数,将”cat /root/flag”写入到regs[2],之后通过rand_r(regs[2])来调用system(“cat /root/flag”)从而获得flag。
9.开始编写poc:
(1)写好访问pmio和mmio空间的调用函数,及前置参数:
1 | #注释头 |
(2)打开resource0文件,利用mmap将mmio空间映射出来:
1 | //注释头 |
(3)对mmio空间进行写操作,调用strng_mmio_write函数,将”cat /root/flag”写入到regs[2]中:
1 | //注释头 |
这里由于需要满足传入的addr右移两位后形成的idx需要>=2,所以从8依次开始。
(4)编写pmio空间越界读和越界写的函数:
1 | uint32_t pmio_arbread(uint32_t offset) |
(5)利用pmio空间越界读取漏洞,泄露libc地址:
1 | //注释头 |
(6)利用越界写,将rand_r函数改写成system函数
1 | //注释头 |
最后编译:gcc -m32 -O0 -static -o exp exp.c,然后传到虚拟机里面,可以用下列两种方法:
①scp -P5555 exp ubuntu@127.0.0.1:/home/ubuntu,由于开了端口,所以可以直接通过scp端口传输。
②使用python库简易搭建一个ftp传输:
1 | #注释头 |
最后可以看到成功运行:这里我修改了cat /root/flag命令,变成/bin/sh,可以看到返回了一个主机里面的终端sh,成功实现逃逸。但是这个终端sh输入命令不显示,回显消息比较慢,但是个确确实实的主机终端,如果有条件的话,应该是可以实现多重逃逸的。
▲qemu逃逸调试:
1.将exp传进qemu之后,在主机上使用命令ps aux|grep qemu,找到qemu的任务id,然后gdb attach qemu_id。
2.下断点在需要的函数:b *strng_mmio_write,然后输入c接着运行。
3.在qemu中sudo ./exp,现在就能在主机的gdb中停下来,就可以调试了。
1 | #注释头 |
参考资料:
https://xz.aliyun.com/search?keyword=qemu
https://ray-cp.github.io/archivers/qemu-pwn-Blizzard-CTF-2017-Strng-writeup