学逆向论坛

找回密码
立即注册

只需一步,快速开始

发新帖

2万

积分

41

好友

1171

主题
发表于 2020-5-4 11:30:25 | 查看: 2433| 回复: 0

相关题目:

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[20];
  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 [_GLOBAL_OFFSET_TABLE_+4] <0x804a004>
  0x8048326                              jmp    dword ptr [0x804a008] <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 [esp + 0x10]
  0xf7feed97 <_dl_runtime_resolve+7>     mov    eax, dword ptr [esp + 0xc]
  0xf7feed9b <_dl_runtime_resolve+11>    call   _dl_fixup <0xf7fe85a0>
  

_dl_runtime_resolve在源码中是用汇编实现的只是压栈并调用_dl_fixup,在跟进去_dl_fixup前,先给出一些与动态链接相关的数据结构。
根据《程序员的自我修养》中的描述:动态链接中最重要的结构应该是dynamic段,这个段里面保存了动态链接器所需要的基本信息。比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。
使用readelf查看该demo文件中dynamic的信息如下:

ret2dl_resolve解析

ret2dl_resolve解析
  其是一个结构体数组,结构体的定义为:
typedef struct {
  Elf32_Sword     d_tag;
  union {
  Elf32_Word  d_val;
  Elf32_Addr  d_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.Value  Sym. Name
  0804a00c  00000107 R_386_JUMP_SLOT   00000000   [email]read@GLIBC_2.0[/email]
  0804a010  00000207 R_386_JUMP_SLOT   00000000   [email]__stack_chk_fail@GLIBC_2.4[/email]
  0804a014  00000407 R_386_JUMP_SLOT   00000000   [email]__libc_start_main@GLIBC_2.0[/email]
  

可以看到重定位表.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:    Value  Size Type    Bind   Vis      Ndx Name
  0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
  1: 00000000     0 FUNC    GLOBAL DEFAULT  UND [email]read@GLIBC_2.0[/email] (2)
  2: 00000000     0 FUNC    GLOBAL DEFAULT  UND [email]__stack_chk_fail@GLIBC_2.4[/email] (3)
  3: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
  4: 00000000     0 FUNC    GLOBAL DEFAULT  UND [email]__libc_start_main@GLIBC_2.0[/email] (2)
  5: 0804853c     4 OBJECT  GLOBAL 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[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)];
  //得到函数对应的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[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;
  }
  ...
  // 接着通过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[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;
  }
  

如果reloc->r_info伪造不当,会使得ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff偏大,导致version = &l->l_versions[ndx]出现访存错误,因此伪造的reloc->r_info,最好使得ndx为0,即vernum[reloc->r_info]为0。
实践--0ctf2018-babystack
这题只是一个简单的栈溢出,但是由于它是被python将程序运行起来,只是将读入的0x100字节数据给程序并没有将任何数据输出,无法进行泄露,因此想到了使用ret2dl-resolve来解这道题。
在bss段上选择一个地址伪造Elf32_Sym,使得由它得到的ndx = vernum[ELFW(R_SYM) (reloc->r_info)]为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[1]可以较好的支持构造ret2dl_resolve数据,但是它好像对于ndx这个没有考虑进去,参考roputils,pwn_debug[2]加入了对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[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;
  }
  

因为64位程序构造的数据一般都是在bss段,如0x601000-0x602000,导致其相对于.dynsym的地址0x400000-0x401000很大,使得reloc->r_info也很大,最后使得访问ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;时程序访存出错,导致程序崩溃。
根据代码可以使得l->l_info[VERSYMIDX (DT_VERSYM)] != NULL这句话不成立来绕过该段代码,即使得l->l_info[VERSYMIDX (DT_VERSYM)]等于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[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)];
  //得到函数对应的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[76];  //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[DT_SYMTAB]);
  //获取函数对应的重定位表结构地址
  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)];
  

最终sym是通过&symtab[ELFW(R_SYM) (reloc->r_info)]得到的,我们能够控制link_map,为简单起见,我们可以控制reloc->r_info为0,因此现在问题就变成了如何控制symtab第一个元素的st_value指向__libc_start_main的got表的位置。symtab是l_info[DT_SYMTAB],根据定义#define DT_SYMTAB 6,symtab即是l_info[0x6],根据Elf64_Dyn以及Elf64_Sym的定义,我们可以构造l_info[0x6]指向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[DT_JMPREL]) + 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[DT_JMPREL]) + reloc_offset,因此要伪造reloc,假设将伪造的reloc设定在link_map+0x30处,其中reloc_offset已经确定,由相对应的函数的reloc_arg*0x8确定。因此我们要伪造l_info[DT_JMPREL],根据定义#define DT_JMPREL 23,我们需要伪造.rel.plt表所对应的Elf64_Dyn结构体,我们将l_info[DT_JMPREL]指向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[DT_JMPREL]) + 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[3]
参考链接
1.ROP之return to dl-resolve[4]2.ret2dl_resolve学习笔记[5]3.roputils/examples/dl-resolve-i386.py[6]4.HITCON 2015 PWN 200 blinkroot[7]5.https://gist.github.com/inaz2/fbff517fc639f69a4309f79506771849 [8]
文章首发于安全客-ret2dl_resolve解析[9]
References
[1] roputils: https://github.com/inaz2/roputils[2] pwn_debug: https://github.com/ray-cp/pwn_debug[3] github: https://github.com/ray-cp/pwn_category/tree/master/stack/ret2dl_resolve[4] ROP之return to dl-resolve: http://rk700.github.io/2015/08/09/return-to-dl-resolve/[5] ret2dl_resolve学习笔记: https://veritas501.space/2017/10/07/ret2dl_resolve学习笔记/[6] roputils/examples/dl-resolve-i386.py: https://github.com/inaz2/roputils/blob/master/examples/dl-resolve-i386.py[7] HITCON 2015 PWN 200 blinkroot: https://ddaa.tw/hitcon_pwn_200_blinkroot.html[8] https://gist.github.com/inaz2/fbff517fc639f69a4309f79506771849 : https://gist.github.com/inaz2/fbff517fc639f69a4309f79506771849[9] 安全客-ret2dl_resolve解析: https://www.anquanke.com/post/id/184099


温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的认可,还可以获得学币奖励,请尊重他人的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
论坛交流群:672619046

小黑屋|手机版|站务邮箱|学逆向论坛 ( 粤ICP备2021023307号 )|网站地图

GMT+8, 2024-12-23 11:06 , Processed in 0.187630 second(s), 38 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表