mirror of
https://github.com/Gallopsled/pwntools.git
synced 2026-10-12 02:02:27 +00:00
1128 lines
42 KiB
Python
1128 lines
42 KiB
Python
"""
|
|
Fetch a LIBC binary based on some heuristics.
|
|
"""
|
|
from collections.abc import Generator
|
|
import os
|
|
import re
|
|
import time
|
|
import struct
|
|
from typing import Generator
|
|
|
|
from pwnlib.context import context
|
|
from pwnlib.elf import ELF
|
|
from pwnlib.filesystem.path import Path
|
|
from pwnlib.internal.typing import BytesPath
|
|
from pwnlib.log import getLogger
|
|
from pwnlib.tubes.process import process
|
|
from pwnlib.util.fiddling import enhex, unhex
|
|
from pwnlib.util.hashes import sha1filehex, sha256filehex, md5filehex
|
|
from pwnlib.util.misc import read
|
|
from pwnlib.util.misc import which
|
|
from pwnlib.util.misc import write
|
|
from pwnlib.util.web import wget
|
|
|
|
log = getLogger(__name__)
|
|
|
|
|
|
def _turbofast_extract_build_id(path):
|
|
"""
|
|
Elf_External_Note:
|
|
|
|
0x00 +--------+
|
|
| namesz | <- Size of entry's owner string
|
|
0x04 +--------+
|
|
| descsz | <- Size of the note descriptor
|
|
0x08 +--------+
|
|
| type | <- Interpretation of the descriptor
|
|
0x0c +--------+
|
|
| name | <- Start of the name+desc data
|
|
... +--------
|
|
| desc |
|
|
... +--------+
|
|
"""
|
|
data = read(path, 0x1000)
|
|
# search NT_GNU_BUILD_ID and b"GNU\x00" (type+name)
|
|
idx = data.find(unhex("03000000474e5500"))
|
|
if idx == -1:
|
|
return enhex(ELF(path, checksec=False).buildid or b'')
|
|
descsz, = struct.unpack("<L", data[idx-4: idx])
|
|
return enhex(data[idx+8: idx+8+descsz])
|
|
|
|
|
|
TYPES = {
|
|
'libs_id': None,
|
|
'build_id': _turbofast_extract_build_id,
|
|
'sha1': sha1filehex,
|
|
'sha256': sha256filehex,
|
|
'md5': md5filehex,
|
|
}
|
|
|
|
# mapping for search result (same as libc.rip)
|
|
MAP_TYPES = {
|
|
'libs_id': 'id',
|
|
'build_id': 'buildid'
|
|
}
|
|
|
|
DEBUGINFOD_SERVERS = [
|
|
'https://debuginfod.ubuntu.com/',
|
|
'https://debuginfod.debian.net/',
|
|
'https://debuginfod.elfutils.org/',
|
|
]
|
|
|
|
if 'DEBUGINFOD_URLS' in os.environ:
|
|
urls = os.environ['DEBUGINFOD_URLS'].split(' ')
|
|
DEBUGINFOD_SERVERS = urls + DEBUGINFOD_SERVERS
|
|
|
|
|
|
def _local_debuginfod_cache_dirs() -> Generator[str, None, None]:
|
|
"""Yield candidate directories where the upstream debuginfod client (used
|
|
by gdb / eu-debuginfod-find / abrt) caches downloaded debuginfo files.
|
|
|
|
The lookup follows the conventions documented in
|
|
`debuginfod-client-config(7) <https://man.archlinux.org/man/debuginfod-client-config.7>`_:
|
|
|
|
1. ``$DEBUGINFOD_CACHE_PATH`` if set.
|
|
2. ``$XDG_CACHE_HOME/debuginfod_client`` if ``XDG_CACHE_HOME`` is set.
|
|
3. ``~/.cache/debuginfod_client`` (the default).
|
|
"""
|
|
explicit = os.environ.get('DEBUGINFOD_CACHE_PATH')
|
|
if explicit:
|
|
yield explicit
|
|
|
|
xdg = os.environ.get('XDG_CACHE_HOME')
|
|
if xdg:
|
|
yield os.path.join(xdg, 'debuginfod_client')
|
|
else:
|
|
home = os.path.expanduser('~')
|
|
if home and home != '~':
|
|
yield os.path.join(home, '.cache', 'debuginfod_client')
|
|
|
|
|
|
def _local_debuginfod_cached_path(hex_encoded_id: str) -> str | None:
|
|
"""Return the path to a cached debuginfo file for ``hex_encoded_id`` if a
|
|
local debuginfod client has already downloaded it, otherwise ``None``.
|
|
|
|
The on-disk layout written by debuginfod-client is
|
|
``<cache>/<hex-build-id>/debuginfo``.
|
|
|
|
Examples:
|
|
|
|
>>> import os, tempfile
|
|
>>> from pwnlib.libcdb import _local_debuginfod_cached_path
|
|
>>> tmp = tempfile.mkdtemp()
|
|
>>> os.environ['DEBUGINFOD_CACHE_PATH'] = tmp
|
|
>>> _local_debuginfod_cached_path('deadbeef') is None
|
|
True
|
|
>>> os.makedirs(os.path.join(tmp, 'deadbeef'))
|
|
>>> _ = open(os.path.join(tmp, 'deadbeef', 'debuginfo'), 'wb').write(b'\\x7FELF')
|
|
>>> _local_debuginfod_cached_path('deadbeef') == os.path.join(tmp, 'deadbeef', 'debuginfo')
|
|
True
|
|
>>> del os.environ['DEBUGINFOD_CACHE_PATH']
|
|
"""
|
|
for cache_dir in _local_debuginfod_cache_dirs():
|
|
candidate = os.path.join(cache_dir, hex_encoded_id, 'debuginfo')
|
|
try:
|
|
with open(candidate, 'rb') as f:
|
|
head = f.read(4)
|
|
except OSError:
|
|
continue
|
|
if head == b'\x7FELF':
|
|
return candidate
|
|
return None
|
|
|
|
# Allow to override url with a caching proxy in CI
|
|
LIBC_RIP_URL = os.environ.get("PWN_LIBCRIP_URL", "https://libc.rip").rstrip("/")
|
|
GITLAB_LIBCDB_URL = os.environ.get("PWN_GITLAB_LIBCDB_URL", "https://gitlab.com").rstrip("/")
|
|
|
|
# Retry failed lookups after some time
|
|
NEGATIVE_CACHE_EXPIRY = 60 * 60 * 24 * 7 # 1 week
|
|
|
|
# https://gitlab.com/libcdb/libcdb wasn't updated after 2019,
|
|
# but still is a massive database of older libc binaries.
|
|
def provider_libcdb(hex_encoded_id, search_type):
|
|
if search_type == 'libs_id':
|
|
return None
|
|
|
|
# Deferred import because it's slow
|
|
import requests
|
|
import urllib.parse
|
|
|
|
# Build the URL using the requested hash type
|
|
url_base = f"{GITLAB_LIBCDB_URL}/libcdb/libcdb/raw/master/hashes/{search_type}/"
|
|
url = urllib.parse.urljoin(url_base, hex_encoded_id)
|
|
|
|
data = b""
|
|
log.debug("Downloading data from LibcDB: %s", url)
|
|
try:
|
|
while not data.startswith(b'\x7fELF'):
|
|
data = wget(url, timeout=20)
|
|
|
|
if not data:
|
|
log.warn_once("Could not fetch libc for %s %s from libcdb", search_type, hex_encoded_id)
|
|
break
|
|
|
|
# GitLab serves up symlinks with
|
|
if data.startswith(b'..'):
|
|
url = os.path.dirname(url) + '/'
|
|
url = urllib.parse.urljoin(url.encode('utf-8'), data)
|
|
except requests.RequestException as e:
|
|
log.warn_once("Failed to fetch libc for %s %s from libcdb: %s", search_type, hex_encoded_id, e)
|
|
return data
|
|
|
|
def query_libc_rip(params):
|
|
# Deferred import because it's slow
|
|
import requests
|
|
|
|
url = f"{LIBC_RIP_URL}/api/find"
|
|
# Handle pagination transparently and fetch all results
|
|
offset = 0
|
|
count = 1
|
|
results = []
|
|
while offset < count:
|
|
try:
|
|
pagination_params = {'offset': offset, 'limit': 100}
|
|
log.debug("Querying libc.rip at %s (pagination %s) with parameters: %s", url, pagination_params, params)
|
|
result = requests.post(url, params=pagination_params, json=params, timeout=20)
|
|
result.raise_for_status()
|
|
if result.status_code != 200:
|
|
log.debug("Error: %s", result.text)
|
|
return None
|
|
imm_results = result.json()
|
|
# non-paginated result. old libc-database API?
|
|
if not isinstance(imm_results, dict):
|
|
return imm_results
|
|
results.extend(imm_results.get('results', []))
|
|
count = imm_results.get('total', 0)
|
|
offset += imm_results['count']
|
|
if not imm_results['has_more']:
|
|
break
|
|
except requests.RequestException as e:
|
|
log.warn_once("Failed to fetch libc info from libc.rip: %s", e)
|
|
return None
|
|
return results
|
|
|
|
# https://libc.rip/
|
|
def provider_libc_rip(search_target, search_type):
|
|
# Build the request for the hash type
|
|
# https://github.com/niklasb/libc-database/blob/master/searchengine/api.yml
|
|
if search_type in MAP_TYPES.keys():
|
|
search_type = MAP_TYPES[search_type]
|
|
|
|
params = {search_type: search_target}
|
|
|
|
libc_match = query_libc_rip(params)
|
|
if not libc_match:
|
|
log.warn_once("Could not find libc info for %s %s on libc.rip", search_type, search_target)
|
|
return None
|
|
|
|
if len(libc_match) > 1:
|
|
log.debug("Received multiple matches. Choosing the first match and discarding the others.")
|
|
log.debug("%r", libc_match)
|
|
|
|
url = libc_match[0]['download_url']
|
|
log.debug("Downloading data from libc.rip: %s", url)
|
|
url = url.replace("https://libc.rip", LIBC_RIP_URL)
|
|
data = wget(url, timeout=20)
|
|
|
|
if not data:
|
|
log.warn_once("Could not fetch libc binary for %s %s from libc.rip", search_type, search_target)
|
|
return None
|
|
return data
|
|
|
|
# Check if the local system libc matches the requested hash.
|
|
def provider_local_system(hex_encoded_id, search_type):
|
|
if search_type == 'libs_id':
|
|
return None
|
|
shell_path = os.environ.get('SHELL', None) or '/bin/sh'
|
|
if not os.path.exists(shell_path):
|
|
log.debug('Shell path %r does not exist. Skipping local system libc matching.', shell_path)
|
|
return None
|
|
local_libc = ELF(shell_path, checksec=False).libc
|
|
if not local_libc:
|
|
log.debug('Cannot lookup libc from shell %r. Skipping local system libc matching.', shell_path)
|
|
return None
|
|
if TYPES[search_type](local_libc.path) == hex_encoded_id:
|
|
return local_libc.data
|
|
return None
|
|
|
|
# Offline search https://github.com/niklasb/libc-database for hash type
|
|
def provider_local_database(search_target, search_type):
|
|
if not context.local_libcdb:
|
|
return None
|
|
|
|
localdb = Path(context.local_libcdb)
|
|
if not localdb.is_dir():
|
|
return None
|
|
|
|
# Handle the specific search type 'libs_id'
|
|
if search_type == 'libs_id':
|
|
libc_list = list(localdb.rglob("%s.so" % search_target))
|
|
if len(libc_list) == 0:
|
|
return None
|
|
return read(libc_list[0])
|
|
|
|
log.debug("Searching local libc database, %s: %s", search_type, search_target)
|
|
for libc_path in localdb.rglob("*.so"):
|
|
if search_target == TYPES[search_type](libc_path):
|
|
return read(libc_path)
|
|
|
|
return None
|
|
|
|
def query_local_database(params):
|
|
if not context.local_libcdb or not params.get("symbols"):
|
|
return None
|
|
|
|
localdb = Path(context.local_libcdb)
|
|
if not localdb.is_dir():
|
|
return None
|
|
|
|
res = []
|
|
query_syms = params["symbols"]
|
|
|
|
# Loop through each '.symbols' file in the local database
|
|
# Make sure `Path.rglod` order stable
|
|
for symbol_file in sorted(localdb.rglob("*.symbols"), key=lambda x: x.as_posix()):
|
|
libc_syms = _parse_libc_symbol(symbol_file)
|
|
|
|
matched = 0
|
|
for name, addr in query_syms.items():
|
|
if isinstance(addr, str):
|
|
addr = int(addr, 16)
|
|
|
|
# Compare last 12 bits
|
|
if libc_syms.get(name) and (libc_syms.get(name) & 0xfff) == (addr & 0xfff):
|
|
matched += 1
|
|
else:
|
|
# aborting this loop once there was a mismatch.
|
|
break
|
|
|
|
# Check if all symbols have been matched
|
|
if matched == len(query_syms):
|
|
libs_id = symbol_file.stem
|
|
libc_path = symbol_file.parent / ("%s.so" % libs_id)
|
|
libs_url = read(symbol_file.parent / ("%s.url" % libs_id)).decode().strip()
|
|
res.append(_pack_libs_info(libc_path, libs_id, libs_url, libc_syms))
|
|
|
|
return res
|
|
|
|
PROVIDERS = {
|
|
"offline": [provider_local_system, provider_local_database],
|
|
"online": [provider_libcdb, provider_libc_rip]
|
|
}
|
|
|
|
def search_by_hash(search_target, search_type='build_id', unstrip=True, offline_only=False):
|
|
"""search_by_hash(str, str, bool, bool) -> str
|
|
Arguments:
|
|
search_target(str):
|
|
Use for searching the libc. This could be a hex encoded ID (`hex_encoded_id`) or a library
|
|
name (`libs_id`). Depending on `search_type`, this can represent different types of encoded
|
|
values or names.
|
|
search_type(str):
|
|
The type of the search to be performed, it should be one of the keys in the `TYPES` dictionary.
|
|
unstrip(bool):
|
|
Try to fetch debug info for the libc and apply it to the downloaded file.
|
|
offline_only(bool):
|
|
If True, restricts the search to offline providers only (local database). If False, it will also
|
|
search online providers. Default is False.
|
|
|
|
Returns:
|
|
The path to the cached directory containing the downloaded libraries.
|
|
"""
|
|
assert search_type in TYPES, search_type
|
|
|
|
# Ensure that the libcdb cache directory exists
|
|
cache, cache_valid = _check_elf_cache('libcdb', search_target, search_type)
|
|
if cache_valid:
|
|
if unstrip:
|
|
unstrip_libc(cache)
|
|
return cache
|
|
|
|
# We searched for this buildid before, but didn't find anything.
|
|
if cache is None:
|
|
return None
|
|
|
|
providers = PROVIDERS["offline"]
|
|
if not offline_only:
|
|
providers += PROVIDERS["online"]
|
|
|
|
# Run through all available libc database providers to see if we have a match.
|
|
for provider in providers:
|
|
data = provider(search_target, search_type)
|
|
if data and data.startswith(b'\x7FELF'):
|
|
break
|
|
|
|
if not data:
|
|
log.warn_once("Could not find libc for %s %s anywhere", search_type, search_target)
|
|
|
|
# Save whatever we got to the cache
|
|
write(cache, data or b'')
|
|
|
|
# Return ``None`` if we did not get a valid ELF file
|
|
if not data or not data.startswith(b'\x7FELF'):
|
|
return None
|
|
|
|
# Try to find debug info for this libc.
|
|
if unstrip:
|
|
unstrip_libc(cache)
|
|
|
|
return cache
|
|
|
|
def _check_elf_cache(cache_type, search_target, search_type):
|
|
"""
|
|
Check if there already is an ELF file for this hash in the cache.
|
|
|
|
>>> cache, _ = _check_elf_cache('libcdb', '2d1c5e0b85cb06ff47fa6fa088ec22cb6e06074e', 'build_id')
|
|
>>> os.unlink(cache) if os.path.exists(cache)
|
|
>>> filename = search_by_hash('2d1c5e0b85cb06ff47fa6fa088ec22cb6e06074e', 'build_id', unstrip=False)
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xe56c0'
|
|
>>> filename == cache
|
|
True
|
|
"""
|
|
# Ensure that the cache directory exists
|
|
cache_dir = os.path.join(context.cache_dir, cache_type, search_type)
|
|
|
|
if not os.path.isdir(cache_dir):
|
|
os.makedirs(cache_dir)
|
|
|
|
# If we already downloaded the file, and it looks even passingly like
|
|
# a valid ELF file, return it.
|
|
cache = os.path.join(cache_dir, search_target)
|
|
|
|
if not os.path.exists(cache):
|
|
return cache, False
|
|
|
|
log.debug("Found existing cached ELF at %r", cache)
|
|
|
|
data = read(cache)
|
|
if not data.startswith(b'\x7FELF'):
|
|
# Retry failed lookups after some time
|
|
if time.time() > os.path.getmtime(cache) + NEGATIVE_CACHE_EXPIRY:
|
|
return cache, False
|
|
log.info_once("Skipping invalid cached ELF %s", search_target)
|
|
return None, False
|
|
|
|
log.info_once("Using cached data from %r", cache)
|
|
return cache, True
|
|
|
|
def unstrip_libc(filename):
|
|
"""
|
|
Given a path to a libc binary, attempt to download matching debug info
|
|
and add them back to the given binary.
|
|
|
|
This modifies the given file.
|
|
|
|
Arguments:
|
|
filename(str):
|
|
Path to the libc binary to unstrip.
|
|
|
|
Returns:
|
|
:const:`True` if binary was unstripped, :const:`False` otherwise.
|
|
|
|
Examples:
|
|
|
|
>>> filename = search_by_build_id('69389d485a9793dbe873f0ea2c93e02efaa9aa3d', unstrip=False)
|
|
>>> libc = ELF(filename)
|
|
>>> 'main_arena' in libc.symbols
|
|
False
|
|
>>> unstrip_libc(filename) # doctest: +SKIP
|
|
True
|
|
>>> libc = ELF(filename)
|
|
>>> hex(libc.symbols.main_arena) # doctest: +SKIP
|
|
'0x219c80'
|
|
>>> unstrip_libc(pwnlib.data.elf.get('test-x86'))
|
|
False
|
|
>>> filename = search_by_build_id('d1704d25fbbb72fa95d517b883131828c0883fe9', unstrip=True)
|
|
>>> 'main_arena' in ELF(filename).symbols # doctest: +SKIP
|
|
True
|
|
"""
|
|
if not which('eu-unstrip'):
|
|
log.warn_once('Couldn\'t find "eu-unstrip" in PATH. Install elfutils first.')
|
|
return False
|
|
|
|
libc = ELF(filename, checksec=False)
|
|
if not libc.buildid:
|
|
log.warn_once('Given libc does not have a buildid. Cannot look for debuginfo to unstrip.')
|
|
return False
|
|
|
|
if libc.debuginfo:
|
|
log.debug('Given libc already contains debug information. Skipping unstrip.')
|
|
return True
|
|
|
|
log.debug('Trying debuginfod servers: %r', DEBUGINFOD_SERVERS)
|
|
|
|
# Deferred import because it's slow
|
|
import requests
|
|
import urllib.parse
|
|
|
|
hex_encoded_id = enhex(libc.buildid)
|
|
|
|
# Check if we tried this buildid before.
|
|
cache, cache_valid = _check_elf_cache('libcdb_dbg', hex_encoded_id, 'build_id')
|
|
if not cache_valid:
|
|
# Cached negative result so we don't look for missing debug info everytime.
|
|
if cache is None:
|
|
return False
|
|
else:
|
|
# Check if a local debuginfod client (e.g. gdb's `set debuginfod enabled on`
|
|
# or eu-debuginfod-find) has already downloaded matching debuginfo. If so,
|
|
# use it instead of doing another network round-trip.
|
|
local_debuginfo = _local_debuginfod_cached_path(hex_encoded_id)
|
|
if local_debuginfo is not None:
|
|
log.debug('Found debuginfo in local debuginfod cache: %s', local_debuginfo)
|
|
write(cache, read(local_debuginfo))
|
|
|
|
# If we didn't find a local debuginfo, try to fetch it from the debuginfod servers.
|
|
else:
|
|
for server_url in DEBUGINFOD_SERVERS:
|
|
# Try to find separate debuginfo.
|
|
url = f'/buildid/{hex_encoded_id}/debuginfo'
|
|
url = urllib.parse.urljoin(server_url, url)
|
|
data = b""
|
|
log.debug("Downloading data from debuginfod: %s", url)
|
|
try:
|
|
data = wget(url, timeout=20)
|
|
|
|
# Try next server if we didn't get a valid ELF file
|
|
if not data or not data.startswith(b'\x7FELF'):
|
|
log.warn_once("Could not fetch libc debuginfo for build_id %s from %s", hex_encoded_id, server_url)
|
|
continue
|
|
break
|
|
except requests.RequestException as e:
|
|
log.warn_once("Failed to fetch libc debuginfo for build_id %s from %s: %s", hex_encoded_id, server_url, e)
|
|
else:
|
|
write(cache, data or b'')
|
|
log.warn_once('Couldn\'t find debug info for libc with build_id %s on any debuginfod server.', enhex(libc.buildid))
|
|
return False
|
|
|
|
# Save whatever we got to the cache
|
|
write(cache, data or b'')
|
|
|
|
# Add debug info to given libc binary inplace.
|
|
p = process(['eu-unstrip', '-o', filename, filename, cache])
|
|
output = p.recvall()
|
|
p.close()
|
|
|
|
if output:
|
|
log.error('Failed to unstrip libc binary: %r', output)
|
|
return False
|
|
|
|
return True
|
|
|
|
def _extract_tarfile(cache_dir, data_filename, tarball):
|
|
import sys
|
|
if sys.version_info >= (3, 14):
|
|
import tarfile
|
|
else:
|
|
# for zstandard support in tarfile
|
|
from backports.zstd import tarfile
|
|
|
|
with tarfile.open(fileobj=tarball) as tar_file:
|
|
# Find the library folder in the archive (e.g. /lib/x86_64-linux-gnu/)
|
|
lib_dir = None
|
|
libc_name = None
|
|
for member in tar_file.getmembers():
|
|
if not member.isfile():
|
|
continue
|
|
libc_name = os.path.basename(member.name)
|
|
if libc_name == 'libc.so.6' or (libc_name.startswith('libc') and libc_name.endswith('.so')):
|
|
lib_dir = os.path.dirname(member.name)
|
|
break
|
|
else:
|
|
log.error('Couldn\'t find library folder containing the libc in the archive.')
|
|
|
|
# Extract everything in the library folder
|
|
for member in tar_file.getmembers():
|
|
if os.path.dirname(member.name) != lib_dir:
|
|
continue
|
|
if not member.isfile() and not member.issym():
|
|
continue
|
|
# Extract while keeping file permissions
|
|
tar_file.extract(member, cache_dir)
|
|
|
|
# Move the files up to the cache root
|
|
target_dir = os.path.join(cache_dir, lib_dir)
|
|
for file in os.listdir(target_dir):
|
|
os.rename(os.path.join(target_dir, file), os.path.join(cache_dir, file))
|
|
os.removedirs(target_dir)
|
|
|
|
return os.path.join(cache_dir, libc_name)
|
|
|
|
def _extract_debfile(cache_dir, package_filename, package):
|
|
# Extract data.tar in the .deb archive.
|
|
import unix_ar
|
|
from io import BytesIO
|
|
ar_file = unix_ar.open(BytesIO(package))
|
|
try:
|
|
data_filename = next(filter(lambda f: f.name.startswith(b'data.tar'), ar_file.infolist())).name.decode()
|
|
tarball = ar_file.open(data_filename)
|
|
return _extract_tarfile(cache_dir, data_filename, tarball)
|
|
finally:
|
|
ar_file.close()
|
|
|
|
def _extract_pkgfile(cache_dir, package_filename, package):
|
|
from io import BytesIO
|
|
return _extract_tarfile(cache_dir, package_filename, BytesIO(package))
|
|
|
|
def _collect_extra_mirrors(extra_mirrors: str | list[str] | None) -> list[str]:
|
|
"""Normalize the user-supplied ``extra_mirrors`` argument and merge it with
|
|
the comma- or whitespace-separated ``PWNLIB_EXTRA_LIBC_MIRRORS`` environment
|
|
variable.
|
|
|
|
Returns a list of ``str`` URL prefixes (each with no trailing slash). Empty
|
|
entries are dropped.
|
|
|
|
Examples:
|
|
|
|
>>> from pwnlib.libcdb import _collect_extra_mirrors
|
|
>>> _collect_extra_mirrors(None)
|
|
[]
|
|
>>> _collect_extra_mirrors('https://example.com/m/')
|
|
['https://example.com/m']
|
|
>>> _collect_extra_mirrors(['https://a/', 'https://b'])
|
|
['https://a', 'https://b']
|
|
"""
|
|
mirrors = []
|
|
if extra_mirrors:
|
|
if isinstance(extra_mirrors, str):
|
|
mirrors.append(extra_mirrors)
|
|
else:
|
|
mirrors.extend(extra_mirrors)
|
|
env_mirrors = os.environ.get('PWNLIB_EXTRA_LIBC_MIRRORS')
|
|
if env_mirrors:
|
|
# Allow either commas or whitespace as separators so URL schemes
|
|
# (https:) don't accidentally get split mid-URL.
|
|
mirrors.extend(re.split(r'[,\s]+', env_mirrors))
|
|
return [m.rstrip('/') for m in mirrors if m]
|
|
|
|
|
|
def _mirror_variants(url: str, extra_mirrors: list[str] | None) -> Generator[str, None, None]:
|
|
"""Yield ``url`` followed by mirror-swapped variants for each entry in
|
|
``extra_mirrors``.
|
|
|
|
A mirror is applied by replacing everything before ``/pool/`` in the
|
|
candidate URL with the mirror prefix. URLs without a ``/pool/`` segment are
|
|
still yielded once but no mirror substitution is performed (for example,
|
|
Launchpad's ``+files`` archive URLs do not follow the Debian pool layout).
|
|
|
|
Examples:
|
|
|
|
>>> from pwnlib.libcdb import _mirror_variants
|
|
>>> list(_mirror_variants('http://archive.ubuntu.com/ubuntu/pool/main/g/glibc/libc6_2.36_amd64.deb', None))
|
|
['http://archive.ubuntu.com/ubuntu/pool/main/g/glibc/libc6_2.36_amd64.deb']
|
|
>>> list(_mirror_variants('http://deb.debian.org/debian/pool/main/g/glibc/libc6_2.36_amd64.deb',
|
|
... ['https://debian.sipwise.com/debian-security']))
|
|
['http://deb.debian.org/debian/pool/main/g/glibc/libc6_2.36_amd64.deb', 'https://debian.sipwise.com/debian-security/pool/main/g/glibc/libc6_2.36_amd64.deb']
|
|
>>> list(_mirror_variants('https://launchpad.net/ubuntu/+archive/primary/+files/libc6_2.36_amd64.deb',
|
|
... ['https://anywhere.example']))
|
|
['https://launchpad.net/ubuntu/+archive/primary/+files/libc6_2.36_amd64.deb']
|
|
"""
|
|
yield url
|
|
if not extra_mirrors:
|
|
return
|
|
match = re.search(r'/(pool/.+)$', url)
|
|
if not match:
|
|
return
|
|
suffix = match.group(1)
|
|
seen = {url}
|
|
for prefix in extra_mirrors:
|
|
candidate = '%s/%s' % (prefix, suffix)
|
|
if candidate not in seen:
|
|
seen.add(candidate)
|
|
yield candidate
|
|
|
|
|
|
def _find_libc_package_lib_url(libc: ELF, extra_mirrors: list[str] | None = None) -> Generator[str, None, None]:
|
|
# Check https://libc.rip for the libc package
|
|
libc_match = query_libc_rip({'buildid': enhex(libc.buildid)})
|
|
if libc_match is not None:
|
|
for match in libc_match:
|
|
# Allow to override url with a caching proxy in CI
|
|
ubuntu_archive_url = os.environ.get('PWN_UBUNTU_ARCHIVE_URL', 'http://archive.ubuntu.com').rstrip('/')
|
|
url = match['libs_url'].replace('http://archive.ubuntu.com', ubuntu_archive_url)
|
|
for variant in _mirror_variants(url, extra_mirrors):
|
|
yield variant
|
|
|
|
# Check launchpad.net if it's an Ubuntu libc
|
|
# GNU C Library (Ubuntu GLIBC 2.36-0ubuntu4)
|
|
version = re.search(br'GNU C Library \(Ubuntu E?GLIBC ([^\)]+)\)', libc.data)
|
|
if version is not None:
|
|
libc_version = version.group(1).decode()
|
|
yield f'https://launchpad.net/ubuntu/+archive/primary/+files/libc6_{libc_version}_{libc.arch}.deb'
|
|
|
|
def download_libraries(libc_path: BytesPath, unstrip: bool = True, extra_mirrors: str | list[str] | None = None) -> str | None:
|
|
"""download_libraries(str, bool, extra_mirrors=None) -> str
|
|
Download the matching libraries for the given libc binary and cache
|
|
them in a local directory. The libraries are looked up using `libc.rip <https://libc.rip>`_
|
|
and fetched from the official package repositories if available.
|
|
|
|
This commonly includes the ``ld-linux-x86-64.so.2`` and ``libpthread.so.0`` binaries
|
|
which can be used to execute the program locally when the given libc is
|
|
incompatible with the local dynamic loader.
|
|
|
|
Note: Only .deb and .pkg.tar.* packages are currently supported (Debian/Ubuntu, Arch).
|
|
|
|
Arguments:
|
|
libc_path(str):
|
|
The path the libc binary.
|
|
unstrip(bool):
|
|
Try to fetch debug info for the libc and apply it to the downloaded file.
|
|
extra_mirrors(str or list of str):
|
|
Optional additional Debian-style apt mirror prefix(es) to try
|
|
after the libc.rip URL. Useful when the official mirror has
|
|
dropped older vulnerable libc versions: each candidate URL is
|
|
re-tried with everything before ``/pool/`` swapped for the
|
|
mirror prefix. Mirrors can also be supplied via the
|
|
``PWNLIB_EXTRA_LIBC_MIRRORS`` environment variable as a
|
|
comma- or whitespace-separated list. Example: pass
|
|
``"https://debian.sipwise.com/debian-security"`` to find
|
|
``GLIBC 2.36-9+deb12u6`` after the official Debian mirrors
|
|
removed it.
|
|
|
|
Returns:
|
|
The path to the cached directory containing the downloaded libraries.
|
|
|
|
Example:
|
|
|
|
>>> libc_path = ELF(which('ls'), checksec=False).libc.path
|
|
>>> lib_path = download_libraries(libc_path)
|
|
>>> lib_path is not None
|
|
True
|
|
>>> os.path.exists(os.path.join(lib_path, 'libc.so.6'))
|
|
True
|
|
>>> os.path.exists(os.path.join(lib_path, 'ld-linux-x86-64.so.2'))
|
|
True
|
|
|
|
Fetch libraries from a different mirror for a vulnerable libc version
|
|
``libc6_2.36-9+deb12u6_amd64`` that was removed from the official Debian mirrors:
|
|
|
|
>>> libc_path = libcdb.search_by_build_id("ee3145ecaaff87a133daea77fbc3eecd458fa0d1") # doctest: +SKIP
|
|
>>> libcdb.download_libraries(libc_path, extra_mirrors="https://debian.sipwise.com/debian-security") # doctest: +ELLIPSIS +SKIP
|
|
'.../libcdb_libs/ee3145ecaaff87a133daea77fbc3eecd458fa0d1'
|
|
"""
|
|
|
|
libc = ELF(libc_path, checksec=False)
|
|
if not libc.buildid:
|
|
log.warn_once('Given libc does not have a buildid.')
|
|
return None
|
|
|
|
# Handle caching and don't redownload if it already exists.
|
|
cache_dir = os.path.join(context.cache_dir, 'libcdb_libs')
|
|
if not os.path.isdir(cache_dir):
|
|
os.makedirs(cache_dir)
|
|
|
|
cache_dir = os.path.join(cache_dir, enhex(libc.buildid))
|
|
if os.path.exists(cache_dir):
|
|
return cache_dir
|
|
|
|
extra_mirrors = _collect_extra_mirrors(extra_mirrors)
|
|
|
|
for package_url in _find_libc_package_lib_url(libc, extra_mirrors=extra_mirrors):
|
|
extension_handlers = {
|
|
'.deb': _extract_debfile,
|
|
'.pkg.tar.xz': _extract_pkgfile,
|
|
'.pkg.tar.zst': _extract_pkgfile,
|
|
}
|
|
|
|
package_filename = os.path.basename(package_url)
|
|
for extension, handler in extension_handlers.items():
|
|
if package_filename.endswith(extension):
|
|
break
|
|
else:
|
|
log.failure('Cannot handle %s (%s)', package_filename, package_url)
|
|
continue
|
|
|
|
# Download the package
|
|
package = wget(package_url, timeout=20)
|
|
if not package:
|
|
continue
|
|
|
|
# Create target cache directory to extract files into
|
|
if not os.path.isdir(cache_dir):
|
|
os.makedirs(cache_dir)
|
|
|
|
try:
|
|
# Extract the archive
|
|
libc_path = handler(cache_dir, package_filename, package)
|
|
except Exception as e:
|
|
os.removedirs(cache_dir)
|
|
log.failure('Failed to extract %s: %s', package_filename, e)
|
|
continue
|
|
# Unstrip the libc binary
|
|
try:
|
|
if unstrip:
|
|
unstrip_libc(libc_path)
|
|
except Exception:
|
|
pass
|
|
|
|
return cache_dir
|
|
|
|
log.warn_once('Failed to find matching libraries for provided libc.')
|
|
return None
|
|
|
|
def _handle_multiple_matching_libcs(matching_libcs):
|
|
from pwnlib.term import text
|
|
from pwnlib.ui import options
|
|
log.info('Multiple matching libc libraries for requested symbols:')
|
|
for idx, libc in enumerate(matching_libcs):
|
|
log.info('%d. %s', idx+1, text.red(libc['id']))
|
|
log.indented('\t%-20s %s', text.green('BuildID:'), libc['buildid'])
|
|
log.indented('\t%-20s %s', text.green('MD5:'), libc['md5'])
|
|
log.indented('\t%-20s %s', text.green('SHA1:'), libc['sha1'])
|
|
log.indented('\t%-20s %s', text.green('SHA256:'), libc['sha256'])
|
|
log.indented('\t%s', text.green('Symbols:'))
|
|
for symbol, address in libc['symbols'].items():
|
|
log.indented('\t%25s = %s', symbol, address)
|
|
|
|
selected_index = options("Select the libc version to use:", [libc['id'] for libc in matching_libcs])
|
|
return matching_libcs[selected_index]
|
|
|
|
def search_by_symbol_offsets(symbols, select_index=None, unstrip=True, offline_only=False, search_type='build_id', return_as_list=False, return_raw=False):
|
|
"""
|
|
Lookup possible matching libc versions based on leaked function addresses.
|
|
|
|
The leaked function addresses have to be provided as a dict mapping the
|
|
function name to the leaked value. Only the lower 3 nibbles are relevant
|
|
for the lookup.
|
|
|
|
If there are multiple matches you are presented with a list to select one
|
|
interactively, unless the ``select_index`` or ``return_as_list`` arguments
|
|
are used.
|
|
|
|
Arguments:
|
|
symbols(dict):
|
|
Dictionary mapping symbol names to their addresses.
|
|
select_index(int):
|
|
The libc to select if there are multiple matches (starting at 1).
|
|
unstrip(bool):
|
|
Try to fetch debug info for the libc and apply it to the downloaded file.
|
|
offline_only(bool):
|
|
When pass `offline_only=True`, restricts search mode to offline sources only,
|
|
disable online lookup. Defaults to `False`, and enable both offline and online providers.
|
|
search_type(str):
|
|
An option to select searched hash.
|
|
return_as_list(bool):
|
|
Return a list of build ids of all matching libc versions
|
|
instead of a path to a downloaded file.
|
|
return_raw(bool):
|
|
Return raw list of matched libc.
|
|
|
|
Returns:
|
|
Path to the downloaded library on disk, or :const:`None`.
|
|
If the ``return_as_list`` argument is :const:`True`, a list of build ids
|
|
is returned instead.
|
|
|
|
Examples:
|
|
|
|
>>> filename = search_by_symbol_offsets({'puts': 0x420, 'printf': 0xc90}, select_index=1)
|
|
>>> libc = ELF(filename)
|
|
>>> libc.sym.system == 0x52290
|
|
True
|
|
>>> matched_libcs = search_by_symbol_offsets({'__libc_start_main_ret': '7f89ad926550'}, return_as_list=True)
|
|
>>> len(matched_libcs) > 1
|
|
True
|
|
>>> for buildid in matched_libcs: # doctest: +SKIP
|
|
... libc = ELF(search_by_build_id(buildid)) # doctest: +SKIP
|
|
"""
|
|
assert search_type in TYPES, search_type
|
|
|
|
for symbol, address in symbols.items():
|
|
if isinstance(address, int):
|
|
symbols[symbol] = hex(address)
|
|
|
|
params = {'symbols': symbols}
|
|
log.debug('Request: %s', params)
|
|
|
|
offline_matching = query_local_database(params)
|
|
online_matching = query_libc_rip(params) if not offline_only else None
|
|
|
|
if offline_matching is None:
|
|
offline_matching = []
|
|
if online_matching is None:
|
|
online_matching = []
|
|
|
|
# Aggregate and deduplicate matches from both sources
|
|
matching_libcs = {}
|
|
for libc in offline_matching + online_matching:
|
|
if libc['id'] not in matching_libcs:
|
|
matching_libcs[libc['id']] = libc
|
|
|
|
log.debug('Offline search result: %s', offline_matching)
|
|
if not offline_only:
|
|
log.debug('Online search result: %s', online_matching)
|
|
|
|
# Check if no matches are found
|
|
if len(matching_libcs) == 0:
|
|
log.warn_once("No matching libc for symbols %r", symbols)
|
|
return None
|
|
|
|
matching_list = list(matching_libcs.values())
|
|
|
|
if return_as_list:
|
|
return [libc['buildid'] for libc in matching_list]
|
|
|
|
if return_raw:
|
|
return matching_list
|
|
|
|
mapped_type = MAP_TYPES.get(search_type, search_type)
|
|
|
|
# If there's only one match, return it directly
|
|
if len(matching_list) == 1:
|
|
return search_by_hash(matching_list[0][mapped_type], search_type=search_type, unstrip=unstrip, offline_only=offline_only)
|
|
|
|
# If a specific index is provided, validate it and return the selected libc
|
|
if select_index is not None:
|
|
if select_index > 0 and select_index <= len(matching_list):
|
|
return search_by_hash(matching_list[select_index - 1][mapped_type], search_type=search_type, unstrip=unstrip, offline_only=offline_only)
|
|
else:
|
|
log.error('Invalid selected libc index. %d is not in the range of 1-%d.', select_index, len(matching_list))
|
|
return None
|
|
|
|
# Handle multiple matches interactively if no index is specified
|
|
selected_libc = _handle_multiple_matching_libcs(matching_list)
|
|
return search_by_hash(selected_libc[mapped_type], search_type=search_type, unstrip=unstrip, offline_only=offline_only)
|
|
|
|
def search_by_libs_id(libs_id, unstrip=True, offline_only=False):
|
|
"""
|
|
Given a Libs ID, attempt to download a matching libc from libcdb.
|
|
|
|
Arguments:
|
|
libs_id(str):
|
|
Libs ID (e.g. ``'libc6_...'``) of the library
|
|
unstrip(bool):
|
|
Try to fetch debug info for the libc and apply it to the downloaded file.
|
|
offline_only(bool):
|
|
When pass ``offline_only=True``, restricts search mode to offline sources only,
|
|
disable online lookup. Defaults to :const:`False`, and enable both offline and online providers.
|
|
|
|
Returns:
|
|
Path to the downloaded library on disk, or :const:`None`.
|
|
|
|
Examples:
|
|
|
|
>>> None == search_by_libs_id('XX')
|
|
True
|
|
>>> filename = search_by_libs_id('libc6_2.31-3_amd64')
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xeef40'
|
|
"""
|
|
return search_by_hash(libs_id, 'libs_id', unstrip, offline_only)
|
|
|
|
def search_by_build_id(hex_encoded_id, unstrip=True, offline_only=False):
|
|
"""
|
|
Given a hex-encoded Build ID, attempt to download a matching libc from libcdb.
|
|
|
|
Arguments:
|
|
hex_encoded_id(str):
|
|
Hex-encoded Build ID (e.g. 'ABCDEF...') of the library
|
|
unstrip(bool):
|
|
Try to fetch debug info for the libc and apply it to the downloaded file.
|
|
offline_only(bool):
|
|
When pass `offline_only=True`, restricts search mode to offline sources only,
|
|
disable online lookup. Defaults to `False`, and enable both offline and online providers.
|
|
|
|
Returns:
|
|
Path to the downloaded library on disk, or :const:`None`.
|
|
|
|
Examples:
|
|
|
|
>>> filename = search_by_build_id('fe136e485814fee2268cf19e5c124ed0f73f4400')
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xda260'
|
|
>>> None == search_by_build_id('XX')
|
|
True
|
|
>>> filename = search_by_build_id('a5a3c3f65fd94f4c7f323a175707c3a79cbbd614')
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xeef40'
|
|
"""
|
|
return search_by_hash(hex_encoded_id, 'build_id', unstrip, offline_only)
|
|
|
|
def search_by_md5(hex_encoded_id, unstrip=True, offline_only=False):
|
|
"""
|
|
Given a hex-encoded md5sum, attempt to download a matching libc from libcdb.
|
|
|
|
Arguments:
|
|
hex_encoded_id(str):
|
|
Hex-encoded md5sum (e.g. 'ABCDEF...') of the library
|
|
unstrip(bool):
|
|
Try to fetch debug info for the libc and apply it to the downloaded file.
|
|
offline_only(bool):
|
|
When pass `offline_only=True`, restricts search mode to offline sources only,
|
|
disable online lookup. Defaults to `False`, and enable both offline and online providers.
|
|
|
|
Returns:
|
|
Path to the downloaded library on disk, or :const:`None`.
|
|
|
|
Examples:
|
|
|
|
>>> filename = search_by_md5('7a71dafb87606f360043dcd638e411bd')
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xda260'
|
|
>>> None == search_by_md5('XX')
|
|
True
|
|
>>> filename = search_by_md5('74f2d3062180572fc8bcd964b587eeae')
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xeef40'
|
|
"""
|
|
return search_by_hash(hex_encoded_id, 'md5', unstrip, offline_only)
|
|
|
|
def search_by_sha1(hex_encoded_id, unstrip=True, offline_only=False):
|
|
"""
|
|
Given a hex-encoded sha1, attempt to download a matching libc from libcdb.
|
|
|
|
Arguments:
|
|
hex_encoded_id(str):
|
|
Hex-encoded sha1sum (e.g. 'ABCDEF...') of the library
|
|
unstrip(bool):
|
|
Try to fetch debug info for the libc and apply it to the downloaded file.
|
|
offline_only(bool):
|
|
When pass `offline_only=True`, restricts search mode to offline sources only,
|
|
disable online lookup. Defaults to `False`, and enable both offline and online providers.
|
|
|
|
Returns:
|
|
Path to the downloaded library on disk, or :const:`None`.
|
|
|
|
Examples:
|
|
|
|
>>> filename = search_by_sha1('34471e355a5e71400b9d65e78d2cd6ce7fc49de5')
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xda260'
|
|
>>> None == search_by_sha1('XX')
|
|
True
|
|
>>> filename = search_by_sha1('0041d2f397bc2498f62aeb4134d522c5b2635e87')
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xeef40'
|
|
"""
|
|
return search_by_hash(hex_encoded_id, 'sha1', unstrip, offline_only)
|
|
|
|
def search_by_sha256(hex_encoded_id, unstrip=True, offline_only=False):
|
|
"""
|
|
Given a hex-encoded sha256, attempt to download a matching libc from libcdb.
|
|
|
|
Arguments:
|
|
hex_encoded_id(str):
|
|
Hex-encoded sha256sum (e.g. 'ABCDEF...') of the library
|
|
unstrip(bool):
|
|
Try to fetch debug info for the libc and apply it to the downloaded file.
|
|
offline_only(bool):
|
|
When pass `offline_only=True`, restricts search mode to offline sources only,
|
|
disable online lookup. Defaults to `False`, and enable both offline and online providers.
|
|
|
|
Returns:
|
|
Path to the downloaded library on disk, or :const:`None`.
|
|
|
|
Examples:
|
|
|
|
>>> filename = search_by_sha256('5e877a8272da934812d2d1f9ee94f73c77c790cbc5d8251f5322389fc9667f21')
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xda260'
|
|
>>> None == search_by_sha256('XX')
|
|
True
|
|
>>> filename = search_by_sha256('5d78fc60054df18df20480c71f3379218790751090f452baffb62ac6b2aff7ee')
|
|
>>> hex(ELF(filename).symbols.read)
|
|
'0xeef40'
|
|
"""
|
|
return search_by_hash(hex_encoded_id, 'sha256', unstrip, offline_only)
|
|
|
|
def _parse_libc_symbol(path):
|
|
"""
|
|
Parse symbols file to `dict`, the format is same as https://github.com/niklasb/libc-database/
|
|
"""
|
|
|
|
syms = {}
|
|
|
|
with open(path, "r") as fd:
|
|
for x in fd:
|
|
name, addr = x.split(" ")
|
|
syms[name] = int(addr, 16)
|
|
|
|
return syms
|
|
|
|
def _pack_libs_info(path, libs_id, libs_url, syms):
|
|
"""
|
|
The JSON format is the same as libc.rip, and the "download_url" field is by default an empty string,
|
|
as it's not required in offline mode.
|
|
"""
|
|
|
|
info = {}
|
|
|
|
info["id"] = libs_id
|
|
info["libs_url"] = libs_url
|
|
info["download_url"] = ""
|
|
|
|
for search_type, hash_func in TYPES.items():
|
|
# pass libs_id
|
|
if search_type == 'libs_id':
|
|
continue
|
|
|
|
# replace search_type
|
|
if search_type in MAP_TYPES.keys():
|
|
search_type = MAP_TYPES[search_type]
|
|
|
|
info[search_type] = hash_func(path)
|
|
|
|
default_symbol_list = [
|
|
"__libc_start_main_ret", "dup2", "printf", "puts", "read", "system", "str_bin_sh"
|
|
]
|
|
|
|
info["symbols"] = {}
|
|
for name in default_symbol_list:
|
|
info["symbols"][name] = hex(syms[name])
|
|
|
|
return info
|
|
|
|
|
|
def get_build_id_offsets():
|
|
"""
|
|
Returns a list of file offsets where the Build ID should reside within
|
|
an ELF file of the currently selected architecture.
|
|
"""
|
|
# Given the corpus of almost all libc to have been released with
|
|
# RedHat, Fedora, Ubuntu, Debian, etc. over the past several years,
|
|
# we can say with 99% certainty that the GNU Build ID section will
|
|
# be at one of the specified addresses.
|
|
#
|
|
# The point here is to get an easy win by reading less DWORDs than would
|
|
# have otherwise been required to walk the section table and the string
|
|
# stable.
|
|
#
|
|
# function check_arch() {
|
|
# readelf -n $(file -L * | grep -i "$1" | cut -d ':' -f 1) \
|
|
# | grep -B3 BUILD_ID \
|
|
# | grep offset \
|
|
# | sort \
|
|
# | uniq -c
|
|
# }
|
|
|
|
return {
|
|
# $ check_arch 80386
|
|
# 181 Displaying notes found at file offset 0x00000174 with length 0x00000024:
|
|
'i386': [0x174, 0x1b4, 0x1d4],
|
|
# $ check_arch "ARM, EABI5"
|
|
# 69 Displaying notes found at file offset 0x00000174 with length 0x00000024:
|
|
'arm': [0x174],
|
|
'thumb': [0x174],
|
|
# $ check_arch "ARM aarch64"
|
|
# 1 Displaying notes found at file offset 0x00000238 with length 0x00000024:
|
|
'aarch64': [0x238],
|
|
# $ check_arch "x86-64"
|
|
# 6 Displaying notes found at file offset 0x00000174 with length 0x00000024:
|
|
# 82 Displaying notes found at file offset 0x00000270 with length 0x00000024:
|
|
'amd64': [0x270, 0x174, 0x2e0, 0x370],
|
|
# $ check_arch "PowerPC or cisco"
|
|
# 88 Displaying notes found at file offset 0x00000174 with length 0x00000024:
|
|
'powerpc': [0x174],
|
|
# $ check_arch "64-bit PowerPC"
|
|
# 30 Displaying notes found at file offset 0x00000238 with length 0x00000024:
|
|
'powerpc64': [0x238],
|
|
# $ check_arch "SPARC32"
|
|
# 32 Displaying notes found at file offset 0x00000174 with length 0x00000024:
|
|
'sparc': [0x174],
|
|
# $ check_arch "SPARC V9"
|
|
# 33 Displaying notes found at file offset 0x00000270 with length 0x00000024:
|
|
'sparc64': [0x270]
|
|
}.get(context.arch, [])
|
|
|
|
|
|
__all__ = ['get_build_id_offsets', 'search_by_build_id', 'search_by_sha1', 'search_by_sha256', 'search_by_md5', 'search_by_libs_id', 'unstrip_libc', 'search_by_symbol_offsets', 'download_libraries']
|