利用栈溢出伪造动态链接结构实现 ret2dlresolve 攻击
字数 7693
更新时间 2026-04-15 13:23:23

利用栈溢出伪造动态链接结构实现 ret2dlresolve 攻击 教学文档

0x0 链接过程基础

静态链接

在静态链接中,编译、链接和重定位过程均在程序执行前完成。

  1. 编译: 将多个源文件(如 main.c, foo.c, lib.c)分别编译成目标文件(.o)。
  2. 打包: 将相关的多个目标文件打包成静态库(.a)。
  3. 链接: 链接器(GNU ld)将目标文件和静态库合并,执行符号解析、地址分配和重定位,生成最终的可执行文件(a.out)。
    • 符号解析: 将代码中的外部符号引用与库中的符号定义进行匹配。
    • 地址分配: 合并各模块的代码段(.text)、数据段(.data)等。
    • 重定位: 根据重定位表修正符号的绝对地址。
  4. 结果: 生成的可执行文件包含所有必需的代码和数据,无未解析符号,无需依赖外部库文件。其缺点是若同一库函数被多个程序使用,会在内存中存在多个副本,造成空间浪费。

动态链接

为解决静态链接的内存浪费问题,现代系统广泛使用动态链接。

  1. 部分链接: 链接器不解析动态库中函数的最终地址,只记录需要使用该符号(例如 puts@plt),并生成相应的过程链接表(PLT)和全局偏移表(GOT)。
  2. 生成关键结构
    • PLT(Procedure Linkage Table): 为每个外部动态库函数生成一个跳板代码(PLT entry)。首次调用函数时,会跳转至此执行解析逻辑。
    • GOT(Global Offset Table): 一个地址数组,用于存储外部函数的最终地址。程序执行前,GOT中相关项为空或指向PLT中的解析代码。
    • 重定位表: 记录哪些GOT项需要填充,以及对应哪个符号。
  3. 依赖信息: 在 .dynamic 节区记录程序依赖的动态库信息。

动态链接后的执行与延迟绑定

程序启动时,GOT表中的外部函数地址并未立即解析,而是采用“延迟绑定”(Lazy Binding)机制。

  1. 首次调用: 当程序第一次调用外部函数(如 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   // 跳转到动态链接器的解析函数
    
  2. 解析函数_dl_runtime_resolve 是动态链接器(ld)中的函数,负责解析符号地址。它会调用 _dl_fixup 完成实际工作。
  3. 地址回填_dl_fixup 解析出 puts 的真实地址后,将其回填到 puts 对应的GOT表项中。
  4. 后续调用: 再次调用 puts 时,jmp [puts@got.plt] 会直接跳转到已解析的真实函数地址,无需再次解析。

_dl_fixup 核心执行流程

_dl_fixup 是攻击的关键目标,其工作依赖于动态链接的多个核心数据结构。

  1. 获取关键表地址: 从 link_map 结构中获取以下信息:
    • symtab: 符号表(Elf64_Sym 结构数组)地址。
    • strtab: 字符串表地址。
    • jmprel: 重定位表地址。
  2. 定位重定位项: 根据传入的 reloc_arg(重定位索引),在重定位表(jmprel)中找到对应的重定位项(Elf64_Rel)。
    • reloc->r_offset: 该符号地址应写入的GOT表位置。
    • reloc->r_info: 高32位是符号在符号表中的索引(symbol_index),低32位是重定位类型。
  3. 定位符号: 根据 symbol_index 在符号表(symtab)中找到对应的符号项(Elf64_Sym)。
    • sym->st_name: 符号名在字符串表中的偏移。
  4. 解析符号名: 计算 strtab + sym->st_name 得到符号名字符串(例如 "puts")。
  5. 查找符号地址: 调用 _dl_lookup_symbol_x,根据符号名在已加载的共享库中查找该函数的真实地址。
  6. 地址回写: 将查找到的函数地址写回 reloc->r_offset 指定的GOT表项中。
  7. 跳转执行: 控制流返回 _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_resolvereloc_arg 参数。_dl_fixup 通过 jmprel + reloc_arg 计算重定位项地址。如果攻击者伪造一个 reloc_arg,使其指向攻击者可控内存中的伪造重定位表,就可以控制后续的解析链:
    • 伪造的重定位项(Elf64_Rel)中的 r_info 可指向一个伪造的符号表项。
    • 伪造的符号表项(Elf64_Sym)中的 st_name 可指向一个伪造的字符串表。
    • 伪造的字符串表中可以放置任意函数名(如 "system")。
  • 攻击效果: 最终 _dl_fixup 会解析攻击者指定的函数名,并将其地址写入攻击者指定的地址(通过伪造重定位项的 r_offset 控制)。
  • 适用条件: 适用于 No RELROPartial 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)。

可利用代码片段

分析程序,发现以下有用片段:

  1. 栈迁移片段 (leave; ret): 用于控制 rsprbp,将栈转移到攻击者控制的内存区域(如.bss段)。
  2. read 片段read 的缓冲区参数来自 rbp-0x70。控制 rbp 后可实现任意地址写
  3. strlen 片段rdi 参数来自 rbp+0x78。控制 rbp 后,结合任意地址写,可控制 rdi 指向 /bin/sh 字符串,为执行 system("/bin/sh") 准备参数。

攻击场景一:No RELRO 下的攻击(修改 .dynamic

核心思路

  1. 目标: 修改 .dynamic 节区中的字符串表指针(DT_STRTAB),使其指向攻击者伪造的字符串表。
  2. 步骤
    • 栈迁移: 利用栈溢出和 leave; ret 将栈迁移到可读写的.bss段。
    • 任意写: 利用 read 片段,通过控制 rbp,将伪造的字符串表(包含 "system" 字符串)和修改后的 .dynamic 数据写入.bss段。修改的关键是将 DT_STRTAB 的值改为伪造字符串表的地址。
    • 触发解析: 由于无法直接控制 rdi 调用 _dl_runtime_resolve,需要迂回。修改 strlen 的GOT表项,使其指向 strlen@plt 的第二条指令(即 push 1; jmp ... 部分,其中 1strlen 的重定位索引 reloc_arg)。这样,当程序后续调用 strlen 时,实际会执行 _dl_runtime_resolve 流程。
    • 解析伪造符号_dl_fixup 根据被修改的 .dynamic 找到伪造的字符串表。当它试图解析 strlen 时,根据伪造的字符串表,实际查找的是 "system" 字符串,从而将 system 函数地址填入 strlen 的GOT项。
    • 获取Shell: 在触发解析后,再次调用 strlen(此时其GOT已指向 system),并且通过精心布局的栈,使得此时的 rdi 指向 /bin/sh 字符串,最终执行 system("/bin/sh")

利用链构造

  1. 第一次ROP: 栈溢出,控制 rbp 指向 bss+0x70,并返回到 read 函数,为后续向.bss段写入数据做准备。
  2. 写入伪造数据: 向.bss段写入后续的ROP链、伪造的字符串表("system"),并控制下一次的 rbp 指向 .dynamicDT_STRTAB 项附近,准备修改它。
  3. 修改 .dynamic: 利用 read 片段,向 .dynamic 区写入数据,在保持其他条目不变的情况下,将 DT_STRTAB 的值修改为伪造字符串表的地址。
  4. 第二次ROP: 通过栈迁移,将执行流引导到bss段上预先布置的ROP链。该ROP链会:
    a. 修改 strlen 的GOT表项,指向 strlen@plt+4(即 push 1; jmp ... 的位置)。
    b. 再次栈迁移,为触发解析准备栈空间。
  5. 触发解析: 执行另一段ROP链,其最终会调用 strlen。由于GOT被修改,实际执行 _dl_runtime_resolve。解析过程中,_dl_fixup 使用被篡改的字符串表,将 system 地址填入 strlen 的GOT。
  6. 执行 system: 在触发解析的ROP链中,已布置好让 rdi 指向 /bin/sh。当流程从 _dl_runtime_resolve 返回后,实际执行的是 system("/bin/sh")

攻击场景二:Partial RELRO 下的攻击(伪造重定位项)

核心思路

由于 .dynamic 变为只读,无法修改 DT_STRTAB。转而利用伪造重定位项的方法。

  1. 目标: 伪造一整套动态链接数据结构(重定位表、符号表、字符串表),并控制 reloc_arg 指向这个伪造的结构链。
  2. 步骤
    • 伪造结构链: 在.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表项。但此时不能简单地指向 pltpush 指令,因为 call 指令会压入返回地址,破坏栈布局。通常修改为一个 pop 指令gadget(如 pop rbp; ret),然后在其后布置 reloc_arg_dl_runtime_resolve 的入口地址(.plt 开头的公共跳转代码),从而正确调用解析函数。
    • 解析与执行_dl_fixup 使用伪造的 reloc_arg 找到伪造的重定位项,进而找到伪造的符号项和字符串 "system",最终将 system 地址写入 strlen 的GOT。后续执行流程与No RELRO场景类似。

关键计算

假设在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

利用链构造

  1. 初始栈迁移与写操作: 与No RELRO类似,将栈迁移到bss,并利用 read 写入伪造的数据结构(字符串表、符号表项、重定位表项)以及第一段ROP链。
  2. GOT劫持与二次迁移: 第一段ROP链负责修改 strlen 的GOT表项,将其改为一个 pop rbp; ret 的gadget地址。然后再次进行栈迁移,将执行流转移到bss段上另一处布置的第二段ROP链。
  3. 触发伪造的解析: 第二段ROP链会:
    a. 通过 strlen 片段设置 rdi 指向 /bin/sh
    b. 然后 retpop rbp; ret gadget(即被修改的 strlen@got),这个gadget会从栈上弹出一个无用值到 rbp
    c. 紧接着 ret_dl_runtime_resolve 的入口(例如 .plt 段的开头)。
    d. 此时栈顶恰好是之前精心计算并布置的 reloc_arg。流程进入 _dl_runtime_resolve_dl_fixup
  4. 解析与执行_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: 常规攻击无效。
  • 利用条件
    1. 存在可劫持控制流的漏洞(如栈溢出)。
    2. 能进行任意地址写(或能在已知地址布置数据)。
    3. 能构造ROP链控制寄存器和函数调用。
    4. 程序使用延迟绑定(非 -z now)。
  • 防御: 开启 Full RELRO 保护是防御此类攻击最有效的手段,它使GOT只读并禁用延迟绑定,从根本上消除了攻击面。
相似文章
相似文章
 全屏