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

Gallopsled / pwntools / 7250413177

18 Dec 2023 03:44PM UTC coverage: 71.866% (-2.7%) from 74.55%
7250413177

Pull #2297

github

web-flow
Merge fbc1d8e0b into c7649c95e
Pull Request #2297: add "retguard" property and display it with checksec

4328 of 7244 branches covered (0.0%)

5 of 6 new or added lines in 1 file covered. (83.33%)

464 existing lines in 9 files now uncovered.

12381 of 17228 relevant lines covered (71.87%)

0.72 hits per line

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

74.29
/pwnlib/libcdb.py
1
"""
2
Fetch a LIBC binary based on some heuristics.
3
"""
4
from __future__ import absolute_import
1✔
5
from __future__ import division
1✔
6

7
import os
1✔
8
import six
1✔
9
import tempfile
1✔
10

11
from pwnlib.context import context
1✔
12
from pwnlib.elf import ELF
1✔
13
from pwnlib.log import getLogger
1✔
14
from pwnlib.tubes.process import process
1✔
15
from pwnlib.util.fiddling import enhex
1✔
16
from pwnlib.util.misc import read
1✔
17
from pwnlib.util.misc import which
1✔
18
from pwnlib.util.misc import write
1✔
19
from pwnlib.util.web import wget
1✔
20

21
log = getLogger(__name__)
1✔
22

23
HASHES = ['build_id', 'sha1', 'sha256', 'md5']
1✔
24
DEBUGINFOD_SERVERS = [
1✔
25
    'https://debuginfod.elfutils.org/',
26
]
27

28
if 'DEBUGINFOD_URLS' in os.environ:
1!
29
    urls = os.environ['DEBUGINFOD_URLS'].split(' ')
×
30
    DEBUGINFOD_SERVERS = urls + DEBUGINFOD_SERVERS
×
31

32
# https://gitlab.com/libcdb/libcdb wasn't updated after 2019,
33
# but still is a massive database of older libc binaries.
34
def provider_libcdb(hex_encoded_id, hash_type):
1✔
35
    # Deferred import because it's slow
36
    import requests
1✔
37
    from six.moves import urllib
1✔
38

39
    # Build the URL using the requested hash type
40
    url_base = "https://gitlab.com/libcdb/libcdb/raw/master/hashes/%s/" % hash_type
1✔
41
    url      = urllib.parse.urljoin(url_base, hex_encoded_id)
1✔
42

43
    data     = b""
1✔
44
    log.debug("Downloading data from LibcDB: %s", url)
1✔
45
    try:
1✔
46
        while not data.startswith(b'\x7fELF'):
1✔
47
            data = wget(url, timeout=20)
1✔
48

49
            if not data:
1✔
50
                log.warn_once("Could not fetch libc for %s %s from libcdb", hash_type, hex_encoded_id)
1✔
51
                break
1✔
52
            
53
            # GitLab serves up symlinks with
54
            if data.startswith(b'..'):
1✔
55
                url = os.path.dirname(url) + '/'
1✔
56
                url = urllib.parse.urljoin(url.encode('utf-8'), data)
1✔
57
    except requests.RequestException as e:
×
58
        log.warn_once("Failed to fetch libc for %s %s from libcdb: %s", hash_type, hex_encoded_id, e)
×
59
    return data
1✔
60

61
def query_libc_rip(params):
1✔
62
    # Deferred import because it's slow
63
    import requests
1✔
64

65
    url = "https://libc.rip/api/find"
1✔
66
    try:
1✔
67
        result = requests.post(url, json=params, timeout=20)
1✔
68
        result.raise_for_status()
1✔
69
        if result.status_code != 200:
1!
70
            log.debug("Error: %s", result.text)
×
71
            return None
×
72
        return result.json()
1✔
73
    except requests.RequestException as e:
×
74
        log.warn_once("Failed to fetch libc info from libc.rip: %s", e)
×
75
        return None
×
76

77
# https://libc.rip/
78
def provider_libc_rip(hex_encoded_id, hash_type):
1✔
79
    # Build the request for the hash type
80
    # https://github.com/niklasb/libc-database/blob/master/searchengine/api.yml
81
    if hash_type == 'build_id':
1✔
82
        hash_type = 'buildid'
1✔
83
    params = {hash_type: hex_encoded_id}
1✔
84

85
    libc_match = query_libc_rip(params)
1✔
86
    if not libc_match:
1✔
87
        log.warn_once("Could not find libc info for %s %s on libc.rip", hash_type, hex_encoded_id)
1✔
88
        return None
1✔
89

90
    if len(libc_match) > 1:
1✔
91
        log.debug("Received multiple matches. Choosing the first match and discarding the others.")
1✔
92
        log.debug("%r", libc_match)
1✔
93

94
    url = libc_match[0]['download_url']
1✔
95
    log.debug("Downloading data from libc.rip: %s", url)
1✔
96
    data = wget(url, timeout=20)
1✔
97

98
    if not data:
1!
99
        log.warn_once("Could not fetch libc binary for %s %s from libc.rip", hash_type, hex_encoded_id)
×
100
        return None
×
101
    return data
1✔
102

103
PROVIDERS = [provider_libcdb, provider_libc_rip]
1✔
104

105
def search_by_hash(hex_encoded_id, hash_type='build_id', unstrip=True):
1✔
106
    assert hash_type in HASHES, hash_type
1✔
107

108
    # Ensure that the libcdb cache directory exists
109
    cache, cache_valid = _check_elf_cache('libcdb', hex_encoded_id, hash_type)
1✔
110
    if cache_valid:
1!
111
        return cache
×
112

113
    # Run through all available libc database providers to see if we have a match.
114
    for provider in PROVIDERS:
1✔
115
        data = provider(hex_encoded_id, hash_type)
1✔
116
        if data and data.startswith(b'\x7FELF'):
1✔
117
            break
1✔
118

119
    if not data:
1✔
120
        log.warn_once("Could not find libc for %s %s anywhere", hash_type, hex_encoded_id)
1✔
121

122
    # Save whatever we got to the cache
123
    write(cache, data or b'')
1✔
124

125
    # Return ``None`` if we did not get a valid ELF file
126
    if not data or not data.startswith(b'\x7FELF'):
1✔
127
        return None
1✔
128

129
    # Try to find debug info for this libc.
130
    if unstrip:
1✔
131
        unstrip_libc(cache)
1✔
132

133
    return cache
1✔
134

135
def _search_debuginfo_by_hash(base_url, hex_encoded_id):
1✔
136
    # Deferred import because it's slow
137
    import requests
1✔
138
    from six.moves import urllib
1✔
139

140
    # Check if we tried this buildid before.
141
    cache, cache_valid = _check_elf_cache('libcdb_dbg', hex_encoded_id, 'build_id')
1✔
142
    if cache_valid:
1!
143
        return cache
×
144

145
    # Try to find separate debuginfo.
146
    url  = '/buildid/{}/debuginfo'.format(hex_encoded_id)
1✔
147
    url  = urllib.parse.urljoin(base_url, url)
1✔
148
    data = b""
1✔
149
    log.debug("Downloading data from debuginfod: %s", url)
1✔
150
    try:
1✔
151
        data = wget(url, timeout=20)
1✔
UNCOV
152
    except requests.RequestException as e:
×
UNCOV
153
        log.warn_once("Failed to fetch libc debuginfo for build_id %s from %s: %s", hex_encoded_id, base_url, e)
×
154
    
155
    # Save whatever we got to the cache
156
    write(cache, data or b'')
1✔
157

158
    # Return ``None`` if we did not get a valid ELF file
159
    if not data or not data.startswith(b'\x7FELF'):
1✔
160
        log.warn_once("Could not fetch libc debuginfo for build_id %s from %s", hex_encoded_id, base_url)
1✔
161
        return None
1✔
162

163
    return cache
1✔
164

165
def _check_elf_cache(cache_type, hex_encoded_id, hash_type):
1✔
166
    """
167
    Check if there already is an ELF file for this hash in the cache.
168

169
    >>> cache, _ = _check_elf_cache('libcdb', '2d1c5e0b85cb06ff47fa6fa088ec22cb6e06074e', 'build_id')
170
    >>> os.unlink(cache) if os.path.exists(cache)
171
    >>> filename = search_by_hash('2d1c5e0b85cb06ff47fa6fa088ec22cb6e06074e', 'build_id', unstrip=False)
172
    >>> hex(ELF(filename).symbols.read)
173
    '0xe56c0'
174
    >>> filename == cache
175
    True
176
    """
177
    # Ensure that the cache directory exists
178
    cache_dir = os.path.join(context.cache_dir, cache_type, hash_type)
1✔
179

180
    if not os.path.isdir(cache_dir):
1✔
181
        os.makedirs(cache_dir)
1✔
182

183
    # If we already downloaded the file, and it looks even passingly like
184
    # a valid ELF file, return it.
185
    cache = os.path.join(cache_dir, hex_encoded_id)
1✔
186

187
    if not os.path.exists(cache):
1✔
188
        return cache, False
1✔
189
    
190
    log.debug("Found existing cached ELF at %r", cache)
1✔
191

192
    data = read(cache)
1✔
193
    if not data.startswith(b'\x7FELF'):
1!
194
        log.info_once("Skipping unavailable ELF %s", hex_encoded_id)
1✔
195
        return cache, False
1✔
196

197
    log.info_once("Using cached data from %r", cache)
×
198
    return cache, True
×
199

200
def unstrip_libc(filename):
1✔
201
    """
202
    Given a path to a libc binary, attempt to download matching debug info
203
    and add them back to the given binary.
204

205
    This modifies the given file.
206

207
    Arguments:
208
        filename(str):
209
            Path to the libc binary to unstrip.
210

211
    Returns:
212
        :const:`True` if binary was unstripped, :const:`False` otherwise.
213

214
    Examples:
215
        >>> filename = search_by_build_id('69389d485a9793dbe873f0ea2c93e02efaa9aa3d', unstrip=False)
216
        >>> libc = ELF(filename)
217
        >>> 'main_arena' in libc.symbols
218
        False
219
        >>> unstrip_libc(filename)
220
        True
221
        >>> libc = ELF(filename)
222
        >>> hex(libc.symbols.main_arena)
223
        '0x219c80'
224
        >>> unstrip_libc(pwnlib.data.elf.get('test-x86'))
225
        False
226
        >>> filename = search_by_build_id('d1704d25fbbb72fa95d517b883131828c0883fe9', unstrip=True)
227
        >>> 'main_arena' in ELF(filename).symbols
228
        True
229
    """
230
    if not which('eu-unstrip'):
1!
231
        log.warn_once('Couldn\'t find "eu-unstrip" in PATH. Install elfutils first.')
×
232
        return False
×
233

234
    libc = ELF(filename, checksec=False)
1✔
235
    if not libc.buildid:
1!
236
        log.warn_once('Given libc does not have a buildid. Cannot look for debuginfo to unstrip.')
×
237
        return False
×
238

239
    log.debug('Trying debuginfod servers: %r', DEBUGINFOD_SERVERS)
1✔
240

241
    for server_url in DEBUGINFOD_SERVERS:
1✔
242
        libc_dbg = _search_debuginfo_by_hash(server_url, enhex(libc.buildid))
1✔
243
        if libc_dbg:
1✔
244
            break
1✔
245
    else:
246
        log.warn_once('Couldn\'t find debug info for libc with build_id %s on any debuginfod server.', enhex(libc.buildid))
1✔
247
        return False
1✔
248

249
    # Add debug info to given libc binary inplace.
250
    p = process(['eu-unstrip', '-o', filename, filename, libc_dbg])
1✔
251
    output = p.recvall()
1✔
252
    p.close()
1✔
253

254
    if output:
1!
255
        log.error('Failed to unstrip libc binary: %r', output)
×
256
        return False
×
257

258
    return True
1✔
259

260
def _extract_tarfile(cache_dir, data_filename, tarball):
1✔
261
    from six import BytesIO
1✔
262
    import tarfile
1✔
263
    # Handle zstandard compression, since tarfile only supports gz, bz2, and xz.
264
    if data_filename.endswith('.zst') or data_filename.endswith('.zstd'):
1!
265
        import zstandard
1✔
266
        dctx = zstandard.ZstdDecompressor()
1✔
267
        decompressed_tar = BytesIO()
1✔
268
        dctx.copy_stream(tarball, decompressed_tar)
1✔
269
        decompressed_tar.seek(0)
1✔
270
        tarball.close()
1✔
271
        tarball = decompressed_tar
1✔
272

273
    if six.PY2 and data_filename.endswith('.xz'):
1!
274
        # Python 2's tarfile doesn't support xz, so we need to decompress it first.
275
        # Shell out to xz, since the Python 2 pylzma module is broken.
276
        # (https://github.com/fancycode/pylzma/issues/67)
277
        if not which('xz'):
×
278
            log.error('Couldn\'t find "xz" in PATH. Please install xz first.')
×
279
        import subprocess
×
280
        try:
×
281
            uncompressed_tarball = subprocess.check_output(['xz', '--decompress', '--stdout', tarball.name])
×
282
            tarball = BytesIO(uncompressed_tarball)
×
283
        except subprocess.CalledProcessError:
×
284
            log.error('Failed to decompress xz archive.')
×
285

286
    with tarfile.open(fileobj=tarball) as tar_file:
1✔
287
        # Find the library folder in the archive (e.g. /lib/x86_64-linux-gnu/)
288
        lib_dir = None
1✔
289
        libc_name = None
1✔
290
        for member in tar_file.getmembers():
1!
291
            if not member.isfile():
1✔
292
                continue
1✔
293
            libc_name = os.path.basename(member.name)
1✔
294
            if libc_name == 'libc.so.6' or (libc_name.startswith('libc') and libc_name.endswith('.so')):
1✔
295
                lib_dir = os.path.dirname(member.name)
1✔
296
                break
1✔
297
        else:
298
            log.error('Couldn\'t find library folder containing the libc in the archive.')
×
299

300
        # Extract everything in the library folder
301
        for member in tar_file.getmembers():
1✔
302
            if os.path.dirname(member.name) != lib_dir:
1✔
303
                continue
1✔
304
            if not member.isfile() and not member.issym():
1!
305
                continue
×
306
            # Extract while keeping file permissions
307
            tar_file.extract(member, cache_dir)
1✔
308

309
        # Move the files up to the cache root
310
        target_dir = os.path.join(cache_dir, lib_dir)
1✔
311
        for file in os.listdir(target_dir):
1✔
312
            os.rename(os.path.join(target_dir, file), os.path.join(cache_dir, file))
1✔
313
        os.removedirs(target_dir)
1✔
314

315
        return os.path.join(cache_dir, libc_name)
1✔
316

317
def _extract_debfile(cache_dir, package_filename, package):
1✔
318
    # Extract data.tar in the .deb archive.
319
    if six.PY2:
1✔
320
        if not which('ar'):
1!
321
            log.error('Missing command line tool "ar" to extract .deb archive. Please install "ar" first.')
×
322

323
        import atexit
1✔
324
        import shutil
1✔
325
        import subprocess
1✔
326

327
        # Use mkdtemp instead of TemporaryDirectory because the latter is not available in Python 2.
328
        tempdir = tempfile.mkdtemp(prefix=".pwntools-tmp")
1✔
329
        atexit.register(shutil.rmtree, tempdir)
1✔
330
        with tempfile.NamedTemporaryFile(mode='wb', dir=tempdir) as debfile:
1!
331
            debfile.write(package)
1✔
332
            debfile.flush()
1✔
333
            try:
1✔
334
                files_in_deb = subprocess.check_output(['ar', 't', debfile.name]).split(b'\n')
1✔
335
            except subprocess.CalledProcessError:
×
336
                log.error('Failed to list files in .deb archive.')
×
337
            [data_filename] = filter(lambda f: f.startswith(b'data.tar'), files_in_deb)
1✔
338

339
            try:
1✔
340
                subprocess.check_call(['ar', 'x', debfile.name, data_filename], cwd=tempdir)
1✔
341
            except subprocess.CalledProcessError:
×
342
                log.error('Failed to extract data.tar from .deb archive.')
×
343

344
            with open(os.path.join(tempdir, data_filename), 'rb') as tarball:
1!
345
                return _extract_tarfile(cache_dir, data_filename, tarball)
1✔
346
    else:
347
        import unix_ar
1✔
348
        from six import BytesIO
1✔
349
        ar_file = unix_ar.open(BytesIO(package))
1✔
350
        try:
1✔
351
            data_filename = next(filter(lambda f: f.name.startswith(b'data.tar'), ar_file.infolist())).name.decode()
1✔
352
            tarball = ar_file.open(data_filename)
1✔
353
            return _extract_tarfile(cache_dir, data_filename, tarball)
1✔
354
        finally:
355
            ar_file.close()
1✔
356

357
def _extract_pkgfile(cache_dir, package_filename, package):
1✔
358
    from six import BytesIO
×
359
    return _extract_tarfile(cache_dir, package_filename, BytesIO(package))
×
360

361
def _find_libc_package_lib_url(libc):
1✔
362
    # Check https://libc.rip for the libc package
363
    libc_match = query_libc_rip({'buildid': enhex(libc.buildid)})
1✔
364
    if libc_match is not None:
1!
365
        for match in libc_match:
1!
366
            yield match['libs_url']
1✔
367
    
368
    # Check launchpad.net if it's an Ubuntu libc
369
    # GNU C Library (Ubuntu GLIBC 2.36-0ubuntu4)
370
    import re
×
371
    version = re.search(br'GNU C Library \(Ubuntu E?GLIBC ([^\)]+)\)', libc.data)
×
372
    if version is not None:
×
373
        libc_version = version.group(1).decode()
×
374
        yield 'https://launchpad.net/ubuntu/+archive/primary/+files/libc6_{}_{}.deb'.format(libc_version, libc.arch)
×
375

376
def download_libraries(libc_path, unstrip=True):
1✔
377
    """download_libraries(str, bool) -> str
378
    Download the matching libraries for the given libc binary and cache
379
    them in a local directory. The libraries are looked up using `libc.rip <https://libc.rip>`_
380
    and fetched from the official package repositories if available.
381

382
    This commonly includes the ``ld-linux-x86-64.so.2`` and ``libpthread.so.0`` binaries
383
    which can be used to execute the program locally when the given libc is
384
    incompatible with the local dynamic loader.
385

386
    Note: Only .deb and .pkg.tar.* packages are currently supported (Debian/Ubuntu, Arch).
387

388
    Arguments:
389
        libc_path(str):
390
            The path the libc binary.
391
        unstrip(bool):
392
            Try to fetch debug info for the libc and apply it to the downloaded file.
393

394
    Returns:
395
        The path to the cached directory containing the downloaded libraries.
396

397
    Example:
398
        >>> libc_path = ELF(which('ls'), checksec=False).libc.path
399
        >>> lib_path = download_libraries(libc_path)
400
        >>> lib_path is not None
401
        True
402
        >>> os.path.exists(os.path.join(lib_path, 'libc.so.6'))
403
        True
404
        >>> os.path.exists(os.path.join(lib_path, 'ld-linux-x86-64.so.2'))
405
        True
406
    """
407

408
    libc = ELF(libc_path, checksec=False)
1✔
409
    if not libc.buildid:
1!
410
        log.warn_once('Given libc does not have a buildid.')
×
411
        return None
×
412
    
413
    # Handle caching and don't redownload if it already exists.
414
    cache_dir = os.path.join(context.cache_dir, 'libcdb_libs')
1✔
415
    if not os.path.isdir(cache_dir):
1!
416
        os.makedirs(cache_dir)
1✔
417
    
418
    cache_dir = os.path.join(cache_dir, enhex(libc.buildid))
1✔
419
    if os.path.exists(cache_dir):
1!
420
        return cache_dir
×
421

422
    for package_url in _find_libc_package_lib_url(libc):
1!
423
        extension_handlers = {
1✔
424
            '.deb': _extract_debfile,
425
            '.pkg.tar.xz': _extract_pkgfile,
426
            '.pkg.tar.zst': _extract_pkgfile,
427
        }
428

429
        package_filename = os.path.basename(package_url)
1✔
430
        for extension, handler in extension_handlers.items():
1!
431
            if package_filename.endswith(extension):
1✔
432
                break
1✔
433
        else:
434
            log.failure('Cannot handle %s (%s)', package_filename, package_url)
×
435
            continue
×
436

437
        # Download the package
438
        package = wget(package_url, timeout=20)
1✔
439
        if not package:
1!
440
            continue
×
441

442
        # Create target cache directory to extract files into
443
        if not os.path.isdir(cache_dir):
1!
444
            os.makedirs(cache_dir)
1✔
445

446
        try:
1✔
447
            # Extract the archive
448
            libc_path = handler(cache_dir, package_filename, package)
1✔
449
        except Exception as e:
×
450
            os.removedirs(cache_dir)
×
451
            log.failure('Failed to extract %s: %s', package_filename, e)
×
452
            continue
×
453
        # Unstrip the libc binary
454
        try:
1✔
455
            if unstrip:
1!
456
                unstrip_libc(libc_path)
1✔
457
        except Exception:
×
458
            pass
×
459

460
        return cache_dir
1✔
461

462
    log.warn_once('Failed to find matching libraries for provided libc.')
×
463
    return None
×
464

465
def _handle_multiple_matching_libcs(matching_libcs):
1✔
466
    from pwnlib.term import text
×
467
    from pwnlib.ui import options
×
468
    log.info('Multiple matching libc libraries for requested symbols:')
×
469
    for idx, libc in enumerate(matching_libcs):
×
470
        log.info('%d. %s', idx+1, text.red(libc['id']))
×
471
        log.indented('\t%-20s %s', text.green('BuildID:'), libc['buildid'])
×
472
        log.indented('\t%-20s %s', text.green('MD5:'), libc['md5'])
×
473
        log.indented('\t%-20s %s', text.green('SHA1:'), libc['sha1'])
×
474
        log.indented('\t%-20s %s', text.green('SHA256:'), libc['sha256'])
×
475
        log.indented('\t%s', text.green('Symbols:'))
×
476
        for symbol, address in libc['symbols'].items():
×
477
            log.indented('\t%25s = %s', symbol, address)
×
478

479
    selected_index = options("Select the libc version to use:", [libc['id'] for libc in matching_libcs])
×
480
    return matching_libcs[selected_index]
×
481

482
def search_by_symbol_offsets(symbols, select_index=None, unstrip=True, return_as_list=False):
1✔
483
    """
484
    Lookup possible matching libc versions based on leaked function addresses.
485

486
    The leaked function addresses have to be provided as a dict mapping the
487
    function name to the leaked value. Only the lower 3 nibbles are relevant
488
    for the lookup.
489

490
    If there are multiple matches you are presented with a list to select one
491
    interactively, unless the ``select_index`` or ``return_as_list`` arguments
492
    are used.
493

494
    Arguments:
495
        symbols(dict):
496
            Dictionary mapping symbol names to their addresses.
497
        select_index(int):
498
            The libc to select if there are multiple matches (starting at 1).
499
        unstrip(bool):
500
            Try to fetch debug info for the libc and apply it to the downloaded file.
501
        return_as_list(bool):
502
            Return a list of build ids of all matching libc versions
503
            instead of a path to a downloaded file.
504

505
    Returns:
506
        Path to the downloaded library on disk, or :const:`None`.
507
        If the ``return_as_list`` argument is :const:`True`, a list of build ids
508
        is returned instead.
509

510
    Examples:
511
        >>> filename = search_by_symbol_offsets({'puts': 0x420, 'printf': 0xc90}, select_index=1)
512
        >>> libc = ELF(filename)
513
        >>> libc.sym.system == 0x52290
514
        True
515
        >>> matched_libcs = search_by_symbol_offsets({'__libc_start_main_ret': '7f89ad926550'}, return_as_list=True)
516
        >>> len(matched_libcs) > 1
517
        True
518
        >>> for buildid in matched_libcs: # doctest +SKIP
519
        ...     libc = ELF(search_by_build_id(buildid)) # doctest +SKIP
520
    """
521
    for symbol, address in symbols.items():
1✔
522
        if isinstance(address, int):
1✔
523
            symbols[symbol] = hex(address)
1✔
524

525
    params = {'symbols': symbols}
1✔
526
    log.debug('Request: %s', params)
1✔
527
    matching_libcs = query_libc_rip(params)
1✔
528
    log.debug('Result: %s', matching_libcs)
1✔
529
    if matching_libcs is None or len(matching_libcs) == 0:
1!
530
        log.warn_once("No matching libc for symbols %r on libc.rip", symbols)
×
531
        return None
×
532

533
    if return_as_list:
1✔
534
        return [libc['buildid'] for libc in matching_libcs]
1✔
535

536
    if len(matching_libcs) == 1:
1!
537
        return search_by_build_id(matching_libcs[0]['buildid'], unstrip=unstrip)
×
538

539
    if select_index is not None:
1!
540
        if select_index > 0 and select_index <= len(matching_libcs):
1!
541
            return search_by_build_id(matching_libcs[select_index - 1]['buildid'], unstrip=unstrip)
1✔
542
        else:
543
            log.error('Invalid selected libc index. %d is not in the range of 1-%d.', select_index, len(matching_libcs))
×
544
            return None
×
545

546
    selected_libc = _handle_multiple_matching_libcs(matching_libcs)
×
547
    return search_by_build_id(selected_libc['buildid'], unstrip=unstrip)
×
548

549
def search_by_build_id(hex_encoded_id, unstrip=True):
1✔
550
    """
551
    Given a hex-encoded Build ID, attempt to download a matching libc from libcdb.
552

553
    Arguments:
554
        hex_encoded_id(str):
555
            Hex-encoded Build ID (e.g. 'ABCDEF...') of the library
556
        unstrip(bool):
557
            Try to fetch debug info for the libc and apply it to the downloaded file.
558

559
    Returns:
560
        Path to the downloaded library on disk, or :const:`None`.
561

562
    Examples:
563
        >>> filename = search_by_build_id('fe136e485814fee2268cf19e5c124ed0f73f4400')
564
        >>> hex(ELF(filename).symbols.read)
565
        '0xda260'
566
        >>> None == search_by_build_id('XX')
567
        True
568
        >>> filename = search_by_build_id('a5a3c3f65fd94f4c7f323a175707c3a79cbbd614')
569
        >>> hex(ELF(filename).symbols.read)
570
        '0xeef40'
571
    """
572
    return search_by_hash(hex_encoded_id, 'build_id', unstrip)
1✔
573

574
def search_by_md5(hex_encoded_id, unstrip=True):
1✔
575
    """
576
    Given a hex-encoded md5sum, attempt to download a matching libc from libcdb.
577

578
    Arguments:
579
        hex_encoded_id(str):
580
            Hex-encoded md5sum (e.g. 'ABCDEF...') of the library
581
        unstrip(bool):
582
            Try to fetch debug info for the libc and apply it to the downloaded file.
583

584
    Returns:
585
        Path to the downloaded library on disk, or :const:`None`.
586

587
    Examples:
588
        >>> filename = search_by_md5('7a71dafb87606f360043dcd638e411bd')
589
        >>> hex(ELF(filename).symbols.read)
590
        '0xda260'
591
        >>> None == search_by_md5('XX')
592
        True
593
        >>> filename = search_by_md5('74f2d3062180572fc8bcd964b587eeae')
594
        >>> hex(ELF(filename).symbols.read)
595
        '0xeef40'
596
    """
597
    return search_by_hash(hex_encoded_id, 'md5', unstrip)
1✔
598

599
def search_by_sha1(hex_encoded_id, unstrip=True):
1✔
600
    """
601
    Given a hex-encoded sha1, attempt to download a matching libc from libcdb.
602

603
    Arguments:
604
        hex_encoded_id(str):
605
            Hex-encoded sha1sum (e.g. 'ABCDEF...') of the library
606
        unstrip(bool):
607
            Try to fetch debug info for the libc and apply it to the downloaded file.
608

609
    Returns:
610
        Path to the downloaded library on disk, or :const:`None`.
611

612
    Examples:
613
        >>> filename = search_by_sha1('34471e355a5e71400b9d65e78d2cd6ce7fc49de5')
614
        >>> hex(ELF(filename).symbols.read)
615
        '0xda260'
616
        >>> None == search_by_sha1('XX')
617
        True
618
        >>> filename = search_by_sha1('0041d2f397bc2498f62aeb4134d522c5b2635e87')
619
        >>> hex(ELF(filename).symbols.read)
620
        '0xeef40'
621
    """
622
    return search_by_hash(hex_encoded_id, 'sha1', unstrip)
1✔
623

624

625
def search_by_sha256(hex_encoded_id, unstrip=True):
1✔
626
    """
627
    Given a hex-encoded sha256, attempt to download a matching libc from libcdb.
628

629
    Arguments:
630
        hex_encoded_id(str):
631
            Hex-encoded sha256sum (e.g. 'ABCDEF...') of the library
632
        unstrip(bool):
633
            Try to fetch debug info for the libc and apply it to the downloaded file.
634

635
    Returns:
636
        Path to the downloaded library on disk, or :const:`None`.
637

638
    Examples:
639
        >>> filename = search_by_sha256('5e877a8272da934812d2d1f9ee94f73c77c790cbc5d8251f5322389fc9667f21')
640
        >>> hex(ELF(filename).symbols.read)
641
        '0xda260'
642
        >>> None == search_by_sha256('XX')
643
        True
644
        >>> filename = search_by_sha256('5d78fc60054df18df20480c71f3379218790751090f452baffb62ac6b2aff7ee')
645
        >>> hex(ELF(filename).symbols.read)
646
        '0xeef40'
647
    """
648
    return search_by_hash(hex_encoded_id, 'sha256', unstrip)
1✔
649

650

651

652

653
def get_build_id_offsets():
1✔
654
    """
655
    Returns a list of file offsets where the Build ID should reside within
656
    an ELF file of the currently selected architecture.
657
    """
658
    # Given the corpus of almost all libc to have been released with
659
    # RedHat, Fedora, Ubuntu, Debian, etc. over the past several years,
660
    # we can say with 99% certainty that the GNU Build ID section will
661
    # be at one of the specified addresses.
662
    #
663
    # The point here is to get an easy win by reading less DWORDs than would
664
    # have otherwise been required to walk the section table and the string
665
    # stable.
666
    #
667
    # function check_arch() {
668
    # readelf -n $(file -L * | grep -i "$1" | cut -d ':' -f 1) \
669
    #       | grep -B3 BUILD_ID \
670
    #       | grep offset \
671
    #       | sort \
672
    #       | uniq -c
673
    # }
674

675
    return {
×
676
    # $ check_arch 80386
677
    #     181 Displaying notes found at file offset 0x00000174 with length 0x00000024:
678
        'i386': [0x174, 0x1b4, 0x1d4],
679
    # $ check_arch "ARM, EABI5"
680
    #      69 Displaying notes found at file offset 0x00000174 with length 0x00000024:
681
        'arm':  [0x174],
682
        'thumb':  [0x174],
683
    # $ check_arch "ARM aarch64"
684
    #       1 Displaying notes found at file offset 0x00000238 with length 0x00000024:
685
        'aarch64': [0x238],
686
    # $ check_arch "x86-64"
687
    #       6 Displaying notes found at file offset 0x00000174 with length 0x00000024:
688
    #      82 Displaying notes found at file offset 0x00000270 with length 0x00000024:
689
        'amd64': [0x270, 0x174, 0x2e0, 0x370],
690
    # $ check_arch "PowerPC or cisco"
691
    #      88 Displaying notes found at file offset 0x00000174 with length 0x00000024:
692
        'powerpc': [0x174],
693
    # $ check_arch "64-bit PowerPC"
694
    #      30 Displaying notes found at file offset 0x00000238 with length 0x00000024:
695
        'powerpc64': [0x238],
696
    # $ check_arch "SPARC32"
697
    #      32 Displaying notes found at file offset 0x00000174 with length 0x00000024:
698
        'sparc': [0x174],
699
    # $ check_arch "SPARC V9"
700
    #      33 Displaying notes found at file offset 0x00000270 with length 0x00000024:
701
        'sparc64': [0x270]
702
    }.get(context.arch, [])
703

704

705
__all__ = ['get_build_id_offsets', 'search_by_build_id', 'search_by_sha1', 'search_by_sha256', 'search_by_md5', 'unstrip_libc', 'search_by_symbol_offsets', 'download_libraries']
1✔
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

© 2025 Coveralls, Inc