利用栈溢出伪造动态链接结构实现 ret2dlresolve 攻击
字数 7693
更新时间 2026-04-15 13:23:23
利用栈溢出伪造动态链接结构实现 ret2dlresolve 攻击 教学文档
0x0 链接过程基础
静态链接
在静态链接中,编译、链接和重定位过程均在程序执行前完成。
- 编译: 将多个源文件(如
main.c,foo.c,lib.c)分别编译成目标文件(.o)。 - 打包: 将相关的多个目标文件打包成静态库(
.a)。 - 链接: 链接器(GNU ld)将目标文件和静态库合并,执行符号解析、地址分配和重定位,生成最终的可执行文件(
a.out)。- 符号解析: 将代码中的外部符号引用与库中的符号定义进行匹配。
- 地址分配: 合并各模块的代码段(
.text)、数据段(.data)等。 - 重定位: 根据重定位表修正符号的绝对地址。
- 结果: 生成的可执行文件包含所有必需的代码和数据,无未解析符号,无需依赖外部库文件。其缺点是若同一库函数被多个程序使用,会在内存中存在多个副本,造成空间浪费。
动态链接
为解决静态链接的内存浪费问题,现代系统广泛使用动态链接。
- 部分链接: 链接器不解析动态库中函数的最终地址,只记录需要使用该符号(例如
puts@plt),并生成相应的过程链接表(PLT)和全局偏移表(GOT)。 - 生成关键结构:
- PLT(Procedure Linkage Table): 为每个外部动态库函数生成一个跳板代码(PLT entry)。首次调用函数时,会跳转至此执行解析逻辑。
- GOT(Global Offset Table): 一个地址数组,用于存储外部函数的最终地址。程序执行前,GOT中相关项为空或指向PLT中的解析代码。
- 重定位表: 记录哪些GOT项需要填充,以及对应哪个符号。
- 依赖信息: 在
.dynamic节区记录程序依赖的动态库信息。
动态链接后的执行与延迟绑定
程序启动时,GOT表中的外部函数地址并未立即解析,而是采用“延迟绑定”(Lazy Binding)机制。
- 首次调用: 当程序第一次调用外部函数(如
puts)时,流程如下:call puts@plt | v puts@plt: jmp [puts@got.plt] // 首次调用,GOT项指向PLT中的下一行指令 | v push <reloc_index> // 将重定位表项索引压栈 jmp .plt // 跳转到公共的PLT解析入口 | v .plt: push [<link_map>] // 将 link_map 结构指针压栈 jmp _dl_runtime_resolve // 跳转到动态链接器的解析函数 - 解析函数:
_dl_runtime_resolve是动态链接器(ld)中的函数,负责解析符号地址。它会调用_dl_fixup完成实际工作。 - 地址回填:
_dl_fixup解析出puts的真实地址后,将其回填到puts对应的GOT表项中。 - 后续调用: 再次调用
puts时,jmp [puts@got.plt]会直接跳转到已解析的真实函数地址,无需再次解析。
_dl_fixup 核心执行流程
_dl_fixup 是攻击的关键目标,其工作依赖于动态链接的多个核心数据结构。
- 获取关键表地址: 从
link_map结构中获取以下信息:symtab: 符号表(Elf64_Sym 结构数组)地址。strtab: 字符串表地址。jmprel: 重定位表地址。
- 定位重定位项: 根据传入的
reloc_arg(重定位索引),在重定位表(jmprel)中找到对应的重定位项(Elf64_Rel)。reloc->r_offset: 该符号地址应写入的GOT表位置。reloc->r_info: 高32位是符号在符号表中的索引(symbol_index),低32位是重定位类型。
- 定位符号: 根据
symbol_index在符号表(symtab)中找到对应的符号项(Elf64_Sym)。sym->st_name: 符号名在字符串表中的偏移。
- 解析符号名: 计算
strtab + sym->st_name得到符号名字符串(例如 "puts")。 - 查找符号地址: 调用
_dl_lookup_symbol_x,根据符号名在已加载的共享库中查找该函数的真实地址。 - 地址回写: 将查找到的函数地址写回
reloc->r_offset指定的GOT表项中。 - 跳转执行: 控制流返回
_dl_runtime_resolve,最终跳转到解析出的函数地址执行。
0x1 攻击思路
ret2dlresolve 攻击的核心是控制 _dl_fixup 的解析过程,使其解析并执行攻击者期望的函数(如 system)。主要思路有三种:
修改 .dynamic 节区
.dynamic 节区存储了动态链接器所需的各种信息表的指针,包括 strtab 的地址。
- 攻击原理: 通过栈溢出等漏洞,修改
.dynamic节中字符串表(DT_STRTAB)的地址指针,使其指向攻击者可控的内存区域。 - 攻击效果: 攻击者可以在可控区域伪造一个字符串表,例如,将
"read"字符串替换为"system"。当_dl_fixup解析read函数时,实际查找的是"system",从而将system的地址写入read的GOT项。后续调用read即变为调用system。 - 适用条件: 仅适用于 No RELRO 保护,因为 Partial/Full RELRO 会使得
.dynamic节区变为只读。
伪造重定位项
控制 _dl_fixup 查找的重定位项、符号项和字符串内容。
- 攻击原理: 控制传递给
_dl_runtime_resolve的reloc_arg参数。_dl_fixup通过jmprel + reloc_arg计算重定位项地址。如果攻击者伪造一个reloc_arg,使其指向攻击者可控内存中的伪造重定位表,就可以控制后续的解析链:- 伪造的重定位项(
Elf64_Rel)中的r_info可指向一个伪造的符号表项。 - 伪造的符号表项(
Elf64_Sym)中的st_name可指向一个伪造的字符串表。 - 伪造的字符串表中可以放置任意函数名(如
"system")。
- 伪造的重定位项(
- 攻击效果: 最终
_dl_fixup会解析攻击者指定的函数名,并将其地址写入攻击者指定的地址(通过伪造重定位项的r_offset控制)。 - 适用条件: 适用于 No RELRO 和 Partial RELRO。不需要修改只读的
.dynamic区,但需要在可写内存中伪造一系列结构。
伪造 link_map 结构
控制整个解析过程的源头数据结构。
- 攻击原理:
_dl_fixup的所有关键信息(symtab,strtab,jmprel的地址)都从link_map结构体中获取。如果能够完全伪造一个link_map结构,并控制_dl_fixup使用它,就可以完全控制解析过程,实现“指哪打哪”。 - 攻击效果: 最灵活的攻击方式,可以指定任意的符号表、字符串表、重定位表,以及符号的写入地址。
- 技术难点: 需要更精细地控制内存布局和初始化流程,通常利用条件更苛刻。
0x2 三种保护机制
GCC编译器的 -z 链接选项决定了程序的RELRO(RELocation Read-Only)保护级别,直接影响上述攻击的可行性。
No RELRO (-z norelro -z lazy)
- GOT表: 完全可写。
- 延迟绑定: 开启(
-z lazy)。程序在函数第一次被调用时才解析其地址。 .dynamic节区: 可写。- 攻击面: 最大。修改
.dynamic和伪造重定位项两种方法均可使用。
Partial RELRO (-z relro -z lazy)
- GOT表: 部分可写。
.got节(已初始化的全局变量偏移)只读,.got.plt节(函数指针)仍可写。 - 延迟绑定: 开启。
.dynamic节区: 变为只读。这是与 No RELRO 的关键区别。- 攻击面: 修改
.dynamic的方法失效。但伪造重定位项的方法仍然可用,因为只需要在可写区域(如.bss)伪造结构,无需修改只读区。
Full RELRO (-z relro -z now)
- GOT表: 完全变为只读。
- 延迟绑定: 关闭(
-z now)。所有外部函数地址在程序启动后、main函数执行前全部解析完毕。 .dynamic节区: 只读。- 攻击面: 常规的
ret2dlresolve攻击完全失效。因为GOT不可写,无法通过_dl_fixup注入新地址;且延迟绑定关闭,_dl_runtime_resolve流程不会在程序运行时触发。
0x3 实例应用
以下通过一个具体的栈溢出漏洞程序,详细阐述在 No RELRO 和 Partial RELRO 保护下的攻击过程。
漏洞程序
#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;
}
程序开启了栈不可执行(NX),因此需要利用ROP(Return-Oriented Programming)。
可利用代码片段
分析程序,发现以下有用片段:
- 栈迁移片段 (
leave; ret): 用于控制rsp和rbp,将栈转移到攻击者控制的内存区域(如.bss段)。 read片段:read的缓冲区参数来自rbp-0x70。控制rbp后可实现任意地址写。strlen片段:rdi参数来自rbp+0x78。控制rbp后,结合任意地址写,可控制rdi指向/bin/sh字符串,为执行system("/bin/sh")准备参数。
攻击场景一:No RELRO 下的攻击(修改 .dynamic)
核心思路
- 目标: 修改
.dynamic节区中的字符串表指针(DT_STRTAB),使其指向攻击者伪造的字符串表。 - 步骤:
- 栈迁移: 利用栈溢出和
leave; ret将栈迁移到可读写的.bss段。 - 任意写: 利用
read片段,通过控制rbp,将伪造的字符串表(包含"system"字符串)和修改后的.dynamic数据写入.bss段。修改的关键是将DT_STRTAB的值改为伪造字符串表的地址。 - 触发解析: 由于无法直接控制
rdi调用_dl_runtime_resolve,需要迂回。修改strlen的GOT表项,使其指向strlen@plt的第二条指令(即push 1; jmp ...部分,其中1是strlen的重定位索引reloc_arg)。这样,当程序后续调用strlen时,实际会执行_dl_runtime_resolve流程。 - 解析伪造符号:
_dl_fixup根据被修改的.dynamic找到伪造的字符串表。当它试图解析strlen时,根据伪造的字符串表,实际查找的是"system"字符串,从而将system函数地址填入strlen的GOT项。 - 获取Shell: 在触发解析后,再次调用
strlen(此时其GOT已指向system),并且通过精心布局的栈,使得此时的rdi指向/bin/sh字符串,最终执行system("/bin/sh")。
- 栈迁移: 利用栈溢出和
利用链构造
- 第一次ROP: 栈溢出,控制
rbp指向bss+0x70,并返回到read函数,为后续向.bss段写入数据做准备。 - 写入伪造数据: 向.bss段写入后续的ROP链、伪造的字符串表(
"system"),并控制下一次的rbp指向.dynamic中DT_STRTAB项附近,准备修改它。 - 修改
.dynamic: 利用read片段,向.dynamic区写入数据,在保持其他条目不变的情况下,将DT_STRTAB的值修改为伪造字符串表的地址。 - 第二次ROP: 通过栈迁移,将执行流引导到bss段上预先布置的ROP链。该ROP链会:
a. 修改strlen的GOT表项,指向strlen@plt+4(即push 1; jmp ...的位置)。
b. 再次栈迁移,为触发解析准备栈空间。 - 触发解析: 执行另一段ROP链,其最终会调用
strlen。由于GOT被修改,实际执行_dl_runtime_resolve。解析过程中,_dl_fixup使用被篡改的字符串表,将system地址填入strlen的GOT。 - 执行 system: 在触发解析的ROP链中,已布置好让
rdi指向/bin/sh。当流程从_dl_runtime_resolve返回后,实际执行的是system("/bin/sh")。
攻击场景二:Partial RELRO 下的攻击(伪造重定位项)
核心思路
由于 .dynamic 变为只读,无法修改 DT_STRTAB。转而利用伪造重定位项的方法。
- 目标: 伪造一整套动态链接数据结构(重定位表、符号表、字符串表),并控制
reloc_arg指向这个伪造的结构链。 - 步骤:
- 伪造结构链: 在.bss段伪造:
- 字符串表: 包含字符串
"system\x00"。 - 符号表项: 一个
Elf64_Sym结构,其st_name指向伪造字符串表中的"system"字符串。 - 重定位表项: 一个
Elf64_Rel结构,其r_info的高位指向伪造的符号表项在符号表中的索引,其r_offset设置为strlen的GOT地址。
- 字符串表: 包含字符串
- 控制
reloc_arg: 计算伪造的重定位表项与原始重定位表(jmprel)的偏移,得到伪造的reloc_arg。 - 触发解析: 与No RELRO场景类似,需要修改
strlen的GOT表项。但此时不能简单地指向plt的push指令,因为call指令会压入返回地址,破坏栈布局。通常修改为一个pop指令gadget(如pop rbp; ret),然后在其后布置reloc_arg和_dl_runtime_resolve的入口地址(.plt开头的公共跳转代码),从而正确调用解析函数。 - 解析与执行:
_dl_fixup使用伪造的reloc_arg找到伪造的重定位项,进而找到伪造的符号项和字符串"system",最终将system地址写入strlen的GOT。后续执行流程与No RELRO场景类似。
- 伪造结构链: 在.bss段伪造:
关键计算
假设在bss段(地址为 fake_addr)伪造了结构链,布局如下:
fake_addr: [ROP chain part1]
[ROP chain part2]
"system\x00\x00" <-- 伪造的字符串表 (fake_strtab)
Elf64_Sym {...} <-- 伪造的符号表项 (fake_sym)
"/bin/sh\x00" <-- 为后续调用准备的参数字符串
Elf64_Rel {r_offset, r_info, ...} <-- 伪造的重定位项 (fake_rel)
sym_index计算: 符号在符号表中的索引。symtab是原始符号表地址。fake_sym相对于symtab的偏移为offset_sym = (fake_addr + offset_to_fake_sym) - symtab。符号表项大小为0x18字节。因此sym_index = offset_sym / 0x18。必须确保offset_sym % 0x18 == 0,否则索引计算会错位。这通常通过调整fake_sym的存放地址(填充字节)来实现对齐。reloc_arg计算: 重定位表项索引。jmprel是原始重定位表地址。fake_rel相对于jmprel的偏移为offset_rel = (fake_addr + offset_to_fake_rel) - jmprel。重定位表项大小为0x18字节。因此reloc_arg = offset_rel / 0x18。同样必须确保offset_rel % 0x18 == 0。
利用链构造
- 初始栈迁移与写操作: 与No RELRO类似,将栈迁移到bss,并利用
read写入伪造的数据结构(字符串表、符号表项、重定位表项)以及第一段ROP链。 - GOT劫持与二次迁移: 第一段ROP链负责修改
strlen的GOT表项,将其改为一个pop rbp; ret的gadget地址。然后再次进行栈迁移,将执行流转移到bss段上另一处布置的第二段ROP链。 - 触发伪造的解析: 第二段ROP链会:
a. 通过strlen片段设置rdi指向/bin/sh。
b. 然后ret到pop rbp; retgadget(即被修改的strlen@got),这个gadget会从栈上弹出一个无用值到rbp。
c. 紧接着ret到_dl_runtime_resolve的入口(例如.plt段的开头)。
d. 此时栈顶恰好是之前精心计算并布置的reloc_arg。流程进入_dl_runtime_resolve和_dl_fixup。 - 解析与执行:
_dl_fixup使用伪造的reloc_arg索引到伪造的重定位项,进而解析出system地址,并写入strlen的GOT表项(由伪造重定位项的r_offset指定)。解析返回后,程序执行system("/bin/sh")。
总结
ret2dlresolve 攻击是一种在无法直接获取目标函数地址(如无libc基址)时,通过利用动态链接器的延迟绑定机制来强制解析并执行任意函数的高级ROP技术。
- 核心: 控制
_dl_fixup函数的解析过程。 - 关键数据结构:
.dynamic节、link_map、重定位表(.rel.plt)、符号表(.dynsym)、字符串表(.dynstr)。 - 攻击方法:
- No RELRO: 可修改
.dynamic或伪造重定位项。 - Partial RELRO: 只能伪造重定位项。
- Full RELRO: 常规攻击无效。
- No RELRO: 可修改
- 利用条件:
- 存在可劫持控制流的漏洞(如栈溢出)。
- 能进行任意地址写(或能在已知地址布置数据)。
- 能构造ROP链控制寄存器和函数调用。
- 程序使用延迟绑定(非
-z now)。
- 防御: 开启 Full RELRO 保护是防御此类攻击最有效的手段,它使GOT只读并禁用延迟绑定,从根本上消除了攻击面。
相似文章
相似文章