文章目录

  • 参考内容
  • 地址分配
  • 外设相关
  • 模板讲解
  • 一些疑问



前几周稍微接触了一下qemu逃逸的相关知识,这里做一点记录,希望能对学习者有所帮助。

地址分配

qemu设备号的查看、地址的读取和地址空间的转换,上面两位师傅的blog里面已经写的很清晰了。这里只想说一下我对qemu地址分配的理解。
众所周知,程序运行时使用的一般是虚拟地址,而与硬件交互时会通过mmu(以及页表相关机制)转换成对应的物理地址。在真机上,这里的物理地址就是放到地址总线上,直接喂给外设(如硬盘)的地址。
很显然,在qemu中,“真机”本身也是虚拟出来的,“真机”的物理地址实际上是通过mmap向宿主机申请得到的,宿主机的一块虚拟地址。于是有以下的转换关系:

QEMU可以转换e01格式吗 qemu edu_安全


这四个地址对应的内存空间均为同一片内存空间(内存条的同一个空间),但是,需要注意,不同地址只能在相应的层次上发挥作用,一个地址在另一个层次下将会变得毫无意义。另一方面,对于同一段内存来说,通过同样的段内偏移访问到的内容都是相同的,也就是说这四个地址的偏移是共同的。

不同地址作用范围如下:

  1. qemu-system中用户程序申请的内存——qemu-system中的用户程序
    (简单来说就是,你写的exp里面mmap分的地址,只在exp程序内,或者说qemu虚拟出来的系统的用户态下有意义,对qemu-system下你自己写的其他程序来说是在同一个地址空间内,但仅此而已)
  2. qemu中的“物理地址”——qemu虚拟的硬件设备
    (外设只接受物理地址,这也是进行地址转换的原因)
  3. 真机mmap申请的地址——qemu-system程序自身
    (在真机角度,qemu-system就是个应用程序,它申请了内存空间自己用,仅此而已)
  4. 真机物理地址——内存条
    (没啥好说的,总的来说qemu逃逸是发生在真机用户态程序中,不需要考虑这一地址)

我们在写exp时,是在qemu-system的用户态下运行,使用的内存是基于1的,而我们在调试和逆向qemu-system的时候,使用的内存是基于3的,也就是qemu-system在真机上申请和使用的内存(不限于mmap的)。这两类内存地址只在各自环境下有效,在对方环境下下是无效的。

在真机中,物理地址会送上总线与外设交互,但qemu内的外设是虚拟的,因而只能将qemu中的“物理地址”交给qemu-system程序,并模拟出外设的相应行为。

例如:

QEMU可以转换e01格式吗 qemu edu_安全_02


这里的hwaddr就是2,即qemu中的“物理地址”。

所以,我们可以通过qemu内的用户态程序直接操纵一片内存空间1,也可以在调试中看到对应的真机内存空间3,但对于2这个物理地址,只能将其喂给qemu的外设api,让qemu-system这个程序模拟相应的行为,而没有办法直接对其内容进行读写等操作。

外设相关

这里先说一个概念:处理器对外设的控制也是通过对地址的读取和写入实现的。

说人话就是,cpu是通过数据、地址、控制三条总线来控制外设的,外设会被映射到计算机地址空间中,对这部分地址空间进行读写,就相当于与外设进行数据交换/命令传递。

实际上,看过单片机datasheet的同学就会发现,单片机的数据读取/发送,命令执行等等都是受外加电平控制的,我们设想计算机将自己的总线分别接到单片机芯片上,就能明白为什么对地址空间读写就相当于操作外设。因为对这段地址空间读写,就相当于在总线上发出了一系列电平接到了芯片的对应引脚上去,从而让外设也进行了相应的变化,即实现操纵。

说回qemu,qemu中的外设也是以这种方式操纵的,只是外设实际上不存在,外设的响应是qemu自己在宿主机上虚拟出来的。也就是说,在qemu-system中的用户态程序通过读写地址操作外设,实际上会在qemu-system程序内部触发相应的操作,这些操作本身可能存在有漏洞,如何利用这些有问题的操作,在宿主机的qemu-system程序中执行命令,从而获取到宿主机权限,这就是qemu逃逸的考察点。

模板讲解

下面我会结合ray-cp师傅的模板讲一下各类操作的含义。

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/io.h>
#include <unistd.h>


#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)

char* userbuf;
uint64_t phy_userbuf, phy_userbuf2;
unsigned char* mmio_mem;


void die(const char* msg)
{
    perror(msg);
    exit(-1);
}

uint64_t page_offset(uint64_t addr)
{
    return addr & ((1 << PAGE_SHIFT) - 1);
}

uint64_t gva_to_gfn(void* addr)
{
    uint64_t pme, gfn;
    size_t offset;

    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0)
    {
        die("open pagemap");
    }
    offset = ((uintptr_t)addr >> 9) & ~7;
    lseek(fd, offset, SEEK_SET);
    read(fd, &pme, 8);
    if (!(pme & PFN_PRESENT))
        return -1;
    gfn = pme & PFN_PFN;
    return gfn;
}

uint64_t gva_to_gpa(void* addr)
{
    uint64_t gfn = gva_to_gfn(addr);
    assert(gfn != -1);
    return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

void mmio_write(uint64_t addr, uint64_t value)
{
    *((uint64_t*)(mmio_mem + addr)) = value;
}
#mmio write,向外设mmio地址写内容,触发对应的操作。
#注意其value对应操作数val,addr对应外设的hwaddr,写入类型(这里是uint64_t*)对应size

void mmio_write_32(uint64_t addr, uint32_t value)
{
    *((uint32_t*)(mmio_mem + addr)) = value;
}
#不同的操作需要的操作数size不同,所以原子操作可能需要写多个


uint64_t mmio_read(uint64_t addr)
{
    return *((uint64_t*)(mmio_mem + addr));
}
#考虑到物理/虚拟地址的对应关系,使用mmio_read或使用mmio_mem对应的虚拟地址进行读取似乎都可以

int main(int argc, char* argv[])
{
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");

    mmio_mem = mmap(0, 0x100000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");
        
    #这一段实际上将mmio_fd映射为一片mmap空间,从而对这片空间读写就是对mmio内存读写
    #进而控制外设,触发对应的操作函数
    
    printf("mmio_mem: %p\n", mmio_mem);

    userbuf = mmap(0, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (userbuf == MAP_FAILED)
        die("mmap userbuf failed");
    mlock(userbuf, 0x10000);
    phy_userbuf = gva_to_gpa(userbuf);
    printf("user buff virtual address: %p\n", userbuf);
    printf("user buff physical address: %p\n", (void*)phy_userbuf);
 	
 	#注意,外设本身只认qemu中的物理地址,也就是2,所以在撰写exp时,操纵外设只能用phy_userbuf
 	#又由于qemu内物理/虚拟地址的对应关系,对物理地址的读写就相当于对对应虚拟地址的读写。
 	#故可以用phy_userbuf进行读操作后,在对应的userbuf中取到相应数据
	return 0;
}

mmio_write函数相当于原子操作,一般用其设定一些硬件值以满足特定条件,从而有能力触发更复杂的操作。

一些疑问

我对qemu也不太了解,只是清楚其似乎是对二进制指令进行翻译并执行,再返回执行结果。

但在调试过程中,发现qemu-system对用户态程序并不是严格按指令顺序进行解释执行的,这也导致即使printf语句在靠前的位置,还是没有办法在打通前输出leak的值。通过写入mmio空间来调用外设时也出现了类似的指令先后顺序颠倒甚至重复的现象,希望有大佬能解释一下。