深入理解-dl_runtime_resolve
深入理解-dl_runtime_resolve
概要
目前大部分漏洞利用常包含两个阶段:
- 首先通过信息泄露获取程序内存布局
- 第二步才进行实际的漏洞利用
然而信息泄露的方法并不总是可行的,且获取的内存信息并不可靠,于是就有了ret2dl_resolve的利用方式。这种方式巧妙的利用了ELF文件格式以及动态装载器的弱点,不需要进行信息泄露就可以直接标识关键函数并调用。
符号解析过程以及结构体定义
解析原理
-
动态装载器负责将二进制文件以及依赖库加载到内存,该过程包含了对导入符号的解析。
-
也就是说,在第一次调用函数时都由
_dl_runtime_resolve
函数来完成,以下是函数原型:
-
resolve函数第二个参数是
reloc_index
,它可以找到文件中.rel.plt
表,.rel.plt
表由Elf Rel
结构体组成,定义如下:
它的
r_offset
用于保存解析后的符号地址写入内存的位置(绝对地址),r_info
的高位3字节用于标识该符号在.dynsym
中的下标。 -
Elf Sym
结构体中前两个成员为重要成员,st_value
是当符号被导出时用于存放虚拟地址,不导出则为NULL。st_name
是相对于.dynstr
段的偏移,.dynstr
保存符号名称字符串, 内容如下:
总结起来就是:
当程序导入函数时,动态链接器在.dynstr
段中添加一个函数名称字符串
在.dynsym
段中添加一个指向函数名称字符串的Elf Sym
结构体
在.rel.plt
段中添加一个指向Elf Sym
的Elf Rel
结构体
最后Elf Rel
的r_offse
构成GOT表,保存在.got.plt
段中
Lazy Binding
-
Lazy Binding机制(延迟绑定)即只有函数被调用时,才会对函数地址进行解析,然后将真实地址写入GOT表中。第二次调用函数时便不再进行加载
-
该过程是通过PLT表进行的。每个函数都在PLT表中有一个条目(PLT[0]),第一条指令无条件跳转到对应的GOT条目保存的地址。在程序中类似于下面这样:
-
然后GOT条目在初始化时默认指向PLT条目的第二条指令位置(PLT[1]),相当于又跳回来了。执行下面两条指令:
在程序中类似于下面这样,并且可以验证0x804A008,也就是GOT[2]是存储的
dl_runtime_resolve()
函数:
-
_dl_runtime_resolve
函数中第一个参数link_map_obj
,用于获取解析导入函数所需的信息,第二个参数reloc_index
则标识了解析哪一个导入函数(当前函数setbuf
的reloc_index
是0,所以是0):
总结起来就是:
首先无条件跳转到GOT表条目,jmp xxx
然后把reloc_index
压栈,再次跳转到GOT条目**
然后把link_map_obj
压栈,参数压栈完成后,执行_dl_runtime_resolve
函数
_dl_runtime_resolve
中的_dl_fixup
完成解析并将真实地址写入GOT表
漏洞利用
程序保护机制RELRO(Relocation Read-Only,重定位只读)是用于缓解由动态解析缺陷而产生的。一般分为三种情况:
- No RELRO
完全关闭。.dynamic
段可写,动态装载器是以.dynamic
段的DT_STRTAB
条目来获取.dynstr
段的地址,而DT_STRTAB
地址是已知的,且默认情况下可写,所以可以改写DT_STRTAB
,欺骗动态装载器,使其找到伪造的.dynstr
段,将我们控制的地址内的字符串解析为函数名称,然后去解析函数地址。比如修改DT_STRTAB
的.dynstr
条目内容为bss段,在bss段中写入execve
字符串,假如现在正要解析printf
函数,那么就会解析成execve
函数的地址。
- Partial RELRO
开启部分保护,.dynamic
段不可写。之前介绍_dl_runtime_resolve
时提到,第二个参数reloc_index
对应Elf Rel
在.rel.plt
中的偏移,动态装载器将reloc_index
加上.rel.plt
的基址来得到目标Elf Rel
的内存地址。
当我们控制reloc_index
的值,使它相加后刚好落在bss段上,就可以在bss段上构造一个Elf Rel
结构体,使Elf Rel
的第一个成员r_offset
的值是一个可写的地址,用来保存解析后的函数地址。然后使r_info
的值导向到可控制的内存下标,指向Elf Sym
,Elf Sym
中的st_name
再指向函数名称字符串,那么就可以解析成我们想要的函数地址。
- FULL RELRO
保护完全开启,开启后立即绑定函数地址,添加 PT_GNU_RELRO
段,.got
只读不可写,.got.plt
节取消,PLT 直接调用.got
节地址。Bypass可参考网上资料。
XDCTF 2015 pwn200
-
程序源码
-
编译为动态链接32位可执行文件,开启Partial RELRO 和NX保护:
- 可以从源码得知有栈溢出漏洞,可以通过泄露libc地址的方式获取flag,但在这里使用ret2dl-resolve的方式。
- 程序开启了Partial RELRO 保护,那么就按照上面介绍的第二种保护情况来做。
- 首先利用栈溢出控制执行流,调用
read
函数将下一阶段的payload读取到bss段上:
- 这里一步一步模拟
write
函数的解析过程,最终实现system("/bin/sh")
。在bss段构造payload,并且打印出我们填入的字符串,以便验证:
- 接下来模拟
write@plt
的执行效果。在bss段构造payload,将_dl_runtime_resolve
函数的参数压栈,也就是reloc_index
,再跳转到PLT[0],就是第一个无条件跳转指令jmp xxx
:
- 然后在bss段中构造一个
Elf Rel
结构,r_offset
设置成write@got
的地址,表示解析后的真实地址填入这里。r_info
直接照搬,设置成0x607,动态加载器会通过这个值找到对应的Elf Sym
。那么现在reloc_index
就不再是0x20了,应该调整为Elf Rel
基地址距离bss段上的偏移:
r_info成员的值是0x607,直接照搬到payload中
- 在bss段中伪造
Elf Sym
。首先使用readelf
命令,查找到write
函数在.dynsym
段的下标,得知下标为6,然后使用objdump
找到下标为6的那一行,数据直接照搬就可以了:
那么之前构造的fake_reloc也要调整,r_info
可以通过r_sym
和r_type
计算得出。r_sym
也就是Elf Sym
相对于.dynsym
段的下标偏移,r_type
则照搬R_386_JUMP_SLOT
的值 0x7
- 最后,在bss段上伪造
.dynstr
,也就是放上"write"字符串,相应的调整fake_sym的st_name
指向伪造的函数名称字符串。st_info
字段的内容被分为高 28 位的st_bind
符号绑定信息,以及低 4 位的st_type
符号类型信息,然后可以通过st_blind
和st_type
来计算st_info
:
最后,只要将字符串“write”改成“system”,调整一下参数即可获得shell。
- 完整exp
- 如果觉得手工构造太麻烦,有一个工具 roputils 可以简化此过程,或者可以使用pwntools中自带的 模块来完成,下面是pwntools构造32位程序exp的例子:
x64的ret2dl-resolve—XMAN 2016-level3
检查保护
- 64 位程序一般情况下使用寄存器传参,但给
_dl_runtime_resolve
传参时使用栈 _dl_runtime_resolve
函数的第二个参数reloc_index
由偏移变为了索引
64位在这种情况下,如果像32位一样依次伪造reloc_index
、symtab
、strtab
会出错,原因是在_dl_fixup
函数执行过程中,访问到了一段未映射的地址处,接下来我们结合 _dl_fixup
完整源码进行分析,源码位于 glibc-2.23/elf/dl-runtime.c , 在关键位置给出了注释,其他位置可忽略:
所以接下来我们的任务就是控制 link_map
中的l_addr
和 sym
中的st_value
。
具体思路为:
- 伪造
link_map->l_addr
为libc中已解析函数与想要执行的目标函数的偏移值,如addr_system - addr_xxx
- 伪造
sym->st_value
为已经解析过的某个函数的 got 表的位置
下面是64位下的sym
结构体:
所以sym
结构体的大小为24字节,st_value
就位于首地址+0x8的位置( 4 + 1 + 1 + 2)。
如果,我们把一个函数的got表地址-0x8的位置当作sym表首地址,那么它的st_value
的值就是这个函数的got表上的值,也就是实际地址,此时它的st_other
恰好不为0
再来看link_map的结构
这里的.dynamic
节就对应Elf64_Dyn * l_info
的内容
所以如果我们伪造一个link_map
表,很容易就可以控制 l_addr
,通过阅读源码,我们知道_dl_fixup
主要用了 l_info
的内容 ,也就是上图中JMPREL
,STRTAB
,SYMTAB
的地址。
所以我们需要伪造这个数组里的几个指针
DT_STRTAB
指针:位于link_map_addr +0x68(32位下是0x34)DT_SYMTAB
指针:位于link_map_addr + 0x70(32位下是0x38)DT_JMPREL
指针:位于link_map_addr +0xF8(32位下是0x7C)
然后伪造三个elf64_dyn即可,dynstr只需要指向一个可读的地方,因为这里我们没有用到
- 64位下重定位表项与32位有所不同,多了
r_addend
成员,三个成员各占8字节,总大小为24字节:
- 在这里可以看到,
write
函数在符号表中的偏移为 2(也就是r_info
的值:0x200000007h>>32)
- 除此之外,在 64 位下,plt 中的代码
push
的是待解析符号在重定位表中的索引,而不是偏移。比如,write
函数对应上图中第一个,下标为0,那么就push 0
:
- 看看另一个,
read
函数对应的下标为1,那么就push 1
:
可以发现针对软件重定位的攻击其实都是围绕函数
_dl_fix_up
的两个参数link_map
和reloc_arg
展开的,再加上相关数据结构的伪造完成攻击。确实感觉这种攻击是格式化的,虽然过程看上去很复杂,但是实际上都有固定的“套路”,只需按照步骤一步一步操作,大多数情况下就可以完成整个攻击。
- 下面是完整的脚本
2021强网杯 [强网先锋]no_output
此题也是考验ret2dl-resolve攻击方式。exp如下:
__EOF__

本文链接:https://www.cnblogs.com/unr4v31/p/15168342.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」