├── __init__.py ├── imgs └── callgraph.png └── readme.md /__init__.py: -------------------------------------------------------------------------------- 1 | from binaryninja import ( 2 | DisassemblyTextLine, 3 | InstructionTextToken, 4 | InstructionTextTokenType, 5 | FlowGraph, 6 | FlowGraphNode, 7 | BranchType, 8 | enums, 9 | PluginCommand, 10 | Settings, 11 | BackgroundTaskThread, 12 | demangle 13 | ) 14 | 15 | Settings().register_group("bn-callgraph", "BN CallGraph") 16 | Settings().register_setting("bn-callgraph.showColorRoot", """ 17 | { 18 | "title" : "Colorize Root", 19 | "type" : "boolean", 20 | "default" : true, 21 | "description" : "Show root node in green" 22 | } 23 | """) 24 | Settings().register_setting("bn-callgraph.showColorLeaves", """ 25 | { 26 | "title" : "Colorize Leaves", 27 | "type" : "boolean", 28 | "default" : true, 29 | "description" : "Show leaves node in red" 30 | } 31 | """) 32 | Settings().register_setting("bn-callgraph.showIndirectCalls", """ 33 | { 34 | "title" : "Show Indirect Calls", 35 | "type" : "boolean", 36 | "default" : true, 37 | "description" : "Show indirect calls as undetermined nodes in the graph" 38 | } 39 | """) 40 | 41 | class UndeterminedFunction(object): 42 | id_num = 0 43 | 44 | def __init__(self, addr): 45 | self.id = UndeterminedFunction.id_num 46 | self.start = addr 47 | self.name = "UndFunction_%d" % self.id 48 | 49 | UndeterminedFunction.id_num += 1 50 | 51 | def __hash__(self): 52 | return hash("und_%d" % self.id) 53 | 54 | def __eq__(self, other): 55 | return isinstance(other, UndeterminedFunction) and self.id == other.id 56 | 57 | class ExternalFunction(object): 58 | def __init__(self, name): 59 | self.start = 0 60 | self.name = "Ext_" + name 61 | 62 | def __hash__(self): 63 | return hash(self.name) 64 | 65 | def __eq__(self, other): 66 | return isinstance(other, ExternalFunction) and self.name == other.name 67 | 68 | def demangle_name(bv, name, max_size=64): 69 | res = name 70 | if bv.platform.name.startswith("linux-") or bv.platform.name.startswith("mac-"): 71 | _, demangled = demangle.demangle_gnu3(bv.arch, name) 72 | if not isinstance(demangled, list): 73 | res = demangled 74 | else: 75 | res = demangle.simplify_name_to_string(demangle.get_qualified_name(demangled)) 76 | elif bv.platform.name.startswith("windows-"): 77 | _, demangled = demangle.demangle_ms(bv.arch, name) 78 | if not isinstance(demangled, list): 79 | res = demangled 80 | else: 81 | res = demangle.simplify_name_to_string(demangle.get_qualified_name(demangled)) 82 | 83 | if len(res) > max_size: 84 | res = res[:max_size//2-3] + "..." + res[-max_size//2:] 85 | return res 86 | 87 | class GraphWrapper(object): 88 | def __init__(self, bv, root_function): 89 | self.bv = bv 90 | self.nodes = {} 91 | self.edges = set() 92 | self.graph = FlowGraph() 93 | self.root_function = root_function 94 | root_node = FlowGraphNode(self.graph) 95 | if Settings().get_bool("bn-callgraph.showColorRoot"): 96 | root_node.highlight = enums.HighlightStandardColor.GreenHighlightColor 97 | root_node.lines = [ 98 | self._build_function_text(root_function) 99 | ] 100 | self.graph.append(root_node) 101 | self.nodes[root_function] = root_node 102 | 103 | def _build_function_text(self, function): 104 | res = \ 105 | DisassemblyTextLine ([ 106 | InstructionTextToken( 107 | InstructionTextTokenType.AddressDisplayToken, 108 | "{:#x}".format(function.start), 109 | value=function.start, 110 | ), 111 | InstructionTextToken( 112 | InstructionTextTokenType.OperandSeparatorToken, 113 | " @ " 114 | ), 115 | InstructionTextToken( 116 | InstructionTextTokenType.CodeSymbolToken, 117 | demangle_name(self.bv, function.name), 118 | function.start 119 | ) 120 | ]) 121 | return res 122 | 123 | def add(self, function, father_function): 124 | assert father_function in self.nodes 125 | if (father_function, function) in self.edges: 126 | return 127 | 128 | if function in self.nodes: 129 | node = self.nodes[function] 130 | else: 131 | node = FlowGraphNode(self.graph) 132 | node.lines = [ 133 | self._build_function_text(function) 134 | ] 135 | self.graph.append(node) 136 | self.nodes[function] = node 137 | 138 | father = self.nodes[father_function] 139 | father.add_outgoing_edge( 140 | BranchType.UnconditionalBranch, 141 | node 142 | ) 143 | self.edges.add( 144 | (father_function, function) 145 | ) 146 | 147 | def show(self): 148 | if Settings().get_bool("bn-callgraph.showColorLeaves"): 149 | nodes_dst = set([edge[1] for edge in self.edges]) 150 | nodes_src = set([edge[0] for edge in self.edges]) 151 | 152 | leaves = nodes_dst - nodes_src 153 | for leave in leaves: 154 | self.nodes[leave].highlight = enums.HighlightStandardColor.RedHighlightColor 155 | if Settings().get_bool("bn-callgraph.showIndirectCalls"): 156 | for fun in self.nodes: 157 | if isinstance(fun, UndeterminedFunction): 158 | self.nodes[fun].highlight = enums.HighlightStandardColor.BlueHighlightColor 159 | 160 | self.graph.show("Callgraph starting from {} @ {:#x}".format( 161 | demangle_name(self.bv, self.root_function.name), self.root_function.start)) 162 | 163 | def callgraph(bv, current_function): 164 | bv.update_analysis_and_wait() 165 | graph = GraphWrapper(bv, current_function) 166 | 167 | show_indirect = False 168 | if Settings().get_bool("bn-callgraph.showIndirectCalls"): 169 | show_indirect = True 170 | 171 | visited = set() 172 | stack = [current_function] 173 | while stack: 174 | func = stack.pop() 175 | 176 | calls = set() 177 | indirect_calls = set() 178 | external_calls = set() 179 | for llil_block in func.llil: 180 | for llil in llil_block: 181 | if llil.operation.name in {"LLIL_CALL", "LLIL_TAILCALL"}: 182 | if llil.dest.possible_values.type.name in {"ImportedAddressValue", "UndeterminedValue"}: 183 | if llil.dest.operation.name == "LLIL_LOAD" and llil.dest.src.possible_values.type.name == "ConstantPointerValue": 184 | # External function 185 | is_in_binary = False 186 | dst_addr = llil.dest.src.possible_values.value 187 | if dst_addr != 0: 188 | dst_fun_addr_raw = bv.read(dst_addr, bv.arch.address_size) 189 | if len(dst_fun_addr_raw) == bv.arch.address_size: 190 | # Its in the binary. Probably a shared library that exports a symbol that uses 191 | dst_fun_addr = int.from_bytes( 192 | dst_fun_addr_raw, "little" if bv.arch.endianness.name == "LittleEndian" else "big") 193 | dst_funs = bv.get_functions_at(dst_fun_addr) 194 | for dst_fun in dst_funs: 195 | calls.add(dst_fun) 196 | is_in_binary = True 197 | if not is_in_binary: 198 | # The function is not here 199 | symb = bv.get_symbol_at(dst_addr) 200 | if symb is not None: 201 | external_calls.add(ExternalFunction(symb.name)) 202 | elif llil.dest.possible_values.type.name == "UndeterminedValue" and show_indirect: 203 | # Indirect call 204 | indirect_calls.add(UndeterminedFunction(llil.address)) 205 | elif llil.dest.possible_values.type.name == "ConstantPointerValue": 206 | dst_funs = bv.get_functions_at(llil.dest.possible_values.value) 207 | for dst_fun in dst_funs: 208 | calls.add(dst_fun) 209 | elif show_indirect: 210 | # Indirect call 211 | indirect_calls.add(UndeterminedFunction(llil.address)) 212 | 213 | for child_func in calls | indirect_calls | external_calls: 214 | graph.add(child_func, func) 215 | 216 | if child_func not in visited: 217 | if not isinstance(child_func, UndeterminedFunction) and not isinstance(child_func, ExternalFunction): 218 | stack.append(child_func) 219 | visited.add(func) 220 | graph.show() 221 | 222 | def callgraph_reversed(bv, current_function): 223 | bv.update_analysis_and_wait() 224 | graph = GraphWrapper(current_function) 225 | 226 | visited = set() 227 | stack = [current_function] 228 | while stack: 229 | func = stack.pop() 230 | for child_func in set(func.callers): 231 | graph.add(child_func, func) 232 | 233 | if child_func not in visited: 234 | stack.append(child_func) 235 | visited.add(func) 236 | graph.show() 237 | 238 | class CallgraphThread(BackgroundTaskThread): 239 | def __init__(self, view, function, mode): 240 | super().__init__('Computing callgraph from {} [{}]...'.format(function.name, mode)) 241 | self.view = view 242 | self.function = function 243 | self.mode = mode 244 | 245 | def run(self): 246 | if self.mode == "reversed": 247 | callgraph_reversed(self.view, self.function) 248 | else: 249 | callgraph(self.view, self.function) 250 | 251 | def _wrapper(mode): 252 | def f(view, function): 253 | thread = CallgraphThread(view, function, mode) 254 | thread.start() 255 | return f 256 | 257 | PluginCommand.register_for_function( 258 | "BNCallGraph\\Compute callgraph", 259 | "", 260 | _wrapper("normal") 261 | ) 262 | 263 | PluginCommand.register_for_function( 264 | "BNCallGraph\\Compute reversed callgraph", 265 | "", 266 | _wrapper("reversed") 267 | ) 268 | -------------------------------------------------------------------------------- /imgs/callgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borzacchiello/bncallgraph/bed512c2ae12d6c32b5a143f1af1caa5c8e3b5e3/imgs/callgraph.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # BNCallGraph 2 | Yet another simple _callgraph plugin_ for Binary Ninja. 3 | The plugin constructs and visualize a callgraph starting from the current function, using the FlowGraph APIs. 4 | 5 | ![example](imgs/callgraph.png) 6 | 7 | # Commands 8 | - **Compute callgraph**: Compute and visualize a callgraph starting from the current function, following calls. The callgraph contains all the functions reachable starting from the current function. 9 | - **Compute reversed callgraph**: Compute and visualize a callgraph starting from the current function, following xrefs. The callgraph contains all the functions that reaches the current function. 10 | 11 | # Settings 12 | - **Colorize Leaves**: Mark nodes without outgoing edges in red. 13 | - **Colorize Root**: Mark current function in green. 14 | - **Show Indirect Calls**: Show indirect calls as undefined blocks in the callgraph. 15 | 16 | # Minumum Version 17 | Tested on `dev-1.2.1987` with `python 3.6.9`. 18 | --------------------------------------------------------------------------------