roger 发表于 2020-5-4 11:30:25

ret2dl_resolve解析

ELF文件利用延迟绑定技术来解决动态程序模块与函数的重定位问题。ret2dl_resolve的原理是基于延迟绑定技术而形成的利用技巧,它通过伪造数据结构以及对函数延迟绑定过程的拦截,实现任意函数调用的目的。本篇文章主要描述延迟绑定的原理、32位以及64位ELF文件的ret2dl_resolve技术。延迟绑定技术针对动态链接会减速程序运行速度的现状,操作系统实现了延迟绑定(Lazy Binding)的技术:函数第一次被用到时才对函数进行绑定。通过延迟绑定大大加快了程序的启动速度。而ELF 则使用了PLT(Procedure Linkage Table,过程链接表)的技术来实现延迟绑定。下面根据一个程序具体来跟下程序看延迟绑定的实现,demo程序如下:#include <stdio.h>
  // gcc -m32 -g demo.c -o demo
  int main()
  {
  char data;
  read(0,data,20);
  return 0;
  }
  
断点下在read函数调用的地方: 0x8048492 <main+39>    call   read@plt <0x8048330>
  
程序先调用read@plt,查看此时read的plt表的内容:pwndbg> x/3i 0x8048330
  0x8048330 <read@plt>:      jmp    DWORD PTR ds:0x804a00c
  0x8048336 <read@plt+6>:      push   0x0
  0x804833b <read@plt+11>:   jmp    0x8048320
  
可以看到它直接跳转到了read的got表中的地址,此时read的got表中的刚好是read@plt下一条地址的值0x08048336:pwndbg> x/wx 0x804a00c
  0x804a00c:      0x08048336
  
如上面代码所示,0x08048336地址处的两条指令,将0压入栈中,并跳转到0x8048320地址执行:0x8048336 <read@plt+6>:      push   0x0
  0x804833b <read@plt+11>:   jmp    0x8048320
  
0x8048320为plt表的起始地址,称其为plt0,其指令为:0x8048320                              push   dword ptr <0x804a004>
  0x8048326                              jmp    dword ptr <0xf7feed90>
  
可以看到它将got+4的值压入到栈中,并跳转到了got+8的地方去执行。跟进去到got+8中的值0xf7feed90,可以看到程序进入到了_dl_runtime_resolve,因此got+8的地方存储的是_dl_runtime_resolve函数的地址。看_dl_runtime_resolve的源码,源码在/sysdeps/i386/dl-trampoline.S中:0xf7feed90 <_dl_runtime_resolve>       push   eax
  0xf7feed91 <_dl_runtime_resolve+1>   push   ecx
  0xf7feed92 <_dl_runtime_resolve+2>   push   edx
  0xf7feed93 <_dl_runtime_resolve+3>   mov    edx, dword ptr
  0xf7feed97 <_dl_runtime_resolve+7>   mov    eax, dword ptr
  0xf7feed9b <_dl_runtime_resolve+11>    call   _dl_fixup <0xf7fe85a0>
  
_dl_runtime_resolve在源码中是用汇编实现的只是压栈并调用_dl_fixup,在跟进去_dl_fixup前,先给出一些与动态链接相关的数据结构。根据《程序员的自我修养》中的描述:动态链接中最重要的结构应该是dynamic段,这个段里面保存了动态链接器所需要的基本信息。比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。使用readelf查看该demo文件中dynamic的信息如下:  其是一个结构体数组,结构体的定义为:
typedef struct {
  Elf32_Sword   d_tag;
  union {
  Elf32_Wordd_val;
  Elf32_Addrd_ptr;
  } d_un;
  } Elf32_Dyn;
  extern Elf32_Dyn_DYNAMIC[];
  
Elf32_Dyn结构由一个类型值加上一个附加的数值或指针,对于不同的类型,后面附加的数值或者指针有着不同的含义。下面给出和延迟绑定相关的类型值的定义。
d_tag类型d_un的定义
#define DT_STRTAB 5动态链接字符串表的地址,d_ptr表示.dynstr的地址 (Address of string table)
#define DT_SYMTAB 6动态链接符号表的地址,d_ptr表示.dynsym的地址 (Address of symbol table)
#define DT_JMPREL 23动态链接重定位表的地址,d_ptr表示.rel.plt的地址 (Address of PLT relocs)
#define DT_RELENT 19单个重定位表项的大小,d_val表示单个重定位表项大小 (Size of one Rel reloc )
#define DT_SYMENT 11单个符号表项的大小,d_val表示单个符号表项大小 (Size of one symbol table entry )
如上图所示,可以看到字符串表.dynstr的地址为0x804822c,符号表.dynsym地址为0x80481cc,其单个符号表项的大小为16,重定位表.rel.plt的地址为 0x80482d8,其单个重定位表项的大小为8.rel.plt重定位表中包含了需要重定位的函数的信息,其也是一个结构体数组,结构体Elf32_Rel定义如下,其中r_offset表示got表地址,即动态解析函数后真正的函数地址需要填入的地方,r_info由两部分构成,r_info>>8表示该函数对应在符号表.dynsym中的下标,r_info&0xff则表示重定位类型。:typedef struct {
  Elf32_Addr      r_offset;
  Elf32_Word       r_info;
  } Elf32_Rel;
  
我们查看demo程序的重定位表0x80482d8:pwndbg> x/6wx 0x80482d8
  0x80482d8:      0x0804a00c      0x00000107      0x0804a010      0x00000207
  0x80482e8:      0x0804a014      0x00000407
  
以及使用readelf -r来查看demo程序的重定位表:$ readelf -r demo
  Relocation section '.rel.plt' at offset 0x2d8 contains 3 entries:
  Offset   Info    Type            Sym.ValueSym. Name
  0804a00c00000107 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
  0804a01000000207 R_386_JUMP_SLOT   00000000   __stack_chk_fail@GLIBC_2.4
  0804a01400000407 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0
  
可以看到重定位表.rel.plt为一个Elf32_Rel数组,demo程序中该数组包含三个元素,第一个是read的重定位表项Elf32_Rel结构体,第二个是__stack_chk_fail,第三个是__libc_start_main。read的重定位表r_offset为0x0804a00c,为read的got地址,即在动态解析函数完成后,将read的函数地址填入到r_offset为0x0804a00c中。r_info为0x00000107表示read函数的符号表为.dynsym数组中的0x00000107>>8(即0x1)个元素,它的类型为0x00000107&0xff(即0x7)对应为R_386_JUMP_SLOT类型。接着我们去看符号表.dynsym节,它也是一个结构体Elf32_Sym数组,其结构体的定义如下:typedef struct
  {
  Elf32_Word    st_name; //符号名,是相对.dynstr起始的偏移
  Elf32_Addr    st_value;
  Elf32_Word    st_size;
  unsigned char st_info; //对于导入函数符号而言,它是0x12
  unsigned char st_other;
  Elf32_Section st_shndx;
  }Elf32_Sym; //对于导入函数符号而言,其他字段都是0
  
其中st_name指向的是函数名称在.dynstr表中的偏移。在dynamic段中我们知道了符号表.dynsym地址为0x80481cc,查看它的值:pwndbg> x/20wx 0x80481cc
  0x80481cc:      0x00000000      0x00000000      0x00000000      0x00000000
  0x80481dc:      0x0000002b      0x00000000      0x00000000      0x00000012
  0x80481ec:      0x0000001a      0x00000000      0x00000000      0x00000012
  0x80481fc:      0x00000042      0x00000000      0x00000000      0x00000020
  0x804820c:      0x00000030      0x00000000      0x00000000      0x00000012
  
以及使用readelf -s查看符号表的内容:$ readelf -s demo
  Symbol table '.dynsym' contains 6 entries:
  Num:    ValueSize Type    Bind   Vis      Ndx Name
  0: 00000000   0 NOTYPELOCALDEFAULTUND
  1: 00000000   0 FUNC    GLOBAL DEFAULTUND read@GLIBC_2.0 (2)
  2: 00000000   0 FUNC    GLOBAL DEFAULTUND __stack_chk_fail@GLIBC_2.4 (3)
  3: 00000000   0 NOTYPEWEAK   DEFAULTUND __gmon_start__
  4: 00000000   0 FUNC    GLOBAL DEFAULTUND __libc_start_main@GLIBC_2.0 (2)
  5: 0804853c   4 OBJECTGLOBAL DEFAULT   16 _IO_stdin_used
  
从重定位表.rel.plt中,我们知道了read的r_info>>8为0x1,即read的符号表项对应的是.dynsym第二个元素,果然可以看到.dynsym第一个元素为read函数的Elf32_Sym结构体,可以看到它的st_name对应的是0x0000002b,即read字符串应该在.dynstr表偏移为0x2b的地方,由dynamic我们知道了.dynstr表的地址为地址为0x804822c,去验证下看其偏移0x2b是否为read字符串:pwndbg> x/s 0x804822c+0x2b
  0x8048257:      "read"
  
可以看到,确实如此。到这里似乎对read函数的解析过程有了一个简单的了解:·可以先通过dynamic段获取各个表的地址,包括有字符串表.dynstr的地址为0x804822c,符号表.dynsym地址为0x80481cc,其单个符号表项的大小为16,重定位表.rel.plt的地址为 0x80482d8,其单个重定位表项的大小为8。·read函数为.rel.plt表中的第一个元素,定位它的重定位表项,知道了read函数的r_offset为0x0804a00c,以及它在符号表中的下标为0x000001,它的类型为0x7,R_386_JUMP_SLOT。·由0x000001知道了read函数的符号表是.dynsym第二个元素,获取到该结构体,得到了它对应的st_name对应的是0x0000002b,即获取了read字符串应该在.dynstr表偏移为0x2b的地方。·最后调用函数解析匹配read字符串所对应的函数地址,将其填至r_offset为0x0804a00c,即read的got地址中。有了前面的基础,现在可以跟进去_dl_fixup,我们知道在调用_dl_runtime_resolve函数之前压入到栈中的参数是0,以及got+4中的值,参考下面_dl_fixup的源码,根据参数列表,知道了眼乳栈中的0为reloc_arg,got+4中的值为struct link_map *l,函数源码在/elf/dl-runtime.c中:_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
  {
  //获取符号表地址
  const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info);
  //获取字符串表地址
  const char *strtab = (const void *) D_PTR (l, l_info);
  //获取函数对应的重定位表结构地址
  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info) + reloc_offset);
  //获取函数对应的符号表结构地址
  const ElfW(Sym) *sym = &symtab;
  //得到函数对应的got地址,即真实函数地址要填回的地址
  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  DL_FIXUP_VALUE_TYPE value;
  //判断重定位表的类型,必须要为7--ELF_MACHINE_JMP_SLOT
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
  /* Look up the target symbol.If the normal lookup rules are not
  used don't look in the global scope.*/
  //需要绕过
  if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
  {
  const struct r_found_version *version = NULL;
  if (l->l_info != NULL)
  {
  const ElfW(Half) *vernum =
  (const void *) D_PTR (l, l_info);
  ElfW(Half) ndx = vernum & 0x7fff;
  version = &l->l_versions;
  if (version->hash == 0)
  version = NULL;
  }
  ...
  // 接着通过strtab+sym->st_name找到符号表字符串
  result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
  version, ELF_RTYPE_CLASS_PLT, flags, NULL);
  ...
  // value为libc基址加上要解析函数的偏移地址,也即实际地址
  value = DL_FIXUP_MAKE_VALUE (result,
  sym ? (LOOKUP_VALUE_ADDRESS (result)
  + sym->st_value) : 0);
  }
  else
  {
  /* We already found the symbol.The module (and therefore its load
  address) is also known.*/
  value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
  result = l;
  }
  ...
  // 最后把value写入相应的GOT表条目rel_addr中
  return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
  }
  
可以看到_dl_fixup函数就如上面描述的过程一般,去定位结构体,最终获取read字符串去libc中找到相应的函数地址并填回到got表中。所以ELF文件的延迟绑定技术总结如下:调用函数时,call func@plt,plt表内容如下:func@plt:
  jmp *(func@GOT)   //仍然首先进行GOT跳转,尝试是否是第一次链接
  push n      //压入需要地址绑定的符号在重定位表中的下标
  jmp PLT0    //跳转到 PLT0
  
由于是第一次调用,func@GOTgot表中的值为jmp *(func@GOT)指令的下一条地址,即push n的地址,接着程序会执行push n; jmp PLT0,n则为该函数在.rel.plt表中的偏移。接着去看PLT0的指令为:push *(got+4)
  jmp *(got+8)
  
其中got+4存储的是link_map的地址,got+8存储的是_dl_runtime_resolve函数的地址。进入到_dl_runtime_resolve函数后,函数会调用_dl_fixup函数,根据源码分析,可以看到该函数功能为:1.程序先从第一个参数link_map获取字符串表.dynstr、符号表.dynsym以及重定位表.rel.plt的地址,2.通过第二个参数n即.rel.plt表中的偏移reloc_arg加上.rel.plt的地址获取函数对应的重定位结构的位置,从而获取函数对应的r_offset以及在符号表中的下标r_info>>8。3.根据符号表地址以及下标获取符号结构体,获得了函数符号表中的st_name,即函数名相对于字符串表.dynstr的偏移。4.最后可得到函数名的字符串,然后去libc中匹配函数名,找到相应的函数并将地址填回到r_offset即函数got表中,延迟绑定完成。32位的ret2dl_resolve原理ret2dl_resolve的适用场景是在无法泄露程序地址时,通过拦截延迟绑定的过程,实现对函数地址解析过程的劫持,使得最终解析出来的函数为特定函数的函数地址,从而实现无泄露达到特定函数调用的目的。32位ELF程序ret2dl_resolve攻击方法,目前最为普遍的是伪造reloc_arg,即伪造重定位表的下标实现相关的利用,具体包括如下步骤:1.伪造reloc_arg,使得reloc_arg加上.rel.plt的地址指向可控的地址,在该地址可伪造恶意的Elf32_Rel结构体。2.伪造Elf32_Rel结构体中的r_offset指向某一可写地址,最终函数地址会写入该地址处;伪造r_info&0xff为0x7,因为类型需为ELF_MACHINE_JMP_SLOT以绕过类型验证;伪造r_info>>8,使得r_info>>8加上.dynsym地址指向可控的地址,并在该地址伪造符号表结构体Elf32_Sym。3.伪造Elf32_Sym结构体中的st_name,使得.dynstr的地址加上该值指向可控地址,并在该地址处写入特定函数的函数名入system。4.最终系统通过函数名匹配,定位到特定函数地址,获取该地址并写入到伪造的r_offset中,实现了函数地址的获取。整个过程看起来比较简单,仍然需要注意一点的是dl_fixup中还存在以下一段代码:if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
  {
  const struct r_found_version *version = NULL;
  if (l->l_info != NULL)
  {
  const ElfW(Half) *vernum =
  (const void *) D_PTR (l, l_info);
  ElfW(Half) ndx = vernum & 0x7fff;
  version = &l->l_versions;
  if (version->hash == 0)
  version = NULL;
  }
  
如果reloc->r_info伪造不当,会使得ndx = vernum & 0x7fff偏大,导致version = &l->l_versions出现访存错误,因此伪造的reloc->r_info,最好使得ndx为0,即vernum为0。实践--0ctf2018-babystack这题只是一个简单的栈溢出,但是由于它是被python将程序运行起来,只是将读入的0x100字节数据给程序并没有将任何数据输出,无法进行泄露,因此想到了使用ret2dl-resolve来解这道题。在bss段上选择一个地址伪造Elf32_Sym,使得由它得到的ndx = vernum为0。并在一个地址填入特定目标函数名称如system字符串,将其相对于.dynstr的地址的偏移填入到伪造的Elf32_Sym结构体中的st_name中。再伪造Elf32_Rel结构体,将r_offset伪造成想要写目标函数的地址,将r_info>>8构造成伪造的Elf32_Sym相对于.dynsym数组的偏移,将r_info&0xff伪造成0x7。最后计算出伪造的Elf32_Rel结构体相对于.rel.plt的偏移,将该偏移压入栈中,最终调用plt0实现函数地址解析。由于该程序无法输出,因此需要一个远程的vps来反弹shell或接收flag。目前roputils可以较好的支持构造ret2dl_resolve数据,但是它好像对于ndx这个没有考虑进去,参考roputils,pwn_debug加入了对32位ret2dl_resolve数据的构造,提供ret2dl_resolve模块,其apibuild_normal_resolve会返回一个构造好的能够实现ret2dl_resolve攻击的数据,且其对应的ndx为0。64位ELF程序的ret2dl_resolve前面描述的是32位程序的ret2dl_resolve,64位程序是否一样可行?对于64位程序而言,理论上而言上述方法也是可行的,但是在实际构造的过程中,有一点是会是程序崩溃的,还是前面提到的那段代码:if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
  {
  const struct r_found_version *version = NULL;
  if (l->l_info != NULL)
  {
  const ElfW(Half) *vernum =
  (const void *) D_PTR (l, l_info);
  ElfW(Half) ndx = vernum & 0x7fff;
  version = &l->l_versions;
  if (version->hash == 0)
  version = NULL;
  }
  
因为64位程序构造的数据一般都是在bss段,如0x601000-0x602000,导致其相对于.dynsym的地址0x400000-0x401000很大,使得reloc->r_info也很大,最后使得访问ElfW(Half) ndx = vernum & 0x7fff;时程序访存出错,导致程序崩溃。根据代码可以使得l->l_info != NULL这句话不成立来绕过该段代码,即使得l->l_info等于NULL,即使得(link_map + 0x1c8) 处为 NULL。这就使问题变成了往link_map写空值,由于link_map在ld.so中,还需要泄露地址。因此实现64位的上述方法的ret2dl_resolve,需要泄露与地址写两个漏洞,如果有这两个漏洞我们应该可以使用更轻松的方法来get shell,因此价值不大。那么是否还有其他方法?可以看到该段代码还有一个条件if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0),我们是否可构造sym->st_other使它不为空,从而绕过该段代码,我们看假设sym->st_other使它不为空,dl_fixup的代码流程:_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
  {
  //获取符号表地址
  const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info);
  //获取字符串表地址
  const char *strtab = (const void *) D_PTR (l, l_info);
  //获取函数对应的重定位表结构地址
  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info) + reloc_offset);
  //获取函数对应的符号表结构地址
  const ElfW(Sym) *sym = &symtab;
  //得到函数对应的got地址,即真实函数地址要填回的地址
  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  DL_FIXUP_VALUE_TYPE value;
  //判断重定位表的类型,必须要为7--ELF_MACHINE_JMP_SLOT
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
  /* Look up the target symbol.If the normal lookup rules are not
  used don't look in the global scope.*/
  if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
  {
  ...
  }
  else
  {
  /* We already found the symbol.The module (and therefore its load
  address) is also known.*/
  value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
  result = l;
  }
  ...
  // 最后把value写入相应的GOT表条目rel_addr中
  return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
  }
  
可以看到当sym->st_other不为0时,会调用DL_FIXUP_MAKE_VALUE,根据代码的注释,该代码认为这个符号已经解析过,直接调用DL_FIXUP_MAKE_VALUE函数赋值。DL_FIXUP_MAKE_VALUE函数的定义如下,直接将l->l_addr + sym->st_value赋值给value:#define DL_FIXUP_MAKE_VALUE(map, addr) (addr)
  /* Extract the code address from a value of type DL_FIXUP_MAKE_VALUE.
  */
  
也可以看到sym等都是从link_map中取出来的,如果我们将控制的目标不设定为reloc_arg,而是伪造第一个参数link_map。如果我们可以控制sym->st_value指向got表中的地址如__libc_start_main的got,而l->l_addr为目标地址如system到__libc_start_main的偏移,则最终得到的value会是l->l_addr + sym->st_value即system地址,从而实现无需leak地址的利用。重新整理下利用思路,在利用中我们控制的不再是reloc_arg,而是struct link_map *l,假设我们可以覆盖got+4,即link_map的值,指向我们可控的目标。首先看link_map的定义:pwndbg> print sizeof(*l)
  $2 = 0x470
  pwndbg> ptype l
  type = struct link_map {
  Elf64_Addr l_addr;
  char *l_name;
  Elf64_Dyn *l_ld;
  struct link_map *l_next;
  struct link_map *l_prev;
  struct link_map *l_real;
  Lmid_t l_ns;
  struct libname_list *l_libname;
  Elf64_Dyn *l_info;//l_info 里面包含的就是动态链接的各个表的信息
  ...
  size_t l_tls_firstbyte_offset;
  ptrdiff_t l_tls_offset;
  size_t l_tls_modid;
  size_t l_tls_dtor_count;
  Elf64_Addr l_relro_addr;
  size_t l_relro_size;
  unsigned long long l_serial;
  struct auditstate l_audit[];
  } *
  
我们最终的目标是伪造sym,使得sym->st_value的地址刚好指向got表中存在libc地址的值,如__libc_start_main,而l->l_addr存储的则是目标函数地址(system)与__libc_start_main的偏移,因此首先伪造l->l_addr为目标地址system与__libc_start_main的偏移。接下来看sym->st_value如何构造才能指向__libc_start_main的got表的位置。我们看sym是如何得到的: //获取符号表地址
  const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info);
  //获取函数对应的重定位表结构地址
  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info) + reloc_offset);
  //获取函数对应的符号表结构地址
  const ElfW(Sym) *sym = &symtab;
  
最终sym是通过&symtab得到的,我们能够控制link_map,为简单起见,我们可以控制reloc->r_info为0,因此现在问题就变成了如何控制symtab第一个元素的st_value指向__libc_start_main的got表的位置。symtab是l_info,根据定义#define DT_SYMTAB 6,symtab即是l_info,根据Elf64_Dyn以及Elf64_Sym的定义,我们可以构造l_info指向link_map+0x70,同时在link_map+0x78的位置填入__libc_start_maingot表地址减8的地址,这样就伪造了DT_SYMTAB对应的Elf64_Dyn为link_map+0x70,它的d_ptr为link_map+0x78,指向了__libc_start_main_got-8,即symtab指向了__libc_start_main_got-8,它的st_value偏移为8,因此实现了st_value指向了__libc_start_main_got,由于symtab指向了__libc_start_main_got-8,其地址一般仍然为got表地址,里面中的数据存在值,因此sym->st_other不为0的条件也是成立的。pwndbg> ptype Elf64_Dyn
  type = struct {
  Elf64_Sxword d_tag;
  union {
  Elf64_Xword d_val;
  Elf64_Addr d_ptr;
  } d_un;
  }
  pwndbg> ptype Elf64_Sym
  type = 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;
  }
  
最后再看写入地址rel_addr的由来: //获取函数对应的重定位表结构地址
  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info) + reloc_offset);
  ...
  //得到函数对应的got地址,即真实函数地址要填回的地址
  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  
假设我们要将最终得到的地址写在link_map+0x28的位置,它最终是由l->l_addr + reloc->r_offset得到的,l->l_addr的值已经确定,为目标地址到got表中libc地址的偏移,因此要控制reloc->r_offset使得它加上l->l_addr为link_map+0x28的地址。reloc = (const void *) (D_PTR (l, l_info) + reloc_offset,因此要伪造reloc,假设将伪造的reloc设定在link_map+0x30处,其中reloc_offset已经确定,由相对应的函数的reloc_arg*0x8确定。因此我们要伪造l_info,根据定义#define DT_JMPREL 23,我们需要伪造.rel.plt表所对应的Elf64_Dyn结构体,我们将l_info指向link_map+0x80-8,即最后使得它对应的d_ptr为link_map+0x80,我们在link_map+0x80中写入link_map+0x30-reloc_offset的值,这样就使得.rel.plt表的地址为link_map+0x30-reloc_offset,最终经过reloc = (const void *) (D_PTR (l, l_info) + reloc_offset得到了reloc指向link_map+0x30,我们在link_map+0x30中构造伪造的Elf64_Rela结构体,并将r_offset填入link_map+0x28减去l->l_addr的值,最终得到rel_addr为link_map+0x28,同时将r_info填入0x7,以绕过前面对类型的判定。pwndbg> ptype Elf64_Rela
  type = struct {
  Elf64_Addr r_offset;
  Elf64_Xword r_info;
  Elf64_Sxword r_addend;
  }
  
最终形成了sym->st_other不为0,rel_addr指向link_map+0x30,sym->st_value地址为__libc_start_maingot表的地址,l->l_addr为system到__libc_start_main的偏移,经过value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);,我们会得到value为system地址,并最后写入到了rel_addr即link_map+0x30中,实现对system函数的调用。写的很绕,主要是里面结构体指针指来指去的,感觉自己也不太愿意回头再看一遍。。。。实例--hitcon2015--blinkroot此题是ELF64程序,在bss段输入了一个0x400字节大小的数据。并给了一个任意地址写,由于使用了movaps指令,所以目标指令地址必须要16字节对齐。由于没有泄露,只存在一次地址写任意值,且程序在读取输入后将输入、输出以及错误句柄都关闭了,所以考虑使用ret2dl_resolve攻击。因此考虑覆盖link_map指向bss段地址,通过伪造link_map实施上述的ret2dl_resolve攻击。最后通过构造好的link_map,将sym->st_value地址为__libc_start_maingot表的地址,l->l_addr为system到__libc_start_main的偏移,通过后面的puts调用实施ret2dl_resolve攻击,它所对应的reloc_offset为0x18,最终实现调用system反弹shell。我也在pwn_debug中加入了伪造link_map的apibuild_link_map,方便较快的构造link_map。小结不得不说延迟绑定是一个比较绕的过程。特别是伪造它们的结构,由于指针的指来指去,感觉很绕,但是在理解了以后其实还是比较简单,可能也是能力不够,表述的比较粗糙,但也是尽力了。exp和文件在github参考链接1.ROP之return to dl-resolve2.ret2dl_resolve学习笔记3.roputils/examples/dl-resolve-i386.py4.HITCON 2015 PWN 200 blinkroot5.https://gist.github.com/inaz2/fbff517fc639f69a4309f79506771849 文章首发于安全客-ret2dl_resolve解析References roputils: https://github.com/inaz2/roputils pwn_debug: https://github.com/ray-cp/pwn_debug github: https://github.com/ray-cp/pwn_category/tree/master/stack/ret2dl_resolve ROP之return to dl-resolve: http://rk700.github.io/2015/08/09/return-to-dl-resolve/ ret2dl_resolve学习笔记: https://veritas501.space/2017/10/07/ret2dl_resolve学习笔记/ roputils/examples/dl-resolve-i386.py: https://github.com/inaz2/roputils/blob/master/examples/dl-resolve-i386.py HITCON 2015 PWN 200 blinkroot: https://ddaa.tw/hitcon_pwn_200_blinkroot.html https://gist.github.com/inaz2/fbff517fc639f69a4309f79506771849 : https://gist.github.com/inaz2/fbff517fc639f69a4309f79506771849 安全客-ret2dl_resolve解析: https://www.anquanke.com/post/id/184099
页: [1]
查看完整版本: ret2dl_resolve解析