├── README.md ├── celeryconfig.py ├── challenges ├── bitpuzzle └── lockpicksim ├── install.sh ├── lib ├── __init__.py ├── back_stitcher.py ├── node_control.py └── run_pass.py ├── requirements.txt ├── rocket_shot.py └── samples.sh /README.md: -------------------------------------------------------------------------------- 1 | # Rocket Shot 2 | 3 | Backwards program slice stitching for automatic CTF problem solving. 4 | 5 | Rocket Shot uses [angr](https://github.com/angr/angr) to concolically analyze basic blocks in a given program, running from the start of the block to the end, looking for interactions with a file descriptor. When reaching that condition, the basic block's control flow graph predessor's are "stitched" into the exploration path and then n-predessor plus original basic block based paths are explored attempting to reveal more modified file descriptor contents. This process continually iterates until terminated with Ctrl+C. 6 | 7 | This technique is inspired in part by angr's [Backward Slice analyzer](https://docs.angr.io/built-in-analyses/backward_slice). 8 | 9 | Slides for the BSidesDC presentation of this tool can be found [here](https://drive.google.com/open?id=15WCMFPGzqbw346a5PKauAZh6cu2efpUG) 10 | 11 | [![asciicast](https://asciinema.org/a/208750.png)](https://asciinema.org/a/208750) 12 | 13 | ## Installing 14 | Rocket Shot has been tested on Ubuntu 16.04 and the install script is setup for Ubuntu 12.04 to Ubuntu 18.04 15 | 16 | ./install.sh 17 | #Ubuntu 18 | sudo apt install rabbitmq 19 | #OSX 20 | brew install rabbitmq 21 | 22 | 23 | ## Usage 24 | Rocket Shot is a python script which accepts a binary as an argument with optional basic block timeout settings, and an optional required string match input. 25 | 26 | ``` 27 | (rocket_shot) chris@ubuntu:~/Tools/auto-re$ python rocket_shot.py -h 28 | usage: rocket_shot.py [-h] [--timeout TIMEOUT] [--string STRING] FILE 29 | 30 | positional arguments: 31 | FILE 32 | 33 | optional arguments: 34 | -h, --help show this help message and exit 35 | --string STRING, -s STRING 36 | ``` 37 | 38 | ## Celery worker 39 | In one terminal run the celery worker and it will be ready tp accept commands 40 | ``` 41 | celery -A lib.run_pass worker --loglevel=info 42 | ``` 43 | 44 | ## Examples 45 | Checkout the samples.sh file. The file contains a small handful of challenges. 46 | 47 | Or any of the reverseing based angr example problems at [here](https://github.com/angr/angr-doc/tree/master/examples) or [here](https://github.com/angr/angr-doc/blob/master/docs/more-examples.md) 48 | ``` 49 | #!/bin/bash 50 | #PicoCTF 2014 Reverseing 51 | python rocket_shot.py challenges/bitpuzzle -s flag 52 | #UMDCTF 2017 Reverseing 53 | python rocket_shot.py challenges/lockpicksim -s Flag 54 | ``` 55 | -------------------------------------------------------------------------------- /celeryconfig.py: -------------------------------------------------------------------------------- 1 | broker_url = 'pyamqp://guest@localhost//' 2 | result_backend = 'rpc://' 3 | task_time_limit = 10 4 | worker_max_memory_per_child = 2048000 #2GB 5 | accept_content = ['pickle', 'json'] 6 | result_serializer = 'pickle' 7 | task_serializer = 'pickle' 8 | -------------------------------------------------------------------------------- /challenges/bitpuzzle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisTheCoolHut/Rocket-Shot/59ae8cd1d0cdc47675efc358334dcaf215632ba6/challenges/bitpuzzle -------------------------------------------------------------------------------- /challenges/lockpicksim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisTheCoolHut/Rocket-Shot/59ae8cd1d0cdc47675efc358334dcaf215632ba6/challenges/lockpicksim -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | sudo pip install virtualenv virtualenvwrapper 2 | 3 | sudo pip install --upgrade pip 4 | 5 | printf '\n%s\n%s\n%s' '# virtualenv' 'export WORKON_HOME=~/virtualenvs' 'source /usr/local/bin/virtualenvwrapper.sh' >> ~/.bashrc 6 | 7 | export WORKON_HOME=~/virtualenvs 8 | source /usr/local/bin/virtualenvwrapper.sh 9 | 10 | mkvirtualenv rocket_shot 11 | 12 | workon rocket_shot 13 | 14 | pip install angr termcolor IPython 15 | 16 | echo "####################" 17 | echo "run: . ~/.bashrc" 18 | echo "run: workon rocket_shot" 19 | 20 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisTheCoolHut/Rocket-Shot/59ae8cd1d0cdc47675efc358334dcaf215632ba6/lib/__init__.py -------------------------------------------------------------------------------- /lib/back_stitcher.py: -------------------------------------------------------------------------------- 1 | from .node_control import * 2 | from .run_pass import * 3 | import tqdm 4 | import time 5 | import signal 6 | 7 | ''' 8 | Running on known good nodes. 9 | No need to check for output 10 | ''' 11 | 12 | def run_pass(p, p_cfg, nodes, req_string): 13 | chained_nodes = [] 14 | node_outputs = [] 15 | 16 | results = [] 17 | for node_list in nodes: 18 | 19 | start_addr = node_list[0].addr 20 | ins_list = [] 21 | for node in node_list: 22 | ins_list.extend(list(node.instruction_addrs)) 23 | if node.block is not None: 24 | ins_list.extend(get_call_ins(p_cfg, node)) 25 | [ins_list.extend(x.instruction_addrs) for x in node_list[-1].successors] 26 | 27 | async_result = do_run.apply_async((p, p_cfg, ins_list, start_addr, [x.addr for x in node_list], req_string), 28 | serializer='pickle') 29 | 30 | results.append(async_result) 31 | 32 | bar = tqdm.tqdm(total=len(results)) 33 | while not all([x.ready() for x in results]): 34 | done_count = len([x.ready() for x in results if x.ready()]) 35 | bar.update(done_count - bar.n) 36 | time.sleep(1) 37 | bar.close() 38 | 39 | for result in [x.get(propagate=False) for x in results if not x.failed()]: 40 | chained_nodes.extend([[p_cfg.get_any_node(y) for y in x] for x in result['chained_nodes']]) 41 | node_outputs.extend(result['node_outputs']) 42 | 43 | chained_nodes = [x for x in chained_nodes if x] 44 | return chained_nodes, node_outputs 45 | 46 | 47 | ''' 48 | Initial run looking for some file 49 | descriptor writing 50 | ''' 51 | 52 | 53 | def first_pass(p, p_cfg, nodes, req_string): 54 | chained_nodes = [] 55 | node_outputs = [] 56 | 57 | results = [] 58 | 59 | for node in nodes: 60 | 61 | ins_list = list(node.instruction_addrs) 62 | ins_list.extend(get_call_ins(p_cfg, node)) 63 | [ins_list.extend(x.instruction_addrs) for x in node.successors] 64 | start_addr = node.addr 65 | node._hash = None 66 | async_result = do_run.apply_async((p, p_cfg, ins_list, start_addr, [node.addr], req_string), serializer='pickle') 67 | 68 | results.append(async_result) 69 | 70 | bar = tqdm.tqdm(total=len(results)) 71 | while not all([x.ready() for x in results]): 72 | done_count = len([x.ready() for x in results if x.ready()]) 73 | bar.update(done_count - bar.n) 74 | time.sleep(1) 75 | bar.close() 76 | 77 | for result in [x.get(propagate=False) for x in results if not x.failed()]: 78 | chained_nodes.extend([[p_cfg.get_any_node(y) for y in x] for x in result['chained_nodes']]) 79 | node_outputs.extend(result['node_outputs']) 80 | 81 | chained_nodes = [x for x in chained_nodes if x] 82 | return chained_nodes, node_outputs 83 | -------------------------------------------------------------------------------- /lib/node_control.py: -------------------------------------------------------------------------------- 1 | from termcolor import colored 2 | 3 | 4 | class bcolors: 5 | HEADER = '\033[95m' 6 | OKBLUE = '\033[94m' 7 | OKGREEN = '\033[92m' 8 | WARNING = '\033[93m' 9 | FAIL = '\033[91m' 10 | ENDC = '\033[0m' 11 | BOLD = '\033[1m' 12 | UNDERLINE = '\033[4m' 13 | 14 | ''' 15 | Do we exist and are we in a simprocedure 16 | Do we exist and are we in a simprocedure 17 | ''' 18 | def in_simproc(p, curr_pc): 19 | return p.kb.functions.contains_addr(curr_pc) and p.kb.functions.get_by_addr(curr_pc).is_simprocedure 20 | 21 | 22 | def print_recent_nodes(node_list, chained): 23 | print(colored("{:<60} {:<20}".format("Node", "File Activity"), 'yellow')) 24 | for node in node_list: 25 | node_text = "{0: <60} {1: <20}".format(str(node), (node in chained)) 26 | print(colored(node_text, 'cyan')) 27 | 28 | 29 | def get_predecessors(nodes_list): 30 | return_list = [] 31 | for node_list in nodes_list: 32 | predecessors = node_list[0].predecessors 33 | for predecessor in predecessors: 34 | ''' 35 | #THIS IS BAD. FIX THIS 36 | #NOT ALL RETURNS IN A FUNCTION POINT TO THE SAME ADDRESS 37 | if predecessor.block is not None and 'ret' in [x.mnemonic for x in predecessor.block.capstone.insns]: 38 | return_addresses[predecessor.addr] = node_list[0].addr 39 | ''' 40 | 41 | path_stitch = list(node_list) 42 | path_stitch.insert(0, predecessor) 43 | 44 | return_list.append(path_stitch) 45 | return return_list 46 | 47 | 48 | ''' 49 | Given a child and parent node, check for shared 50 | whether they are running in the same function. 51 | If there is a call give us all basic blocks in 52 | that function. 53 | ''' 54 | 55 | 56 | # Very Naive approach toward return pointer problem 57 | def check_get_func_nodes(p_cfg, child, parent): 58 | new_nodes = [] 59 | 60 | # Check for None 61 | if child.function_address is not None and parent.function_address is not None: 62 | # Check for new function 63 | if child.function_address is not parent.function_address: 64 | 65 | new_func = p_cfg.functions.get_by_addr(parent.function_address) 66 | 67 | top_node = p_cfg.get_any_node(parent.function_address) 68 | 69 | for block in new_func.blocks: 70 | n_node = p_cfg.get_any_node(block.addr) 71 | new_nodes.append(n_node) 72 | 73 | # Gaurentee top node runes first 74 | if top_node in new_nodes: 75 | new_nodes.remove(top_node) 76 | 77 | new_nodes.insert(0, top_node) 78 | 79 | return new_nodes 80 | 81 | def visual_step(simgr): 82 | 83 | #Creating string to print 84 | my_str = "" 85 | for stash in simgr.stashes: 86 | if len(simgr.stashes[stash]) > 0: 87 | my_str += "{} : ".format(stash) 88 | for path in simgr.stashes[stash]: 89 | cur_pc = path.se.eval(path.regs.pc) 90 | my_str += str(hex(path.se.eval(path.regs.pc))) + ',' 91 | # my_str += "\n" 92 | # print(my_str+'\r'), 93 | 94 | #Check for looping 95 | for stash in simgr.stashes: 96 | for path in simgr.stashes[stash]: 97 | if path.globals['last_set'] == my_str: 98 | # print(colored('[-] Explorer Stuck... Fixing\r','yellow')), 99 | simgr.stashes[stash].remove(path) 100 | else: 101 | path.globals['last_set'] = my_str 102 | 103 | #Remove paths not in ins list 104 | #Ignores simprocedures 105 | if len(simgr.active) > 0: 106 | #Get ins list 107 | ins_list = simgr.active[0].globals['addrs'] 108 | for path in simgr.active: 109 | curr_pc = path.se.eval(path.regs.pc) 110 | if curr_pc not in ins_list and not in_simproc(path.project, curr_pc): 111 | if (curr_pc not in [x.se.eval(x.regs.pc) for x in simgr.stashes['pruned']]): 112 | simgr.stashes['pruned'].append(path) 113 | simgr.stashes['active'].remove(path) 114 | 115 | return simgr 116 | 117 | ''' 118 | Iterate over all the paths looks for any 119 | stdin/stdout/stderr activity 120 | ''' 121 | def check_fds(simgr, node, opt_string=""): 122 | chained_nodes = [] 123 | node_outputs = [] 124 | #Check for anything 125 | for stash in simgr.stashes: 126 | if len(simgr.stashes) > 0: 127 | for path in simgr.stashes[stash]: 128 | for i in range(3): 129 | posix_string = path.posix.dumps(i).decode('utf-8','ignore') 130 | if len(posix_string) > 0 and opt_string in posix_string: 131 | if "flag{" in posix_string and "}" in posix_string: 132 | ''' 133 | try: 134 | flag_str = posix_string[posix_string.index('flag{'):posix_string.index('}') +1] 135 | flag_list.add(flag_str) 136 | except: 137 | flag_list.add(posix_string) 138 | ''' 139 | node_outputs.append((path.posix.dumps(0).decode("utf-8", 'ignore'), 140 | path.posix.dumps(1).decode("utf-8", 'ignore'), 141 | path.posix.dumps(2).decode("utf-8", 'ignore'))) 142 | chained_nodes.append(node) 143 | break 144 | 145 | return chained_nodes, node_outputs 146 | 147 | ''' 148 | pretty print a block at the address 149 | ''' 150 | def print_block(p_cfg, cur_pc): 151 | my_nodes = p_cfg.get_all_nodes(cur_pc) 152 | for node in my_nodes: 153 | if node.block is not None: 154 | print(bcolors.OKGREEN) 155 | node.block.pp() 156 | 157 | 158 | ''' 159 | I need a better check 160 | ''' 161 | 162 | 163 | def check_output(nodes_out): 164 | strings_list = [] 165 | for output_fds in nodes_out: # 3 166 | for output_fd in output_fds: 167 | while type(output_fd) == tuple: 168 | output_fd = [x.decode('utf-8', "ignore") for x in output_fd] 169 | while type(output_fd) == list: 170 | output_fd = ','.join([x.decode('utf-8', "ignore") if type(x) == 'str' else x for x in output_fd]) 171 | try: 172 | output_fd = output_fd.decode("utf-8", 'ignore') 173 | except: 174 | pass 175 | # output_fd.replace('b\'\'','') 176 | if len(output_fd) > 0: 177 | strings_list.append(repr(output_fd)) 178 | if "{" in output_fd and "}" in output_fd: 179 | print(output_fd) 180 | return strings_list 181 | return strings_list 182 | 183 | 184 | ''' 185 | get instructions for all calls in a given node 186 | This function goes 1 level deep, and does not recurse 187 | ''' 188 | 189 | 190 | def get_call_ins(p_cfg, node): 191 | added_isns = [] 192 | instructions = node.block.capstone.insns 193 | for insc in instructions: 194 | my_op_str = insc.op_str 195 | my_mnemonic = insc.mnemonic 196 | if 'call' in my_mnemonic and " " not in my_op_str and "0x" in my_op_str: 197 | addr = int(my_op_str, 16) 198 | try: 199 | called_func = p_cfg.functions.get_by_addr(addr) 200 | except: 201 | # KeyError 202 | continue 203 | if len(called_func.name) > 0: 204 | pass 205 | #print(colored("Added blocks for {}".format(called_func.name), 'yellow')) 206 | for block in called_func.blocks: 207 | added_isns.extend(block.instruction_addrs) 208 | return added_isns 209 | 210 | 211 | ''' 212 | Could I shorten this? yes 213 | Should I shorten this? no 214 | ''' 215 | 216 | 217 | def is_node_plt(p, node): 218 | segment = p.loader.find_section_containing(node.addr) 219 | if segment is not None: 220 | return '.plt' in segment.name 221 | 222 | 223 | ''' 224 | plt and simprocedures don't work as well as you would 225 | want when doing backwards stitching. This is a performance 226 | instensive workaround 227 | ''' 228 | 229 | 230 | def process_plt_and_sim(p, nodes_list): 231 | add_list = [] 232 | remove_list = [] 233 | temp_remove = [] 234 | 235 | # Iterate through regular nodes 236 | for node_list in nodes_list: 237 | if (node_list[0].is_simprocedure or is_node_plt(p, node_list[0])): 238 | remove_list.append(node_list) 239 | for item_list in get_predecessors([node_list]): 240 | add_list.append(item_list) 241 | 242 | while (not all([not x[0].is_simprocedure and not is_node_plt(p, x[0]) for x in add_list])): 243 | for node_list in add_list: 244 | if node_list[0].is_simprocedure or is_node_plt(p, node_list[0]): 245 | temp_remove.append(node_list) 246 | for item_list in get_predecessors([node_list]): 247 | add_list.append(item_list) 248 | 249 | # Can't remove element while in for each 250 | for node_list in temp_remove: 251 | add_list.remove(node_list) 252 | temp_remove = [] 253 | return add_list, remove_list 254 | -------------------------------------------------------------------------------- /lib/run_pass.py: -------------------------------------------------------------------------------- 1 | 2 | from celery import Celery 3 | import pickle 4 | from .node_control import * 5 | #angr logging is way too verbose 6 | import logging 7 | 8 | app = Celery('CeleryTask') 9 | app.config_from_object('celeryconfig') 10 | 11 | log_things = ["angr", "pyvex", "claripy", "cle"] 12 | for log in log_things: 13 | logger = logging.getLogger(log) 14 | logger.disabled = True 15 | logger.propagate = False 16 | 17 | 18 | @app.task 19 | def do_run(proj, p_cfg, ins_list, start_addr, node_list, req_string): 20 | # Build simulator 21 | node_list = [p_cfg.get_any_node(x) for x in node_list] 22 | state = proj.factory.blank_state(addr=start_addr) 23 | state.globals['addrs'] = ins_list 24 | state.globals['last_set'] = [] 25 | simgr = proj.factory.simgr(state, save_unconstrained=True) 26 | 27 | chained_nodes =[] 28 | node_outputs = [] 29 | node_output_dict = {} 30 | ret_strings = [] 31 | 32 | try: 33 | import sys 34 | sys.stdout.encoding = 'UTF-8' #AttributeError: 'LoggingProxy' object has no attribute 'encoding' 35 | except AttributeError as e: #AttributeError: readonly attribute 36 | pass 37 | 38 | while (len(simgr.active) > 0 and any( 39 | x.se.eval(x.regs.pc) in ins_list or in_simproc(proj, x.se.eval(x.regs.pc)) for x in simgr.active)): 40 | simgr.step(step_func=visual_step) 41 | nodes_ret, nodes_out = check_fds(simgr, node_list, req_string) 42 | chained_nodes.extend(nodes_ret) 43 | if len(nodes_out) > 0: 44 | node_outputs.append(nodes_out) 45 | ret_strings.extend(check_output(nodes_out)) 46 | if len(nodes_ret) > 0: 47 | for i in range(len(nodes_ret)): 48 | node_output_dict[str(nodes_ret[i])] = nodes_out[i] 49 | 50 | chained_nodes = [[y.addr for y in x] for x in chained_nodes] 51 | 52 | ret_dict = {} 53 | ret_dict['chained_nodes'] = chained_nodes 54 | ret_dict['node_outputs'] = node_outputs 55 | ret_dict['nodes_dict'] = node_output_dict 56 | ret_dict['strings'] = ret_strings 57 | 58 | return ret_dict 59 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | angr 2 | termcolor 3 | IPython 4 | tqdm 5 | celery 6 | -------------------------------------------------------------------------------- /rocket_shot.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import angr 3 | from lib.back_stitcher import * 4 | logging.getLogger('angr').disabled = True 5 | logger = logging.getLogger('angr') 6 | logger.disabled = True 7 | logger.propagate = False 8 | 9 | 10 | def main(): 11 | 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument('FILE') 14 | parser.add_argument('--string','-s', default="") 15 | 16 | args = parser.parse_args() 17 | 18 | p = angr.Project(args.FILE, load_options={'auto_load_libs': False}) 19 | print('[+] Building CFGFast') 20 | p_cfg = p.analyses.CFGFast() 21 | node_output = [] 22 | strings_list = set() 23 | 24 | nodes = [x for x in p_cfg.nodes() if len(x.instruction_addrs) > 0] 25 | try: 26 | chained_nodes, node_output = first_pass(p, p_cfg, nodes, args.string) 27 | for y in node_output: 28 | for x in y: 29 | strings_list.add(x[0]) 30 | strings_list.add(x[1]) 31 | strings_list.add(x[2]) 32 | except KeyboardInterrupt: 33 | print(colored("[ -- Strings --]",'white')) 34 | for string_item in strings_list: 35 | print(colored(string_item,'cyan')) 36 | print("[~] Exitting") 37 | exit(0) 38 | 39 | nodes_list = chained_nodes #[p_cfg.get_any_node(x.addr) for x in chained_nodes] 40 | round_iter = 1 41 | for string in strings_list: 42 | print(string) 43 | while len(nodes_list) > 0: 44 | nodes_list = get_predecessors(nodes_list) 45 | 46 | add_list, remove_list = process_plt_and_sim(p, nodes_list) 47 | 48 | #Add in extra addressed nodes 49 | nodes_list.extend(add_list) 50 | 51 | #Take out simprocedures and plts 52 | for item in remove_list: 53 | nodes_list.remove(item) 54 | 55 | remove_indexes = [] 56 | 57 | for item_index in range(len(nodes_list)): 58 | 59 | if(len(nodes_list[item_index]) > 1 and len(nodes_list[item_index]) > 0 and not is_node_plt(p, nodes_list[item_index][0]) and not nodes_list[item_index][0].is_simprocedure): 60 | #Not an amazing fix to just assume we can run the whole called func... 61 | func_fix_list = check_get_func_nodes(p_cfg, nodes_list[item_index][1], nodes_list[item_index][0]) 62 | 63 | 64 | #Only if we don't already have nodes from that function, add e'n 65 | if len(func_fix_list) > 0 and not func_fix_list[0].function_address in [x.function_address for x in nodes_list[item_index][1:]]: 66 | func_fix_list.extend(nodes_list[item_index]) 67 | 68 | func_fix_list = get_predecessors([func_fix_list]) 69 | 70 | nodes_list.extend(func_fix_list) 71 | #remove_indexes.append(item_index) 72 | 73 | remove_indexes.reverse() 74 | for item_index in remove_indexes: 75 | if item_index < len(nodes_list): #What a terrible fix -- Sorry 76 | nodes_list.remove(nodes_list[item_index]) 77 | try: 78 | nodes_list, node_output = run_pass(p, p_cfg, nodes_list, args.string) 79 | for y in node_output: 80 | for x in y: 81 | strings_list.add(x[0]) 82 | strings_list.add(x[1]) 83 | strings_list.add(x[2]) 84 | except KeyboardInterrupt: 85 | print(colored("[ -- Strings --]",'white')) 86 | for string_item in strings_list: 87 | print(colored(string_item,'cyan')) 88 | print("[~] Exitting") 89 | exit(0) 90 | round_iter += 1 91 | strings_list = list(strings_list) 92 | strings_list.sort() 93 | strings_list = set(strings_list) 94 | for string in strings_list: 95 | print(string) 96 | 97 | print(colored("[ -- Strings --]",'white')) 98 | for string_item in strings_list: 99 | print(colored(string_item,'cyan')) 100 | print("[~] Exitting") 101 | 102 | 103 | if __name__ == '__main__': 104 | main() 105 | -------------------------------------------------------------------------------- /samples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #PicoCTF 2014 Reverseing 4 | python rocket_shot.py challenges/bitpuzzle -t 15 -s flag 5 | #UMDCTF 2017 Reverseing 6 | python rocket_shot.py challenges/lockpicksim -t 15 -s Flag 7 | --------------------------------------------------------------------------------