Skip to content

Commit fa14663

Browse files
libcdb-cli: add --offline-only, refactor unstrip and add fetch parser for download libc-database (#2478)
* Add `return_raw` for `search_by_symbol_offsets` * Add `--offline-only` and unstrip libc as default behavior * Add short arg name for `--download-libc` * Fix bugs * Add fetch parser * Fix bugs * file parse not unstrip default * Update function docs * Update CHANGELOG * Edit dest of `--no-strip` * Update CHANGELOG * Unstrip before `return cache` * Revert `--unstrip` help * Improve `libcdb fetch` default behavior * Perf fetch command * Fix fetch command `-u` --------- Co-authored-by: peace-maker <peacemakerctf@gmail.com>
1 parent d225311 commit fa14663

File tree

3 files changed

+123
-74
lines changed

3 files changed

+123
-74
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ The table below shows which release corresponds to each branch, and what date th
8282
- [#2479][2479] Support extracting libraries from Docker image in `pwn template`
8383
- [#2483][2483] Only print `checksec` output of `ELF.libc` when it was printed for the `ELF` already
8484
- [#2482][2482] Throw error when using `sni` and setting `server_hostname` manually in `remote`
85+
- [#2478][2478] libcdb-cli: add `--offline-only`, refactor unstrip and add fetch parser for download libc-database
8586

8687
[2471]: https://github.com/Gallopsled/pwntools/pull/2471
8788
[2358]: https://github.com/Gallopsled/pwntools/pull/2358
@@ -92,6 +93,7 @@ The table below shows which release corresponds to each branch, and what date th
9293
[2479]: https://github.com/Gallopsled/pwntools/pull/2479
9394
[2483]: https://github.com/Gallopsled/pwntools/pull/2483
9495
[2482]: https://github.com/Gallopsled/pwntools/pull/2482
96+
[2478]: https://github.com/Gallopsled/pwntools/pull/2478
9597

9698
## 4.14.0 (`beta`)
9799

pwnlib/commandline/libcdb.py

+110-70
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,27 @@
3737
)
3838

3939
lookup_parser.add_argument(
40-
'--download-libc',
40+
'-d', '--download-libc',
4141
action = 'store_true',
4242
default = False,
4343
help = 'Attempt to download the matching libc.so'
4444
)
4545

46-
lookup_parser.add_argument(
47-
'--unstrip',
48-
action = 'store_true',
49-
default = True,
50-
help = 'Attempt to unstrip the libc binary with debug symbols from a debuginfod server'
51-
)
52-
5346
lookup_parser.add_argument(
5447
'--no-unstrip',
5548
action = 'store_false',
5649
dest = 'unstrip',
5750
help = 'Do NOT attempt to unstrip the libc binary with debug symbols from a debuginfod server'
5851
)
5952

53+
lookup_parser.add_argument(
54+
'--offline-only',
55+
action = 'store_true',
56+
default = False,
57+
dest = 'offline_only',
58+
help = 'Attempt to searching with offline only mode'
59+
)
60+
6061
hash_parser = libc_commands.add_parser(
6162
'hash',
6263
help = 'Display information of a libc version given an unique hash',
@@ -80,26 +81,27 @@
8081
)
8182

8283
hash_parser.add_argument(
83-
'--download-libc',
84+
'-d', '--download-libc',
8485
action = 'store_true',
8586
default = False,
8687
help = 'Attempt to download the matching libc.so'
8788
)
8889

89-
hash_parser.add_argument(
90-
'--unstrip',
91-
action = 'store_true',
92-
default = True,
93-
help = 'Attempt to unstrip the libc binary with debug symbols from a debuginfod server'
94-
)
95-
9690
hash_parser.add_argument(
9791
'--no-unstrip',
9892
action = 'store_false',
9993
dest = 'unstrip',
10094
help = 'Do NOT attempt to unstrip the libc binary with debug symbols from a debuginfod server'
10195
)
10296

97+
hash_parser.add_argument(
98+
'--offline-only',
99+
action = 'store_true',
100+
default = False,
101+
dest = 'offline_only',
102+
help = 'Attempt to searching with offline only mode'
103+
)
104+
103105
file_parser = libc_commands.add_parser(
104106
'file',
105107
help = 'Dump information about a libc binary',
@@ -130,25 +132,34 @@
130132
file_parser.add_argument(
131133
'--unstrip',
132134
action = 'store_true',
133-
default = False,
135+
dest = 'unstrip',
134136
help = 'Attempt to unstrip the libc binary inplace with debug symbols from a debuginfod server'
135137
)
136138

137-
common_symbols = ['dup2', 'printf', 'puts', 'read', 'system', 'write']
139+
fetch_parser = libc_commands.add_parser(
140+
'fetch',
141+
help = 'Fetch libc database',
142+
description = 'Fetch libc database. If no argument passed, it will init and upgrade libc-database repository',
143+
)
138144

139-
def find_libc(params):
140-
import requests
141-
url = "https://libc.rip/api/find"
142-
result = requests.post(url, json=params, timeout=20)
143-
log.debug('Request: %s', params)
144-
log.debug('Result: %s', result.json())
145-
if result.status_code != 200 or len(result.json()) == 0:
146-
log.failure("Could not find libc for %s on libc.rip", params)
147-
return []
145+
fetch_parser.add_argument(
146+
'path',
147+
nargs = '?',
148+
default = context.local_libcdb,
149+
help = 'Set libc-database path, If it is empty, the default path will be `context.local_libcdb` (%s)' % context.local_libcdb
150+
)
151+
152+
fetch_parser.add_argument(
153+
'-u', '--update',
154+
metavar = 'update',
155+
nargs = '+',
156+
choices = ['all', 'ubuntu', 'debian', 'rpm', 'centos', 'arch', 'alpine', 'kali', 'parrotsec', 'launchpad'],
157+
help = 'Fetch the desired libc categories'
158+
)
148159

149-
return result.json()
160+
common_symbols = ['dup2', 'printf', 'puts', 'read', 'system', 'write']
150161

151-
def print_libc(libc):
162+
def print_libc_info(libc):
152163
log.info('%s', text.red(libc['id']))
153164
log.indented('\t%-20s %s', text.green('BuildID:'), libc['buildid'])
154165
log.indented('\t%-20s %s', text.green('MD5:'), libc['md5'])
@@ -158,14 +169,39 @@ def print_libc(libc):
158169
for symbol in libc['symbols'].items():
159170
log.indented('\t%25s = %s', symbol[0], symbol[1])
160171

161-
def handle_remote_libc(args, libc):
162-
print_libc(libc)
163-
if args.download_libc:
164-
path = libcdb.search_by_build_id(libc['buildid'], args.unstrip)
165-
if path:
166-
if args.unstrip:
167-
libcdb.unstrip_libc(path)
168-
shutil.copy(path, './{}.so'.format(libc['id']))
172+
def print_libc_elf(exe):
173+
from hashlib import md5, sha1, sha256
174+
175+
log.info('%s', text.red(os.path.basename(exe.path)))
176+
177+
libc_version = get_libc_version(exe)
178+
if libc_version:
179+
log.indented('%-20s %s', text.green('Version:'), libc_version)
180+
181+
if exe.buildid:
182+
log.indented('%-20s %s', text.green('BuildID:'), enhex(exe.buildid))
183+
184+
log.indented('%-20s %s', text.green('MD5:'), md5(exe.data).hexdigest())
185+
log.indented('%-20s %s', text.green('SHA1:'), sha1(exe.data).hexdigest())
186+
log.indented('%-20s %s', text.green('SHA256:'), sha256(exe.data).hexdigest())
187+
188+
# Always dump the basic list of common symbols
189+
log.indented('%s', text.green('Symbols:'))
190+
synthetic_symbols = collect_synthetic_symbols(exe)
191+
192+
symbols = common_symbols + (args.symbols or []) + synthetic_symbols
193+
symbols.sort()
194+
for symbol in symbols:
195+
if symbol not in exe.symbols:
196+
log.indented('%25s = %s', symbol, text.red('not found'))
197+
else:
198+
log.indented('%25s = %#x', symbol, translate_offset(exe.symbols[symbol], args, exe))
199+
200+
def get_libc_version(exe):
201+
res = re.search(br'libc[ -](\d+\.\d+)', exe.data)
202+
if res:
203+
return res.group(1).decode()
204+
return None
169205

170206
def translate_offset(offs, args, exe):
171207
if args.offset:
@@ -182,7 +218,7 @@ def collect_synthetic_symbols(exe):
182218
available_symbols.append('str_bin_sh')
183219
except StopIteration:
184220
pass
185-
221+
186222
libc_start_main_return = exe.libc_start_main_return
187223
if libc_start_main_return > 0:
188224
exe.symbols['__libc_start_main_ret'] = libc_start_main_return
@@ -200,52 +236,56 @@ def main(args):
200236
if len(pairs) % 2 != 0:
201237
log.failure('Uneven number of arguments. Please provide "symbol offset" pairs')
202238
return
203-
239+
204240
symbols = {pairs[i]:pairs[i+1] for i in range(0, len(pairs), 2)}
205-
matched_libcs = find_libc({'symbols': symbols})
241+
matched_libcs = libcdb.search_by_symbol_offsets(symbols, offline_only=args.offline_only, return_raw=True)
242+
206243
for libc in matched_libcs:
207-
handle_remote_libc(args, libc)
244+
print_libc_info(libc)
245+
if args.download_libc:
246+
path = libcdb.search_by_build_id(libc['buildid'], args.unstrip)
247+
if path:
248+
shutil.copy(path, './{}.so'.format(libc['id']))
208249

209250
elif args.libc_command == 'hash':
251+
inverted_map = {v: k for k, v in libcdb.MAP_TYPES.items()}
252+
hash_type = inverted_map.get(args.hash_type, args.hash_type)
253+
210254
for hash_value in args.hash_value:
211-
matched_libcs = find_libc({args.hash_type: hash_value})
212-
for libc in matched_libcs:
213-
handle_remote_libc(args, libc)
255+
path = libcdb.search_by_hash(hash_value, hash_type, unstrip=args.unstrip, offline_only=args.offline_only)
256+
exe = ELF(path, checksec=False)
257+
print_libc_elf(exe)
258+
259+
if args.download_libc:
260+
# if we cannot get actual libc version then copy with cache name
261+
shutil.copy(path, './libc-{}.so'.format(get_libc_version(exe) or Path(path).stem))
214262

215263
elif args.libc_command == 'file':
216-
from hashlib import md5, sha1, sha256
217264
for file in args.files:
218265
if not os.path.exists(file) or not os.path.isfile(file):
219266
log.failure('File does not exist %s', args.file)
220267
continue
221-
268+
222269
if args.unstrip:
223270
libcdb.unstrip_libc(file)
224271

225-
exe = ELF(file, checksec=False)
226-
log.info('%s', text.red(os.path.basename(file)))
227-
228-
libc_version = re.search(br'libc[ -](\d+\.\d+)', exe.data)
229-
if libc_version:
230-
log.indented('%-20s %s', text.green('Version:'), libc_version.group(1).decode())
231-
232-
if exe.buildid:
233-
log.indented('%-20s %s', text.green('BuildID:'), enhex(exe.buildid))
234-
log.indented('%-20s %s', text.green('MD5:'), md5(exe.data).hexdigest())
235-
log.indented('%-20s %s', text.green('SHA1:'), sha1(exe.data).hexdigest())
236-
log.indented('%-20s %s', text.green('SHA256:'), sha256(exe.data).hexdigest())
237-
238-
# Always dump the basic list of common symbols
239-
log.indented('%s', text.green('Symbols:'))
240-
synthetic_symbols = collect_synthetic_symbols(exe)
241-
242-
symbols = common_symbols + (args.symbols or []) + synthetic_symbols
243-
symbols.sort()
244-
for symbol in symbols:
245-
if symbol not in exe.symbols:
246-
log.indented('%25s = %s', symbol, text.red('not found'))
247-
else:
248-
log.indented('%25s = %#x', symbol, translate_offset(exe.symbols[symbol], args, exe))
272+
print_libc_elf(ELF(file, checksec=False))
273+
274+
elif args.libc_command == 'fetch':
275+
276+
if args.update:
277+
subprocess.check_call(['./get'] + args.update, cwd=args.path)
278+
279+
else:
280+
if not Path(args.path).exists():
281+
if yesno("Would you like to initialize the libc-database repository? "
282+
"If the path already exists, this prompt will not display, and automatically upgrade repository."):
283+
log.waitfor("init libc-database repository")
284+
subprocess.check_call(['git', 'clone', 'https://github.com/niklasb/libc-database/', args.path])
285+
else:
286+
log.waitfor("upgrade libc-database repository")
287+
subprocess.check_call(['git', 'pull'], cwd=args.path)
288+
249289

250290
if __name__ == '__main__':
251291
pwnlib.commandline.common.main(__file__, main)

pwnlib/libcdb.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ def search_by_hash(search_target, search_type='build_id', unstrip=True, offline_
259259
# Ensure that the libcdb cache directory exists
260260
cache, cache_valid = _check_elf_cache('libcdb', search_target, search_type)
261261
if cache_valid:
262+
if unstrip:
263+
unstrip_libc(cache)
262264
return cache
263265

264266
# We searched for this buildid before, but didn't find anything.
@@ -653,7 +655,7 @@ def _handle_multiple_matching_libcs(matching_libcs):
653655
selected_index = options("Select the libc version to use:", [libc['id'] for libc in matching_libcs])
654656
return matching_libcs[selected_index]
655657

656-
def search_by_symbol_offsets(symbols, select_index=None, unstrip=True, return_as_list=False, offline_only=False, search_type='build_id'):
658+
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):
657659
"""
658660
Lookup possible matching libc versions based on leaked function addresses.
659661
@@ -672,14 +674,16 @@ def search_by_symbol_offsets(symbols, select_index=None, unstrip=True, return_as
672674
The libc to select if there are multiple matches (starting at 1).
673675
unstrip(bool):
674676
Try to fetch debug info for the libc and apply it to the downloaded file.
675-
return_as_list(bool):
676-
Return a list of build ids of all matching libc versions
677-
instead of a path to a downloaded file.
678677
offline_only(bool):
679678
When pass `offline_only=True`, restricts search mode to offline sources only,
680679
disable online lookup. Defaults to `False`, and enable both offline and online providers.
681680
search_type(str):
682681
An option to select searched hash.
682+
return_as_list(bool):
683+
Return a list of build ids of all matching libc versions
684+
instead of a path to a downloaded file.
685+
return_raw(bool):
686+
Return raw list of matched libc.
683687
684688
Returns:
685689
Path to the downloaded library on disk, or :const:`None`.
@@ -735,6 +739,9 @@ def search_by_symbol_offsets(symbols, select_index=None, unstrip=True, return_as
735739
if return_as_list:
736740
return [libc['buildid'] for libc in matching_list]
737741

742+
if return_raw:
743+
return matching_list
744+
738745
mapped_type = MAP_TYPES.get(search_type, search_type)
739746

740747
# If there's only one match, return it directly

0 commit comments

Comments
 (0)