技术社区
安全培训
技术社群
积分商城
先知平台
漏洞库
历史记录
清空历史记录
相关的动态
相关的文章
相关的用户
相关的圈子
相关的话题
注册
登录
单字节延迟绑定实现rop
1673799454684900
历史精选
245浏览 · 2025-03-20 13:13
返回文档
单字节延迟绑定实现rop
之前遇到过一个有意思的题目,在只有一个字节的情况下,利用延迟绑定机制,最终实现shell或者orw,记录一下,便于后续复习
题目分析
先看一下保护措施
题目反汇编代码大概是这个样子,稍微重命名了一下
题目还给了一个txt文件
可能是想让大家知道,malloc申请这个大小的堆块是会贴着libc,然后输入偏移量,输入数据,相当于任意地址写一个字节的数据,毕竟是贴着libc,我们也完全是有偏移的,这一点很容易想到,但是当时比赛完全想不到怎么利用,想过各种hook,但是一个字节实在是太少了,什么都不够做的。
仔细观察可以发现
main函数里面其实只有一个exit,而我们的漏洞函数,暂且就重命名为vuln函数
这个函数其实是在init函数里面被调用,而如果了解基础知识,应该是知道我们的init函数其实是位于start函数
start函数里面调用mian函数,而其实mian函数的调用是在后面的函数之后,也就是说,程序在启动的时候是先进行init函数调用的,那这有什么用呢,我们暂且放下这一个知识点,来介绍一些别的
ret2dl_reslove
这其实算是一个高级栈知识,而事实上,在堆里面的house of banana就是以这个为基础的,我们以64位程序为例
延迟绑定基础简略来说,就是函数第一次调用的时候,程序先通过plt表,检索到got表,got表会寻找函数真实地址并写入got表当中,而第二次及以后的函数调用,则是从plt到got再直接去真实地址,中间会略过寻找的过程,而我们现在要做的,就是把这个过程展开说
我们来看看调用write函数
第一次调用的时候,程序会call这个函数的plt表
我们可以很轻易的发现,plt里面会有一个jmp语句
这个位置正是got表
后续会调用到
dl_runtime_resolve_xsavec,而这个函数里面,又会调用到
dl_fixup函数
这个函数就很有意思了
在源码里面涉及到很多结构体
Dyn 结构体用于描述动态链接时需要使用到的信息,其成员含义如下:
●
d_tag
表示标记值,指明了该结构体的具体类型。比如,
DT_NEEDED
表示需要链接的库名,
DT_PLTRELSZ
表示 PLT 重定位表的大小等。
●
d_un是一个联合体,用于存储不同类型的信息。具体含义取决于d_tag的值。
○
如果
d_tag
的值是一个整数类型,则用
d_val
存储它的值。
○
如果
d_tag
的值是一个指针类型,则用
d_ptr
存储它的值。
Sym 结构体用于描述 ELF 文件中的符号(Symbol)信息,其成员含义如下:
●
st_name
:指向一个存储符号名称的字符串表的索引,即
字符串相对于字符串表起始地址的偏移
。
●
st_info
:如果
st_other
为 0
则设置成 0x12 即可。
●
st_other
:决定
函数参数
link_map
参数是否有效。如果该值不为 0 则直接通过
link_map
中的信息计算出目标函数地址。否则需要调用
_dl_lookup_symbol_x
函数查询出新的
link_map
和
sym
来计算目标函数地址。
●
st_value
:符号地址相对于模块基址的偏移值。
Rel 结构体用于描述重定位(Relocation)信息,其成员含义如下:
●
r_offset
:加上
传入的参数
link_map->l_addr
等于该函数对应 got 表地址。
●
r_info
:符号索引的低 8 位(32 位 ELF)或低 32 位(64 位 ELF)指示符号的类型这里设为 7 即可,高 24 位(32 位 ELF)或高 32 位(64 位 ELF)指示符号的索引即
Sym
构造的数组中的索引。
link_map
是存储目标函数查询结果的一个结构体,我们主要关心
l_addr
和
l_info
两个成员即可。
●
l_addr
:目标函数所在 lib 的基址。
●
l_info:Dyn结构体指针,指向各种结构对应的Dyn。
○
l_info[DT_STRTAB]
:即
l_info
数组第 5 项,指向
.dynstr
对应的
Dyn
。
○
l_info[DT_SYMTAB]
:即
l_info
数组第 6 项,指向
Sym
对应的
Dyn
。
○
l_info[DT_JMPREL]
:即
l_info
数组第 23 项,指向
Rel
对应的
Dyn
。
视线回到
_dl_fixup
函数,重点关注这句话
if (__builtin_expect(ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
根据这个判断决定了重定位的策略。
_dl_fixup
函数在计算出目标函数地址并更新 got 表之后会回到
_dl_runtime_resolve
函数,之后
_dl_runtime_resolve
函数会
调用目标函数
。
大概是这样的
●
用
link_map
访问
.dynamic
,取出
.dynstr
,
.dynsym
,
.rel.plt
的指针。
●
.rel.plt + 第二个参数
求出当前函数的重定位表项
Elf32_Rel
的指针,记作
rel
。
●
rel->r_info >> 8
作为
.dynsym
的下标,求出当前函数的符号表项
Elf32_Sym
的指针,记作
sym
。
●
.dynstr + sym->st_name
得出符号名字符串指针。
●
在动态链接库查找这个函数的地址,并且把地址赋值给
*rel->r_offset
,即 GOT 表。
●
调用这个函数。
改写 .dynamic 的 DT_STRTAB
这个只有在 checksec 时
NO RELRO
可行,即
.dynamic
可写。因为
ret2dl-resolve
会从
.dynamic
里面拿
.dynstr
字符串表的指针,然后加上 offset 取得函数名并且在动态链接库中搜索这个函数名,然后调用。而假如说我们能够改写这个指针到一块我们能够操纵的内存空间,当 resolve 的时候,就能 resolve 成我们所指定的任意库函数。
操纵第二个参数,使其指向我们所构造的 Rel
由于
_dl_runtime_resolve
函数各种按下标取值的操作都没有进行越界检查,因此如果
.dynamic
不可写就操纵
_dl_runtime_resolve
函数的第二个参数,使其访问到可控的内存,然后在该内存中伪造
.rel.plt
,进一步可以伪造
.dynsym
和
.dynstr
,最终调用目标函数。
而上述的这些就是我们的ret2dl_reslove攻击方式
了解这些有什么用呢,上述循环存在exit函数,而这个函数在程序开始并没有被调用,刚好我们的got表可写,我们又知道,got表回填地址,是通过link_map->l_addr来确定的,那如果我们把write的回填改到exit,exit调用就不在是退出,而我们init函数里面又是循环,而且由于write并没有被正确填写,每一次都会执行一次延迟绑定,这就意味这我们可以循环写入数据,这比正常一字节要好太多了。
exp编写
改写exit
那么首先,我们需要完成这个改写exit的操作,那第一步就是找link map相对于libc的偏移量,事实上这个数据并不好找,它其实位于ld文件里面,而在缺失符号表的情况下,找到它就更加艰难了,只能硬解析,从偏移出发,在_rtld_global里面找的对应偏移的位置(演示为2.35的环境)
里面的ns_loaded段指向的就是link map,我这里是0x7ffff7ffe2e0,我们需要修改link_map->l_addr,
把这个数据改成exit的got表与write的got表的偏移
计算写入位置和link map的偏移,我们就能写出如下代码
我们来看看写入情况
在执行完_dl_runtime_resolve_xsavec函数之后,我们来看看got表
我们可以神奇的发现,write的got并没有被写入,反而写到exit上面了,那这样的话,我们就不会exit,而由于write也没有正确写入,每次都会重新覆写寻找
再之后的思路就比较简单了,我们覆写stdout去泄露libc
泄露libc
通过无限次修改,改stdout的flag字段和write指针,就可以在调用write的时候触发io,泄露libc出来,可是我们一开始看的时候,会发现里面是空的
原因也很简单,我们需要刷新一次stdout,不然是不会存在libc的,所以我们可以通过修改_IO_flush_all进行刷新,那这怎么办呢,上面讲dlreslove的时候介绍过,索引函数的时候会根据函数名来定位,那么我们便可以去伪造这一部分
可能前面那一段有些师傅是跳着看的,所以这里我再写一下
如果你了解过
"House Of Blindness"
的分析文章,会知道通过
修改 Elf64_Dyn 指针的最低有效字节(LSB)
,可以让程序从
可写的内存区域(而非只读的二进制文件)
中读取关键资源(如符号表、字符串表等)。
关键结构解析
1
l_info
数组
:
○
l_info
是一个紧凑的指针数组,每个指针指向
.dynamic
段中的一个
Elf64_Dyn
结构体。
○
通过覆盖
l_info
中某个指针的最低有效字节(LSB),可以改变其指向的Elf64_Dyn。例如:
■
将
l_info[DT_SYMTAB]
(符号表指针)的 LSB 改为
0x78
,使其指向
DT_STRTAB
(字符串表)对应的
Elf64_Dyn
。
利用核心:劫持
DT_DEBUG
与
_r_debug
1
DT_DEBUG
的特殊性
:
○
DT_DEBUG
对应的
Elf64_Dyn
中存储了一个指针
_r_debug
,该指针位于
动态链接器(ld.so)的可写内存区域
。
○
由于
_r_debug
所在内存可写,我们可以
伪造任意的
Elf64_Dyn
值
!
2
攻击流程
:
○
通过 LSB 覆盖,使
l_info
中的某个条目(如
DT_SYMTAB
)指向
DT_DEBUG
。
○
此时,程序会从
_r_debug
指向的可写内存中读取伪造的
Elf64_Dyn
结构。
○
例如:将
字符串表(DT_STRTAB)的地址
篡改为指向
_r_debug
,并在此区域伪造字符串 "system"。
○
当
_dl_fixup
尝试解析符号时,会读取伪造的字符串,从而将函数调用重定向到
system
。
所以我们现在可以修改
l_info[DT_SYMTAB]
(符号表指针)的 LSB ,修改到DT_DEBUG,而提前在DT_DEBUG里面写上_IO_flush_all字符串,这样的话,解析别的函数的时候就会触发io刷新,所以现在问题就是,怎么找这些数据,还是使用gdb,我们先找到r_debug的地址
然后将其转换为
struct r_debug *
并访问
r_map
,
pwndbg> p/x ((struct r_debug *)0x7ffff7ffe118)->r_map
将上一步得到的
r_map
地址转换为
struct link_map *
,并访问其
l_info[6]
pwndbg> p/x ((struct link_map *)0x7ffff7ffe2d0)->l_info[5]
当然也可以一步完成
像这样我两次得到的地址也是相同的,至于为什么是5,这是因为在GDB中直接使用
DT_SYMTAB
符号可能会未定义错误,因为它属于动态链接器的内部定义(需加载相关符号),而在ELF标准中,
DT_SYMTAB
的值为
5
。直接替换数值即可,这样我们就找到
l_info[DT_SYMTAB]
的地址了,里面存放的地址就是0x5开头的这个数据,我们要做的就是把这个里面的地址修改成DT_DEBUG的地址,现在第二个问题来了,DT_DEBUG的地址怎么办,
DT_DEBUG
是
.dynamic
段中的一个独立条目,
不在
l_info
数组中
。所以我们需要通过遍历
.dynamic
段找到它。
当然其实并不需要这么麻烦,我们可以从已知的 DT_SYMTAB 地址附近开始搜索
这个前面为0x15的,就是
DT_DEBUG
地址,那也就是说,我们只要修改
这个7e78为7eb8即可,其实也就是一个字节,当我们完成修改之后,我们可以调试看看什么情况
我们重点关注一下_r_debug的地址
在write函数进去,最终会进入_dl_lookup_symbol_x函数(其实名字就能看出来是和解析符号表有关系的)
注意看这一行,上面关注过_r_debug的地址,rdi的值就是r_debug加上0x3e,而我们把修改语句去掉可以对比看看
在没有修改之前,这里是一个elf文件地址,并且不可写,这个地址正是
l_info[DT_SYMTAB]
,所以在修改成一个libc地址之后,我们又可以对libc进行修改,这样就可以在这里填上字符串,而在把rdi伪造成字符串地址也是有原因的,因为
_dl_lookup_symbol_x
函数的第一个参数就是待查找函数的函数名,也就是rdi
那接下来就应该在r_debug加上0x3e的位置写上_IO_flush_all(这个是修改前伪造,这样下一次修改完直接就运行到这个函数了,不然因为本来加0x3e的位置是空的,程序解析不了,会报错退出)
可以看到,我们解析的函数名就变成了_IO_flush_all,而调用write就会变成调用IO_flush_all,但是运行结束后依然没有libc地址在stdout上面,其实是因为需要提前布置一下flag字段和write的指针,把flag改成0xfbad3887,IO_write_ptr改成0xff,这里解释一下为什么,虽然大家io都挺了解的,但是这个参数用的确实少
0x3887 = 0011 1000 1000 0111 (二进制)
●
_IO_USER_BUF (0x0001)
:用户分配的缓冲区。
●
_IO_UNBUFFERED (0x0002)
:无缓冲模式(直接写入)。
●
_IO_NO_READS (0x0004)
:禁止读操作(
stdout
仅用于写)。
●
_IO_LINKED (0x0080)
:结构体在全局文件链表中。
●
_IO_CURRENTLY_PUTTING (0x0800)
:当前正在写入数据。
●
_IO_IS_APPENDING (0x1000)
:以追加模式打开。
这意味着当前
stdout
处于
全缓冲模式
,会刷新自己
可以看到,我们已经把对应libc的地址写到io里面了,然后就正常去修改结构体泄露
短期总结一下
伪造io结构体
有了libc,有了任意地址写,怎么打其实都行,这题最开始是2.31的环境,只不过我是2.35,就用io进行攻击好了,直接改io_list_all,就到这个大堆块上面,不过由于我们一次只能写一个字节,所以我们要先把write改回来,这样改完字节之后不会出问题
write(0x2C9338, 0x78)
这样就改好了,现在要做的就是伪造结构体,这一部分不再多说,直接套公式都行(严格来说,并非伪造,而是直接在上面改)
这里直接给出最后完整的exp
这样就拿到shell了,至于orw,和io的orw操作相同,这里不再赘述
0
人收藏
1
人喜欢
转载
分享
0
条评论
某人
表情
可输入
255
字
评论
发布投稿
热门文章
1
从零掌握java内存马大全(基于LearnJavaMemshellFromZero复现重组)
2
突破网络限制,Merlin Agent助你轻松搭建跳板网络!
3
从白帽角度浅谈SRC业务威胁情报挖掘与实战
4
基于规则的流量加解密工具-CloudX
5
从0到1大模型MCP自动化漏洞挖掘实践
近期热点
一周
月份
季度
1
从零掌握java内存马大全(基于LearnJavaMemshellFromZero复现重组)
2
突破网络限制,Merlin Agent助你轻松搭建跳板网络!
3
从白帽角度浅谈SRC业务威胁情报挖掘与实战
4
基于规则的流量加解密工具-CloudX
5
从0到1大模型MCP自动化漏洞挖掘实践
暂无相关信息
暂无相关信息
优秀作者
1
T0daySeeker
贡献值:38700
2
一天
贡献值:24800
3
Yale
贡献值:21000
4
1674701160110592
贡献值:18000
5
1174735059082055
贡献值:16000
6
Loora1N
贡献值:13000
7
bkbqwq
贡献值:12800
8
手术刀
贡献值:11000
9
lufei
贡献值:11000
10
xsran
贡献值:10600
目录
单字节延迟绑定实现rop
题目分析
ret2dl_reslove
exp编写
改写exit
泄露libc
伪造io结构体
转载
标题
作者:
你好
http://www.a.com/asdsabdas
文章
转载
自
复制到剪贴板