0.重要前提

Linux下,函数采用延迟绑定技术,是用到哪个函数才对哪个函数进行重定位.

1.基础知识

一个ELF文件是有很多个section构成的

ret2dl_solve学习笔记 - 图1

section的结构

  1. typedef struct {
  2. Elf32_Word sh_name; // section头部字符串表节区的索引
  3. Elf32_Word sh_type; // section类型
  4. Elf32_Word sh_flags; // section标志,用于描述属性
  5. Elf32_Addr sh_addr; // section的内存映像
  6. Elf32_Off sh_offset; // section的文件偏移
  7. Elf32_Word sh_size; // section的长度
  8. Elf32_Word sh_link; // section头部表索引链接
  9. Elf32_Word sh_info; // 附加信息
  10. Elf32_Word sh_addralign; // section对齐约束
  11. Elf32_Word sh_entsize; // 固定大小的section表项的长度
  12. } Elf32_Shdr;

其中包括有.dynsym, .dynstr, .rel.dyn, rel.plt, .plt, .plt.got, .got,.got.plt

  1. .dynsym --> 动态链接符号表, _dl_fixup会用到
  2. (dynamic linking symbol table)
  3. .dynstr --> 动态链接字符串表, _dl_fixup会用到
  4. .rel.dyn --> 变量重定位
  5. .rel.plt --> 函数重定位
  6. .plt --> 跳转表, 俗称PLT[0]
  7. .got --> 全局变量偏移
  8. .got.plt --> 保存全局函数偏移表

下面以read函数作为例讲解整个延迟绑定过程:

第一步

ret2dl_solve学习笔记 - 图2

从上面整个执行流程来看: 压入了0x0(reloc_arg) 和 ds:0x804a004(link_map), 作为 _dl_runtime_resolve的参数.

接下来解释参数:

  1. reloc_arg

使用命令分析ELF文件:readelf -r ./pwn01

ret2dl_solve学习笔记 - 图3

0x80482b0: 0x0804a00c 0x00000107 0x0804a010 0x00000207
0x80482c0: 0x0804a014 0x00000407 0x08ec8353 0x00009fe8
(与上面图中的数值一致)
  1. link_map

链接器的表示信息, 链接的时候就已经写入了.

结论: 以上过程想当于执行了_dl_runtime_resolve(link_map, reloc_arg), 该函数会完成符号解析, 将真正的地址写入到read@got中.

第二步

0xf7fee001<_dl_runtime_resolve+1>  : push ecx
0xf7fee002<_dl_runtime_resolve+2>  : push edx
0xf7fee003<_dl_runtime_resolve+3>  : mov edx,DWORD PTR [esp+0x10]
0xf7fee007<_dl_runtime_resolve+7>  : mov eax,DWORD || PTR [esp+0xc]
0xf7fee00b<_dl_runtime_resolve+11> : call 0xf7fe77e0 <_dl_fixup>
......

可以看到_dl_runtime_resolve中又调用了_dl_fixup

//部分_dl_fixup源码
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg){
      //首先通过参数reloca_arg计算入口地址,DT_JMPREL即.rel.plt,reloc_offset就是reloc_arg
      const PLTREL *const reloc=(const void*)(D_PTR(l,l_info[DT_JMPREL])+reloc_offset);
      // 通过reloc->r_info(0x107)找到.dynsym中对应的条目
      const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
      // 检查reloc->r_info的最低位是否为0x7, 不是则退出
      assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
      //接着到strtab + sym->st_name中找到符号字符串, result为libc的基地址
      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);
    // 写入指定的.got表
    return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);      
}

有童鞋可能有疑问: _dl_fix前面有push 指令, 为什么_dl_fixup_dl_runtime_resolve的参数一样都是reloc_arg, link_map呢?

ret2dl_solve学习笔记 - 图4
小结论: _dl_fixup 最终会通过函数名来查找函数对应的地址, 因此函数名是不可能重复的.

第三步
接下来实际调试~

.rel.plt = 0x080482b4, reloc_arg = 0
typedef struct {
    Elf32_Addr r_offset;    // 对于可执行文件,此值为虚拟地址
    Elf32_Word r_info;      // 符号表索引
} Elf32_Rel;

ret2dl_solve学习笔记 - 图5

reloc-> info = 0x107; reloc-> r_offset = 0x0804a00c

接着

const ElfW(Sym) *sym = (Elf32_Rel->r_info) >> 8 = 0x1

ret2dl_solve学习笔记 - 图6

第四步
_dl_runtime_resolve将控制权交给目标函数

2.攻击利用

_dl_runtime_resolve有两个参数, 我们就是要控制这两个参数, 让系统最终来查找我们自己构造的函数名, 最终实现执行system("/bin/sh")

3.参考资料

0CTF 2018 BabyStack