├── bin_check ├── __init__.py ├── function_models.pyc ├── filter_binary.py ├── celery_app.py ├── tracing.py ├── backward_slicing.py └── function_models.py ├── examples ├── upload.cgi └── medium_format ├── tox.ini ├── tests ├── __pycache__ │ └── injection_test.cpython-36-pytest-6.2.5.pyc ├── vulnerability_test.py └── backward_slicing_test.py ├── setup.py ├── readme.md └── bin └── bcheck.py /bin_check/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/upload.cgi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisTheCoolHut/bcheck/HEAD/examples/upload.cgi -------------------------------------------------------------------------------- /examples/medium_format: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisTheCoolHut/bcheck/HEAD/examples/medium_format -------------------------------------------------------------------------------- /bin_check/function_models.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisTheCoolHut/bcheck/HEAD/bin_check/function_models.pyc -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py3 3 | #skipsdist = true 4 | 5 | [testenv] 6 | deps = pytest 7 | commands = python -m pytest -s -v -------------------------------------------------------------------------------- /tests/__pycache__/injection_test.cpython-36-pytest-6.2.5.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisTheCoolHut/bcheck/HEAD/tests/__pycache__/injection_test.cpython-36-pytest-6.2.5.pyc -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | setuptools.setup( 5 | name='bin_check', 6 | version='1.0', 7 | scripts=['bin/bcheck.py'] , 8 | author="Christoppher Roberts", 9 | author_email="", 10 | description="Printf and Command injection testing tool", 11 | url="https://github.com/ChrisTheCoolHut/Not_a_repo_yet", 12 | packages=["bin_check"], 13 | install_package_data=True, 14 | install_requires=[ 15 | "angr", 16 | "celery", 17 | "pyelftools", 18 | "r2pipe", 19 | "tox", 20 | "tqdm", 21 | ], 22 | 23 | ) 24 | -------------------------------------------------------------------------------- /tests/vulnerability_test.py: -------------------------------------------------------------------------------- 1 | from angr import sim_options as so 2 | import angr 3 | 4 | from bin_check.tracing import get_funcs_and_prj 5 | from bin_check.celery_app import do_trace, fix_sys_bugs 6 | 7 | 8 | def test_detect_injection(): 9 | test_file = "examples/upload.cgi" 10 | mtd_write_bootloader = 0x00401338 11 | 12 | funcs, proj = get_funcs_and_prj(test_file, True, False) 13 | 14 | assert len(funcs) > 0 15 | 16 | addr, cmd = do_trace(proj, mtd_write_bootloader) 17 | 18 | assert addr is not None 19 | assert cmd is not None 20 | 21 | 22 | def test_detect_format(): 23 | test_file = "examples/medium_format" 24 | main = 0x08048583 25 | 26 | funcs, proj = get_funcs_and_prj(test_file, False, True) 27 | 28 | assert len(funcs) > 0 29 | 30 | addr, cmd = do_trace(proj, main) 31 | 32 | assert addr is not None 33 | assert cmd is not None 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # bcheck 2 | 3 | Binary check tool to identify command injection and format string vulnerabilities in blackbox binaries. Using xrefs to commonly injected and format string'd files, it will scan binaries faster than Firmware Slap. 4 | 5 | ## Install 6 | 7 | ``` 8 | sudo apt install rabbitmq 9 | pip install -e . 10 | ``` 11 | 12 | ## Usage 13 | ``` 14 | bcheck.py -h 15 | usage: bcheck.py [-h] [-p] [-s] [-f] [-t TIMEOUT] [-m MEMORY_LIMIT] [-v] file 16 | 17 | positional arguments: 18 | file Binary file to check 19 | 20 | optional arguments: 21 | -h, --help show this help message and exit 22 | -p, --printf Enable printf checking 23 | -s, --system Enable command injection checking 24 | -f, --filter Enables basic binary filtering 25 | -v, --verbose Increases logging verbosity 26 | 27 | Worker Options: 28 | -t TIMEOUT, --timeout TIMEOUT 29 | Set worker timeout. Default 60 seconds 30 | -m MEMORY_LIMIT, --memory_limit MEMORY_LIMIT 31 | Set worker memory limit in GB. Default 2GB 32 | ``` 33 | 34 | ## Example 35 | 36 | ``` 37 | $ bcheck.py -s examples/upload.cgi 38 | [~] Checking for command injections 39 | 100% |############################################################| Elapsed Time: 0:00:01 Time: 0:00:01 40 | Found 5 test sites in binary 41 | [-] Scanned functions: 42 | [-] : 0x401a28 : getLanIP 43 | [+] : 0x4012b8 : mtd_write_firmware 44 | 0x7ffefdf8 -> b'/bin/mtd_write -o 0 -l 0 write AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x01 Kernel' 45 | [-] : 0x4009f0 : main 46 | [+] : 0x4010d0 : write_flash_kernel_version 47 | 0x7ffefdf8 -> b'nvram_set 2860 old_firmware "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00"' 48 | [+] : 0x401338 : mtd_write_bootloader 49 | 0x7ffefdf8 -> b'/bin/mtd_write -o 0 -l 0 write AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x01 Bootloader' 50 | 51 | ``` 52 | 53 | ## Test 54 | 55 | ```bash 56 | tox 57 | ``` -------------------------------------------------------------------------------- /tests/backward_slicing_test.py: -------------------------------------------------------------------------------- 1 | from angr import sim_options as so 2 | import angr 3 | 4 | from bin_check.tracing import get_funcs_and_prj 5 | from bin_check.celery_app import do_trace 6 | from bin_check.backward_slicing import ( 7 | create_initial_program_slice, 8 | get_next_predecessor_path, 9 | get_r2_instance, 10 | ) 11 | 12 | 13 | def test_can_create_xref_slice(): 14 | test_file = "examples/upload.cgi" 15 | mtd_write_firmware = 0x004012B8 16 | 17 | p_slice = create_initial_program_slice(mtd_write_firmware) 18 | r2 = get_r2_instance(test_file) 19 | 20 | p_slices = get_next_predecessor_path(r2, p_slice) 21 | 22 | assert len(p_slices) > 0 23 | 24 | 25 | def test_can_create_block_slice(): 26 | test_file = "examples/upload.cgi" 27 | mtd_write_firmware = 0x004012B8 28 | 29 | p_slice = create_initial_program_slice(mtd_write_firmware) 30 | r2 = get_r2_instance(test_file) 31 | 32 | # This will use xref 33 | p_slices = get_next_predecessor_path(r2, p_slice) 34 | 35 | assert len(p_slices) > 0 36 | 37 | # This will use bb predecessors 38 | p_slices_2 = get_next_predecessor_path(r2, p_slices[0]) 39 | 40 | assert len(p_slices_2) > 0 41 | 42 | 43 | def test_find_bug_in_slices(): 44 | test_file = "examples/upload.cgi" 45 | mtd_write_firmware = 0x004012B8 46 | 47 | p_slice = create_initial_program_slice(mtd_write_firmware) 48 | r2 = get_r2_instance(test_file) 49 | 50 | p_slices = get_next_predecessor_path(r2, p_slice) 51 | 52 | assert len(p_slices) > 0 53 | 54 | funcs, proj = get_funcs_and_prj( 55 | test_file, system_check=True, printf_check=False, use_angr=False, r2=r2 56 | ) 57 | 58 | assert len(funcs) > 0 59 | 60 | # Xref slices working? 61 | for slice in p_slices: 62 | addr, cmd = do_trace(proj, slice.end_addr, avoid_list=slice.avoid_list) 63 | 64 | assert addr is not None 65 | assert cmd is not None 66 | 67 | # This will use bb predecessors 68 | p_slices_2 = get_next_predecessor_path(r2, p_slices[0]) 69 | 70 | assert len(p_slices_2) > 0 71 | 72 | for slice in p_slices_2: 73 | addr, cmd = do_trace(proj, slice.end_addr, avoid_list=slice.avoid_list) 74 | 75 | assert addr is not None 76 | assert cmd is not None -------------------------------------------------------------------------------- /bin_check/filter_binary.py: -------------------------------------------------------------------------------- 1 | from elftools.elf.elffile import ELFFile 2 | from elftools.elf.sections import SymbolTableSection 3 | from elftools.elf.descriptions import describe_symbol_type, describe_symbol_shndx 4 | from subprocess import check_output 5 | 6 | from bin_check.function_models import system_list 7 | import logging 8 | 9 | string_cmd = "strings {}" 10 | 11 | file_name_blacklist = [ 12 | "cli", 13 | "busybox", 14 | "ssi", 15 | "dns", 16 | "wpa_", 17 | "hostapd", 18 | "lld2d", 19 | "dhcp", 20 | "pppd", 21 | "dropbear", 22 | "smbd", 23 | "pppoe", 24 | "wget", 25 | "curl", 26 | "mDNS", 27 | "sendmail", 28 | "wifi_", 29 | "openssl", 30 | "telnet", 31 | "libcrypto", 32 | "snmpd", 33 | "wlanconfig", 34 | "ldap", 35 | "ssh", 36 | "iwconfig", 37 | ] 38 | 39 | NETWORK_KEYWORDS = [ 40 | b"Content-Length", 41 | b"Content-Type", 42 | b"GET", 43 | b"HTTP", 44 | b"HTTP_", 45 | b"POST", 46 | b"QUERY_STRING", 47 | b"REMOTE_ADDR", 48 | b"boundary=", 49 | b"http", 50 | b"http_", 51 | b"index.", 52 | b"query", 53 | b"remote", 54 | b"soap", 55 | b"user-agent", 56 | ] 57 | 58 | 59 | def get_symbol_names_elf(filename): 60 | 61 | with open(filename, "rb") as f: 62 | elffile = ELFFile(f) 63 | 64 | symbols = [] 65 | 66 | for section in elffile.iter_sections(): 67 | if not isinstance(section, SymbolTableSection): 68 | continue 69 | 70 | if section["sh_entsize"] == 0: 71 | continue 72 | 73 | for _, symbol in enumerate(section.iter_symbols()): 74 | if ( 75 | describe_symbol_shndx(symbol["st_shndx"]) != "UND" 76 | and describe_symbol_type(symbol["st_info"]["type"]) == "FUNC" 77 | ): 78 | symbols.append(symbol.name) 79 | 80 | return symbols 81 | 82 | 83 | def get_strings(filename): 84 | 85 | strings = check_output(string_cmd.format(filename), shell=True) 86 | strings = strings.split(b"\n") 87 | 88 | return strings 89 | 90 | 91 | def should_check_binary(filename): 92 | 93 | # Filename blacklist 94 | logging.debug("Filtering blacklisted files") 95 | for blacklist_filename in file_name_blacklist: 96 | if blacklist_filename.lower() in filename.lower(): 97 | print("[-] Filter on {}".format(blacklist_filename)) 98 | return False 99 | 100 | # Check symbols 101 | logging.debug("Filtering binaries without system calls") 102 | has_function_to_check = False 103 | symbols = get_symbol_names_elf(filename) 104 | for symbol in system_list: 105 | if symbol not in symbols: 106 | has_function_to_check = True 107 | 108 | if not has_function_to_check: 109 | return False 110 | 111 | # Check strings 112 | logging.debug("Filtering by network strings") 113 | strings = get_strings(filename) 114 | interesting_strings = False 115 | for string in strings: 116 | for keyword in NETWORK_KEYWORDS: 117 | if keyword in string: 118 | logging.debug("[+] Found {}".format(string)) 119 | interesting_strings = True 120 | 121 | return interesting_strings 122 | -------------------------------------------------------------------------------- /bin_check/celery_app.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | import multiprocessing 3 | from celery.result import allow_join_result 4 | import tqdm 5 | import time 6 | import atexit 7 | import sys 8 | import os 9 | import logging 10 | 11 | options = { 12 | "broker_url": "pyamqp://guest@localhost//", 13 | "result_backend": "rpc://", 14 | "worker_max_memory_per_child": 2048000, # 2GB 15 | "task_time_limit": 60, # 1 minute timeout 16 | "accept_content": ["pickle", "json"], 17 | "result_serializer": "pickle", 18 | "task_serializer": "pickle", 19 | "task_acks_late" : True 20 | } 21 | 22 | 23 | app = Celery("CeleryTask") 24 | app.config_from_object(options) 25 | 26 | @app.task 27 | def do_trace_with_id(proj, func_addr, avoid_list=[], id=None): 28 | return do_trace(proj, func_addr, avoid_list) + (id,) 29 | 30 | # Keep it simple. Make a blank state and let it run 31 | # arguments are already symbolic data so we don't 32 | # need to build a call state. 33 | @app.task 34 | def do_trace(proj, func_addr, avoid_list=[]): 35 | fix_sys_bugs() 36 | 37 | state = proj.factory.blank_state(addr=func_addr) 38 | state.globals["func_addr"] = func_addr 39 | simgr = proj.factory.simgr(state) 40 | simgr.explore(find=lambda s: "exploitable" in s.globals, avoid=avoid_list) 41 | 42 | if "found" in simgr.stashes and len(simgr.found): 43 | return (func_addr, simgr.found[0].globals["cmd"]) 44 | if "errored" in simgr.stashes and len(simgr.errored): 45 | for err_record in simgr.errored: 46 | #print(err_record) 47 | #print(err_record.state.globals.items()) 48 | if "game over" in str(err_record.state): 49 | return (func_addr, err_record.state.globals["cmd"]) 50 | if "exploitable" in err_record.state.globals: 51 | return (func_addr, err_record.state.globals["cmd"]) 52 | if len(simgr.stashes): 53 | for stash in simgr.stashes: 54 | for state in simgr.stashes[stash]: 55 | if "exploitable" in state.globals: 56 | return (func_addr, state.globals["cmd"]) 57 | return None, None 58 | 59 | 60 | def mute_worker(): 61 | logging.basicConfig() 62 | print = logging.info 63 | 64 | 65 | def quiet_worker_launch(worker): 66 | logger = logging.getLogger() 67 | logger.disabled = True 68 | logger.propagate = False 69 | sys.stdout = open(os.devnull, "w") 70 | worker.start() 71 | 72 | 73 | def start_workers(worker): 74 | p = multiprocessing.Process(target=quiet_worker_launch, args=(worker,)) 75 | # p = multiprocessing.Process(target=worker.start, initializer=mute_worker) 76 | p.start() 77 | atexit.register(worker.stop) 78 | atexit.register(p.join) 79 | return p 80 | 81 | 82 | def async_and_iter(async_function, async_list): 83 | 84 | async_funcs = [] 85 | 86 | for item in async_list: 87 | # n_task = async_function.delay(item) 88 | n_task = async_function.apply_async(args=item) 89 | # n_task = async_function.apply_async(args=[item[0], item[1]]) 90 | async_funcs.append(n_task) 91 | 92 | bar = tqdm.tqdm(total=len(async_funcs)) 93 | # If a process get's sigkilled, it kills this main parent 94 | # Sp just break when it fully fails 95 | while True: 96 | try: 97 | while not all([x.successful() or x.failed() for x in async_funcs]): 98 | done_count = len( 99 | [ 100 | x.successful() or x.failed() 101 | for x in async_funcs 102 | if x.successful() or x.failed() 103 | ] 104 | ) 105 | bar.update(done_count - bar.n) 106 | time.sleep(1) 107 | break 108 | except ConnectionResetError: 109 | pass 110 | bar.close() 111 | 112 | with allow_join_result(): 113 | return [x.get(propagate=False) for x in async_funcs if not x.failed()] 114 | 115 | 116 | def fix_sys_bugs(): 117 | try: 118 | import sys 119 | 120 | sys.stdout.encoding = ( 121 | "UTF-8" # AttributeError: 'LoggingProxy' object has no attribute 'encoding' 122 | ) 123 | except (AttributeError, TypeError): # AttributeError: readonly attribute 124 | pass 125 | -------------------------------------------------------------------------------- /bin/bcheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import logging 4 | import shutil 5 | 6 | logging.basicConfig() 7 | 8 | from bin_check.filter_binary import should_check_binary 9 | from bin_check.celery_app import * 10 | from bin_check.tracing import get_funcs_and_prj 11 | 12 | log_things = ["angr", "pyvex", "claripy", "cle"] 13 | for log in log_things: 14 | logger = logging.getLogger(log) 15 | logger.disabled = True 16 | logger.propagate = False 17 | 18 | def r2_installed(): 19 | return shutil.which("r2") 20 | 21 | def main(): 22 | 23 | parser = argparse.ArgumentParser() 24 | parser.add_argument("file", help="Binary file to check") 25 | 26 | parser.add_argument( 27 | "-p", 28 | "--printf", 29 | help="Enable printf checking", 30 | action="store_true", 31 | default=False, 32 | ) 33 | parser.add_argument( 34 | "-s", 35 | "--system", 36 | help="Enable command injection checking", 37 | action="store_true", 38 | default=False, 39 | ) 40 | parser.add_argument( 41 | "-f", 42 | "--filter", 43 | help="Enables basic binary filtering", 44 | action="store_true", 45 | default=False, 46 | ) 47 | worker_options = parser.add_argument_group(title="Worker Options") 48 | worker_options.add_argument( 49 | "-t", 50 | "--timeout", 51 | help="Set worker timeout. Default 60 seconds", 52 | type=int, 53 | default=60, 54 | ) 55 | worker_options.add_argument( 56 | "-m", 57 | "--memory_limit", 58 | help="Set worker memory limit in GB. Default 2GB", 59 | type=int, 60 | default=2097152, 61 | ) 62 | parser.add_argument( 63 | "-v", 64 | "--verbose", 65 | help="Increases logging verbosity", 66 | action="store_true", 67 | default=False, 68 | ) 69 | parser.add_argument( 70 | "--use_angr_cfg", 71 | action="store_true", 72 | help="Use angr to generate CFG (Warning it's slow and memory intensive)", 73 | default=False 74 | ) 75 | 76 | if not r2_installed: 77 | print("Can't find r2, using angr CFG. Results will be SLOW. reccomend installing radare2.") 78 | args.use_angr_cfg = True 79 | 80 | args = parser.parse_args() 81 | 82 | if args.verbose: 83 | logging.getLogger().setLevel(logging.DEBUG) 84 | 85 | app.conf["task_time_limit"] = args.timeout 86 | app.conf["worker_max_memory_per_child"] = args.memory_limit * 1024 * 1024 87 | 88 | if not args.system and not args.printf: 89 | print("Please select check mode of printf checking (-p) or command injection testing (-s). Or both") 90 | exit(0) 91 | 92 | print("[~] {}".format(args.file)) 93 | 94 | if args.filter: 95 | if not should_check_binary(args.file): 96 | print("Filtered out binary") 97 | exit(0) 98 | 99 | if args.system: 100 | print("[~] Checking for command injections") 101 | 102 | if args.printf: 103 | print("[~] Checking for format string vulnerabilities") 104 | 105 | funcs, proj = get_funcs_and_prj(args.file, args.system, args.printf, use_angr=args.use_angr_cfg) 106 | 107 | if len(funcs) == 0: 108 | print("No test sites found. Exitting (Maybe no xrefs to checked functions?)") 109 | exit(0) 110 | 111 | # Remove any previous exitted runs 112 | app.control.purge() 113 | 114 | worker = app.Worker(quiet=args.verbose) 115 | 116 | t = start_workers(worker) 117 | 118 | # Do them in batches 119 | batch_size = 50 120 | def divide_chunks(l, n): 121 | 122 | # looping till length l 123 | for i in range(0, len(l), n): 124 | yield l[i:i + n] 125 | 126 | func_batches = divide_chunks(funcs, batch_size) 127 | 128 | results = [] 129 | 130 | for func_batch in func_batches: 131 | pool_args = [] 132 | for func in func_batch: 133 | pool_args.append((proj, func)) 134 | 135 | print("itering") 136 | results = [] 137 | results = async_and_iter(do_trace, pool_args) 138 | 139 | func_addres = [x for x, y in results] 140 | 141 | print("[-] Scanned functions:") 142 | for func in func_batch: 143 | func_name = proj.loader.find_symbol(func) 144 | if func_name: 145 | func_name = func_name.name 146 | if func in func_addres: 147 | print("[+] : {} : {}".format(hex(func), func_name)) 148 | pair = [(x, y) for x, y in results if x == func][0] 149 | print(pair[1]) 150 | else: 151 | print("[-] : {} : {}".format(hex(func), func_name)) 152 | 153 | print("[*] Done scanning. Shutting down") 154 | app.control.shutdown() 155 | 156 | t.join() 157 | 158 | if __name__ == "__main__": 159 | main() 160 | -------------------------------------------------------------------------------- /bin_check/tracing.py: -------------------------------------------------------------------------------- 1 | import angr 2 | from angr import sim_options as so 3 | from bin_check.function_models import * 4 | 5 | 6 | # function name and format arg position 7 | printf_list = {"printf", "fprintf", "sprintf", "snprintf", "vsnprinf"} 8 | 9 | ''' 10 | # Stdio based ones 11 | p.hook_symbol("printf", printFormat(0)) 12 | p.hook_symbol("fprintf", printFormat(1)) 13 | p.hook_symbol("dprintf", printFormat(1)) 14 | p.hook_symbol("sprintf", printFormat(1)) 15 | p.hook_symbol("snprintf", printFormat(2)) 16 | 17 | # Stdarg base ones 18 | p.hook_symbol("vprintf", printFormat(0)) 19 | p.hook_symbol("vfprintf", printFormat(1)) 20 | p.hook_symbol("vdprintf", printFormat(1)) 21 | p.hook_symbol("vsprintf", printFormat(1)) 22 | p.hook_symbol("vsnprintf", printFormat(2)) 23 | ''' 24 | 25 | # Best effort system hooks 26 | def hook_list(p, hooks): 27 | for sys_type in hooks: 28 | try: 29 | p.hook_symbol(sys_type.name, SystemLibc()) 30 | except Exception as e: 31 | print(e) 32 | pass 33 | 34 | 35 | def hook_printf_list(p, hooks): 36 | # Stdio based ones 37 | p.hook_symbol("printf", printFormat(0)) 38 | p.hook_symbol("fprintf", printFormat(1)) 39 | p.hook_symbol("dprintf", printFormat(1)) 40 | p.hook_symbol("sprintf", printFormat(1)) 41 | p.hook_symbol("snprintf", printFormat(2)) 42 | 43 | # Stdarg base ones 44 | p.hook_symbol("vprintf", printFormat(0)) 45 | p.hook_symbol("vfprintf", printFormat(1)) 46 | p.hook_symbol("vdprintf", printFormat(1)) 47 | p.hook_symbol("vsprintf", printFormat(1)) 48 | p.hook_symbol("vsnprintf", printFormat(2)) 49 | 50 | 51 | def get_funcs_and_prj(filename, system_check=False, printf_check=False, use_angr=False, r2=None): 52 | 53 | # Give us tracing information 54 | my_extras = { 55 | so.REVERSE_MEMORY_NAME_MAP, 56 | so.TRACK_ACTION_HISTORY, 57 | so.TRACK_MEMORY_ACTIONS, 58 | so.ACTION_DEPS, 59 | so.UNDER_CONSTRAINED_SYMEXEC, 60 | } 61 | 62 | # Don't fail at first issue 63 | my_extra = angr.options.resilience.union(my_extras) 64 | # Run faster 65 | my_extra = angr.options.unicorn.union(my_extra) 66 | 67 | if not use_angr: 68 | import r2pipe 69 | import json 70 | if r2 == None: 71 | r = r2pipe.open(filename, ['-2']) 72 | r.cmd('aaaa') 73 | else: 74 | r = r2 75 | bin_info = r.cmd("ij") 76 | bin_info = json.loads(bin_info) 77 | base_addr = bin_info["bin"]["baddr"] 78 | 79 | p = angr.Project(filename, auto_load_libs=False, main_opts={"base_addr": base_addr}) 80 | else: 81 | p = angr.Project(filename, auto_load_libs=False) 82 | 83 | binary_system_list = [x for x in p.loader.symbols if x.name in system_list] 84 | binary_printf_list = [x for x in p.loader.symbols if x.name in printf_list] 85 | 86 | xrefs = set() 87 | 88 | check_list = [] 89 | 90 | if system_check: 91 | hook_list(p, binary_system_list) 92 | check_list.extend(binary_system_list) 93 | if printf_check: 94 | hook_printf_list(p, binary_printf_list) 95 | check_list.extend(binary_printf_list) 96 | 97 | if not use_angr: 98 | check_list = [] 99 | funcs = r.cmd("aflj") 100 | funcs = json.loads(funcs) 101 | binary_system_list = [] 102 | binary_printf_list = [] 103 | for func in funcs: 104 | func_name = func.get("name", None) 105 | if func_name: 106 | if any([x in func_name for x in system_list]): 107 | binary_system_list.append(func["offset"]) 108 | if any([x in func_name for x in printf_list]): 109 | binary_printf_list.append(func["offset"]) 110 | 111 | if system_check: 112 | check_list.extend(binary_system_list) 113 | if printf_check: 114 | check_list.extend(binary_printf_list) 115 | 116 | if use_angr: 117 | print("[~] Building CFG") 118 | import os 119 | import pickle 120 | if os.path.exists('bin.cfg'): 121 | with open('bin.cfg', 'rb') as f: 122 | print("loading cfg") 123 | cfg = pickle.load(f) 124 | else: 125 | cfg = p.analyses.CFG(cross_references=True, show_progressbar=True) 126 | with open('bin.cfg', 'wb') as f: 127 | pickle.dump(cfg, f) 128 | 129 | # Get all functions that have a system call 130 | # reference 131 | for func in check_list: 132 | if isinstance(func,int): 133 | addr = func 134 | else: 135 | addr = func.rebased_addr 136 | func_node = cfg.model.get_any_node(addr) 137 | if not func_node: 138 | continue 139 | func_callers = func_node.predecessors 140 | for func_caller in func_callers: 141 | xrefs.add(func_caller.function_address) 142 | 143 | xrefs = list(xrefs) 144 | 145 | del cfg 146 | 147 | else: 148 | 149 | # Get all functions that have a system call 150 | # reference 151 | for func in check_list: 152 | if isinstance(func,int): 153 | addr = func 154 | else: 155 | addr = func.rebased_addr 156 | 157 | xref_list = r.cmd('axtj @ {}'.format(addr)) 158 | xref_list = json.loads(xref_list) 159 | for caller in xref_list: 160 | if caller["type"] == "NULL": 161 | continue 162 | fcn_addr = caller.get('fcn_addr',None) 163 | if fcn_addr: 164 | xrefs.add(fcn_addr) 165 | 166 | xrefs = list(xrefs) 167 | 168 | print("Found {} test sites in binary".format(len(xrefs))) 169 | 170 | return xrefs, p -------------------------------------------------------------------------------- /bin_check/backward_slicing.py: -------------------------------------------------------------------------------- 1 | import r2pipe 2 | import json 3 | import copy 4 | import sys 5 | import uuid 6 | 7 | class program_slice: 8 | end_addr = 0 9 | avoid_list = [] 10 | top_addr = 0 11 | basic_block_list = [] 12 | funcs = [] 13 | id = None 14 | 15 | def __init__(self, end_addr): 16 | self.end_addr = end_addr 17 | self.top_addr = end_addr 18 | self.avoid_list = [] 19 | self.basic_block_list = [] 20 | self.funcs = [] 21 | self.id = uuid.uuid4().hex 22 | 23 | def __repr__(self) -> str: 24 | ret = "[{} - {}]\n".format(hex(self.top_addr), hex(self.end_addr)) 25 | for bb in self.basic_block_list: 26 | ret += "\t{}\n".format(hex(bb["addr"]) ) 27 | return ret 28 | 29 | def get_basic_block_addrs(self): 30 | return [x["addr"] for x in self.basic_block_list] 31 | 32 | def get_r2_instance(filename): 33 | r = r2pipe.open(filename ,['-2']) 34 | r.cmd('aaaa') 35 | return r 36 | 37 | def get_xrefs_to_func(r2,addr): 38 | ''' 39 | Expected JSON/Dict coming out 40 | [ 41 | {"from":4197904,"type": 42 | "CALL","perm":"--x", 43 | "opcode":"jal sym.mtd_write_firmware", 44 | "fcn_addr":4196848, 45 | "fcn_name":"main", 46 | "refname":"sym.mtd_write_firmware"} 47 | ] 48 | ''' 49 | json_text = r2.cmd('axtj @ {}'.format(addr)) 50 | xrefs = json.loads(json_text) 51 | return xrefs 52 | 53 | def get_basic_block_from_addr(r2,addr): 54 | ''' 55 | [ 56 | { 57 | "addr": 4199216, 58 | "size": 8, 59 | "jump": 4199200, 60 | "opaddr": 18446744073709552000, 61 | "inputs": 1, 62 | "outputs": 1, 63 | "ninstr": 3, 64 | "instrs": [ 65 | 4199216, 66 | 4199220, 67 | 4199216 68 | ], 69 | "traced": true 70 | } 71 | ] 72 | ''' 73 | r2.cmd('s {}'.format(addr)) 74 | json_text = r2.cmd('afbij') 75 | bbs = json.loads(json_text) 76 | return bbs 77 | 78 | def get_func_basic_blocks(r2, func_addr): 79 | json_text = r2.cmd('afbj {}'.format(func_addr)) 80 | bbs = json.loads(json_text) 81 | return bbs 82 | 83 | def get_predecessor_blocks(func_blocks, target_block): 84 | ret_block_list = [] 85 | avoid_list = [] 86 | 87 | bb_tuples = [] 88 | 89 | if target_block.get("addr",None) is None: 90 | return [] 91 | 92 | for block in func_blocks: 93 | found_block = False 94 | 95 | # Can this basic block jump to us? 96 | jump_addr = block.get("jump",None) 97 | if jump_addr and jump_addr== target_block["addr"]: 98 | found_block = True 99 | ret_block_list.append(block) 100 | 101 | # Check for fails 102 | fail_addr = block.get("fail",None) 103 | if fail_addr and fail_addr != target_block["addr"]: 104 | avoid_list.append(fail_addr) 105 | 106 | # Does this basic block have a fail? 107 | fail_addr = block.get("fail",None) 108 | if fail_addr and fail_addr == target_block["addr"]: 109 | found_block = True 110 | ret_block_list.append(block) 111 | 112 | # Check for jumps not to us 113 | jump_addr = block.get("jump",None) 114 | if jump_addr and jump_addr != target_block["addr"]: 115 | avoid_list.append(jump_addr) 116 | 117 | # Does this basic block execute us next without jumps? 118 | fall_in_range = target_block["addr"] - 0x4 119 | block_size = block.get("size", None) 120 | if block_size: 121 | if block['addr'] < target_block["addr"] and (block['addr'] + block_size) > fall_in_range: 122 | found_block = True 123 | ret_block_list.append(block) 124 | # If we get here there is no other branch we want to avoid. 125 | # No need to add to the avoid list 126 | 127 | if found_block: 128 | bb_tuples.append((ret_block_list[0], avoid_list)) 129 | 130 | ret_block_list = [] 131 | avoid_list = [] 132 | 133 | return bb_tuples 134 | 135 | def get_one_predecessor_block_slice_xref(r2, p_slice): 136 | 137 | program_slices = [] 138 | 139 | xrefs = get_xrefs_to_func(r2, p_slice.top_addr) 140 | for xref in xrefs: 141 | xref_addr = xref.get('from',None) # Exact calling address 142 | xref_func = xref.get('fcn_addr',None) # Function containing address 143 | 144 | func_blocks = get_func_basic_blocks(r2, xref_func) 145 | xref_block = get_basic_block_from_addr(r2, xref_addr) 146 | 147 | basic_block_tuples = get_predecessor_blocks(func_blocks, xref_block) 148 | 149 | for ref_block,avoid_block_addrs in basic_block_tuples: 150 | p_slice_copy = copy.copy(p_slice) 151 | 152 | p_slice_copy.funcs.append(xref) 153 | p_slice_copy.avoid_list.extend(avoid_block_addrs) 154 | p_slice_copy.basic_block_list.append(ref_block) 155 | p_slice_copy.top_addr = ref_block.get("addr",None) 156 | 157 | program_slices.append(p_slice_copy) 158 | 159 | return program_slices 160 | 161 | def get_one_predecessor_block_slice_block(r2, p_slice): 162 | 163 | program_slices = [] 164 | 165 | xref = p_slice.funcs[0] 166 | 167 | # xref_addr = xref.get('from',None) # Exact calling address 168 | xref_func = xref.get('fcn_addr',None) # Function containing address 169 | 170 | func_blocks = get_func_basic_blocks(r2, xref_func) 171 | xref_block = get_basic_block_from_addr(r2, p_slice.top_addr) 172 | 173 | basic_block_tuples = get_predecessor_blocks(func_blocks, xref_block) 174 | 175 | for ref_block,avoid_block_addrs in basic_block_tuples: 176 | p_slice_copy = copy.copy(p_slice) 177 | 178 | p_slice_copy.avoid_list.extend(avoid_block_addrs) 179 | p_slice_copy.basic_block_list.append(ref_block) 180 | p_slice_copy.top_addr = ref_block.get("addr",None) 181 | 182 | program_slices.append(p_slice_copy) 183 | 184 | return program_slices 185 | 186 | def top_block_has_xrefs(r2, p_slice): 187 | return len(get_xrefs_to_func(r2, p_slice.top_addr)) > 0 188 | 189 | def top_block_has_bb_refs(r2, p_slice): 190 | if p_slice.top_addr == p_slice.end_addr: 191 | return False 192 | if len (p_slice.funcs) == 0: # Haven't done initial xref 193 | return False 194 | xref = p_slice.funcs[0] 195 | xref_block = get_basic_block_from_addr(r2, p_slice.top_addr) 196 | xref_func = xref.get('fcn_addr',None) # Function containing address 197 | 198 | func_blocks = get_func_basic_blocks(r2, xref_func) 199 | basic_block_tuples = get_predecessor_blocks(func_blocks, xref_block) 200 | 201 | return len(basic_block_tuples) > 0 202 | 203 | ''' 204 | Provide the address from bcheck and we'll start creating 205 | backward program slice to see how far into the program we can 206 | go while still getting an exploitable program state. 207 | ''' 208 | def create_initial_program_slice(vuln_addr): 209 | p_slice = program_slice(vuln_addr) 210 | return p_slice 211 | 212 | def get_next_predecessor_path(r2, p_slice): 213 | if top_block_has_xrefs(r2, p_slice): 214 | return get_one_predecessor_block_slice_xref(r2, p_slice) 215 | if top_block_has_bb_refs(r2, p_slice): 216 | return get_one_predecessor_block_slice_block(r2, p_slice) 217 | print("Error no basic block or xrefs found for slice : {}".format(p_slice)) 218 | return None 219 | -------------------------------------------------------------------------------- /bin_check/function_models.py: -------------------------------------------------------------------------------- 1 | import angr 2 | import logging 3 | 4 | log = logging.getLogger(__name__) 5 | 6 | system_list = [ 7 | "system", 8 | "execv", 9 | "execve", 10 | "popen", 11 | "execl", 12 | "execle", 13 | "execlp", 14 | "do_system", 15 | "doSystembk", 16 | ] 17 | 18 | MAX_READ_LEN = 1024 19 | 20 | # Better symbolic strlen 21 | def get_max_strlen(state, value): 22 | i = 0 23 | for c in value.chop(8): # Chop by byte 24 | i += 1 25 | if not state.solver.satisfiable([c != 0x00]): 26 | return i - 1 27 | return i 28 | 29 | 30 | def get_largest_symbolic_buffer(symbolic_list): 31 | 32 | """ 33 | Iterate over the characters in the string 34 | Checking for where our symbolic values are 35 | This helps in weird cases like: 36 | char myVal[100] = "I\'m cool "; 37 | strcat(myVal,STDIN); 38 | printf(myVal); 39 | """ 40 | position = 0 41 | count = 0 42 | greatest_count = 0 43 | for i in range(1, len(symbolic_list)): 44 | if symbolic_list[i] and symbolic_list[i] == symbolic_list[i - 1]: 45 | count = count + 1 46 | if count > greatest_count: 47 | greatest_count = count 48 | position = i - count 49 | else: 50 | if count > greatest_count: 51 | greatest_count = count 52 | position = i - 1 - count 53 | # previous position minus greatest count 54 | count = 0 55 | 56 | return position, greatest_count 57 | 58 | 59 | # Better symbolic strlen 60 | def get_max_strlen(state, value): 61 | i = 0 62 | for c in value.chop(8): # Chop by byte 63 | i += 1 64 | if not state.solver.satisfiable([c != 0x00]): 65 | log.info("Found the null at offset : {}".format(i)) 66 | return i-1 67 | return i 68 | 69 | 70 | """ 71 | Model either printf("User input") or printf("%s","Userinput") 72 | """ 73 | 74 | 75 | class printFormat(angr.procedures.libc.printf.printf): 76 | IS_FUNCTION = True 77 | input_index = 0 78 | """ 79 | Checks userinput arg 80 | """ 81 | 82 | def __init__(self, input_index): 83 | # Set user input index for different 84 | # printf types 85 | self.input_index = input_index 86 | angr.procedures.libc.printf.printf.__init__(self) 87 | 88 | def checkExploitable(self, fmt): 89 | 90 | bits = self.state.arch.bits 91 | load_len = int(bits / 8) 92 | max_read_len = 1024 93 | """ 94 | For each value passed to printf 95 | Check to see if there are any symbolic bytes 96 | Passed in that we control 97 | """ 98 | i = self.input_index 99 | state = self.state 100 | solv = state.solver.eval 101 | 102 | if len(self.arguments) <= i: 103 | #print("{} vs {}".format(len(self.arguments),i)) 104 | #print(hex(state.globals["func_addr"])) 105 | return False 106 | printf_arg = self.arguments[i] 107 | 108 | var_loc = solv(printf_arg) 109 | 110 | # Parts of this argument could be symbolic, so we need 111 | # to check every byte 112 | var_data = state.memory.load(var_loc, max_read_len) 113 | var_len = get_max_strlen(state, var_data) 114 | 115 | fmt_len = self._sim_strlen(fmt) 116 | 117 | # Reload with just our max len 118 | var_data = state.memory.load(var_loc, var_len) 119 | 120 | log.info("Building list of symbolic bytes") 121 | symbolic_list = [ 122 | state.memory.load(var_loc + x, 1).symbolic for x in range(var_len) 123 | ] 124 | log.info("Done Building list of symbolic bytes") 125 | 126 | """ 127 | Iterate over the characters in the string 128 | Checking for where our symbolic values are 129 | This helps in weird cases like: 130 | 131 | char myVal[100] = "I\'m cool "; 132 | strcat(myVal,STDIN); 133 | printf(myVal); 134 | """ 135 | position = 0 136 | count = 0 137 | greatest_count = 0 138 | prev_item = symbolic_list[0] 139 | for i in range(1, len(symbolic_list)): 140 | if symbolic_list[i] and symbolic_list[i] == symbolic_list[i - 1]: 141 | count = count + 1 142 | if count > greatest_count: 143 | greatest_count = count 144 | position = i - count 145 | else: 146 | if count > greatest_count: 147 | greatest_count = count 148 | position = i - 1 - count 149 | # previous position minus greatest count 150 | count = 0 151 | log.info( 152 | "[+] Found symbolic buffer at position {} of length {}".format( 153 | position, greatest_count 154 | ) 155 | ) 156 | 157 | if greatest_count > 0: 158 | command_string = state.solver.eval(var_data, cast_to=bytes) 159 | print_formated = "{}\t->\t{}".format(hex(var_loc), command_string) 160 | log.info( 161 | "Format String bug in function at {}".format( 162 | hex(state.globals["func_addr"]) 163 | ) 164 | ) 165 | log.info(print_formated) 166 | 167 | state.globals["exploitable"] = True 168 | state.globals["cmd"] = print_formated 169 | return True 170 | return False 171 | 172 | def run(self, _, fmt): 173 | if not self.checkExploitable(fmt): 174 | return super(type(self), self).run(fmt) 175 | """ 176 | Basic check to see if symbolic input makes it's way into 177 | an argument for a system call 178 | """ 179 | 180 | 181 | class SystemLibc(angr.procedures.libc.system.system): 182 | def check_exploitable(self, cmd): 183 | 184 | state = self.state 185 | 186 | # If you're not using the latest angr you'll get errors here. Uncomment 187 | # if "claripy.ast.bv.BV" in str(type(cmd)): 188 | # print("raw bit vector") 189 | # return False 190 | 191 | clarip = cmd.to_claripy() 192 | location = self.state.solver.eval(clarip) 193 | 194 | # Parts of this argument could be symbolic, so we need 195 | # to check every byte 196 | var_data = state.memory.load(location, MAX_READ_LEN) 197 | var_len = get_max_strlen(state, var_data) 198 | 199 | # Reload with just our max len 200 | var_data = state.memory.load(location, var_len) 201 | 202 | symbolic_list = [ 203 | state.memory.load(location + x, 1).symbolic for x in range(var_len) 204 | ] 205 | 206 | symbolic_list = [ 207 | state.memory.load(location + x, 1).symbolic for x in range(var_len) 208 | ] 209 | 210 | position, greatest_count = get_largest_symbolic_buffer(symbolic_list) 211 | 212 | if greatest_count > 0: 213 | for i in range(greatest_count): 214 | # Get symbolic byte 215 | curr_byte = state.memory.load(location + position + i, 1) 216 | if state.solver.satisfiable(extra_constraints=[curr_byte == b"A"]): 217 | state.add_constraints(curr_byte == b"A") 218 | 219 | command_string = state.solver.eval(var_data, cast_to=bytes) 220 | print_formated = "{}\t->\t{}".format(hex(location), command_string) 221 | logging.info( 222 | "Command Injection in function at {}".format( 223 | hex(state.globals["func_addr"]) 224 | ) 225 | ) 226 | logging.info(print_formated) 227 | 228 | state.globals["exploitable"] = True 229 | state.globals["cmd"] = print_formated 230 | 231 | def run(self, cmd): 232 | self.check_exploitable(cmd) 233 | return super(type(self), self).run(cmd) 234 | --------------------------------------------------------------------------------