Ret2dlresolve 技术详解
1. ELF 文件格式基础
ELF (Executable and Linkable Format) 是 Linux 和类 Unix 系统上常见的可执行文件和可链接文件格式。ELF 文件主要包含以下几种类型:
- 可执行文件 (ET_EXEC):可直接执行的程序
- 共享目标文件 (ET_DYN):动态链接的共享库 (.so)
- 可重定位文件 (ET_REL):编译器生成的目标文件 (.o)
- 核心转储文件 (ET_CORE):程序崩溃时生成的核心转储文件
ELF 文件结构定义在 /usr/include/elf.h 中,有 32 位和 64 位两种版本。
1.1 ELF 文件结构
ELF 文件主要由以下几部分组成:
- 文件头 (Elf32_Ehdr/Elf64_Ehdr):描述文件的基本信息
- 程序头表 (Program Header Table):描述段(Segment)信息
- 节表 (Section Header Table):描述节(Section)信息
- 各种节(Section):包含实际的代码、数据等
1.2 段(Segment)与节(Section)的区别
- 段(Segment):逻辑组织单位,定义内存中的连续区域,包含多个属性相同的节
- 节(Section):更细粒度的组织单位,包含特定类型的数据或代码
装载程序时,属性相同的节会被合并到一个段中加载到内存。
2. 动态链接相关结构
2.1 .interp 段
包含动态链接器的路径字符串,如 /lib64/ld-linux-x86-64.so.2。
2.2 .dynamic 段
动态链接最重要的结构,保存动态链接器需要的基本信息,由 Elf*_Dyn 结构体数组组成。
重要成员:
DT_SYMTAB:动态符号表(.dynsym)地址DT_STRTAB:动态字符串表(.dynstr)地址DT_JMPREL:PLT 重定位表(.rel.plt)地址DT_RELENT:单个重定位项大小DT_SYMENT:单个符号表项大小
2.3 动态符号表(.dynsym)
保存动态链接相关的符号信息,由 Elf*_Sym 结构体数组组成。
Elf*_Sym 结构体成员:
st_name:符号名称在字符串表中的偏移st_value:符号地址st_size:符号大小st_info:符号类型和绑定信息st_other:通常为0st_shndx:符号所在节的索引
2.4 动态链接重定位表(.rel.plt)
保存函数引用的重定位信息,由 Elf*_Rel 结构体数组组成。
Elf*_Rel 结构体成员:
r_offset:需要重定位的位置r_info:低8位表示重定位类型,高24/32位表示符号索引
2.5 PLT 和 GOT 表
- PLT (Procedure Linkage Table):包含跳转到GOT的代码
- GOT (Global Offset Table):分为
.got和.got.plt.got:保存全局变量引用地址.got.plt:保存函数引用地址,前三项有特殊意义:.dynamic段偏移/地址link_map结构体指针_dl_runtime_resolve地址
3. 延迟绑定机制
动态链接采用延迟绑定(Lazy Binding)技术,函数在第一次调用时才进行解析。
3.1 第一次调用函数流程
- 调用 PLT 表中的跳转指令
- 跳转到 GOT 表中保存的下一条指令地址
- 压入重定位表索引并跳转到 PLT0
- 调用
_dl_runtime_resolve进行符号解析 - 解析完成后更新 GOT 表并调用函数
3.2 第二次调用函数流程
- 调用 PLT 表中的跳转指令
- 直接跳转到 GOT 表中保存的函数地址
4. ret2dlresolve 技术原理
ret2dlresolve 是一种利用动态链接解析过程进行攻击的技术,通过伪造动态链接相关的数据结构来控制程序执行流程。
4.1 _dl_runtime_resolve 函数流程
当 ELFW(ST_VISIBILITY)(sym->st_other) 为0时的执行流程:
- 用
link_map访问.dynamic,获取.dynstr、.dynsym、.rel.plt指针 .rel.plt+ 第二个参数得到Elf*_Rel指针relrel->r_info >> 8作为.dynsym下标,得到Elf*_Sym指针sym.dynstr+sym->st_name得到符号名字符串指针- 在动态链接库中查找函数地址并更新 GOT 表
- 调用该函数
4.2 攻击思路
根据 RELRO 保护级别的不同,攻击方法也有所不同:
4.2.1 NO RELRO 情况
.dynamic 段可写,可以改写 .dynamic 的 DT_STRTAB 指针,使其指向伪造的字符串表。
攻击步骤:
- 伪造
.dynstr字符串表 - 修改
.dynamic中的DT_STRTAB指针指向伪造的字符串表 - 调用
_dl_runtime_resolve解析伪造的函数名
4.2.2 Partial RELRO 情况
.dynamic 段不可写,需要操纵 _dl_runtime_resolve 的第二个参数,使其访问到可控内存并伪造相关结构。
攻击步骤:
- 伪造
Elf*_Rel结构 - 伪造
Elf*_Sym结构 - 伪造
.dynstr字符串 - 控制第二个参数指向伪造的
Elf*_Rel结构 - 调用
_dl_runtime_resolve
4.3 32位与64位的区别
-
参数传递:
- 32位:通过栈传递参数
- 64位:通过寄存器传递参数
-
索引计算:
- 32位:
reloc_arg是偏移量 - 64位:
reloc_arg是索引
- 32位:
-
结构体大小:
- 32位:
Elf32_Rel8字节,Elf32_Sym16字节 - 64位:
Elf64_Rel16字节,Elf64_Sym24字节
- 32位:
5. 实际利用示例
5.1 32位 Partial RELRO 利用
# 伪造 .dynstr
fake_dynstr = '\x00libc.so.6\x00system\x00'
# 伪造 Elf32_Sym
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
# 伪造 Elf32_Rel
fake_rel = p32(r_offset) + p32(r_info)
# 计算 reloc_arg
reloc_arg = (fake_rel_addr - rel_plt_addr) // 8
# 调用 _dl_runtime_resolve
rop.raw(plt0)
rop.raw(reloc_arg)
rop.raw('aaaa') # 返回地址
rop.raw(binsh_addr) # system参数
5.2 64位 Partial RELRO 利用
# 伪造 link_map
fake_link_map = p64(0) # l_addr
fake_link_map += p64(0) # l_name
fake_link_map += p64(0) # l_ld
fake_link_map += p64(dt_strtab) # l_info[5]
fake_link_map += p64(dt_symtab) # l_info[6]
fake_link_map += p64(dt_jmprel) # l_info[23]
# 伪造 Elf64_Rel
fake_rel = p64(r_offset) + p64(r_info)
# 伪造 Elf64_Sym
fake_sym = p32(st_name) + p8(0x12) + p8(0) + p16(0) + p64(st_value)
# 调用 _dl_runtime_resolve
rop.raw(plt0)
rop.raw(fake_link_map_addr)
rop.raw(reloc_arg)
rop.raw(binsh_addr) # system参数
6. 防御措施
-
RELRO 保护:
NO RELRO:不保护Partial RELRO:部分保护,.dynamic只读Full RELRO:完全保护,所有重定位在加载时完成
-
其他保护:
- ASLR:地址空间布局随机化
- Stack Canary:栈保护
- NX:数据执行保护
7. 总结
ret2dlresolve 是一种强大的攻击技术,可以在不知道 libc 版本的情况下完成利用。理解 ELF 文件格式、动态链接机制和延迟绑定原理是掌握该技术的关键。根据不同的保护级别,需要采用不同的利用方法,32位和64位环境下的实现也有所不同。