ret2dl_runtime_resolve

认真梳理一下re2dl_runtime_resolve

原理

写个c程序自己测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
void vuln(){
char buf[28];
read(0,buf,128);
}
int main(int argc, char const *argv[])
{
char name[] = "input your name: ";
write(1,name,strlen(name));
vuln();
return 0;
}
// gcc -g -m32 -fno-stack-protector test.c test32

第一次调用strlen函数会先跳到 strlen plt表中去,接着执行jmp指令到0x804a010去,这里是 got 表

可以看到strlen@got中存放的是strlen@plt的第二条指令:0x8048336 <strlen@plt+6> push 8对比一下__libc_start_main(已经调用过一次的函数)

放着的则是__libc_start_main的真实地址,而由于strlen函数第一次调用,所以got表上还没有绑定上真实地址,一律放着的都是 fun@plt+6这个地址,看下面未调用的write也是一样

而执行完 push 指令后,执行jmp 0x8048310,再 push 0x804a004的内容后跳到_dl_runtime_resolve

0x804a004正是GOT[1],也就是push GOT[1],后jmp GOT[2],而GOT[2]放着的正是_dl_runtime_resolve的真实地址。

1
2
3
4
5
6
GOT表内容
GOT[0] 0x804a000 —▸ 0x8049f14 --> .dynamic的地址
GOT[1] 0x804a004 —▸ 0xf7ffd918 --> link_map :此处包含链接器的标识信息
GOT[2] 0x804a008 —▸ 0xf7fee000 --> dl_runtime_resolve 动态链接器的入口
GOT[3] 0x804a00c —▸ 0x8048326 (read@plt+6)
.....

所以实际上,就是执行了_dl_runtime_resolve(link_map, reloc_arg)来得到函数的真实地址,并写到got表中去,之后 call fun@plt的第一次jmp的时候,就可以直接跳到真实地址上去了。

用一张图直观地显示函数第一次调用和第二次调用的流程:

接着我们往下看看link_map里面有什么东西

可以看到有个.dynamic地址,这里简单介绍一下程序中的各种段

.dynamic,动态节一般保存了ELF文件如下信息:

  • 依赖于哪些动态库
  • 动态符号节信息
  • 动态字符串信息

动态节的结构是这样的:

1
2
3
4
5
6
7
8
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn_DYNAMIC[];

readelf -d test32可以打印出程序的动态节内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Dynamic section at offset 0xf14 contains 24 entries:
标记 类型 名称/值
0x00000001 (NEEDED) 共享库:[libc.so.6]
0x0000000c (INIT) 0x80482ec
0x0000000d (FINI) 0x8048554
0x00000019 (INIT_ARRAY) 0x8049f08
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049f0c
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x804823c
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 87 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x804a000
0x00000002 (PLTRELSZ) 32 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x80482cc
0x00000011 (REL) 0x80482c4
0x00000012 (RELSZ) 8 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x80482a4
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x8048294
0x00000000 (NULL) 0x0

这里主要关注以下东西:

1
2
3
0x00000005 (STRTAB)                     0x804823c
0x00000006 (SYMTAB) 0x80481cc
0x00000017 (JMPREL) 0x80482cc

STRTAB ,SYMTAB ,JMPREL分别指向.dynstr,.dynsym,.rel.plt节段

  • 动态符号表(.dynsym)用来保存与动态链接相关的导入导出符号,不包括模块内部的符号。而.symtab则保存所有符号,包括.dynsym中的符号,因此一般来说,.symtab的内容多一点
  • .dynsym是运行时所需的,ELF文件中 export/import 的符号信息全在这里,而.symtab节中存储的信息是编译时的符号信息,用strip工具会被删除,或者编译里加入-s参数也会删除。

我们主要关注动态符号.dynsym中的两个成员

  • st_name,该成员保存着动态符号在.dynstr表(动态字符串表)中的偏移
  • st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。

.rel.plt包含了需要重定位的函数信息,使用如下的结构 ,需要区别:.rel.plt节是用于函数重定位,rel.dyn节是用于变量重定位

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
//32 位程序只使用 Elf32_Rel
//64 位程序只使用 Elf32_Rela
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;

r_offset: 指向对应got表的指针

r_info: r_info >> 8后得到一个下标,对应此导入符号在.dynsym中的下标

现在我们回到_dl_runtime_resolve(link_map, reloc_arg)

这里的link_map就是GOT[1],reloc_arg就是函数在.rel.plt中的偏移,就是之前的push 8

我们继续 跟进_dl_runtime_resolve函数到call _dl_fixup,这个函数就是绑定真实地址到got的核心了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
{
// 首先通过参数reloc_arg计算重定位入口,这里的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找到.dynsym中对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7
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);
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

综上,整个过程是这样的

1
2
3
4
5
6
7
1、第一次执行函数,到plt表,接下去got表,由于没有真实地址,又返回plt表的第一项,压入reloc_arg和link_map后调用_dl_runtime_resolve(link_map,reloc_arg)
2、link_map访问.dynamic节段,并获得.dynstr,.dynsym,.rel.plt节段地址
3、.rel.plt + reloc_arg = 0,求出对应函数重定位表项Elf32_Rel的指针
4、通过重定位表项Elf32_Rel的指针,得到对应函数的r_info,r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针:
5、利用Elf32_Sym的指针得到对应的st_name,.dynstr + st_name即为符号名字符串指针
6、在动态链接库查找这个函数,并且把地址赋值给.rel.plt中对应条目的r_offset:指向对应got表的指针,由此puts的got表就被写上了真实的地址
7、赋值给GOT表后,把程序流程返回给strlen
0%