• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Gallopsled / pwntools / 26699038648

31 May 2026 12:35AM UTC coverage: 73.985%. First build
26699038648

Pull #87

github

web-flow
Merge 57de79f0e into fb35a123b
Pull Request #87: Made DynELF much more modular and able to solve more tasks

3949 of 6618 branches covered (59.67%)

72 of 91 new or added lines in 1 file covered. (79.12%)

13611 of 18397 relevant lines covered (73.98%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.64
/pwnlib/dynelf.py
1
"""
2
Resolve symbols in loaded, dynamically-linked ELF binaries.
3
Given a function which can leak data at an arbitrary address,
4
any symbol in any loaded library can be resolved.
5

6
Example
7
^^^^^^^^
8

9
::
10

11
    # Assume a process or remote connection
12
    p = process('./pwnme')
13

14
    # Declare a function that takes a single address, and
15
    # leaks at least one byte at that address.
16
    def leak(address):
17
        data = p.read(address, 4)
18
        log.debug("%#x => %s", address, enhex(data or ''))
19
        return data
20

21
    # For the sake of this example, let's say that we
22
    # have any of these pointers.  One is a pointer into
23
    # the target binary, the other two are pointers into libc
24
    main   = 0xfeedf4ce
25
    libc   = 0xdeadb000
26
    system = 0xdeadbeef
27

28
    # With our leaker, and a pointer into our target binary,
29
    # we can resolve the address of anything.
30
    #
31
    # We do not actually need to have a copy of the target
32
    # binary for this to work.
33
    d = DynELF(leak, main)
34
    assert d.lookup(None,     'libc') == libc
35
    assert d.lookup('system', 'libc') == system
36

37
    # However, if we *do* have a copy of the target binary,
38
    # we can speed up some of the steps.
39
    d = DynELF(leak, main, elf=ELF('./pwnme'))
40
    assert d.lookup(None,     'libc') == libc
41
    assert d.lookup('system', 'libc') == system
42

43
    # Alternately, we can resolve symbols inside another library,
44
    # given a pointer into it.
45
    d = DynELF(leak, libc + 0x1234)
46
    assert d.lookup('system')      == system
47

48
DynELF
49
"""
50
import ctypes
1✔
51

52
from elftools.elf.enums import ENUM_D_TAG
1✔
53

54
from pwnlib import elf
1✔
55
from pwnlib import libcdb
1✔
56
from pwnlib.context import context
1✔
57
from pwnlib.elf import ELF
1✔
58
from pwnlib.elf import constants
1✔
59
from pwnlib.log import getLogger
1✔
60
from pwnlib.memleak import MemLeak
1✔
61
from pwnlib.util.fiddling import enhex
1✔
62
from pwnlib.util.packing import _need_bytes
1✔
63

64
log    = getLogger(__name__)
1✔
65
sizeof = ctypes.sizeof
1✔
66

67
def sysv_hash(symbol):
1✔
68
    """sysv_hash(str) -> int
69

70
    Function used to generate SYSV-style hashes for strings.
71
    """
NEW
72
    h = 0
×
NEW
73
    g = 0
×
NEW
74
    for c in bytearray(_need_bytes(symbol, 4, 0x80)):
×
NEW
75
        h = (h << 4) + c
×
NEW
76
        g = h & 0xf0000000
×
NEW
77
        h ^= (g >> 24)
×
NEW
78
        h &= ~g
×
NEW
79
    return h & 0xffffffff
×
80

81
def gnu_hash(s):
1✔
82
    """gnu_hash(str) -> int
83

84
    Function used to generated GNU-style hashes for strings.
85
    """
86
    s = bytearray(_need_bytes(s, 4, 0x80))
1✔
87
    h = 5381
1✔
88
    for c in s:
1✔
89
        h = h * 33 + c
1✔
90
    return h & 0xffffffff
1✔
91

92
class DynELF:
1✔
93
    '''
94
    DynELF knows how to resolve symbols in remote processes via an infoleak or
95
    memleak vulnerability encapsulated by :class:`pwnlib.memleak.MemLeak`.
96

97
    Implementation Details:
98

99
        Resolving Functions:
100

101
            In all ELFs which export symbols for importing by other libraries,
102
            (e.g. ``libc.so``) there are a series of tables which give exported
103
            symbol names, exported symbol addresses, and the ``hash`` of those
104
            exported symbols.  By applying a hash function to the name of the
105
            desired symbol (e.g., ``'printf'``), it can be located in the hash
106
            table.  Its location in the hash table provides an index into the
107
            string name table (strtab_), and the symbol address (symtab_).
108

109
            Assuming we have the base address of ``libc.so``, the way to resolve
110
            the address of ``printf`` is to locate the ``symtab``, ``strtab``,
111
            and hash table. The string ``"printf"`` is hashed according to the
112
            style of the hash table (SYSV_ or GNU_), and the hash table is
113
            walked until a matching entry is located. We can verify an exact
114
            match by checking the string table, and then get the offset into
115
            ``libc.so`` from the ``symtab``.
116

117
        Resolving Library Addresses:
118

119
            If we have a pointer into a dynamically-linked executable, we can
120
            leverage an internal linker structure called the `link map`_. This
121
            is a linked list structure which contains information about each
122
            loaded library, including its full path and base address.
123

124
            A pointer to the ``link map`` can be found in two ways.  Both are
125
            referenced from entries in the DYNAMIC_ array.
126

127
            - In non-RELRO binaries, a pointer is placed in the `.got.plt`_ area
128
              in the binary. This is marked by finding the DT_PLTGOT_ area in the
129
              binary.
130
            - In all binaries, a pointer can be found in the area described by
131
              the DT_DEBUG_ area.  This exists even in stripped binaries.
132

133
            For maximum flexibility, both mechanisms are used exhaustively.
134

135
    .. _symtab:    https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html
136
    .. _strtab:    https://refspecs.linuxbase.org/elf/gabi4+/ch4.strtab.html
137
    .. _.got.plt:  https://refspecs.linuxbase.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/specialsections.html
138
    .. _DYNAMIC:   http://www.sco.com/developers/gabi/latest/ch5.dynamic.html#dynamic_section
139
    .. _SYSV:      https://refspecs.linuxbase.org/elf/gabi4+/ch5.dynamic.html#hash
140
    .. _GNU:       https://blogs.oracle.com/solaris/post/gnu-hash-elf-sections
141
    .. _DT_DEBUG:  https://reverseengineering.stackexchange.com/questions/6525/elf-link-map-when-linked-as-relro
142
    .. _link map:  https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/link.h;h=eaca8028e45a859ac280301a6e955a14eed1b887;hb=HEAD#l84
143
    .. _DT_PLTGOT: https://refspecs.linuxfoundation.org/ELF/zSeries/lzsabi0_zSeries/x2251.html
144
    '''
145

146
    def __init__(self, leak, pointer=None, elf=None, libcdb=True):
1✔
147
        '''
148
        Instantiates an object which can resolve symbols in a running binary
149
        given a :class:`pwnlib.memleak.MemLeak` leaker and a pointer inside
150
        the binary.
151

152
        Arguments:
153
            leak(MemLeak): Instance of pwnlib.memleak.MemLeak for leaking memory
154
            pointer(int):  A pointer into a loaded ELF file
155
            elf(str,ELF):  Path to the ELF file on disk, or a loaded :class:`pwnlib.elf.ELF`.
156
            libcdb(bool):  Attempt to use libcdb to speed up libc lookups
157
        '''
158
        self.libcdb    = libcdb
1✔
159
        self._elfclass = None
1✔
160
        self._elftype  = None
1✔
161
        self._link_map = None
1✔
162
        self._waitfor  = None
1✔
163
        self._bases    = {}
1✔
164
        self._dynamic  = None
1✔
165
        self.elf = None
1✔
166

167
        if elf:
1✔
168
            path = elf
1✔
169
            if isinstance(elf, ELF):
1!
170
                path = elf.path
1✔
171

172
            # Load a fresh copy of the ELF
173
            with context.local(log_level='error'):
1✔
174
                w = self.waitfor("Loading from %r" % path)
1✔
175
                self.elf = ELF(path)
1✔
176
                w.success("[LOADED]")
1✔
177

178
        if not (pointer or (elf and elf.address)):
1!
NEW
179
            log.error("Must specify either a pointer into a module and/or an ELF file with a valid base address")
×
180

181
        pointer = pointer or elf.address
1✔
182

183
        if not isinstance(leak, MemLeak):
1!
NEW
184
            leak = MemLeak(leak)
×
185

186
        if not elf:
1✔
187
            log.warn_once("No ELF provided.  Leaking is much faster if you have a copy of the ELF being leaked.")
1✔
188

189
        self.leak    = leak
1✔
190
        self.libbase = self._find_base(pointer or elf.address)
1✔
191

192
        if elf:
1✔
193
            self._elftype = self.elf.elftype
1✔
194
            self._elfclass = self.elf.elfclass
1✔
195
            self.elf.address = self.libbase
1✔
196
            self._dynamic = self.elf.get_section_by_name('.dynamic').header.sh_addr
1✔
197
            self._dynamic = self._make_absolute_ptr(self._dynamic) 
1✔
198

199
    @classmethod
1✔
200
    def for_one_lib_only(cls, leak, ptr):
1✔
201
        return cls(leak, ptr)
×
202

203
    @classmethod
1✔
204
    def from_lib_ptr(cls, leak, ptr):
1✔
NEW
205
        return cls(leak, ptr)
×
206

207
    @staticmethod
1✔
208
    def find_base(leak, ptr):
1✔
209
        """Given a :class:`pwnlib.memleak.MemLeak` object and a pointer into a
210
        library, find its base address.
211
        """
NEW
212
        return DynELF(leak, ptr).libbase
×
213

214
    @property
1✔
215
    def elfclass(self):
1✔
216
        """32 or 64"""
217
        if not self._elfclass:
1✔
218
            elfclass = self.leak.field(self.libbase, elf.Elf_eident.EI_CLASS)
1✔
219
            self._elfclass =  {constants.ELFCLASS32: 32,
1✔
220
                              constants.ELFCLASS64: 64}[elfclass]
221
        return self._elfclass
1✔
222

223
    @property
1✔
224
    def elftype(self):
1✔
225
        """e_type from the elf header. In practice the value will almost always
226
        be 'EXEC' or 'DYN'. If the value is architecture-specific (between
227
        ET_LOPROC and ET_HIPROC) or invalid, KeyError is raised.
228
        """
229
        if not self._elftype:
1✔
230
            Ehdr  = {32: elf.Elf32_Ehdr, 64: elf.Elf64_Ehdr}[self.elfclass]
1✔
231
            elftype = self.leak.field(self.libbase, Ehdr.e_type)
1✔
232
            self._elftype = {constants.ET_NONE: 'NONE',
1✔
233
                             constants.ET_REL: 'REL',
234
                             constants.ET_EXEC: 'EXEC',
235
                             constants.ET_DYN: 'DYN',
236
                             constants.ET_CORE: 'CORE'}[elftype]
237
        return self._elftype
1✔
238

239
    @property
1✔
240
    def link_map(self):
1✔
241
        """Pointer to the runtime link_map object"""
242
        if not self._link_map:
1✔
243
            self._link_map = self._find_linkmap()
1✔
244
        return self._link_map
1✔
245

246
    @property
1✔
247
    def dynamic(self):
1✔
248
        """
249
        Returns:
250
            Pointer to the ``.DYNAMIC`` area.
251
        """
252
        if not self._dynamic:
1!
NEW
253
            self._dynamic = self._find_dynamic_phdr()
×
254
        return self._dynamic
1✔
255

256
    def _find_base(self, ptr):
1✔
257
        page_size = 0x1000
1✔
258
        page_mask = ~(page_size - 1)
1✔
259

260
        ptr &= page_mask
1✔
261
        w = None
1✔
262

263
        while True:
1✔
264
            if self.leak.compare(ptr, b'\x7fELF'):
1✔
265
                break
1✔
266

267
            # See if we can short circuit the search
268
            fast = self._find_base_optimized(ptr)
1✔
269
            if fast:
1!
270
                ptr = fast
1✔
271
                continue
1✔
272

273
            ptr -= page_size
×
274

275
            if ptr < 0:
×
276
                raise ValueError("Address is negative, something is wrong!")
×
277

278
            # Defer creating the spinner in the event that 'ptr'
279
            # is already the base address
NEW
280
            w = w or self.waitfor("Finding base address")
×
281
            self.status('%#x' % ptr)
×
282

283
        # If we created a spinner, print the success message
284
        if w:
1!
NEW
285
            self.success('%#x' % ptr)
×
286

287
        return ptr
1✔
288

289
    def _find_base_optimized(self, ptr):
1✔
290
        if not self.elf:
1!
291
            return None
×
292

293
        # If we have an ELF< we can probably speed this up a little bit?
294
        # Note that we add +0x20 onto the offset in order to avoid needing
295
        # to leak any bytes which contain '\r\n\t\b '
296
        ptr += 0x20
1✔
297
        data = self.leak.n(ptr, 32)
1✔
298
        if not data:
1!
299
            return None
×
300

301
        # Do not permit multiple matches
302
        matches = list(self.elf.search(data))
1✔
303
        if len(matches) != 1:
1!
304
            return None
×
305

306
        candidate = matches[0]
1✔
307
        candidate -= self.elf.address
1✔
308

309
        # The match should have the same page-alignment as our leaked data.
310
        if candidate & 0xfff != 0x20:
1!
311
            return None
×
312

313
        # Adjust based on the original pointer we got, and the ELF's address.
314
        ptr -= candidate
1✔
315
        return ptr
1✔
316

317
    def _find_dynamic_phdr(self):
1✔
318
        """
319
        Returns the address of the first Program Header with the type
320
        PT_DYNAMIC.
321
        """
322
        leak  = self.leak
×
323
        base  = self.libbase
×
324

325
        #First find PT_DYNAMIC
326
        Ehdr  = {32: elf.Elf32_Ehdr, 64: elf.Elf64_Ehdr}[self.elfclass]
×
327
        Phdr  = {32: elf.Elf32_Phdr, 64: elf.Elf64_Phdr}[self.elfclass]
×
328

329
        self.status("PT_DYNAMIC")
×
330

331
        phead = base + leak.field(base, Ehdr.e_phoff)
×
332
        self.status("PT_DYNAMIC header = %#x" % phead)
×
333

NEW
334
        phnum = leak.field(base, Ehdr.e_phnum)
×
335
        self.status("PT_DYNAMIC count = %#x" % phnum)
×
336

337
        for i in range(phnum):
×
338
            if leak.field_compare(phead, Phdr.p_type, constants.PT_DYNAMIC):
×
339
                break
×
340
            phead += sizeof(Phdr)
×
341
        else:
342
            self.failure("Could not find Program Header of type PT_DYNAMIC")
×
343
            return None
×
344

345
        dynamic = leak.field(phead, Phdr.p_vaddr)
×
346
        self.status("PT_DYNAMIC @ %#x" % dynamic)
×
347

348
        dynamic = self._make_absolute_ptr(dynamic)
×
349

350
        return dynamic
×
351

352
    def _find_dt_optimized(self, name):
1✔
353
        """
354
        Find an entry in the DYNAMIC array through an ELF
355

356
        Arguments:
357
            name(str): Name of the tag to find ('DT_DEBUG', 'DT_PLTGOT', ...)
358

359
        Returns:
360
            Pointer to the data described by the specified entry.
361
        """
362
        if not self.elf:
1✔
363
            return None
1✔
364

365
        ptr = self.elf.dynamic_value_by_tag(name)
1✔
366
        if ptr:
1!
367
            ptr = self._make_absolute_ptr(ptr)
1✔
368
            self.success("Found %s at %#x" % (name, ptr))
1✔
369
            return ptr
1✔
370
        return None
×
371

372

373
    def _find_dt(self, tag):
1✔
374
        """
375
        Find an entry in the DYNAMIC array.
376

377
        Arguments:
378
            tag(int): Single tag to find
379

380
        Returns:
381
            Pointer to the data described by the specified entry.
382
        """
383
        base    = self.libbase
1✔
384
        dynamic = self.dynamic
1✔
385
        leak    = self.leak
1✔
386
        name    = next(k for k,v in ENUM_D_TAG.items() if v == tag)
1✔
387

388
        # Read directly from the ELF if possible
389
        ptr = self._find_dt_optimized(name)
1✔
390
        if ptr:
1✔
391
            return ptr
1✔
392

393
        Dyn = {32: elf.Elf32_Dyn,    64: elf.Elf64_Dyn}     [self.elfclass]
1✔
394

395
        # Found the _DYNAMIC program header, now find PLTGOT entry in it
396
        # An entry with a DT_NULL tag marks the end of the DYNAMIC array.
397
        while not leak.field_compare(dynamic, Dyn.d_tag, constants.DT_NULL):
1!
398
            if leak.field_compare(dynamic, Dyn.d_tag, tag):
1✔
399
                break
1✔
400
            dynamic += sizeof(Dyn)
1✔
401
        else:
402
            self.failure("Could not find tag %s" % name)
×
403
            return None
×
404

405
        ptr = leak.field(dynamic, Dyn.d_ptr)
1✔
406

407
        ptr = self._make_absolute_ptr(ptr)
1✔
408
        self.status("Found %s at %#x" % (name, ptr))
1✔
409

410
        return ptr
1✔
411

412

413
    def _find_linkmap(self, pltgot=None, debug=None):
1✔
414
        """
415
        The linkmap is a chained structure created by the loader at runtime
416
        which contains information on the names and load addresses of all
417
        libraries.
418

419
        For non-RELRO binaries, a pointer to this is stored in the .got.plt
420
        area.
421

422
        For RELRO binaries, a pointer is additionally stored in the DT_DEBUG
423
        area.
424
        """
425
        w = self.waitfor("Finding linkmap")
1✔
426

427
        Got     = {32: elf.Elf_i386_GOT, 64: elf.Elf_x86_64_GOT}[self.elfclass]
1✔
428
        r_debug = {32: elf.Elf32_r_debug, 64: elf.Elf64_r_debug}[self.elfclass]
1✔
429

430
        linkmap = None
1✔
431

432
        if not pltgot:
1!
433
            w.status("Finding linkmap: DT_PLTGOT")
1✔
434
            pltgot = self._find_dt(constants.DT_PLTGOT)
1✔
435

436
        if pltgot:
1!
437
            w.status("GOT.linkmap")
1✔
438
            linkmap = self.leak.field(pltgot, Got.linkmap)
1✔
439
            w.status("GOT.linkmap %#x" % linkmap)
1✔
440

441
        if not linkmap:
1!
442
            debug = debug or self._find_dt(constants.DT_DEBUG)
×
443
            if debug:
×
444
                w.status("r_debug.linkmap")
×
445
                linkmap = self.leak.field(debug, r_debug.r_map)
×
NEW
446
                w.status("r_debug.linkmap %#x" % linkmap)
×
447

448
        if not linkmap:
1!
449
            w.failure("Could not find DT_PLTGOT or DT_DEBUG")
×
450
            return None
×
451

452
        linkmap = self._make_absolute_ptr(linkmap)
1✔
453

454
        w.success('%#x' % linkmap)
1✔
455
        return linkmap
1✔
456

457
    def waitfor(self, msg):
1✔
458
        if not self._waitfor:
1✔
459
            self._waitfor = log.waitfor(msg)
1✔
460
        else:
461
            self.status(msg)
1✔
462
        return self._waitfor
1✔
463

464
    def failure(self, msg):
1✔
465
        if not self._waitfor:
×
NEW
466
            log.failure(msg)
×
467
        else:
NEW
468
            self._waitfor.failure(msg)
×
469
            self._waitfor = None
×
470

471
    def success(self, msg):
1✔
472
        if not self._waitfor:
1✔
473
            log.success(msg)
1✔
474
        else:
475
            self._waitfor.success(msg)
1✔
476
            self._waitfor = None
1✔
477

478
    def status(self, msg):
1✔
479
        if not self._waitfor:
1✔
480
            log.info(msg)
1✔
481
        else:
482
            self._waitfor.status(msg)
1✔
483

484
    @property
1✔
485
    def libc(self):
1✔
486
        """libc(self) -> ELF
487

488
        Leak the Build ID of the remote libc.so, download the file,
489
        and load an ``ELF`` object with the correct base address.
490

491
        Returns:
492
            An ELF object, or None.
493
        """
494
        libc = b'libc.so'
×
495

496
        with self.waitfor('Downloading libc'):
×
497
            dynlib = self._dynamic_load_dynelf(libc)
×
498

499
            self.status("Trying lookup based on Build ID")
×
500
            build_id = dynlib._lookup_build_id(libc)
×
501

502
            if not build_id:
×
503
                return None
×
504

505
            self.status("Trying lookup based on Build ID: %s" % build_id)
×
506
            path = libcdb.search_by_build_id(build_id)
×
507

508
            if not path:
×
509
                return None
×
510

511
            libc = ELF(path)
×
512
            libc.address = dynlib.libbase
×
513
            return libc
×
514

515
    def lookup (self, symb = None, lib = None):
1✔
516
        """lookup(symb = None, lib = None) -> int
517

518
        Find the address of ``symbol``, which is found in ``lib``.
519

520
        Arguments:
521
            symb(str): Named routine to look up
522
              If omitted, the base address of the library will be returned.
523
            lib(str): Substring to match for the library name.
524
              If omitted, the current library is searched.
525
              If set to ``'libc'``, ``'libc.so'`` is assumed.
526

527
        Returns:
528
            Address of the named symbol, or :const:`None`.
529
        """
530
        result = None
1✔
531

532
        if lib == 'libc':
1✔
533
            lib = 'libc.so'
1✔
534

535
        if symb:
1!
536
            symb = _need_bytes(symb, min_wrong=0x80)
1✔
537

538
        #
539
        # Get a pretty name for the symbol to show the user
540
        #
541
        if symb and lib:
1✔
542
            pretty = '%r in %r' % (symb, lib)
1✔
543
        else:
544
            pretty = repr(symb or lib)
1✔
545

546
        if not pretty:
1!
547
            self.failure("Must specify a library or symbol")
×
548

549
        self.waitfor('Resolving %s' % pretty)
1✔
550

551
        #
552
        # If we are loading from a different library, create
553
        # a DynELF instance for it.
554
        #
555
        if lib is not None: dynlib = self._dynamic_load_dynelf(lib)
1✔
556
        else:   dynlib = self
1✔
557

558
        if dynlib is None:
1!
559
            log.failure("Could not find %r", lib)
×
560
            return None
×
561

562
        #
563
        # If we are resolving a symbol in the library, find it.
564
        #
565
        if symb and self.libcdb:
1!
566
            # Try a quick lookup by build ID
567
            self.status("Trying lookup based on Build ID")
×
568
            build_id = dynlib._lookup_build_id(lib=lib)
×
569
            if build_id:
×
570
                log.info("Trying lookup based on Build ID: %s", build_id)
×
571
                path = libcdb.search_by_build_id(build_id)
×
572
                if path:
×
573
                    with context.local(log_level='error'):
×
574
                        e = ELF(path)
×
575
                        e.address = dynlib.libbase
×
576
                        result = e.symbols[symb]
×
577
        if symb and not result:
1!
578
            self.status("Trying remote lookup")
1✔
579
            result = dynlib._lookup(symb)
1✔
580
        if not symb:
1!
581
            result = dynlib.libbase
×
582

583
        #
584
        # Did we win?
585
        #
586
        if result: self.success("%#x" % result)
1!
587
        else:      self.failure("Could not find %s" % pretty)
×
588

589
        return result
1✔
590

591
    def bases(self):
1✔
592
        '''Resolve base addresses of all loaded libraries.
593

594
        Return a dictionary mapping library path to its base address.
595
        '''
596
        if not self._bases:
×
597
            if self.link_map is None:
×
598
                self.failure("Cannot determine bases without linkmap")
×
599
                return {}
×
600
                
601
            leak    = self.leak
×
602
            LinkMap = {32: elf.Elf32_Link_Map, 64: elf.Elf64_Link_Map}[self.elfclass]
×
603

604
            cur = self.link_map
×
605

606
            # make sure we rewind to the beginning!
607
            while leak.field(cur, LinkMap.l_prev):
×
608
                cur = leak.field(cur, LinkMap.l_prev)
×
609

610
            while cur:
×
611
                p_name = leak.field(cur, LinkMap.l_name)
×
612
                name   = leak.s(p_name)
×
613
                addr   = leak.field(cur, LinkMap.l_addr)
×
614
                cur    = leak.field(cur, LinkMap.l_next)
×
615

616
                log.debug('Found %r @ %#x', name, addr)
×
617

618
                self._bases[name] = addr
×
619

620
        return self._bases
×
621

622
    def _dynamic_load_dynelf(self, libname):
1✔
623
        """_dynamic_load_dynelf(libname) -> DynELF
624

625
        Looks up information about a loaded library via the link map.
626

627
        Arguments:
628
            libname(str):  Name of the library to resolve, or a substring (e.g. 'libc.so')
629

630
        Returns:
631
            A DynELF instance for the loaded library, or None.
632
        """
633
        cur     = self.link_map
1✔
634
        leak    = self.leak
1✔
635
        LinkMap = {32: elf.Elf32_Link_Map, 64: elf.Elf64_Link_Map}[self.elfclass]
1✔
636

637
        # make sure we rewind to the beginning!
638
        while leak.field(cur, LinkMap.l_prev):
1!
639
            cur = leak.field(cur, LinkMap.l_prev)
×
640

641
        libname = _need_bytes(libname, 2, 0x80)
1✔
642

643
        while cur:
1!
644
            self.status("link_map entry %#x" % cur)
1✔
645
            p_name = leak.field(cur, LinkMap.l_name)
1✔
646
            name   = leak.s(p_name)
1✔
647

648
            if libname in name:
1✔
649
                break
1✔
650

651
            if name:
1✔
652
                self.status('Skipping %r' % name)
1✔
653

654
            cur = leak.field(cur, LinkMap.l_next)
1✔
655
        else:
656
            self.failure("Could not find library with name containing %r" % libname)
×
657
            return None
×
658

659
        libbase = leak.field(cur, LinkMap.l_addr)
1✔
660

661
        self.status("Resolved library %r at %#x" % (libname, libbase))
1✔
662

663
        lib = DynELF(leak, libbase)
1✔
664
        lib._dynamic = leak.field(cur, LinkMap.l_ld)
1✔
665
        lib._waitfor = self._waitfor
1✔
666
        return lib
1✔
667

668
    def _rel_lookup(self, symb, strtab=None, symtab=None, jmprel=None):
1✔
669
        """Performs slower symbol lookup using DT_JMPREL(.rela.plt)"""
670
        leak = self.leak
×
671
        elf_obj = self.elf
×
672
        symb_name = symb.decode()
×
673

674
        # If elf is available look for the symbol in it
675
        if elf_obj and symb_name in elf_obj.symbols:
×
676
            self.success("Symbol '%s' found in ELF!" % symb_name)
×
677
            return elf_obj.symbols[symb_name]
×
678

679
        log.warning("Looking up symbol through DT_JMPREL. This might be slower...")
×
680

681

682
        strtab  = strtab or self._find_dt(constants.DT_STRTAB)
×
683
        symtab  = symtab or self._find_dt(constants.DT_SYMTAB)
×
684
        jmprel  = jmprel or self._find_dt(constants.DT_JMPREL) # .rela.plt
×
685

686
        strtab = self._make_absolute_ptr(strtab)
×
687
        symtab = self._make_absolute_ptr(symtab)
×
688
        jmprel = self._make_absolute_ptr(jmprel)
×
689

690
        w = self.waitfor("Looking for %s in .rel.plt" % symb)
×
691
        # We look for the symbol by iterating through each Elf64_Rel entry.
692
        # For each Elf64_Rel, get the Elf64_Sym for that entry
693
        # Then compare the Elf64_Sym.st_name with the symbol name
694
       
695
        Rel = {32: elf.Elf32_Rel, 64: elf.Elf64_Rel}[self.elfclass]
×
696
        Sym = {32: elf.Elf32_Sym, 64: elf.Elf64_Sym}[self.elfclass]
×
697

698
        rel_addr = jmprel
×
699
        rel_entry = None
×
700
        while True:
×
701
            rel_entry = leak.struct(rel_addr, Rel)
×
702

703
            # We ran out of entries in DT_JMPREL 
704
            if rel_entry.r_offset == 0:
×
705
                return None
×
706

707
            sym_idx = rel_entry.r_info >> 32 # might be different for 32-bit
×
708
            sym_entry_address = symtab + ( sym_idx * sizeof(Sym) )
×
709
            sym_str_off = leak.field(sym_entry_address, Sym.st_name)
×
710
            symb_str = leak.s(strtab+sym_str_off)
×
711

712
            if symb_str == symb:
×
713
                w.success("Found matching Elf64_Rel entry!")
×
714
                break
×
715

716
            rel_addr += sizeof(Rel)
×
717

718
        symbol_address = self._make_absolute_ptr(rel_entry.r_offset)
×
719

720
        return symbol_address
×
721

722

723

724
    def _lookup(self, symb):
1✔
725
        """Performs the actual symbol lookup within one ELF file."""
726
        leak = self.leak
1✔
727
        Dyn  = {32: elf.Elf32_Dyn, 64: elf.Elf64_Dyn}[self.elfclass]
1✔
728
        name = lambda tag: next(k for k,v in ENUM_D_TAG.items() if v == tag)
1✔
729

730
        self.status('.gnu.hash/.hash, .strtab and .symtab offsets')
1✔
731

732
        #
733
        # We need all three of the hash, string table, and symbol table.
734
        #
735
        hshtab  = self._find_dt(constants.DT_GNU_HASH)
1✔
736
        strtab  = self._find_dt(constants.DT_STRTAB)
1✔
737
        symtab  = self._find_dt(constants.DT_SYMTAB)
1✔
738

739
        # Assume GNU hash will hit, since it is the default for GCC.
740
        if hshtab:
1!
741
            hshtype = 'gnu'
1✔
742
        else:
743
            hshtab  = self._find_dt(constants.DT_HASH)
×
744
            hshtype = 'sysv'
×
745

746
        if not all([strtab, symtab, hshtab]):
1!
747
            self.failure("Could not find all tables")
×
748

749
        strtab = self._make_absolute_ptr(strtab)
1✔
750
        symtab = self._make_absolute_ptr(symtab)
1✔
751
        hshtab = self._make_absolute_ptr(hshtab)
1✔
752

753
        #
754
        # Perform the hash lookup
755
        #
756

757
        # Save off our real leaker in case we use the fake leaker
758
        real_leak = self.leak
1✔
759
        if self.elf:
1✔
760

761
            # Create a fake leaker which just leaks out of the 'loaded' ELF
762
            # However, we may load things which are outside of the ELF (e.g.
763
            # the linkmap or GOT) so we need to fall back on the real leak.
764
            @MemLeak
1✔
765
            def fake_leak(address):
1✔
766
                try:
1✔
767
                    return self.elf.read(address, 4)
1✔
768
                except ValueError:
×
769
                    return real_leak.b(address)
×
770
            # Use fake leaker since ELF is available
771
            self.leak = fake_leak
1✔
772

773
        routine = {'sysv': self._resolve_symbol_sysv,
1✔
774
                   'gnu':  self._resolve_symbol_gnu}[hshtype]
775
        resolved_addr = routine(self.libbase, symb, hshtab, strtab, symtab)
1✔
776

777
        if resolved_addr:
1!
778
            # Restore the original leaker
779
            self.leak = real_leak
1✔
780
            return resolved_addr
1✔
781

782
        # if symbol not found in GNU_Hash, try looking in JMPREL
783
        resolved_addr = self._rel_lookup(symb, strtab, symtab)
×
784

785
        # Restore the original leaker
786
        self.leak = real_leak
×
787

788
        return resolved_addr
×
789

790
    def _resolve_symbol_sysv(self, libbase, symb, hshtab, strtab, symtab):
1✔
791
        """
792
        Internal Documentation:
793
            See the ELF manual for more information.  Search for the phrase
794
            "A hash table of Elf32_Word objects supports symbol table access", or see:
795
            https://docs.oracle.com/cd/E19504-01/802-6319/6ia12qkfo/index.html#chapter6-48031
796

797
            .. code-block:: c
798

799
                struct Elf_Hash {
800
                    uint32_t nbucket;
801
                    uint32_t nchain;
802
                    uint32_t bucket[nbucket];
803
                    uint32_t chain[nchain];
804
                }
805

806
            You can force an ELF to use this type of symbol table by compiling
807
            with 'gcc -Wl,--hash-style=sysv'
808
        """
809
        self.status('.hash parms')
×
810
        leak       = self.leak
×
811
        Sym        = {32: elf.Elf32_Sym, 64: elf.Elf64_Sym}[self.elfclass]
×
812

813
        nbucket   = leak.field(hshtab, elf.Elf_HashTable.nbucket)
×
814
        bucketaddr = hshtab + sizeof(elf.Elf_HashTable)
×
815
        chain      = bucketaddr + (nbucket * 4)
×
816

817
        self.status('hashmap')
×
818
        hsh = sysv_hash(symb) % nbucket
×
819

820
        # Get the index out of the bucket for the hash we computed
821
        idx = leak.d(bucketaddr, hsh)
×
822

823
        while idx != constants.STN_UNDEF:
×
824
            # Look up the symbol corresponding to the specified index
825
            sym     = symtab + (idx * sizeof(Sym))
×
826
            symtype = leak.field(sym, Sym.st_info) & 0xf
×
827

828
            # We only care about functions
829
            if symtype == constants.STT_FUNC:
×
830

831
                # Leak the name of the function from the symbol table
832
                name = leak.s(strtab + leak.field(sym, Sym.st_name))
×
833

834
                # Make sure it matches the name of the symbol we were looking for.
835
                if name == symb:
×
836
                    #Bingo
837
                    addr = libbase + leak.field(sym, Sym.st_value)
×
838
                    return addr
×
839

840
                self.status("%r (hash collision)" % name)
×
841

842
            # The name did not match what we were looking for, or we assume
843
            # it did not since it was not a function.
844
            # Follow the chain for this particular hash.
845
            idx = leak.d(chain, idx)
×
846
        else:
847
            self.failure('Could not find a SYSV hash that matched %#x' % hsh)
×
848
            return None
×
849

850
    def _resolve_symbol_gnu(self, libbase, symb, hshtab, strtab, symtab):
1✔
851
        """
852
        Internal Documentation:
853
            The GNU hash structure is a bit more complex than the normal hash
854
            structure.
855

856
            Again, Oracle has good documentation.
857
            https://blogs.oracle.com/solaris/post/gnu-hash-elf-sections
858

859
            You can force an ELF to use this type of symbol table by compiling
860
            with 'gcc -Wl,--hash-style=gnu'
861
        """
862
        self.status('.gnu.hash parms')
1✔
863
        leak = self.leak
1✔
864
        Sym  = {32: elf.Elf32_Sym, 64: elf.Elf64_Sym}[self.elfclass]
1✔
865

866
        # The number of hash buckets (hash % nbuckets)
867
        nbuckets  = leak.field(hshtab, elf.GNU_HASH.nbuckets)
1✔
868

869
        # Index of the first accessible symbol in the hash table
870
        # Numbering doesn't start at zero, it starts at symndx
871
        symndx    = leak.field(hshtab, elf.GNU_HASH.symndx)
1✔
872

873
        # Number of things in the bloom filter.
874
        # We don't care about the contents, but we have to skip over it.
875
        maskwords = leak.field(hshtab, elf.GNU_HASH.maskwords)
1✔
876

877
        # Skip over the bloom filter to get to the buckets
878
        elfword = self.elfclass // 8
1✔
879
        buckets = hshtab + sizeof(elf.GNU_HASH) + (elfword * maskwords)
1✔
880

881
        # The chains come after the buckets
882
        chains  = buckets + (4 * nbuckets)
1✔
883

884
        self.status('hash chain index')
1✔
885

886
        # Hash the symbol, find its bucket
887
        hsh    = gnu_hash(symb)
1✔
888
        bucket = hsh % nbuckets
1✔
889

890
        # Get the first index in the chain for that bucket
891
        ndx    = leak.d(buckets, bucket)
1✔
892
        if ndx == 0:
1!
893
            self.failure('Empty chain')
×
894
            return None
×
895

896
        # Find the start of the chain, taking into account that numbering
897
        # effectively starts at 'symndx' within the chains.
898
        chain  = chains + 4 * (ndx - symndx)
1✔
899

900
        self.status('hash chain')
1✔
901

902
        # Iteratively get the I'th entry from the hash chain, until we find
903
        # one that matches.
904
        i    = 0
1✔
905
        hsh &= ~1
1✔
906

907
        # The least significant bit is used as a stopper bit.
908
        # It is set to 1 when a symbol is the last symbol in a given hash chain.
909
        hsh2 = 0
1✔
910
        while not hsh2 & 1:
1!
911
            hsh2 = leak.d(chain, i)
1✔
912
            if hsh == (hsh2 & ~1):
1✔
913
                # Check for collision on hash values
914
                sym  = symtab + sizeof(Sym) * (ndx + i)
1✔
915
                name = leak.s(strtab + leak.field(sym, Sym.st_name))
1✔
916

917
                if name == symb:
1!
918
                    # No collision, get offset and calculate address
919
                    offset = leak.field(sym, Sym.st_value)
1✔
920
                    addr   = offset + libbase
1✔
921
                    return addr
1✔
922

923
                self.status("%r (hash collision)" % name)
×
924

925
            # Collision or no match, continue to the next item
926
            i += 1
1✔
927
        else:
928
            self.failure('Could not find a GNU hash that matched %#x' % hsh)
×
929
            return None
×
930

931
    def _lookup_build_id(self, lib = None):
1✔
932

933
        libbase = self.libbase
×
934
        if not self.link_map:
×
935
            self.status("No linkmap found")
×
936
            return None
×
937

938
        if lib is not None:
×
939
            libbase = self.lookup(symb = None, lib = lib)
×
940

941
        if not libbase:
×
942
            self.status("Couldn't find libc base")
×
943
            return None
×
944

945
        for offset in libcdb.get_build_id_offsets():
×
946
            address = libbase + offset
×
947
            if self.leak.compare(address + 0xC, b"GNU\x00"):
×
948
                return enhex(b''.join(self.leak.raw(address + 0x10, 20)))
×
949
            else:
950
                self.status("Build ID not found at offset %#x" % offset)
×
951
                pass
×
952

953
    def _make_absolute_ptr(self, ptr_or_offset):
1✔
954
        """For shared libraries (or PIE executables), many ELF fields may
955
        contain offsets rather than actual pointers. If the ELF type is 'DYN',
956
        the argument may be an offset. It will not necessarily be an offset,
957
        because the run-time linker may have fixed it up to be a real pointer
958
        already. In this case an educated guess is made, and the ELF base
959
        address is added to the value if it is determined to be an offset.
960
        """
961
        if_ptr = ptr_or_offset
1✔
962
        if_offset = ptr_or_offset + self.libbase
1✔
963

964
        # if the ELF type is not DYN, the value is a pointer
965

966
        if self.elftype != 'DYN':
1!
967
            return if_ptr
×
968

969
        # if the ELF type may be DYN, guess
970

971
        if 0 < ptr_or_offset < self.libbase:
1✔
972
            return if_offset
1✔
973
        else:
974
            return if_ptr
1✔
975

976
    def stack(self):
1✔
977
        """Finds a pointer to the stack via __environ, which is an exported
978
        symbol in libc, which points to the environment block.
979
        """
980
        symbols = ['environ', '_environ', '__environ']
1✔
981

982
        for symbol in symbols:
1!
983
            environ = self.lookup(symbol, 'libc')
1✔
984

985
            if environ:
1!
986
                break
1✔
987
        else:
988
            log.error("Could not find the stack")
×
989

990
        stack = self.leak.p(environ)
1✔
991

992
        self.success('*environ: %#x' % stack)
1✔
993

994
        return stack
1✔
995

996
    def heap(self):
1✔
997
        """Finds the beginning of the heap via __curbrk, which is an exported
998
        symbol in the linker, which points to the current brk.
999
        """
1000
        curbrk = self.lookup('__curbrk', 'libc')
1✔
1001
        brk    = self.leak.p(curbrk)
1✔
1002

1003
        self.success('*curbrk: %#x' % brk)
1✔
1004

1005
        return brk
1✔
1006

1007
    def _find_mapped_pages(self, readonly = False, page_size = 0x1000):
1✔
1008
        """
1009
        A generator of all mapped pages, as found using the Program Headers.
1010

1011
        Yields tuples of the form: (virtual address, memory size)
1012
        """
1013
        leak  = self.leak
×
1014
        base  = self.libbase
×
1015

1016
        Ehdr  = {32: elf.Elf32_Ehdr, 64: elf.Elf64_Ehdr}[self.elfclass]
×
1017
        Phdr  = {32: elf.Elf32_Phdr, 64: elf.Elf64_Phdr}[self.elfclass]
×
1018

1019
        phead = base + leak.field(base, Ehdr.e_phoff)
×
1020
        phnum = leak.field(base, Ehdr.e_phnum)
×
1021

1022
        for i in range(phnum):
×
1023
            if leak.field_compare(phead, Phdr.p_type, constants.PT_LOAD) :
×
1024
                # the interesting pages are those that are aligned to PAGE_SIZE
1025
                if leak.field_compare(phead, Phdr.p_align, page_size) and \
×
1026
                    (readonly or leak.field(phead, Phdr.p_flags) & 0x02 != 0):
1027
                    vaddr = leak.field(phead, Phdr.p_vaddr)
×
1028
                    memsz = leak.field(phead, Phdr.p_memsz)
×
1029
                    # fix relative offsets
1030
                    if vaddr < base :
×
1031
                        vaddr += base
×
1032
                    yield vaddr, memsz
×
1033
            phead += sizeof(Phdr)
×
1034

1035
    def dump(self, libs = False, readonly = False):
1✔
1036
        """dump(libs = False, readonly = False)
1037

1038
        Dumps the ELF's memory pages to allow further analysis.
1039

1040
        Arguments:
1041
            libs(bool, optional): True if should dump the libraries too (False by default)
1042
            readonly(bool, optional): True if should dump read-only pages (False by default)
1043

1044
        Returns:
1045
            a dictionary of the form: { address : bytes }
1046
        """
1047
        leak      = self.leak
×
1048
        page_size = 0x1000
×
1049
        pages     = {}
×
1050

1051
        for vaddr, memsz in self._find_mapped_pages(readonly, page_size) :
×
1052
            offset    = vaddr % page_size
×
1053
            if offset != 0 :
×
1054
                memsz += offset
×
1055
                vaddr -= offset
×
1056
            memsz += (page_size - (memsz % page_size)) % page_size
×
1057
            pages[vaddr] = leak.n(vaddr, memsz)
×
1058

1059
        if libs:
×
1060
            for lib_name in self.bases():
×
1061
                if len(lib_name) == 0:
×
1062
                    continue
×
1063
                dyn_lib = self._dynamic_load_dynelf(lib_name)
×
1064
                if dyn_lib is not None:
×
1065
                    pages.update(dyn_lib.dump(readonly = readonly))
×
1066

1067
        return pages
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc