Repository: Gallopsled/pwntools

Issue:
Support for libc databases with offsets using https://libc.rip/api/
Thanks for contributing to Pwntools!   Ideas from the community help make Pwntools an amazing tool for everybody.

If you've got an idea for a new feature, please provide information about:

* What the feature does
takes a leaked offset and find the correct libc binary and downloads it, then returns and ELF object with that file. 

* Why the feature should exist
To (1) create an abstraction allowing using the same exploit locally with one version of libc and remote with another version of LIBC 
* What tests should be included

If you think you can write the feature yourself, please submit a Pull Request and we can review your changes!
IF nobody is working on that i will be glad to try go build it. 


Discussion:
I thought #1704 does that already, see https://docs.pwntools.com/en/latest/libcdb.html
(it looks up by build ID, not offsets, but it works for most cases). Please close if it works for you, or submit a PR if you have more ideas.
nope, I am talking of a different use case.  Let's say I can leak actual addresses of some libc functions and now want to search by offsets and download the correct libs to build my rop chain. I will propose a PR. 

Any updates on this?
Here's what I used to solve the same scenario if it's any use to anyone finding this issue.

`leak` is a `printf("%<n>$sAAAA<address>")` primitive that can be used repeatedly.
This assumes that cost(leak) > cost(libc.rip lookup). 

If you were using e.g. a ROP that `write`s GOT entries you wouldnt need the loop and could dump a bunch of `known_symbols` at once and make a single request to libc.rip.

```
log.info('Leaking GOT symbols addresses until libc is known')
libc_db_data = None
known_symbols = {}
prev = None
for f in elf.got:
    if f in ['_ITM_deregisterTMCloneTable', '__gmon_start__', '_ITM_registerTMCloneTable', 'stdout', 'stdin']:
        # ignore - address is likely 0
        continue

    address = u64(leak(elf.got[f]).ljust(8, b'\x00'))
    log.info(f'\t{f.ljust(24)}: 0x{address:016x}')

    if address:
        known_symbols[f] = address
        r = requests.post(
            'https://libc.rip/api/find', 
            json={
                'symbols': {
                    s: hex(a) for s, a in known_symbols.items()
                }
            }
        )
        r.raise_for_status()
        found_libcs = r.json()
        if not found_libcs:
            log.warning('\tCould not find libc in libc.rip database - ignoring symbol')
            del known_symbols[f]
            continue
        elif len(found_libcs) == 1:
            libc_db_data = found_libcs[0]
            break
        else:
            log.info(f'{len(found_libcs)} matching libcs found in libc.rip database')
            if len(found_libcs) == prev:
                # Note: could be more forgiving than failing after the first symbol that fails to refine the list
                log.info('Giving up on resolving and using first')
                libc_db_data = found_libcs[0]
                break
            prev = len(found_libcs)


log.info(f'Found singular matching libc:\n{pformat(libc_db_data)}')
libc_elf_path = libc_db_data['id']
try:
    libc = ELF(libc_elf_path)
except:
    download(libc_db_data['download_url'], libc_elf_path)
    libc = ELF(libc_elf_path)
```
Partially addressed by #1828

Resolving patch:
diff --git a/pwnlib/libcdb.py b/pwnlib/libcdb.py
--- a/pwnlib/libcdb.py
+++ b/pwnlib/libcdb.py
@@ -75,13 +75,15 @@ def provider_libc_rip(hex_encoded_id, hash_type):
     data = b""
     try:
         result = requests.post(url, json=params, timeout=20)
-        if result.status_code != 200 or len(result.json()) == 0:
+        result.raise_for_status()
+        libc_match = result.json()
+        if not libc_match:
             log.warn_once("Could not find libc for %s %s on libc.rip", hash_type, hex_encoded_id)
-            log.debug("Error: %s", result.text)
             return None
 
-        libc_match = result.json()
-        assert len(libc_match) == 1, 'Invalid libc.rip response.'
+        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)
@@ -253,6 +255,98 @@ def unstrip_libc(filename):
 
     return True
 
+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, return_as_list=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.
+        return_as_list(bool):
+            Return a list of build ids of all matching libc versions
+            instead of a path to a downloaded file.
+
+    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
+    """
+    import requests
+    for symbol, address in symbols.items():
+        if isinstance(address, int):
+            symbols[symbol] = hex(address)
+    try:
+        params = {'symbols': symbols}
+        url    = "https://libc.rip/api/find"
+        log.debug('Request: %s', params)
+        result = requests.post(url, json=params, timeout=20)
+        result.raise_for_status()
+
+        matching_libcs = result.json()
+        log.debug('Result: %s', matching_libcs)
+        if len(matching_libcs) == 0:
+            log.warn_once("No matching libc for symbols %r on libc.rip", symbols)
+            return None
+
+        if return_as_list:
+            return [libc['buildid'] for libc in matching_libcs]
+
+        if len(matching_libcs) == 1:
+            return search_by_build_id(matching_libcs[0]['buildid'], unstrip=unstrip)
+
+        if select_index is not None:
+            if select_index > 0 and select_index <= len(matching_libcs):
+                return search_by_build_id(matching_libcs[select_index - 1]['buildid'], unstrip=unstrip)
+            else:
+                log.error('Invalid selected libc index. %d is not in the range of 1-%d.', select_index, len(matching_libcs))
+                return None
+
+        selected_libc = _handle_multiple_matching_libcs(matching_libcs)
+        return search_by_build_id(selected_libc['buildid'], unstrip=unstrip)
+    except requests.RequestException as e:
+        log.warn_once("Failed to lookup libc for symbols %r from libc.rip: %s", symbols, e)
+        return None
+
 def search_by_build_id(hex_encoded_id, unstrip=True):
     """
     Given a hex-encoded Build ID, attempt to download a matching libc from libcdb.
@@ -409,4 +503,4 @@ def get_build_id_offsets():
     }.get(context.arch, [])
 
 
-__all__ = ['get_build_id_offsets', 'search_by_build_id', 'search_by_sha1', 'search_by_sha256', 'search_by_md5', 'unstrip_libc']
+__all__ = ['get_build_id_offsets', 'search_by_build_id', 'search_by_sha1', 'search_by_sha256', 'search_by_md5', 'unstrip_libc', 'search_by_symbol_offsets']


Test patch: