学习qemu,对于虚拟化的相关知识有了一些提升,先通过做题来巩固学到一些概念
2021 HWS FastCP
漏洞分析
1 | void __fastcall FastCP_class_init(ObjectClass_0 *a1, void *data) |
注册了verdor_id=0xBEEFDEAD
和class_id = 0xff
。
1 | 00000000 FastCPState struc ; (sizeof=0x1A30, align=0x10, copyof_4530) |
FastCPState
结构体如上所示,其中可以看到CP_buffer
的大小为0x1000
,其紧邻cp_timer
函数指针。CP_state
结构体有CP_list_src
、CP_list_cnt
和cmd
。
1 | uint64_t __fastcall fastcp_mmio_read(FastCPState *opaque, hwaddr addr, unsigned int size) |
fastcp_mmio_read
函数,主要是返回FastCPState
变量的值。
1 | void __fastcall fastcp_mmio_write(FastCPState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
fastcp_mmio_write
函数的功能主要是设置CP_list_cnt
、CP_list_src
以及执行opaque->cp_timer
函数指针
1 | void __fastcall fastcp_cp_timer(FastCPState *opaque) |
重点需要关注fastcp_cp_timer
函数:
case 2
:从opaque->cp_state.CP_list_src
读取到cp_info
,然后将cp_info.CP_src
写入到opaque->CP_buffer
,长度为cp_info.CP_cnt
,计算(opaque->cp_state.cmd & 0xFFFFFFFFFFFFFFFCLL)&8
判断执行pci_set_irq
;
case 4
:先从opaque->cp_state.CP_list_src
读取cp_info
,然后将opaque->CP_buffer
读取到cp_info.CP_dst
,长度为cp_info.CP_cnt
。
case 1
:如果opaque->cp_state.CP_list_cnt
大小大于0x10
,则会根据cp_state.CP_list_cnt
的大小循环从opaque->cp_state.CP_list_src
读取结构体到cp_info
,然后依次将CP_src
中的数据写入到CP_buffer
,然后从CP_buffer
中读取数据到CP_dst
,长度由CP_cnt
指定。
这里需要注意除了case 2
对cnt
长度进行了检查,其他操作都没有检查cnt
的长度,也就是存在一个数组越界的漏洞。
漏洞利用
这道题的利用思路其实不难,但是一直卡在一个关键的地方,花了一天多时间才搞定。
泄漏地址
前面已经说到case 4
是有一个越界读,而在buffer
缓冲区下面紧邻的是cp_timer
函数指针。
1 | 00000000 QEMUTimer_0 struc ; (sizeof=0x30, align=0x8, copyof_1181) |
那么我们直接利用这个越界读,读取cb
指针,就能泄漏qemu
地址。那么就需要越界读取0x1010
处的地址。
我这里刚开始的思路是直接申请一个0x2000
的缓冲区userbuf
获得其物理地址phy_userbuf
,然后用来读取数据。但是经过调试我读取完之后,在0x1010
的数据并非我泄漏的数据。就是这个问题,困扰了我一天。
知道后面我才想到,如果一个物理页大小为0x1000
,我就算使用mmap
直接申请0x2000
大小的缓冲区,虽然获得了连续的虚拟地址,但是这两个虚拟页对应的物理页并不一定是连续的。也就是说我最终读取了0x1010
的数据到 phy_userbuf+0x1010
的地址,然后我通过访问虚拟地址userbuf+0x1010
得到的数据并不对应phy_userbuf+0x1010
的数据。
所以这里我就需要去申请两个连续的物理页,这样的申请就需要不断去分配尝试,分配的两个虚拟页获得其物理地址,然后判断其对应的物理地址,看是否是连续的,然后来判断是否对应两个连续的物理页。
当我们能够获得两个连续的物理页地址时,再去泄漏地址,就能保证访问userbuf+0x1010
对应的数据是phy_userbuf+0x1010
的数据。
getshell
能够泄漏地址后,就能够得到system
地址。
我们只需要修改QEMUTimer_0.cb
的函数指针为system_plt
地址,修改QEMUTimer_0.opaque
为buffer
地址,在buffer
中构造cat /root/flag
,最终通过fastcp_mmio_write
中的case 0x18
去触发即可。
EXP
1 |
|
2019 XNUCA vexx
漏洞分析
1 | void __fastcall pci_vexx_realize(PCIDevice_0 *pdev, Error_0 **errp) |
可以看到注册了两个mmio
内存,分别为vexx-cmb
和vexx-mmio
。
漏洞点在于vexx_cmb_write
和vexx-cmb_read
的越界读写
1 | void __fastcall vexx_cmb_write(VexxState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
可以看到将val
赋值给req.req_buf[addr]
,而addr=opaque->req.offset + addr
,这里的offset
和addr
都由我们指定,所以这里存在越界读写漏洞。
漏洞利用
越界读,泄漏地址。越界写,修改函数指针。
EXP
1 |
|
2021 强网杯 EzQtest
漏洞分析
1 | uint64_t qwb_mmio_read(struct qwb_state* arg1, int64_t arg2, int32_t arg3) |
在qwb_mmio_read
函数中,主要是能够读取dam_info
的成员变量
1 | struct qwb_state* qwb_mmio_write(struct qwb_state* arg1, int64_t arg2, struct qwb_state* arg3, int32_t arg4) |
qwb_mmio_write
函数主要是能够设置dma_info
的成员变量。
1 | void __cdecl qwb_do_dma(QWBState_0 *opaque) |
重点关注的就是这个qwb_do_dma
函数。该函数能够对dma_buf
缓冲区进行读取。这里首先会检查src+cnt
和dst+cnt
是否大于0x1000
,以及检查cnt
是否大于0x1000
。
但是,这里只检查了上界,并没有检查下界。也就是src+cnt
和dst+cnt
是可以为负数,那么这样就可以对dma_buf
向上读取。
而在向上读取时,与dma_buf
紧邻的是dma_info
结构体,那么我们就可以修改dma_info
来实现任意地址读写。
这道题还有一个特殊点在于,需要与2021 年强网杯的另一道题EzCloud
结合起来,需要利用那道题的功能与qemu
建立连接,全部操作都是以monitor
命令的形式操作。这里本地搭建时为了简便,便直接将exp
脚本的数据包转发到qemu
内,使其直接与qemu
通信,不需要再通过EzCloud
转发。这里转发数据包使用socat
:
1 | #!/bin/sh |
关于Qtest
命令行的命令,可以参考这篇文章。
漏洞利用
PCI设备初始化
这道题目的一个难点就在于,他并没有完整实现整个PCI
设备的初始化,这里需要我们自己去完成PCI
设备地址的初始化。
参考自这篇文章。
1 | 1、在 do_pci_register_device 中分配内存,对config内容进行设置,如 pci_config_set_vendor_id |
根据这个流程,要正确完成设备配置的需要向BAR
写MMIO
的地址。通过文档可以知道i440fx-pcihost
初始化操作如下:
1 | static void i440fx_pcihost_realize(DeviceState *dev, Error **errp) |
这里如果在命令行中之心命令info qtree
可以知道qwb
这个设备是i440fx-pcihost
下面的设备,则该设备在初始化阶段会沿用父类i44fx
绑定的端口。
这里初始化所需要的步骤如下:
1、 将MMIO
地址写入到qwb
设备的BAR0
地址
通过 0xcf8 端口设置目标地址
通过 0xcfc 端口写值
2、 将命令写入qwb设备的COMMAND地址,触发pci_update_mappings
通过 0xcf8 端口设置目标地址
通过 0xcfc 端口写值
这里首先需要知道qwb
设备的地址,qwb
设备的Bus number
为0,Device number
为2,Function number
为0,得出qwb
的地址为0x80001000
。
再结合设备中寄存器映射图,可以看到BAR0
的偏移为0x10
,COMMAND
的偏移为4.
然后我们需要解决写什么值的问题,MMIO
地址可以直接拿文档一种的地址0xfebc0000
。而COMMAND
值的设置就另有说法了,文档二种给出了COMMAND
的比特位定义:
这里选择0x107
,即设置SERR
,Memory space
和IO space
、Bus Master
。
所以最后初始化阶段需要执行的命令如下:
1、outl 0xcf8 0x80001010
2、outl 0xcfc 0xfebc0000
3、outl 0xcf8 0x80001004
4、outw 0xcfc 0x107
执行上述命令之后观察pci
设备可以看到BAR0
已经设置上了0xfeb00000
,对该地址进行读写能正确触发MMIO handler
的断点。
利用思路
越界读泄漏地址
首先通过上溢0xee0
处可以读取到一个libc
地址。再通过上溢到opaque->pdev
可以泄漏一个qemu
程序基址。
越界写提权
最开始想直接将QWBState->pdev->config_read
指针覆盖为system_plt
地址,将QWBState->pdev
覆盖为/bin/sh
地址。但是这样做会使得在覆盖完后,执行inw
指令时报错。
关于如何getshell
卡了很久。最终参考这篇博客看到了一种提权的方法。
matshao
大佬找到两个gadget
:
1 | gadget1: 0x3d2f05:lea rdi, "/bin/sh"; call execv |
所以这里将QWBState->pdev->config_read
指针覆盖为gadget2
地址,将QWBState->pdev+0x20
处覆盖为gadget1
地址即可。
EXP
1 | from pwn import * |
2018 seccon q-escape
对这种pci设备的利用不了解,主要参考这篇文章
漏洞分析
1 | void __fastcall cydf_vga_class_init(ObjectClass_0 *klass, void *data) |
可以看到注册了名为cydf_vga
的pci
设备,vendor_id=0x1013
和device_id=0x300
,可以看到其parent_class
描述为Cydf CLGD 54xx VGA
,可以在源码中搜索发现在cirrus_vga.c
出现类似的字符串,应该就是通过魔改Cirrus CLGD 54xx VGA Emulator
设备形成的Cydf
设备。
1 | /sys/devices/pci0000:00/0000:00:04.0 |
可以看到存在3个mmio
空间,
pci_cydf_vga_realize
函数中,只关注cydf_init_common
函数
1 | //cydf_init_common |
只关注跟cydf
这个pci
设备有关的内存空间函数,可以发现注册了一个0x30
的PMIO
空间、一个0x20000
的MMIO
空间以及一个0x1000
的MMIO
空间。但是在resource
文件中没有看到PMIO
的端口范围,于是查看/proc/ioports
1 | / |
发现vga+
的端口范围正好为0x30
的大小
根据vgamem中所描述,VGA显存在地址空间中的映射范围为0xa0000-0xbffff
。因此查看/proc/iomem
找到vga
的地址空间
1 | / |
这里可以通过源码对比,原来cirrus_vga_mmio_write
函数只有2种情况:addr < 0x10000、0x18000 <= addr < 0x18100
,而cydf_vga_mem_write
多出一种情况:addr>=0x18100
,需要重点分析这部分分支的逻辑。
1 | void __fastcall cydf_vga_mem_write(CydfVGAState *opaque, hwaddr addr, uint64_t mem_value, uint32_t size) |
最主要的区别是增加了0x10000-0x18000
地址空间的处理代码,通过代码可以看到增加的功能为vs
的处理代码,opaque->vga.sr[0xCC]
为cmd
,opaque->vga.sr[0xCD]
为idx,功能描述如下:
- cmd为0时,申请value&0xffff空间大小的堆,并放置
vs[vulncnt]
中,同时初始化max_size
。 - cmd为1时,设置
idx
所对应的vs[idx]
的max_size
为value&0xffff
。 - cmd为2时,
printf_chk(1,vs[idx].buff)
。 - cmd为3时,当
cur_size<max_size
时,vs[idx].buff[cur_sizee++]=value&0xff
。 - cmd为4时,
vs[idx].buff[cur_sizee++]=value&0xff
。
漏洞主要有两个地方:
- 一个是堆溢出。cmd为4时,可以设置
max_size
,对max_size
没有进行检查也没有对堆块进行realloc
,后续按这个size进行写,导致溢出。 - 另一个是数组越界。idx最多可以为0x10,即最多可以寻址
vs[0x10]
,而vs
大小只有16,即vs[0xf]
。vs[0x10]则士后面的latch[0]
,导致会越界访问到后面的latch数组的第一个元素。
还有要解决的问题就是如何触发漏洞代码。除了addr
之外,还需要使得(opaque->vga.sr[7]&1 ==1)
以绕过前面的if
判断、设置opaque->vga.sr[0xCC]
来设置cmd以及设置opaque->vga.sr[0xCD]
设置idx。
在代码中可以找到cydf_vga_ioport_write
函数中可以设置opaque->vga.sr
。addr
为0x3C4
,vulue
为vga.sr
的index
;当addr
为0x3C5
时,value
为vga.sr[index]
的值。从而可以通过cydf_vga_ioport_write
设置vga.sr[7]
、vga.sr[0xCC]
以及vga.sr[0xCD]
。
还需要说明的是可以通过cydf_vga_mem_read
函数来设置opaque->latch[0]
,latch[0]
刚好是vs
越界访问到的元素。
漏洞利用
- 往bss段数据中写入要执行的命令
cat /root/flag
。 - 将该bss地址写入到全局变量
qemu_logfile
中。 - 将
vfprintf
函数got表覆盖为system
函数的plt表地址。 - 将
printf_chk
函数got表覆盖为qemu_log
函数的地址。 - 利用cmd为2时,触发
printf_chk
,最终实现system函数的调用,同时参数也可控。
访问PMIO
UAFIO
描述说有三种方式访问PMIO
,这里的简便方法是:通过IN
以及OUT
指令去访问,可以使用IN
和OUT
去读写相应字节的1、2、4
字节数据(outb/inb,outw//inw,outl/inl)
,函数的头文件为<sys/io.h>
。
还需要注意访问相应的端口需要一定的权限,程序应使用root
权限运行,对于0x000-0x3ff
之间的端口,使用ioperm(form, num, turn_on)
即可,对于0x3ff
以上的端口,则该调用执行iopl(3)
函数去访问所有的端口。
这题vga+
的端口为03c0-03df
,因此只需要靶机具有root权限,并调用ioperm(0x3b0, 0x30, 1)
打开端口。在调用cydf_vga_ioport_write
函数时使用outl
和outw
指令不能将val
参数传进去,而用outb
指令就能成功。
访问vga_mem
vga_mem
的内存空间并没有在resource
文件中体现,根据源码中对cirrus_vga_mem_read
函数有个描述,vga
的内存空间在0xa0000-0xbffff
中,与cat /proc/iomem
的结果一致。
1 | /*************************************** |
有一个访问物理内存的简单方法是映射/dev/mem
到我们的进程中,然后我们就可以像正常访存一样进行读写。但是提供的环境中并没有挂载/dev/mem
文件,可以通过mknod -m 660 /dev/mem c 1 1
命令挂载上去。
1 | system( "mknod -m 660 /dev/mem c 1 1" ); |
EXP
1 |
|
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/10/13/从qemu逃逸到逃跑/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!