Return to dl_resolve
一无所有的此刻有亿万阵风吹过
https://www.cnblogs.com/GKLBB/p/19603282
https://www.cnblogs.com/unr4v31/p/15168342.html
延迟绑定
之前好像记过一次但是时间紧迫不找在哪了先复习一下
1 | 第一次调用 printf(): |
关键问题:GOT 表必须可写,因为动态链接器需要在运行时填入解析后的地址。
_dl_runtime_resolve
函数原型
1 | _dl_runtime_resolve(link_map_obj, reloc_index) |

解析器的工作流程:
- 读取 .dynamic 节,找到 .rel.plt / .dynsym / .dynstr 的基址
- 根据reloc_offset这个偏移量在.rel.plt中找到重定位表项Elf32_Rel
- 根据其中的r_info提取出 下标 (Index),然后通过
symtab_base + Index * 16来定位Elf32_Sym。 - 其中的st_name指向函数名
表项
Elf32_Dyn
8字节
1 | typedef struct { |
_dl_runtime_resolve获取的基址:
| d_tag | d_un(d_ptr) | |
|---|---|---|
| DT_STRTAB | 5 | .dynstr 字符串表基址 |
| DT_SYMTAB | 6 | .dynsym 符号表基址 |
| DT_JMPREL | 23 (0x17) | .rel.plt 重定位表基址 |
重定位表项Elf32_Rel
1 | typedef struct { |
r_info 的计算:((index) << 8 | 0x7)
index:告诉解析器去符号表.dynsym的第几个槽位读
Elf32_Sym。0x7:这是R_386_JMP_SLOT,告诉解析器这是一个函数跳转槽位。<< 8:r_info的高 24 位存放的是该符号在符号表(Symbol Table)里的下标(Index)。
符号表项Elf32_Sym
1 | typedef struct { |
涉及的节(Section)
.dynamic动态节(Dynamic Section)
存放Elf32_Dyn 结构体。
JMPREL:.rel.plt (Relocation PLT)
重定位表。存放Elf32_Rel 结构体
SYMTAB:.dynsym (Dynamic Symbol Table)
动态符号表。存放Elf32_Sym 结构体。
STRTAB:.dynstr (Dynamic String Table)
动态字符串表。Elf32_Sym中的st_name会指向这里
攻击方式:GOT Overwrite
将 GOT 中某个函数条目改为 system(目标函数) 的地址
Relocation Read-Only (RELRO)
将与重定位相关的内存区域(特别是 GOT 表)在运行时标记为只读
Partial RELRO
是gcc的默认设置
1 | ELF 内存布局调整: |
- 重新排列 ELF 段的顺序,将
.got、.ctors、.dtors、.dynamic等节放在.data/.bss之前 - 对
.got(非函数引用的 GOT 条目)设置只读 .got.plt(函数引用的 GOT 条目)仍然可写 → 仍可被攻击.dynamic段被标记为只读(防止修改DT_STRTAB等攻击)
Full RELRO
1 | 程序启动时(加载阶段): |
整个 GOT 表只读,GOT Overwrite 攻击完全失效
其实就是直接不用延迟绑定了
ret2dlresolve
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/
如何攻击动态链接过程?
思路1:修改动态字符串表 .dynstr
直接改不可行,动态字符串表 .dynstr是只读的。
但是可以造假表,劫持 reloc_offset 让解析器去读我们的假表
思路2:修改.dynamic 节
动态链接器会从 .dynamic 节中索引到各个目标节,可以劫持 .dynamic 里的指针 (基址)
我开始理解大概是让基址指向,比如说.got.plt的中段,使得加上偏移后正好是目标函数的表项
似乎更常用的是,将 DT_STRTAB 的 d_ptr 改为(加上偏移后是)目标函数字符串,或者改为自己伪造的字符串表地址
直接移动基址会更加不可控,如果后面还要执行其他函数的话
需要.dynamic 节可写。Partial RELRO就不可用
思路3:伪造 link_map
动态连接器在解析符号地址时,主要依赖于 link_map 来查询相关的地址。因此,如果我们可以成功伪造 link_map,也就可以控制程序执行目标函数。
劫持 PLT[0] 的参数 link_map 地址,并且伪造一个完整的 link_map 结构体
板子
做题看题之后发现,怎么全都要栈迁移啊
题-ret2dl
使用Gemini辅助分析和写脚本
据说是板子题
程序分析
用checksec查防护机制:

main函数非常简单,调用了alarm和read_w
1 | int main() |
read_w中有一个栈溢出漏洞,但是溢出空间很小,只有24 字节
1 | ssize_t read_w() |
解题思路
由于程序中没有system也没有puts等输出函数,并且只开了Partial RELRO,可以用ret2dlresolve。
payload1
ret2dlresolve需要的空间比较多,24字节不够用,需要先做栈迁移。借助read函数读取payload1,用leave;ret将栈迁移到bss段的bss_addr,再次调用read函数。leave_ret gadget的地址可以用ROPgadget工具找到
1 | bss_addr = 0x0804A020 + 0x800 |
payload2
转移到bss段,读入payload2,payload2包含两部分:
ROP链:
1 | # 调用 PLT_0 执行解析 |
- 跳转到PLT[0],从而调用
_dl_runtime_resolve函数,提供伪造的偏移reloc_offset - 给system函数的返回地址和参数(/bin/sh字符串的地址)
数据部分:
伪造的Elf32_Rel和Elf32_Sym表,用于欺骗_dl_runtime_resolve,让其认为自己需要找叫“system”的函数
1
2
3
4# 构造伪造的结构体
fake_reloc = p32(elf.got['alarm']) + p32(r_info)
# st_name + st_value + st_size + st_info
fake_sym = p32(st_name) + p32(0) + p32(0) + p8(0x12) + p8(0) + p16(0)存放system和/bin/sh字符串
1
2
3
4payload2 = payload2.ljust(system_str - bss_addr, b'\x00')
payload2 += b"system\x00"
payload2 = payload2.ljust(binsh_str - bss_addr, b'\x00')
payload2 += b"/bin/sh\x00"
需要注意的是Elf32_Rel和Elf32_Sym表有严格的结构,需要小心对齐
调试的一些:
进入dl_runtime_resolve之后在gdb里验证一下有没有改成功
这个地址是elf.got[‘alarm’],也就是.got.plt里的alarm(要改的函数的)
1 | x/wx 0804A010 |
然后再看看system的地址。看有没有改对
exp
1 | from pwn import * |
得到flag:
1 | ctf{ret_2_dl_resolve} |
技术点总结
栈迁移:当函数栈帧内的缓冲区溢出空间不足以布置完整的 ROP 链时,需将程序执行流引导至内存空间更充裕的区域(如 BSS 段)。通常可以利用
leave; ret这个gadget实现劫持,将esp切换至目标地址,获取更大的操作空间32位函数调用约定:在 i386 架构中,函数参数通过栈传递,而非寄存器。调用者在执行
call指令前,按参数反序将其压入栈中。函数执行时,通过相对于esp或ebp的偏移量来访问这些参数。这种机制决定了在构造 ROP 链时,只需在栈上模拟该布局:将目标函数地址作为返回地址写入,其后紧跟该函数执行完后的返回地址,再之后依次排列各项参数。动态链接与延迟绑定 :Linux 默认采用延迟绑定机制,只有在函数首次被调用时才进行地址解析。首次调用时,程序跳转至 PLT 项,压入该函数的重定位偏移量后进入
PLT[0]。PLT[0]将代表库信息的link_map地址压栈并跳转至动态链接器的_dl_runtime_resolve函数。该函数根据偏移量查找符号表,定位真实函数地址并写入 GOT 表,最后直接跳转执行。Ret2dlresolve 攻击技术:通过伪造动态链接器解析过程所需的数据结构,欺骗链接器加载任意函数。攻击者在可写内存中手工构造虚假的 Elf32_Rel(重定位项)、Elf32_Sym(符号项)以及函数名字符串,控制传给
_dl_runtime_resolve的reloc_offset参数,引导链接器读取这些伪造结构。在Full RELRO防护下不可用。