├── .gitmodules ├── README.md ├── devi_frida.py ├── devi_frida_tracer.js ├── devi_ghidra.py ├── devi_ida.py ├── images ├── cpp-test-assembly-w-devi.PNG ├── cpp-test-assembly-wo-devi.png ├── cpp-test-xrefs-graphs-w-devi.PNG ├── cpp-test-xrefs-graphs-wo-devi2.PNG ├── cpp-test-xrefs-w-devi.PNG └── cpp-test-xrefs-wo-devi.PNG └── tests ├── HelloWorld ├── HelloWorld.cpp └── a.out /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "devi_binja"] 2 | path = devi_binja 3 | url = https://github.com/murx-/devi_binja 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # devi - DEvirtualize VIrtual calls 2 | 3 | Devi is a simple tool that uses runtime information to devirtualize virtual calls in c++ binaries. 4 | 5 | ## Usage 6 | 7 | Devi consits of two components, one for dynamic analysis (DBI) and one for static analysis (disassembler). 8 | 9 | ### Running the Frida Tracer 10 | 11 | #### Minimal Command Line 12 | 13 | Spawn process: 14 | 15 | ``` 16 | python devi_frida.py -m -o -- 17 | ``` 18 | 19 | Attach to process: 20 | 21 | ``` 22 | python devi_frida.py -m -s -o -p 23 | ``` 24 | 25 | ### Disassembler Plugin: 26 | 27 | For Binary Ninja see https://github.com/murx-/devi_binja for IDA follow along here. 28 | 29 | Copy devi\_ida.py to your IDA plugin folder or load the script via File -> Script file... and load devi\_ida.py. 30 | 31 | Once devi is loaded you can load the JSON file containing the virtual calls via File -> Load File -> Load Virtual Calls. 32 | 33 | ## Minimal Example 34 | 35 | ```bash 36 | python devi_frida.py -m main -o virtual_calls.json -- tests/HelloWorld myArgs 37 | ``` 38 | 39 | Load JSON file into IDA Pro. 40 | 41 | ### Disassembly 42 | 43 | Before: 44 | 45 | ![Disassembly before devi](https://github.com/murx-/devi/blob/master/images/cpp-test-assembly-wo-devi.png) 46 | 47 | 48 | After: 49 | 50 | ![Disassembly with devi](https://github.com/murx-/devi/blob/master/images/cpp-test-assembly-w-devi.PNG) 51 | 52 | ### Xrefs 53 | 54 | Before: 55 | 56 | ![Xrefs before devi](https://github.com/murx-/devi/blob/master/images/cpp-test-xrefs-wo-devi.PNG) 57 | 58 | After: 59 | 60 | ![Xrefs after devi](https://github.com/murx-/devi/blob/master/images/cpp-test-xrefs-w-devi.PNG) 61 | 62 | ### Xref Graph 63 | 64 | Before: 65 | 66 | ![Xrefs graph before devi](https://github.com/murx-/devi/blob/master/images/cpp-test-xrefs-graphs-wo-devi2.PNG) 67 | 68 | After: 69 | 70 | ![Xrefs graph after devi](https://github.com/murx-/devi/blob/master/images/cpp-test-xrefs-graphs-w-devi.PNG) 71 | 72 | ## Supported Frameworks 73 | 74 | Supported DBIs: 75 | 76 | - Frida 77 | 78 | Supported Disassemblers: 79 | 80 | - IDA 81 | - [Binary Ninja](https://github.com/murx-/devi_binja) 82 | 83 | ## Misc 84 | 85 | This tool is heavily inspired by [Ablation](https://github.com/cylance/Ablation). 86 | -------------------------------------------------------------------------------- /devi_frida.py: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/python3 2 | 3 | import argparse 4 | import json 5 | import sys 6 | from pathlib import Path 7 | from os import path 8 | from time import sleep 9 | 10 | import frida 11 | 12 | 13 | class Devi: 14 | 15 | def __init__(self, binary, traced_module, out_file, symbol, pid=None, kill=False, verbose=False, debug=False): 16 | self.version = 0.2 17 | self.binary = binary 18 | self.traced_module = traced_module 19 | self.out_file = out_file 20 | self.pid = pid 21 | self.session = None 22 | self.calls = list() 23 | self.error = False 24 | self.kill = kill 25 | self.verbose = verbose 26 | self.debug_level = debug 27 | self.symbol = symbol 28 | self.modules = None 29 | 30 | def load_script(self): 31 | absolute_path = path.join(sys.path[0], "devi_frida_tracer.js") 32 | script = self.session.create_script( 33 | Path(absolute_path).read_text() % (self.debug_level, self.traced_module, self.symbol)) 34 | 35 | def on_message(message, data): 36 | """handle messages send by frida""" 37 | self.debug("[{}] -> {}".format(message, data)) 38 | if message["type"] == "error": 39 | self.error = True 40 | self.warn(" - Frida Error - " + message["description"]) 41 | self.warn(message["stack"]) 42 | self.warn("lineNuber: " + str(message["lineNumber"]) + 43 | ", columnNumber: " + str(message["columnNumber"])) 44 | elif message["type"] == "send": 45 | if "callList" in message["payload"]: 46 | self.calls.extend(message["payload"]["callList"]) 47 | elif "moduleMap" in message["payload"]: 48 | self.info("ModuleMap updated.") 49 | self.modules = message["payload"]["moduleMap"] 50 | elif "symbolMap" in message["payload"]: 51 | pass 52 | elif "deviFinished" in message["payload"]: 53 | self.log(message["payload"]["deviFinished"]) 54 | elif "deviError" in message["payload"]: 55 | # Some Error occoured 56 | self.error = True 57 | self.warn(" - Error - " + message["payload"]["deviError"]) 58 | 59 | script.on("message", on_message) 60 | script.load() 61 | # only resume if spawed 62 | # if we resume before we load the script we can not intercept main 63 | try: 64 | frida.resume(int(self.pid)) 65 | except frida.InvalidArgumentError: 66 | pass 67 | 68 | def spawn_binary(self): 69 | 70 | if not self.pid: 71 | if self.binary[:2] == './': 72 | self.binary = self.binary[2:] 73 | 74 | self.pid = frida.spawn(self.binary) 75 | self.debug("Spawned binay {} with pid {}".format(self.binary, self.pid)) 76 | self.pid = int(self.pid) 77 | self.session = frida.attach(self.pid) 78 | self.debug("Attached to process {}".format(self.pid)) 79 | 80 | self.load_script() 81 | 82 | if self.error: 83 | self.warn("error") 84 | self.session.detach() 85 | self.info("An error occoured while attaching!") 86 | sys.exit(-1) 87 | 88 | self.log('Tracing binary press control-D to terminate....') 89 | 90 | sys.stdin.read() 91 | 92 | try: 93 | self.log('Detaching, this might take a second...') 94 | if self.kill: 95 | frida.kill(self.pid) 96 | self.log('Killing process {}'.format(self.pid)) 97 | self.session.detach() 98 | except frida.ProcessNotFoundError: 99 | self.log('Process already terminated') 100 | self.debug("Call Overview:") 101 | self.debug(str(self.calls)) 102 | 103 | with open(self.out_file, "w+") as self.out_file: 104 | result = dict() 105 | result["deviVersion"] = self.version 106 | result["calls"] = self.calls 107 | result["modules"] = self.modules 108 | json.dump(result, self.out_file) 109 | 110 | 111 | def info(self, message): 112 | if self.verbose or self.debug_level: 113 | print("[-] " + message) 114 | 115 | def debug(self, message): 116 | if self.debug_level: 117 | print("[+] " + message) 118 | 119 | def warn(self, message): 120 | print("[!] " + message) 121 | 122 | def log(self, message): 123 | print("[*] " + message) 124 | 125 | 126 | if __name__ == '__main__': 127 | parser = argparse.ArgumentParser(description="Devirtualize Virtual Calls", 128 | usage="\tdevi_frida.py -m -o -- \n"+ 129 | "\tdevi_frida.py -m -s -o -p ") 130 | parser.add_argument("-o", "--out-file", 131 | help="Output location", required=True) 132 | parser.add_argument( 133 | "-m", "--module", help="Module to trace", required=True) 134 | 135 | parser.add_argument( 136 | "-p", "--pid", help="Attach to PID", required=False) 137 | 138 | parser.add_argument( 139 | "-s", "--symbol", help="Hook symbol, default main, either offset or mangled name!", required=False, default="main") 140 | 141 | parser.add_argument( 142 | "-v", "--verbose", help="Set verbose logging", required=False, action='store_true') 143 | parser.add_argument( 144 | "-vv", "--debug", help="Set debug logging", required=False, action='store_true') 145 | parser.add_argument( 146 | "-k", "--kill", help="Kill process after detach", required=False, action='store_true') 147 | 148 | # add -t thread option if there are threads and so.. 149 | 150 | parser.add_argument("cmdline", nargs=argparse.REMAINDER, 151 | help="Command line for process to spawn, e.g. ls -lah") 152 | 153 | args = parser.parse_args() 154 | 155 | if args.cmdline: 156 | if (args.cmdline[0] != "--") and not args.pid: 157 | parser.print_help() 158 | exit(1) 159 | args.cmdline = args.cmdline[1:] 160 | elif not args.pid: 161 | parser.print_help() 162 | target = None 163 | exit(1) 164 | 165 | devi = Devi(args.cmdline, args.module, args.out_file, args.symbol, 166 | args.pid, args.kill, args.verbose, args.debug) 167 | devi.spawn_binary() 168 | -------------------------------------------------------------------------------- /devi_frida_tracer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Version 0.2 4 | 5 | var debug = "%s" 6 | var moduleName = '%s' 7 | var myModule = Process.findModuleByName(moduleName) 8 | 9 | var symboleInput = '%s' 10 | if (isNaN(Number(symboleInput))) { 11 | var symbolAddress = getSymbolAddress(moduleName, symboleInput); 12 | } else { 13 | var symbolAddress = Number(symboleInput) + parseInt(myModule.base, 16); 14 | } 15 | 16 | if (symbolAddress == undefined) { 17 | console.log("[!] Unable to finde symbole " + symboleInput) 18 | send({ "deviError": "No symbole found named: " + symboleInput }) 19 | } 20 | 21 | 22 | if (myModule != null) { 23 | myModule.end = parseInt(myModule.base, 16) + myModule.size; 24 | attachInterceptor(); 25 | } else { 26 | send({ "deviError": "No module found named: " + moduleName }); 27 | // exit javascript here? 28 | console.log("[!] No module found named: " + moduleName ) 29 | } 30 | 31 | /** 32 | * 33 | * @param {*} str 34 | * 35 | * Debug print 36 | * 37 | */ 38 | function log_d(str) { 39 | if (debug == "True") 40 | console.log("[+] " + str); 41 | } 42 | 43 | /** 44 | * 45 | * @param {*} moduleName 46 | * @param {*} symbolName 47 | * 48 | * return the symbol in the module 49 | * 50 | */ 51 | function getSymbolAddress(moduleName, symbolName) { 52 | var symbols = Module.enumerateSymbols(moduleName); 53 | for (var i = 0; i < symbols.length; i++) { 54 | if (symbols[i].name == symbolName) 55 | return symbols[i].address 56 | } 57 | } 58 | 59 | /** 60 | * 61 | * @param {*} codePointer 62 | * check if the instruction at codePointer is an indrect call. 63 | */ 64 | function isIndirectCall(codePointer) { 65 | return !Instruction.parse(codePointer).toString().startsWith('call 0x') 66 | } 67 | 68 | function printDebugCallEvent(callEvent) { 69 | log_d((callEvent[1] - myModule.base).toString(16) + ' -> ' + (callEvent[2] - myModule.base).toString(16)); 70 | log_d(Instruction.parse(callEvent[1]) + " -> " + Instruction.parse(callEvent[2])); 71 | log_d("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 72 | } 73 | 74 | /** 75 | * stalk the current thread. 76 | */ 77 | function traceCalls() { 78 | Stalker.follow({ 79 | events: { 80 | call: true 81 | }, 82 | 83 | 84 | onReceive: function (events) { 85 | 86 | var callList = []; 87 | 88 | var call_events = Stalker.parse(events); 89 | call_events.forEach(function (event) { 90 | //todo change ifs 91 | if ((myModule.base <= event[1]) && (event[1] <= myModule.end)) { 92 | if (isIndirectCall(ptr(event[1]))) { 93 | 94 | //debug! 95 | printDebugCallEvent(event); 96 | 97 | var src = (event[1]); 98 | var payload = {}; 99 | payload[src] = (event[2]).toString(10); 100 | callList.push(payload); 101 | } 102 | } 103 | }); 104 | 105 | send({ "callList": callList }) 106 | }, 107 | 108 | onLeave: function (retval) { 109 | //console.log("onLeave Called"); 110 | Stalker.unfollow(Process.getCurrentThreadId()); 111 | Stalker.flush(); 112 | Stalker.garbageCollect(); 113 | send({ "finished": true }); 114 | } 115 | 116 | }); 117 | } 118 | 119 | 120 | /** 121 | * attach interceptor to main, and start stalker 122 | */ 123 | function attachInterceptor() { 124 | Interceptor.attach(symbolAddress, { 125 | onEnter: function (args) { 126 | log_d('[-] Start Tracing'); 127 | log_d("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") 128 | 129 | // Send ModuleMap to devi 130 | send({"moduleMap":Process.enumerateModulesSync()}) 131 | 132 | traceCalls(); 133 | }, 134 | onLeave: function () { 135 | Stalker.flush(); 136 | Stalker.unfollow(Process.getCurrentThreadId()); 137 | Stalker.garbageCollect(); 138 | log_d('[-] Done Tracing') 139 | // send here that we are done and user should detach 140 | send({"deviFinished":"Execution finished detach with ctrl + d"}) 141 | } 142 | }); 143 | } -------------------------------------------------------------------------------- /devi_ghidra.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import sys 5 | from ghidra.program.model.symbol.FlowType import UNCONDITIONAL_CALL 6 | 7 | DEBUG = True 8 | VERSION = 0.2 9 | 10 | def dprint(str): 11 | if DEBUG: 12 | print(str) 13 | 14 | def get_call_info(addr, modules): 15 | if addr[:2] == '0x': 16 | addr = int(addr, 16) 17 | else: 18 | addr = int(addr) 19 | for module in modules: 20 | try: 21 | module_start = int(module['base'], 16) 22 | except TypeError: 23 | dprint(module) 24 | continue 25 | module_end = module_start + module['size'] 26 | if module_start <= addr <= module_end: 27 | # calc offset 28 | call_info = dict() 29 | call_info['rel_addr'] = addr - module_start 30 | call_info['module'] = module['name'] 31 | call_info['comment'] = module['name'] + '+' + hex(addr - module_start) 32 | return call_info 33 | 34 | def add_xref(call_src, call_dst , module_name, modules): 35 | #print(call_src) 36 | call_dst = get_call_info(call_dst, modules) 37 | 38 | # TODO ghidra 39 | ghidra_offset = str(currentProgram.getMinAddress()) 40 | ghidra_offset = int(ghidra_offset, 16) 41 | new_target = hex(call_src['rel_addr'] + ghidra_offset)[:-1] 42 | ghidra_src_address = currentProgram.getMinAddress().getAddress(new_target) 43 | 44 | if module_name in call_dst['comment']: 45 | 46 | # TODO ghidra 47 | new_dst = hex(call_dst['rel_addr'] + ghidra_offset)[:-1] 48 | ghidra_dst_address = currentProgram.getMinAddress().getAddress(new_dst) 49 | addInstructionXref( ghidra_src_address, ghidra_dst_address, -1, UNCONDITIONAL_CALL) 50 | 51 | # TODO ghidra 52 | setEOLComment(ghidra_src_address, str(call_dst['comment'])) 53 | #dprint('added xref: '+ hex(call_src['rel_addr']) +' -> ' + str(call_dst['comment'])) 54 | dprint('added xref: '+ str(ghidra_src_address) +' -> ' + str(ghidra_dst_address)) 55 | 56 | 57 | # TODO ghidra 58 | json_file = askFile('JSON File', 'Open') 59 | 60 | 61 | # TODO ghidra 62 | working_module_name = askString('Module Name Working on', 'Module Name') 63 | 64 | # jython seems to want a str conversion... 65 | f = open(str(json_file)) 66 | json_data = json.load(f) 67 | f.close() 68 | 69 | json_version = json_data['deviVersion'] 70 | 71 | if json_version < VERSION: 72 | print("[!] Ghidra Plugin might not support the version of the loaded json file!") 73 | 74 | call_list = json_data['calls'] 75 | modules = json_data['modules'] 76 | 77 | # make sure list is unique 78 | done = list() 79 | 80 | #print(module_name_and_offset_for_address("0x6c7c800b", modules)) 81 | for call in call_list: 82 | for i in call: 83 | src = i 84 | target = call[i] 85 | cur_call = get_call_info(src, modules) 86 | if working_module_name in cur_call['comment']: 87 | if src+target not in done: 88 | add_xref(cur_call, target, working_module_name, modules) -------------------------------------------------------------------------------- /devi_ida.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | import idautils 3 | import idc 4 | import json 5 | import traceback 6 | from ida_xref import add_cref 7 | from ida_nalt import get_root_filename 8 | from ida_kernwin import ask_file 9 | 10 | # From http://www.hexblog.com/?p=886 11 | # 1) Create the handler class 12 | class DeviIDAHandler(idaapi.action_handler_t): 13 | def __init__(self): 14 | idaapi.action_handler_t.__init__(self) 15 | self.version = 0.2 16 | 17 | # Executed when Menu is selected. 18 | def activate(self, ctx): 19 | json_file = ask_file(0, ".json", "Load Virtual Calls") 20 | with open(json_file) as f: 21 | devi_json_data = json.load(f) 22 | if self.version < devi_json_data["deviVersion"]: 23 | print("[!] devi JSON file has a more recent version than IDA plugin!") 24 | print("[!] we try parsing anyway!") 25 | if self.version > devi_json_data["deviVersion"]: 26 | print("[!] Your devi_ida and devi_frida versions are out of sync. Update your devi_ida!") 27 | 28 | try: 29 | self.devirtualize_calls(devi_json_data["calls"], devi_json_data["modules"]) 30 | return 1 31 | except: 32 | print("[!] An error was encountered!") 33 | traceback.print_exc() 34 | 35 | 36 | 37 | # This action is always available. 38 | def update(self, ctx): 39 | return idaapi.AST_ENABLE_ALWAYS 40 | 41 | def devirtualize_calls(self, call_list, modules): 42 | ida_file_name = get_root_filename() 43 | 44 | call_cnt = 0 45 | 46 | for module in modules: 47 | if module["name"] == ida_file_name: 48 | loaded_module = module 49 | break 50 | 51 | start = int(loaded_module["base"], 16) 52 | end = start + loaded_module["size"] 53 | 54 | print("[*] Adding virtual calls for module " + ida_file_name) 55 | 56 | for v_call in call_list: 57 | for call in v_call: 58 | # Check if call belongs to the current module 59 | if start <= int(call, 16) <= end: 60 | 61 | src = int(call, 16) - start 62 | dst = int(v_call[call]) - start 63 | add_cref(src, dst, fl_CN | XREF_USER) 64 | 65 | call_cnt += 1 66 | 67 | print("[*] Added {} virtual calls for module {}!".format(call_cnt, ida_file_name)) 68 | 69 | 70 | # 2) Describe the action 71 | action_desc = idaapi.action_desc_t( 72 | 'devi:loadJSON', # The action name. This acts like an ID and must be unique 73 | 'Load Virtual Calls', # The action text. 74 | DeviIDAHandler(), # The action handler. 75 | '', # Optional: the action shortcut 76 | 'Load JSON file with virtual calls', # Optional: the action tooltip (available in menus/toolbar) 77 | ) # Optional: the action icon (shows when in menus/toolbars) 78 | 79 | # 3) Register the action 80 | idaapi.register_action(action_desc) 81 | 82 | idaapi.attach_action_to_menu( 83 | 'File/Load file/', # The relative path of where to add the action 84 | 'devi:loadJSON', # The action ID (see above) 85 | idaapi.SETMENU_APP) # We want to append the action after the 'Manual instruction...' -------------------------------------------------------------------------------- /images/cpp-test-assembly-w-devi.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murx-/devi/fa6c1799c6d07af4fd8e6ae39a8f7d132ac34008/images/cpp-test-assembly-w-devi.PNG -------------------------------------------------------------------------------- /images/cpp-test-assembly-wo-devi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murx-/devi/fa6c1799c6d07af4fd8e6ae39a8f7d132ac34008/images/cpp-test-assembly-wo-devi.png -------------------------------------------------------------------------------- /images/cpp-test-xrefs-graphs-w-devi.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murx-/devi/fa6c1799c6d07af4fd8e6ae39a8f7d132ac34008/images/cpp-test-xrefs-graphs-w-devi.PNG -------------------------------------------------------------------------------- /images/cpp-test-xrefs-graphs-wo-devi2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murx-/devi/fa6c1799c6d07af4fd8e6ae39a8f7d132ac34008/images/cpp-test-xrefs-graphs-wo-devi2.PNG -------------------------------------------------------------------------------- /images/cpp-test-xrefs-w-devi.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murx-/devi/fa6c1799c6d07af4fd8e6ae39a8f7d132ac34008/images/cpp-test-xrefs-w-devi.PNG -------------------------------------------------------------------------------- /images/cpp-test-xrefs-wo-devi.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murx-/devi/fa6c1799c6d07af4fd8e6ae39a8f7d132ac34008/images/cpp-test-xrefs-wo-devi.PNG -------------------------------------------------------------------------------- /tests/HelloWorld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murx-/devi/fa6c1799c6d07af4fd8e6ae39a8f7d132ac34008/tests/HelloWorld -------------------------------------------------------------------------------- /tests/HelloWorld.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | using namespace std; 7 | 8 | class Hello { 9 | public: 10 | Hello() {} 11 | virtual void printWorld() { cout << "Hello" << endl; } 12 | }; 13 | 14 | class HelloWorld : public Hello { 15 | public: 16 | HelloWorld() {} 17 | virtual void printWorld() { cout << "Hello World" << endl; } 18 | }; 19 | 20 | 21 | int main(int argc, char **argv) { 22 | 23 | Hello *my_hello; 24 | my_hello = new HelloWorld(); 25 | my_hello->printWorld(); 26 | 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /tests/a.out: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murx-/devi/fa6c1799c6d07af4fd8e6ae39a8f7d132ac34008/tests/a.out --------------------------------------------------------------------------------