[TOC]
网上冲浪得到一些概要与总结,ret2dlresolve是一种高级ROP手法,难度较大,具备完整流传与教程的支撑材料不多,且大多含糊不清,环境不对应。
在CTFshow - 入门 PWN-writeup中的pwn_082/083/084/085认识到新的节/结构体,比如.dynamic、.rela.plt等。
结构体
.dynamic
该节存放了许多Elf64_Dyn结构体,在IDA中位于got表的上面,保存了动态链接器所需要的基本信息,比如存放了ELF文件其他节的标识和起始地址。结构体定义如下所示。

|
|
d_tag定义如下:
|
|
.rela.plt
该节存放了许多Elf64_Rela结构体,是对函数引用的修正,修正的位置在got.plt表,每个libc库函数都有自己的Elf64_Rela结构体。在程序入口附近,位于LOAD段。
|
|
r_info是一个复合值,其高32位表示该重定位项在动态链接符号表.dynsym中对应项的下标,低32位表示该重定位项的`重定向类型。
重定位类型(Relocation Types)
|
|
**32位ELF一般用来函数重定位的重定位类型就是R_386_JMP_SLOT类型,64位ELF**函数重定位的重定位类型就是R_X86_64_JUMP_SLOT类型,源码对其的注释是Create PLT entry。这种类型的函数重定位都会在ELF中创建一个PLT入口。
这个入口称作plt0。
.rela.dyn
该节,用于普通动态重定位,程序加载时处理,常见定位项:
|
|
其中结构体也为Elf64_Rela。
.dynsym
该节存放了许多Elf64_Sym结构体,同样地,每个libc函数都有自己的Elf64_Sym。在程序入口附近,位于LOAD段。
|
|
st_info大小为 1 Btyes,高4位表示符号的绑定特征,低4位表示符号类型。
绑定特征(高四位)
|
|
符号类型(低四位)
|
|
变量、函数分别取1, 2 即可。
那么稍微总结一下得到st_info取值的一般规律如下:
|
|
节头表:
| 节头表索引 | 节名 |
|---|---|
| 0 | SHN_UNDEF / 空节 |
| 1 | .text |
| 2 | .data |
| 3 | .bss |
| 4 | .rodata |
.dynstr
即字符串表(STRTAB),该节存放的是libc函数的符号名,就是一个一个的字符串,诸如此类'exit'、'read'。在程序入口附近,位于LOAD段。
link_map
link_map 是 glibc 动态链接器 ld-linux 用来描述“一个已加载 ELF 对象”的核心结构体。一个进程里每加载一个 ELF 对象,就会有一个对应的 struct link_map:
|
|
它们通过链表串起来:main link_map -> libc link_map -> ld-linux link_map -> ...
该结构体在 ld 中。
|
|
无pie保护的示例:
|
|
开pie后随机化保护的实例:
|
|
宏
ELFW( 是 glibc / ELF 代码里常见的宏,用来根据当前平台自动选择 32 位 ELF 类型/宏 或 64 位 ELF 类型/宏。
找 .dynamic 中的 tab 的地址
|
|
类型定义,用于定义需要绑定的函数所在的link_map
|
|
处理偏移参数(参数从0开始,.rela.plt、.rela.dyn的参数分开计算)
|
|
函数
SYMBOL_ADDRESS(map, ref, map_set)
计算符号 ref 的运行时地址。
- 如果 ref == NULL,返回 0;
- 如果 ref 是 SHN_ABS 绝对符号(st_shndx == SHN_ABS),直接返回
st_value; - 否则使用 map->l_addr 作为模块基址,加上 ref->st_value(
st_value + l_addr); - map_set 为 true 时,表示调用者保证 map 有效,不额外检查 map 是否为 NULL。
elf_machine_fixup_plt
|
|
_dl_fixup()
|
|
执行流
elf中的link_map与_dl_runtime_resolve_xsavec()指针在.bss段中.got段开头
下图省略所有被调用函数,调试模块,部分结构体,绿色为常规执行(判断返回为 ture),红色为可控执行流,蓝色为相加:

利用思路
该方法最大的作用在无法泄露libc时可以调用libc中的函数,其本质是向一个地址,写入一个值,而这两个部分都可以在一定程度上被控制:
|
|
该利用并不只限制于覆盖got表段,可以覆盖任意位置可被覆盖(可写)的能被调用的函数指针,然后调用 libc 任意位置的代码片段(内核未开启ibt保护时)。
触发原理
汇编层面,延迟绑定第一次执行函数执行流如下:
|
|
所以,有如下两种触发方式:
- 用任意写覆盖部分指针、偏移,或进行伪造,利用未加载的函数触发;
- 通过栈溢出,劫持执行流,在栈上布置两个指针(可以为真实指针或伪造的结构体指针)、返回地址,模拟执行。
接下来是应对RELRO保护情况做具体分析与漏洞利用思路。
NO RELRO
该模式下,上如所有表段中的结构体可写
有任意写可以根据以上执行流,覆盖任意偏移进行利用;
只有溢出,需泄露pie或无pie,泄露 ld 地址,伪造link_map。
这种情况下思路比较广泛,只需知道调用过程、各结构体位置,就可以根据具体情况利用。
例题:CTFshow - 入门 PWN-writeup-pwn_082
Partial RELRO
该模式下,所有表段不可被写,但是.bss段的link_map、_dl_runtime_resolve_xsavec()的指针可以被写
有任意写,可以覆盖.bss段指针,伪造link_map;
只有溢出,需泄露pie或无pie,需泄露 ld 地址,伪造link_map。
伪造link_map:
根据上面的执行流,可以有如下的利用思路:

通过将sym结构体伪造在got表附近,使st_value与st_other命中合适位置:
|
|
-
使
st_other非 0 ,将执行流导向非常规流程(红色) -
使
vt_value为已经被写过的 libc 函数入口地址,此时link_map为通过参数传入的link_map,通过l_addr调整偏移,到目标代码段地址 -
由于调整了传入的
link_map的l_addr,所以在写rela结构体时,需将r_offset设置为target - l_addr
以上思路,只是需要一个共享器内的地址,以及其版本,就能通过偏移计算调用共享器中的函数,调用 ld 的思路相同。
例题:CTFshow - 入门 PWN-writeup-pwn_083
FULL_RELERO
当开启FULL_RELERO时,整个GOT表将标记为read-only,并且所有的外部引用变量/函数都将在程序装载时由动态链接器解析完成。
此时.got.plt表中的第二项 表项GOT[1]装载的link_map地址 以及第二项 表项GOT[2]装载的dl_runtime_resolve函数地址将是0。
所以此时,利用方式与上面完全一样,仍然可以在.got表进行伪造,只是很难泄露 ld 的地址。
下面介绍一个结构体:
|
|
是一个用于向调试器传递信息的结构体,也被定义在.synamic中:
|
|
该结构体中有link_map的地址,此时,通过多次访问该link_map的双链表,找到另一非FULL_RELERO的程序,读取其got表中的地址就能找到dl_runtime_resolve的地址。
注,link_map在 ld 的堆区中,其与 ld 中的函数相对偏移并不固定
总结
ret2dlresolve 的适用场景与关键条件
适用场景:
- 程序开启 PIE + ASLR,无法泄露 libc 基址;
- 题目未提供 libc 文件,无法匹配版本;
- 程序中无可用的泄露函数(如
puts、printf被移除);- 栈溢出漏洞可控制足够大的栈空间(需布置伪造的结构体和字符串)。
关键条件:
- 程序存在栈溢出漏洞,可控制栈上数据;
- 能找到
pop rdi ; ret、pop rsi ; ret等传参 gadget;- 程序的
plt[0]地址已知(PLT表地址固定,即使开启 PIE,相对程序基址的偏移也固定);- 程序的
DT_DEBUG指针地址已知(link_map参数,固定在程序的.dynamic段中)。