Skip to content

Commit a2b6771

Browse files
Bl4ck-C4tBl4ckC4t
and
Bl4ckC4t
authored
Improved DynELF address resolutions and symbol lookups (#2335)
* Optimized dynelf when using an ELF object * Fixed minor bugs, added a new way to lookup symbols Improved _rel_lookup and added Elf64_Rel datatype Added support for _rel_lookup in x86 binaries * Spelling fix * Converted prints to python2 style + changelog entry * Fixed a bug with real leaker not being resotred --------- Co-authored-by: Bl4ckC4t <[email protected]>
1 parent 73f2a31 commit a2b6771

File tree

3 files changed

+147
-48
lines changed

3 files changed

+147
-48
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ The table below shows which release corresponds to each branch, and what date th
8080
- [#2308][2308] Fix WinExec shellcraft to make sure it's 16 byte aligned
8181
- [#2279][2279] Make `pwn template` always set context.binary
8282
- [#2310][2310] Add support to start a process on Windows
83+
- [#2335][2335] Add lookup optimizations in DynELF
8384
- [#2334][2334] Speed up disasm commandline tool with colored output
8485
- [#2328][2328] Lookup using $PATHEXT file extensions in `which` on Windows
8586
- [#2189][2189] Explicitly define p64/u64 functions for IDE support
@@ -105,6 +106,7 @@ The table below shows which release corresponds to each branch, and what date th
105106
[2308]: https://github.com/Gallopsled/pwntools/pull/2308
106107
[2279]: https://github.com/Gallopsled/pwntools/pull/2279
107108
[2310]: https://github.com/Gallopsled/pwntools/pull/2310
109+
[2335]: https://github.com/Gallopsled/pwntools/pull/2335
108110
[2334]: https://github.com/Gallopsled/pwntools/pull/2334
109111
[2328]: https://github.com/Gallopsled/pwntools/pull/2328
110112
[2189]: https://github.com/Gallopsled/pwntools/pull/2189

pwnlib/dynelf.py

+136-48
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ def __init__(self, leak, pointer=None, elf=None, libcdb=True):
165165
self._waitfor = None
166166
self._bases = {}
167167
self._dynamic = None
168+
self.elf = None
169+
170+
if elf:
171+
path = elf
172+
if isinstance(elf, ELF):
173+
path = elf.path
174+
175+
# Load a fresh copy of the ELF
176+
with context.local(log_level='error'):
177+
w = self.waitfor("Loading from %r" % path)
178+
self.elf = ELF(path)
179+
w.success("[LOADED]")
168180

169181
if not (pointer or (elf and elf.address)):
170182
log.error("Must specify either a pointer into a module and/or an ELF file with a valid base address")
@@ -177,12 +189,15 @@ def __init__(self, leak, pointer=None, elf=None, libcdb=True):
177189
if not elf:
178190
log.warn_once("No ELF provided. Leaking is much faster if you have a copy of the ELF being leaked.")
179191

180-
self.elf = elf
181192
self.leak = leak
182193
self.libbase = self._find_base(pointer or elf.address)
183194

184195
if elf:
185-
self._find_linkmap_assisted(elf)
196+
self._elftype = self.elf.elftype
197+
self._elfclass = self.elf.elfclass
198+
self.elf.address = self.libbase
199+
self._dynamic = self.elf.get_section_by_name('.dynamic').header.sh_addr
200+
self._dynamic = self._make_absolute_ptr(self._dynamic)
186201

187202
@classmethod
188203
def for_one_lib_only(cls, leak, ptr):
@@ -241,49 +256,6 @@ def dynamic(self):
241256
self._dynamic = self._find_dynamic_phdr()
242257
return self._dynamic
243258

244-
def _find_linkmap_assisted(self, path):
245-
"""Uses an ELF file to assist in finding the link_map.
246-
"""
247-
if isinstance(path, ELF):
248-
path = path.path
249-
250-
# Load a fresh copy of the ELF
251-
with context.local(log_level='error'):
252-
elf = ELF(path)
253-
elf.address = self.libbase
254-
255-
w = self.waitfor("Loading from %r" % elf.path)
256-
257-
# Save our real leaker
258-
real_leak = self.leak
259-
260-
# Create a fake leaker which just leaks out of the 'loaded' ELF
261-
# However, we may load things which are outside of the ELF (e.g.
262-
# the linkmap or GOT) so we need to fall back on the real leak.
263-
@MemLeak
264-
def fake_leak(address):
265-
try:
266-
return elf.read(address, 4)
267-
except ValueError:
268-
return real_leak.b(address)
269-
270-
# Save off our real leaker, use the fake leaker
271-
self.leak = fake_leak
272-
273-
# Get useful pointers for resolving the linkmap faster
274-
w.status("Searching for DT_PLTGOT")
275-
pltgot = self._find_dt(constants.DT_PLTGOT)
276-
277-
w.status("Searching for DT_DEBUG")
278-
debug = self._find_dt(constants.DT_DEBUG)
279-
280-
# Restore the real leaker
281-
self.leak = real_leak
282-
283-
# Find the linkmap using the helper pointers
284-
self._find_linkmap(pltgot, debug)
285-
self.success('Done')
286-
287259
def _find_base(self, ptr):
288260
page_size = 0x1000
289261
page_mask = ~(page_size - 1)
@@ -380,6 +352,27 @@ def _find_dynamic_phdr(self):
380352

381353
return dynamic
382354

355+
def _find_dt_optimized(self, name):
356+
"""
357+
Find an entry in the DYNAMIC array through an ELF
358+
359+
Arguments:
360+
name(str): Name of the tag to find ('DT_DEBUG', 'DT_PLTGOT', ...)
361+
362+
Returns:
363+
Pointer to the data described by the specified entry.
364+
"""
365+
if not self.elf:
366+
return None
367+
368+
ptr = self.elf.dynamic_value_by_tag(name)
369+
if ptr:
370+
ptr = self._make_absolute_ptr(ptr)
371+
self.success("Found %s at %#x" % (name, ptr))
372+
return ptr
373+
return None
374+
375+
383376
def _find_dt(self, tag):
384377
"""
385378
Find an entry in the DYNAMIC array.
@@ -390,11 +383,16 @@ def _find_dt(self, tag):
390383
Returns:
391384
Pointer to the data described by the specified entry.
392385
"""
393-
leak = self.leak
394386
base = self.libbase
395387
dynamic = self.dynamic
388+
leak = self.leak
396389
name = next(k for k,v in ENUM_D_TAG.items() if v == tag)
397390

391+
# Read directly from the ELF if possible
392+
ptr = self._find_dt_optimized(name)
393+
if ptr:
394+
return ptr
395+
398396
Dyn = {32: elf.Elf32_Dyn, 64: elf.Elf64_Dyn} [self.elfclass]
399397

400398
# Found the _DYNAMIC program header, now find PLTGOT entry in it
@@ -407,10 +405,10 @@ def _find_dt(self, tag):
407405
self.failure("Could not find tag %s" % name)
408406
return None
409407

410-
self.status("Found %s at %#x" % (name, dynamic))
411408
ptr = leak.field(dynamic, Dyn.d_ptr)
412409

413410
ptr = self._make_absolute_ptr(ptr)
411+
self.status("Found %s at %#x" % (name, ptr))
414412

415413
return ptr
416414

@@ -599,6 +597,10 @@ def bases(self):
599597
Return a dictionary mapping library path to its base address.
600598
'''
601599
if not self._bases:
600+
if self.link_map is None:
601+
self.failure("Cannot determine bases without linkmap")
602+
return {}
603+
602604
leak = self.leak
603605
LinkMap = {32: elf.Elf32_Link_Map, 64: elf.Elf64_Link_Map}[self.elfclass]
604606

@@ -666,6 +668,62 @@ def _dynamic_load_dynelf(self, libname):
666668
lib._waitfor = self._waitfor
667669
return lib
668670

671+
def _rel_lookup(self, symb, strtab=None, symtab=None, jmprel=None):
672+
"""Performs slower symbol lookup using DT_JMPREL(.rela.plt)"""
673+
leak = self.leak
674+
elf_obj = self.elf
675+
symb_name = symb.decode()
676+
677+
# If elf is available look for the symbol in it
678+
if elf_obj and symb_name in elf_obj.symbols:
679+
self.success("Symbol '%s' found in ELF!" % symb_name)
680+
return elf_obj.symbols[symb_name]
681+
682+
log.warning("Looking up symbol through DT_JMPREL. This might be slower...")
683+
684+
685+
strtab = strtab or self._find_dt(constants.DT_STRTAB)
686+
symtab = symtab or self._find_dt(constants.DT_SYMTAB)
687+
jmprel = jmprel or self._find_dt(constants.DT_JMPREL) # .rela.plt
688+
689+
strtab = self._make_absolute_ptr(strtab)
690+
symtab = self._make_absolute_ptr(symtab)
691+
jmprel = self._make_absolute_ptr(jmprel)
692+
693+
w = self.waitfor("Looking for %s in .rel.plt" % symb)
694+
# We look for the symbol by iterating through each Elf64_Rel entry.
695+
# For each Elf64_Rel, get the Elf64_Sym for that entry
696+
# Then compare the Elf64_Sym.st_name with the symbol name
697+
698+
Rel = {32: elf.Elf32_Rel, 64: elf.Elf64_Rel}[self.elfclass]
699+
Sym = {32: elf.Elf32_Sym, 64: elf.Elf64_Sym}[self.elfclass]
700+
701+
rel_addr = jmprel
702+
rel_entry = None
703+
while True:
704+
rel_entry = leak.struct(rel_addr, Rel)
705+
706+
# We ran out of entries in DT_JMPREL
707+
if rel_entry.r_offset == 0:
708+
return None
709+
710+
sym_idx = rel_entry.r_info >> 32 # might be different for 32-bit
711+
sym_entry_address = symtab + ( sym_idx * sizeof(Sym) )
712+
sym_str_off = leak.field(sym_entry_address, Sym.st_name)
713+
symb_str = leak.s(strtab+sym_str_off)
714+
715+
if symb_str == symb:
716+
w.success("Found matching Elf64_Rel entry!")
717+
break
718+
719+
rel_addr += sizeof(Rel)
720+
721+
symbol_address = self._make_absolute_ptr(rel_entry.r_offset)
722+
723+
return symbol_address
724+
725+
726+
669727
def _lookup(self, symb):
670728
"""Performs the actual symbol lookup within one ELF file."""
671729
leak = self.leak
@@ -698,9 +756,39 @@ def _lookup(self, symb):
698756
#
699757
# Perform the hash lookup
700758
#
759+
760+
# Save off our real leaker in case we use the fake leaker
761+
real_leak = self.leak
762+
if self.elf:
763+
764+
# Create a fake leaker which just leaks out of the 'loaded' ELF
765+
# However, we may load things which are outside of the ELF (e.g.
766+
# the linkmap or GOT) so we need to fall back on the real leak.
767+
@MemLeak
768+
def fake_leak(address):
769+
try:
770+
return self.elf.read(address, 4)
771+
except ValueError:
772+
return real_leak.b(address)
773+
# Use fake leaker since ELF is available
774+
self.leak = fake_leak
775+
701776
routine = {'sysv': self._resolve_symbol_sysv,
702777
'gnu': self._resolve_symbol_gnu}[hshtype]
703-
return routine(self.libbase, symb, hshtab, strtab, symtab)
778+
resolved_addr = routine(self.libbase, symb, hshtab, strtab, symtab)
779+
780+
if resolved_addr:
781+
# Restore the original leaker
782+
self.leak = real_leak
783+
return resolved_addr
784+
785+
# if symbol not found in GNU_Hash, try looking in JMPREL
786+
resolved_addr = self._rel_lookup(symb, strtab, symtab)
787+
788+
# Restore the original leaker
789+
self.leak = real_leak
790+
791+
return resolved_addr
704792

705793
def _resolve_symbol_sysv(self, libbase, symb, hshtab, strtab, symtab):
706794
"""

pwnlib/elf/datatypes.py

+9
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,15 @@ class Elf64_Sym(ctypes.Structure):
399399
("st_value", Elf64_Addr),
400400
("st_size", Elf64_Xword),]
401401

402+
class Elf64_Rel(ctypes.Structure):
403+
_fields_ = [("r_offset", Elf64_Addr),
404+
("r_info", Elf64_Xword),
405+
("r_addend", Elf64_Sxword),]
406+
407+
class Elf32_Rel(ctypes.Structure):
408+
_fields_ = [("r_offset", Elf32_Addr),
409+
("r_info", Elf32_Word),]
410+
402411
class Elf32_Link_Map(ctypes.Structure):
403412
_fields_ = [("l_addr", Elf32_Addr),
404413
("l_name", Elf32_Addr),

0 commit comments

Comments
 (0)