重要结构
link_map
| |
- l_addr (uint64_t) : ELF 文件中定义的地址与内存中实际地址之间的差值
这就是该模块的【基地址 (Base Address)】 对于开启了 PIE 的程序或 .so 库,这里存的是随机化后的基址 对于未开启 PIE (No-PIE) 的主程序,这里通常是 0
- l_name : 找到该对象的绝对文件名
这是一个指针,指向存储库文件路径的字符串(例如 “/lib/x86_64-linux-gnu/libc.so.6”) 对于主程序,这里通常是空字符串
- l_ld (Elf64_Dyn) : 该共享对象的动态段(.dynamic section)的地址
指向内存中 .dynamic 段的指针。这个段里存着 DT_STRTAB, DT_SYMTAB 等标签 _dl_fixup 实际上并不直接用这个 l_ld,而是用后面定义的 l_info 数组(它是由 l_ld 解析生成的)
- l_next, l_prev : 已加载对象的链表
双向链表指针 l_next 指向下一个加载的库,l_prev 指向上一个 攻击时通常不需要伪造这两个指针,除非你的攻击链涉及遍历这个链表
- l_real : 这是一个通常指向它自己的指针
只有当动态链接器(ld.so)在多个命名空间(namespace)中被使用时,这个指针才会指向不同的副本
- l_ns (8 bytes) : 该 link map 所属的命名空间编号
Linux 支持多个链接器命名空间(比如 dlmopen 可以加载一个隔离的库) LM_ID_BASE (通常是 0) 表示主程序所在的默认命名空间
- l_libname : 指向一个链表,存储了该共享对象的名称(可能有别名,比如 libc.so.6 和 libc-2.31.so)
- l_info (Elf64_Dyn) : 指向动态段(dynamic section)条目的指针数组。
[0, DT_NUM) 范围内的元素:使用**处理器无关的标签(Tag)**直接作为下标索引。 [DT_NUM, …) 范围内的元素:使用 标签值减去 DT_LOPROC 作为下标索引。 […] 范围内的元素:使用 DT_VERSIONTAGIDX(tagvalue) 计算出的值作为下标索引。 …(后面是关于 Extra, Val, Addr 等特殊标签的索引计算方式)。
Elf64_Dyn
ELF 64位 动态段条目
| |
- d_tag (int64_t) : 这是“类型标签”,用来告诉链接器后面那个 d_un 存的是什么东西
DT_NULL (0): 标记动态段的结束 DT_STRTAB (5): 字符串表(String Table)的地址 DT_SYMTAB (6): 符号表(Symbol Table)的地址 DT_JMPREL (23): 重定位表(Relocation Table,即 .rela.plt)的地址
- d_un.d_val (uint64_t) : 当标签表示大小或数量时使用(例如 DT_SYMENT 表示符号表每项的大小)
- d_un.d_ptr (uint64_t) : 当标签表示地址时使用
Elf64_Sym
ELF 64位 符号表条目
| |
- st_name (uint32_t) : 指向字符串表(String Table, DT_STRTAB)的相对偏移
函数名地址 = DT_STRTAB 基址 + st_name
- st_info : 这 1 个字节包含了两个信息
高 4 位 (Bind): 绑定属性(Global, Local, Weak) 低 4 位 (Type): 符号类型(Object, Func, None) 常见值:0x12:即 STB_GLOBAL (1) « 4 | STT_FUNC (2) ,表示这是一个全局函数
- st_other : 符号的可见性
STV_DEFAULT (0): 默认可见性(通常是公开的,可被外部链接) STV_INTERNAL (1): 处理器特定的隐藏类型(很少用) STV_HIDDEN (2): 符号在模块内可见,外部不可见(即使是全局符号) STV_PROTECTED (3): 符号可见,但不能被抢占(Preempted)
- st_shndx (uint16_t) : 该符号定义在哪个节(Section)里,它是一个索引值,指向 Section Header Table
几个特殊的保留索引值: SHN_UNDEF (0): 未定义符号,表示该符号在本模块中被引用,但定义在其他模块(如 libc.so)中 SHN_ABS (0xfff1): 绝对符号,该符号的值是绝对地址,不随重定位改变 SHN_COMMON (0xfff2): 通用块符号(通常用于未初始化的全局变量)
- st_value (uint64_t) : 符号的值(通常是地址)
在可重定位文件 (.o) 中:它是相对于所在节(Section)的偏移量 在可执行文件或共享库 (.so) 中: 如果符号已定义(st_shndx != 0):它是符号的虚拟地址(Virtual Address) 如果符号未定义(st_shndx == 0):通常为 0 ,但如果是对齐的 Common 符号,它表示对齐约束
- st_size (uint64_t) : 函数或变量的大小
Elf64_Rela
ELF 64位 重定位条目
| |
- r_offset : 修正地址,即动态链接器解析出函数的真实地址后,应该把这个地址写到哪里去
指向 GOT 表(Global Offset Table)中的某个条目
- r_info : 这是一个复合字段,高 32 位和低 32 位分别代表不同含义
r_info = (Symbol Index « 32) + Relocation Type 高 32 位:Symbol Index (符号表索引) 告诉链接器:“去符号表(Symbol Table)的第几个条目找这个函数的信息” 低 32 位:Relocation Type (重定位类型) 告诉链接器如何进行重定位 7 即 R_X86_64_JUMP_SLOT
- r_addend : 加数,用于计算最终值的常数偏移
最终值 = Symbol Value + Addend
_dl_fixup 源码分析
_dl_fixup 源码
| |
前置声明
| |
翻译: 这个函数是通过一个特殊的跳板(trampoline)从 PLT(过程链接表)中调用的,时机是每个 PLT 条目第一次被调用的时候。 我们必须按照给定共享对象(shared object)的 PLT 中指定的要求,执行重定位操作(relocation),并将解析出来的函数地址返回给那个跳板。 随后,跳板会重新向该地址发起原始的函数调用。 未来的调用将直接从 PLT 跳转到该函数(而不再经过这里)。
ElfW(Word) 为 uint32_t
PLTREL 为 Elf64_Rela
| |
整理一下:
| |
版本校验
| |
首先要求 reloc->r_info 的低 4 字节为 7 ,不然报错
若 sym->st_other 为 0 ,则进入 if 内部
然后是一层检测,决定是否要进行版本校验(Version Check),检查 l_info[50] 是否为 NULL ,正常情况下不为 NULL ,执行后面 if 内部代码
但是进入 if 内部后在 64 位下会报错,因为在 vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff 的过程中,由于我们一般伪造的 symtab 位于 bss 段,导致在 64 位下 reloc->r_info 较大,发生段错误
然后是锁相关操作
符号查找
| |
进入 _dl_lookup_symbol_x
出来后是锁操作
收尾处理
| |
if 分支:
当前 result 变量包含了定义该符号的对象的基加载地址(或者 link_map 指针),现在加上符号的偏移量
即 value = result->l_addr + sym->st_value
else 分支:
我们已经找到了符号,模块(因此也包括它的加载地址)也是已知的
即 value = l->l_addr + sym->st_value
接下来处理 GNU Indirect Function (IFUNC)
最后把 value 写入相应的 GOT 表条目中
要求 rel_addr = (void *)(l->l_addr + reloc->r_offset) 可写
_dl_runtime_resolve 部分分析
| |
LOCAL_STORAGE_AREA(%BASE) 为进入 _dl_runtime_resolve 时 rsp 位置上的值,将传入 rdi ,作为 link_map 指针
(LOCAL_STORAGE_AREA + 8)(%BASE) 为进入 _dl_runtime_resolve 时 rsp + 8 位置上的值,将传入 rsi ,作为 reloc_arg (uint32_t)
64 位下利用
为了避免段错误,我们引导函数执行至以下片段
| |
这要求 sym->st_other 不为 0
然后通过 value = l->l_addr + sym->st_value 计算出我们希望写入 got 表中的那个地址
我们需要:
- 伪造
link_map->l_addr为 libc 中已解析函数与想要执行的目标函数的偏移值,如addr_system - addr_xxx - 伪造
sym->st_value为已经解析过的某个函数的 got 表的位置,这需要布置 sym 的位置 - 也就是相当于
value = l_addr + st_value = addr_system - addr_xxx + real_xxx = real_system
又
| |
即
| |
再放一遍
| |
综上,所有需要伪造的数据为:
| |
根据偏移对 link_map 进行压缩布局:
| |
stack 布局:
注意 _dl_runtime_resolve 有点像 srop 会还原寄存器
| |
