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[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的信息如下:

其是一个结构体数组,结构体的定义为:

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   read@GLIBC_2.0
0804a010  00000207 R_386_JUMP_SLOT   00000000   __stack_chk_fail@GLIBC_2.4
0804a014  00000407 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0

可以看到重定位表.rel.plt为一个Elf32_Rel数组,demo程序中该数组包含三个元素,第一个是read的重定位表项Elf32_Rel结构体,第二个是__stack_chk_fail,第三个是__libc_start_mainread的重定位表r_offset0x0804a00c,为read的got地址,即在动态解析函数完成后,将read的函数地址填入到r_offset0x0804a00c中。r_info0x00000107表示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 read@GLIBC_2.0 (2)
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)
     3: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 00000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.0 (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_offset0x0804a00c,以及它在符号表中的下标为0x000001,它的类型为0x7R_386_JUMP_SLOT
  • 0x000001知道了read函数的符号表是.dynsym第二个元素,获取到该结构体,得到了它对应的st_name对应的是0x0000002b,即获取了read字符串应该在.dynstr表偏移为0x2b的地方。
  • 最后调用函数解析匹配read字符串所对应的函数地址,将其填至r_offset0x0804a00c,即read的got地址中。

有了前面的基础,现在可以跟进去_dl_fixup,我们知道在调用_dl_runtime_resolve函数之前压入到栈中的参数是0,以及got+4中的值,参考下面_dl_fixup的源码,根据参数列表,知道了眼乳栈中的0reloc_arggot+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 PLT0n则为该函数在.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可以较好的支持构造ret2dl_resolve数据,但是它好像对于ndx这个没有考虑进去,参考roputilspwn_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[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_mapld.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表的位置。symtabl_info[DT_SYMTAB],根据定义#define DT_SYMTAB 6symtab即是l_info[0x6],根据Elf64_Dyn以及Elf64_Sym的定义,我们可以构造l_info[0x6]指向link_map+0x70,同时在link_map+0x78的位置填入__libc_start_maingot表地址减8的地址,这样就伪造了DT_SYMTAB对应的Elf64_Dynlink_map+0x70,它的d_ptrlink_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_addrlink_map+0x28的地址。reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset,因此要伪造reloc,假设将伪造的reloc设定在link_map+0x30处,其中reloc_offset已经确定,由相对应的函数的reloc_arg0x8确定。因此我们要伪造l_info[DT_JMPREL],根据定义#define DT_JMPREL 23,我们需要伪造.rel.plt表所对应的Elf64_Dyn结构体,我们将l_info[DT_JMPREL]指向link_map+0x80-8,即最后使得它对应的d_ptrlink_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_addrlink_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+0x30sym->st_value地址为__libc_start_maingot表的地址,l->l_addrsystem__libc_start_main的偏移,经过value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);,我们会得到value为system地址,并最后写入到了rel_addrlink_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_addrsystem__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-resolve
  2. ret2dl_resolve学习笔记
  3. roputils/examples/dl-resolve-i386.py
  4. HITCON 2015 PWN 200 blinkroot
  5. https://gist.github.com/inaz2/fbff517fc639f69a4309f79506771849
(完)