逃逸三部曲,qemu学习
qemu基础知识
qemu内存配置
1 | Guest' processes |
地址转换
qemu内存的地址需要经过两次转换,首先是用户态虚拟地址转换为用户态物理地址,随后用户态物理底子好转换为qemu
虚拟地址空间。
用户虚拟地址->用户物理地址
用户物理地址->qemu的虚拟地址空间
1 | 7f1824ecf000-7f1828000000 rw-p 00000000 00:00 0 |
pagemap
/proc/$pid/pagemap
存储进程虚拟地址的页表项,也就是page table
VPFN:virtual page frame number
,虚拟页号
PFN:page frame number frame number
,页号
pagemap的格式
- Bits 0-54 page frame number (PFN) if present
- Bits 0-4 swap type if swapped
- Bits 5-54 swap offset if swapped
- Bit 55 pte is soft-dirty (see Documentation/vm/soft-dirty.txt)
- Bit 56 page exclusively mapped (since 4.2)
- Bits 57-60 zero
- Bit 61 page is file-page or shared-anon (since 3.5)
- Bit 62 page swapped
- Bit 63 page present
PCI
符合 PCI 总线标准的设备就被称为 PCI 设备,PCI 总线架构中可以包含多个 PCI 设备。图中的 Audio、LAN 都是一个 PCI 设备。PCI 设备同时也分为主设备和目标设备两种,主设备是一次访问操作的发起者,而目标设备则是被访问者。
mmio
内存映射io,和内存共享一个地址空间。可以和像读写内存一样读写其内容。
pmio
端口映射io,内存和io设备有各自独立的地址空间,cpu需要通过专门的指令才能去访问。在intel的微处理器中使用的指令是IN和OUT。
lspci命令
pci外设地址,形如0000:00:1f.1
。第一个部分16位表示域;第二个部分8位表示总线编号;第三个部分5位表示设备号;最后一个部分3位表示功能号。下面是lspci
的输出,其中pci
设备的地址,在最头部给出,由于pc
设备总只有一个0号域,所以会省略域。
lspci -v -t
会用树状图的形式输出pci设备,会显得更加直观
lspci -v
就能输出设备的详细信息
仔细观察相关的输出,可以从中知道mmio
的地址是0xfebf1000
,pmio
的端口是0xc050
。
在/sys/bus/pci/devices
可以找到每个总线设备相关的一写文件。
每个设备的目录下resource0
对应MMIO
空间。resource1
对应PMIO
空间。resource
文件里面会记录相关的数据,第一行就是mimo
的信息,从左到右是:起始地址、结束地址、标识位。
QOM(qemu object model)
TypeInfo
TypeInfo定义了一个类,下面代码所示
1 | static const TypeInfo strng_info = { |
定义中包含这个类的
- 名称 name
- 父类 parent
- 实例的大小 instance_size
- 是否是抽象类 abstract
- 初始化函数 class_init
代码底部有type*init
函数,可以看到这个函数实际是执行的register\*module_init
,由于有__attribute**((constructor))
关键字,所以这个函数会在main
函数之前执行。
1 | type_init(pci_strng_register_types) |
这里register_module_init
中创建了一个type
为MODULE_INIT_QOM
,init
为pci_strng_register_types
的一个 ModuleEntry
,并且他加入到MODULE_INIT_QOM
的ModuleTypeList
链表上。
在main
函数中会调用module_call_init(MODULE_INIT_QOM);
将MODULE_INIT_QOM)
对应的ModuleTypeList
上的每个 ModuleEntry
都调用其init函数。对于以上的例子来说就会掉用pci_strng_register_types
。
1 | static void pci_strng_register_types(void) |
这里初始化了一个strng_info
的TypeInfo
,然后掉用type_register_static
1 | TypeImpl_0 *__fastcall type_register_static(const TypeInfo_0 *info) |
可以看到type_register_static
掉用了type_register
,在type_register
中执行了v1 = type_new(info), type_table_add(v1),
这部操作。这两个函数分别初根据info
初始化了一个TypeImpl
对象v1
,然后把v1
添加到全局的type_table
中。
TypeImpl
这个结构是根据TypeInfo
来进行创建,各个类之间的继承关系都依赖这个结构,这个结构中还包含了所对应的类的构造和析构函数,还有实例的构造和析构函数。
1 | struct TypeImpl |
ObjectClass
这是所有class的基类或者说是是所有class的结构,在ObjectClass对象创建时会更具type(TypeImpl)根据类之间的继承关系逐个进行初始化。
1 | struct ObjectClass |
Object
所有object的基类,或者可以说是所有object的结构,他其中会包含指向对应类对象的指针。object会根据class中type的继承关系递归的初始化实例。
1 | struct Object |
制作文件系统
编译busybox
make menuconfig
设置
Busybox Settings -> Build Options -> Build Busybox as a static binary 编译成 静态文件
关闭下面两个选项
Linux System Utilities -> [] Support mounting NFS file system 网络文件系统
Networking Utilities -> [] inetd (Internet超级服务器)
启动qemu
1 | /CTF/qemu_escape/qemu/bin/debug/native/x86_64-softmmu/qemu-system-x86_64 -m 2048 --nographic \ |
退出qemu的终端用ctrl+a+x
AntCTF 2021 d3dev
漏洞分析
直接分析qemu
程序,找到其中关于d3dev
的PCI设备:
这里首先看一个结构体:d3devState
1 | 00000000 d3devState struc ; (sizeof=0x1300, align=0x10, copyof_4545) |
对于该结构体中各个变量的含义,我们结合后续的函数来进一步分析。
这里首先分析d3dev_mmio_read
函数,如下所示:
1 | uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size) |
该函数首先通过seek
和addr
来从opaque->blocks
中取出block
,然后经过tea
编码后,返回给用户。
从上面数据结构中,可知block
的长度为0x100
,而我们这里传入的addr
并没有检查范围,所以可以超过0x100
,从而发生越界读取。而这里越界之后,可以读取key
和rand_r
的值。
接着看d3dev_write
:
1 | void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
该函数主要是将传入的val
赋值给opaque->blocks[offset]
。如果是奇数次,则直接赋值。如果是偶数次则先加密再赋值。这里也没有对addr
进行范围检查,可以越界写。
1 | uint64_t __fastcall d3dev_pmio_read(d3devState *opaque, hwaddr addr, unsigned int size) |
这里伪代码展示不完整,我们直接看汇编:
1 | .text:00000000004D7D00 ; uint64_t __fastcall d3dev_pmio_read(d3devState *opaque, hwaddr addr, unsigned int size) |
d3dev_pmio_read
基本功能就是,通过输入不同的addr
,会进入不同switch-case
。这里就会将opaque->key
的四个值进行返回。
d3dev_pmio_write
会去调用rand_r
函数指针,这个指针存储的是rand
函数地址。
1 | // local variable allocation has failed, the output may be wrong! |
漏洞利用
程序总体漏洞就是对mmio
的越界读取。
地址泄漏
上面提到rand_r
存储的是rand
函数指针,可以从d3dev_instance_init
如下看到:
1 | void __fastcall d3dev_instance_init(Object_0 *obj) |
这里的rand_r
函数是位于qemu
程序中的函数,我们通过越界读泄漏该地址,那么就可以得到qemu
的基址。
越界写
得到了qemu
基址后,我们就可以计算得到system
函数的地址。
然后通过越界写,修改rand_r
存储的函数指针为system
。然后去触发system
函数。
getshell
这里想实现getshell
,可以去执行rand_r
函数,并设置参数为cat flag
。
EXP
1 |
|
HITB GSEC2017 babyqemu
漏洞分析
在init
初始化函数中,需要将设备类型定义为PCIDeviceClass
结构体,PCIDeviceClass
结构体如下:
1 | 00000000 PCIDeviceClass struc ; (sizeof=0x108, align=0x8, copyof_1371) |
接着我们看一下hitb_class_init
函数:
1 | void __fastcall hitb_class_init(ObjectClass_0 *a1, void *data) |
可以看到设备号device_id=0x2333
,功能号vendor_id=0x1234
。
接着在ubuntu
中查看pci
的I/O
信息,运行sudo ./launch.sh
,遇到错误./qemu-system-x86_64: /usr/lib/x86_64-linux-gnu/libcurl.so.4: version CURL_OPENSSL_3' not found (required by ./qemu-system-x86_64)
运行sudo apt-get install libcurl3
可以解决
1 |
|
resource
文件内容的格式为start end flag
,在resource0
文件中,根据最后一位为0
可知存在一个MMIO
的内存空间,地址为0xfea00000
,大小为0x100000
。
然后看hitb
设备注册了什么函数,分析pci_hitb_realize
函数:
1 | void __fastcall pci_hitb_realize(PCIDevice_0 *pdev, Error_0 **errp) |
可以看到主要注册了timer
结构体,其回调函数为hitb_dma_timer
;同时也注册了hitb_mmio_ops
内存操作的结构体,其包含hitb_mmio_read
和hitb_mmio_write
两个操作。
在分析具体处理函数之前,需要先搞懂设备的结构体,如下是HitbState
结构体:
1 | 00000000 HitbState struc ; (sizeof=0x1BD0, align=0x10, copyof_1494) |
1 | struct dma_state |
Hit_mmio_read
1 | uint64_t __fastcall hitb_mmio_read(HitbState *opaque, hwaddr addr, unsigned int size) |
通过设置不同的addr
,能够读取HitbState
不同的变量。
hitb_mmio_write
1 | void __fastcall hitb_mmio_write(HitbState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
这里主要是对成员变量进行赋值,在addr=0x98
时触发timer
,调用hitb_dma——timer
函数。
Hitb_dma_timer
1 | void __fastcall hitb_dma_timer(HitbState *opaque) |
cpu_physical_memory_rw
函数的第一个参数时物理地址,虚拟地址需要通过读取/proc/$pid/pagemap
转换为物理地址。
1、 dma.cmd==7
时,idx=dma.src - 0x40000
,addr = dma_buf[idx]
,调用enc
加密函数加密,并写入到dma.dst
中;
2、 dma.cmd==3
时,idx=dma.src - 0x40000
,addr=dma_buf[idx]
,写入到dma.dst
中;
3、dma.cmd==1
时,idx=dma.dst-0x40000
,addr=dma_buf[idx]
,将其写入到dma.src
中(第二个参数可以通过调试得到其地址就是dms_buf[dma.dst-0x40000]
)
上面的分析可以看到,这里没有对dma.s rc
进行检查,那么idx
也是没有经过检查,那么这里就可以实现对dma_buf
的越界读写。
漏洞利用
DMA
DMA(Direct Memory Access)
:直接内存访问
有两种方式引发数据传输:
第一种情况:软件对数据的请求
当进程调用
read
,驱动程序函数分配一个DMA
缓冲区,并让硬件将数据传输到这个缓冲区中,进程处于睡眠状态;硬件将数据写入到
DMA
缓冲区中,当写入完毕,产生一个中断中断处理程序获取输入的数据,应答中断,并唤起进程,该进程现在即可读取数据
第二种情况:在异步使用
DMA
时硬件产生中断,宣告新数据的到来
中断处理程序分配一个缓冲区,并且告诉硬件向哪里传输数据
外围设备将数据写入数据区,完成后,产生另外一个中断
处理程序分发数据,唤醒任何相关进程,然后执行清理工作
DMA控制器必须有以下功能:
1、 能向CPU发出系统保持(HOLD)信号,提出总线接管请求;
2、 当CPU发出允许接管信号后,负责对总线的控制,进入DMA方式;
3、 能对存储器寻址及能修改地址指针,实现对内存的读写操作;
4、 能决定本次DMA传送的字节数,判断DMA传送是否结束;
5、 发出DMA结束信号,使CPU恢复正常工作状态。
注意:当虚拟机通过DMA(Direct Memory Access)
访问大块I/O
时,QEMU
模拟程序将不会把结果放进共享页中,而是通过内存映射的方式将结果直接写到虚拟机的内存中,然后通知KVM
模块告诉客户机DMA
操作已经完成。
通过pagemap
将虚拟机中的虚拟地址转换为物理地址。
根据内核文档可知,每个虚拟页在/proc/pid/pagemap
中对应一项长度为64 bits
的数据,其中Bit 63
为page present
,表示物理内存页是否已存在;若物理页已存在,则Bits 0-54
表示物理页号,此外,需要root
权限的进程才能读取/proc/pid/pagemap
中的内容。
1 | pagemap is a new (as of 2.6.25) set of interfaces in the kernel that allow |
根据以上信息,利用/proc/pid/pagemap
可将虚拟地址转换为物理地址,具体步骤如下:
1、 计算虚拟地址所在虚拟页对应的数据项在/proc/pid/pagemap
中的偏移,offset=(viraddr/pagesize)*sizeof(uint64_t)
2、 读取长度为64bits
的数据项
3、 根据Bit 63
判断物理内存页是否存在
4、 若物理内存页已存在,则取bits 0-54
作为物理页号
5、 计算出物理页起始地址加上页内偏移即得到物理地址,phtaddr = pageframenum * pagesize + viraddr % pagesize
对应代码如下:
1 |
|
利用思路为:
1、泄漏user_buf
的物理地址,因为我们后续调用dma_timer
实现越界读写时,需要传入的是物理地址,所以这里需要用到上面的方法先得到user_buf
的物理地址。
2、 使得dma.src=0x41000
,使dma.dst = user_buf_phy_addr
,使得cmd=3
,那么则能够将enc
函数的地址泄漏出来,从而计算得到qemu
基址,进而得到system_plt
地址;
2、 使得dma.dst=0x41000
,使dma.src=user_buf_phy_addr
,使得cmd=1
,并将user_buf
赋值为system_plt
地址,从而修改enc
函数指针为system
函数
3、 在&dma_buf[0]
中写入cat flag
指令,并调用enc
加密函数,最终触发system
命令
EXP
由于是刚开始接触qemu
逃逸exp
的编写,所以这里就写得详细一点,把每个函数都解释清楚,也增强自己的理解。
初始化mmio
1 | // Open and map I/O memory for the strng device |
这里打开设备/sys/devices/pci0000:00/0000:00:04.0/resource0
,前面提到这个文件存储的是对应设备的mmio
空间。如果有pmio
设备,那么这里就需要打开resource1
。
创建一个DMA空间
1 | // Allocate DMA buffer and obtain its physical address |
可以看到首先在用户态分配了一块0x1000
的内存userbuf
,然后对这块内存执行力mlock
。使用了mlock
,会把内存lock
在内存中,不会被交换,在一定场景下,可以提高性能。 虚拟化场景下,qemu
也可以选择lock
住一部分内存,来提高Guest
的性能。
随后调用gva_to_gpa
函数来获取这块空间的物理地址,获取的方法就是如上介绍所示。
越界读
1 | // out of bound to leak enc ptr |
随后执行越界读,读取buffer
后面的 enc
函数地址,从而泄漏得到qemu
基址。
1 | void dma_do_read(uint32_t addr, size_t len) |
这里要实现越界读,首先需要通过hitb_mmio_write
设置dst
为userbuf
的物理地址,随后设置src
为越界的位置即buffer+0x1000
,然后设置读取的长度,最后调用cmd=2
,执行越界读取。
这里dma_set_dst
函数如下:
1 | void dma_set_dst(uint32_t dst_addr) |
通过将对mmio_mem
写的函数封装为mmio_write
函数,这里需要传入的两个参数addr
和value
就是我们执行hitb_mmio_write
需要传入的两个参数。这里dma_set_dst
传入参数分别为0x88
和dst_addr
。
越界写
1 | // out of bound to overwrite enc ptr to system ptr |
获取程序基址后,通过越界写去修改buffer+0x1000
处的enc
函数指针为system_plt
地址。
1 | void dma_do_write(uint32_t addr, void *buf, size_t len) |
dma_do_write
函数首先将我们想写入的数据通过memcpy
拷贝给userbuf
内存,随后通过dma_set_src
将src
设置为userbuf
的物理地址。然后设置dst
为目标地址buffer+0x1000
,设置长度,执行cmd=1
。上面讲到对dma
的读写,需要物理地址,所以这里我们需要将userbuf
的物理地址传入。
getshell
1 | // deply the parameter of system function |
先是通过越界写,将cat /root/flag
写入buffer+0x200
处,随后调用enc
函数,执行system
。
1 |
|
2020华为云 qemuzzz
漏洞分析
1 | __int64 __fastcall zzz_instance_init(__int64 a1) |
首先可以看到初始化时,在0x19f0
处存储了object
对象的基址,在0x19f8
处存储了cpu_physical_memory_rw
函数指针。
然后主要注册了两个函数,这里先看一下zzz_mmio_write
:
1 | void __fastcall zzz_mmio_write(__int64 opcade, unsigned __int64 addr, unsigned __int64 value) |
这里我们首先看一下dev
结构体的大概结构:
1 | struct opcade{ |
这里zzz_mmio_write
,主要功能如下:
addr=0x20
:可以设置buf1
addr=0x10
:可以设置off
addr=0x18
:可以设置len
addr=0x50
:可以执行异或流程
重点关注addr=0x60
,首先需要满足buf1
的地址末尾是0x000
。
其主要功能,是首先取出len
和off
,然后检查(int)(off + (len0 & 0x7FFE) - 1) <= 0x1000
,这里可以看到这个检查存在缺陷,如果将off=0x1000
,len=0x1
,那么这个检查可以通过。
但是后续去执行cpu_physical_memory_rw
函数时,我们就可以对buf2+0x1000
处修改一个字节,而这一字节刚好是dev_fd
的地址,也就是我们可以修改dev_fd
的最低一字节地址。那么后续我们通过该地址去寻址时肯定会存在问题。
1 | __int64 __fastcall zzz_mmio_read(__int64 opcade, unsigned __int64 addr) |
zzz_mmio_read
功能就是返回buf2
的数据。
漏洞利用
数组越界
这里的漏洞点在于一个off-by-one
漏洞,我们可以将dev_fd
的基址的末尾改大,从而使得后续执行0x60
读写时,能够向下溢出更多,从而实现越界读写。
这里我们思考是利用off-by-one
漏洞将dev_fd
末尾字节改为0x60
,从而使得基址增大了0x60
。由于我们如果将基址改大后,我们在通过其他set
功能设置的src\len\off
就发生了变化,所以我们需要先布置好我们后续要用的src\off\len
。
1 | int offset = 0x60 - 0; |
这里我们先在buf+0x50
处布置userbuf
的物理地址phy_addr
,布置长度为0xf
,布置偏移为0x1000-0x60
。并且在buf+0x210
处布置好cat /root/flag
字符串,这里选择0x210
的原因是经过调试buf_addr+0x210
的地址低12位刚好为0x000
,也就是该地址后续可以被传入当作cpu_physical_memory_rw
的参数。
随后,利用off-by-one
,修改基址:
1 | printf("change base last byte as 0x60\n"); |
泄漏地址
现在相当于基址增大了0x60
,那么我们读取的时候也就可以相应读取buf
溢出0x60
的数据,这里与buf
紧邻的就有dev_fd
地址和cpu_physical_memory_rw
函数指针。
原结构大体如下所示:
1 | | dev_fd | 0x0 <----------+ |
修改后的结构体如下所示:
1 | | dev_fd | 0x0 |
此时,再去执行0x60
读取函数时,此时取出的src\len\num
为我们上面自己布置的数据,src=phy_userbuf
、len=0xf
、off=0x1000-0x60
。
那么也就是会执行cpu_physical_memory_rw_func(*(dev_fd+0x60+0x1000-0x60), *(dev_fd+0x60+0x9e0), (dev_fd+0x60+0x9e8), 1LL);
,也即执行cpu_physical_memory_rw_func(*(buf+0x1000), *(phy_userbuf), 0xf, 1LL);
。
那么此时就能讲dev_fd
和函数指针泄漏出来
getshell
getshell
的方法很通用,修改函数指针为system_plt
,然后使得buf
地址指向我们之前布置到 cat flag
字符串地址即可。
EXP
1 |
|
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/09/17/qemu逃逸学习/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!