前言
一种基于延迟绑定机制的利用办法 适用于没有puts等输出函数的情况下 伪造结构体 使得任意函数的got表解析成system函数 从而getshell 应该算是pwn学习初期最早的伪造思想了 对于初期的学习还是不那么容易懂的 这个知识点我学的也比较晚 最早出现是刚学pwn没几个月的ISCTF 2022 那个时候做不出来 然后也没去复现 就一直拖到了现在 所以这篇文章的一些描述可能不是很详细 新学pwn的如果哪里看不懂 记得联系我 我删改一下
利用本质
32位 Partical RELRO
被我们熟知的plt表和got表 是因为延迟绑定机制的出现 这个机制主要是由于可以大幅度减少程序的体积 部分程序调用到的函数比较少 如果全部加载库 很容易就造成体积过大 所以出现了动态链接
RELRO这个保护机制跟动态链接关系比较大 如果是FULL RELRO 那么就会在程序载入时就完成所有函数的got表解析 也就无从谈起利用
观察一下一个read函数的解析过程 在初次调用的时候 got表中指向的是plt表 把0入栈了 同时跳转到了0x8048380
同样入栈了一个参数 这个参数位于got表的第二个元素 我们在动态延迟绑定的文章说过 是模块的ID 随后跳转的地址是got表的第三个元素 同时是我们今天利用的关键 _dl_runtime_resolve
函数的地址
下面就来详细分析一下这个函数是如何解析真实地址的
诸如io链中存在vtable这样的结构体 用来索引函数 _dl_runtime_resolve也有对应的结构体来索引各种表项 用来满足其寻找对应函数真实地址的需求
.dynamic段就是这样一个结构体 其存放了动态链接的几个关键表项.dynsym .dynstr .rela.plt等
_dl_runtime_resolve利用入栈的模块ID 这里又称为link_map 可以访问到dynamic段 从而获取其他表的地址
这里我们先不讲这几个表项的作用 跟着思路往下走
在_dl_runtime_resolve拿到表项地址以后 他怎么知道要寻找的是哪个函数的真实地址呢 或者说 小明妈妈让他买酱油 这个行为确实是触发了 但是小明不知道要买什么品牌的酱油
这个时候其会先寻找重定位表项 同样的 其仍然需要一个索引 否则他怎么知道要找哪个函数的重定位表项
而这个偏移 就是最开始入栈参数
索引到重定位表项后 其就需要函数名 以此在so文件中寻找对应的真实地址 不过在索引dynstr到这个段获取字符串前 先需要到达dynsym段进行一个中转 而寻找到对应地址的偏移 就是上面的第二个参数
就拿我们上面的read的重定位表举例 第二个参数是0x107 这个值是怎么得到的呢
首先 7是固定的 可以理解为函数的标识 这个值可不能进行更改 在寻址过程中还会进行检查
前面的0x100是如何得到的呢 实际是由1<<8得到 这里的逻辑左移八位是固定的格式 _dl_runtime_resolve在读取到0x107后 会自动的逻辑右移 得到一个1
这个1乘以0x10 (这个值是dym结构体一个成员的大小) 就是dynsym表的偏移了
我们来观察一下dynsym表的内容
除开0x804820c所对应的_gmon_start函数比较特殊之外 其余的sym表的第四个参数固定为0x12 这在后续中也存在检查
中间两个参数都是0 没啥可说的 来看一下第一个参数 这个参数就是用来索引dynstr表 直接对应的就是函数字符串存储地址减去dynstr的起始地址
到此为止 _dl_runtime_resolve函数的调用流程就已经清晰了 稍微总结一下 索引的流程为:
压入link_map参数 索引dynamic段 根据压入的偏移参数寻址重定位表项
根据重定位表项寻址dynsym表 根据dynsym表索引dynstr表 最后获取到函数名
所以我们只要环环相扣 一步步伪造偏移 劫持索引流 就可以做到劫持read函数(任意函数都行) 将其真实地址的解析修改为system函数 就可以getshell
伪造
32位 Partical RELRO
借助这样一个32位的程序来演示 程序的逻辑非常简单 就一个read 构成栈溢出 可以自行编译
ssize_t vuln() |
由于我们需要伪造偏移 所以需要有一个可写可读的地址来存放我们的fake struct
理所当然的是bss段 于是这里直接构造read链 往bss段上读入数据
随后我们来想一下如何伪造偏移
上面提到 _dl_runtime_resolve解析dynamic段 需要靠压入的link_map参数 我们可以利用这个gadget来实现
.plt:08048380 sub_8048380 proc near ; CODE XREF: .plt:0804839B↓j |
随后我们应该跟上用来索引重定位表项的偏移 这里的偏移就是fake_relplt_addr - relplt_addr
随后 我们需要伪造重定位表项 其第一个参数为想要误导的函数got表地址(哪怕是已经解析过真实地址的函数也可以 甚至是任意的可写地址都行 不过为了方便getshell 一般都是放到函数的got表里) 第二个参数用来索引dynsym表
第二个参数的伪造可以说是最关键的一步 因为dynsym其一个成员是0x10字节 同时还存在着对齐 所以要留意一下原本的dynsym起始地址的最后一位 比如我这个程序其最后一位是0xc 所以我伪造的dynsym的起始地址最后一位也应该为0xc
同时 fake_dynsym与dynsym的偏移 需要经过我上面提到的计算 最后的结果用来充当第二个参数
接着就是dynsym的伪造问题 一共有四个参数 后三个参数不用管 固定是 0,0,0x12
而第一个参数为fake_dynstr和dynstr的偏移 直接相减就行了
如果最后我们成功劫持了函数的解析 成功在read函数的got表中写入了system函数的真实地址 我们要如何getshell呢
我们知道 动态链接的最后 在得到了真实地址后 会重新调用一次该函数 所以 我们只需要重新调用函数时 rsp指针指向的第二个字长处(第一个字长为返回地址 拿垃圾数据填就行了)存放着binsh字符串地址即可
参考模板:
plt0 = elf.get_section_by_name('.plt').header.sh_addr |