├── modules ├── __init__.py ├── hilbert.py ├── fingerprint.py └── function_report.py ├── data ├── function_call_row.tpl ├── function_xref_row.tpl ├── fingerprint_image.tpl ├── function_table_row.tpl ├── function_pane.tpl └── functions_report.html ├── __init__.py ├── README.md ├── plugin.json └── .gitignore /modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/function_call_row.tpl: -------------------------------------------------------------------------------- 1 | 2 | 0x{addr:0{w}x}: {name} 3 | 4 | -------------------------------------------------------------------------------- /data/function_xref_row.tpl: -------------------------------------------------------------------------------- 1 | 2 | 0x{addr:0{w}x}: {name} 3 | 4 | -------------------------------------------------------------------------------- /data/fingerprint_image.tpl: -------------------------------------------------------------------------------- 1 | Binary Fingerprint 2 | -------------------------------------------------------------------------------- /data/function_table_row.tpl: -------------------------------------------------------------------------------- 1 | 2 | 0x{start:0{w}x}: {name} 3 | {instructions} 4 | {blocks} 5 | {calls} 6 | {xrefs} 7 | 8 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # author: carstein 3 | # 10k foot view on binary 4 | 5 | from binaryninja import PluginCommand 6 | 7 | from modules import function_report 8 | 9 | # register plugin 10 | PluginCommand.register_for_function( 11 | "[Keyhole] Function report", 12 | "Report about functions in binary", 13 | function_report.run_plugin) 14 | -------------------------------------------------------------------------------- /modules/hilbert.py: -------------------------------------------------------------------------------- 1 | # Author: carstein 2 | # Hilbert curve 3 | 4 | class Hilbert: 5 | def __init__(self, n): 6 | self.size = n 7 | 8 | @staticmethod 9 | def last2bits(x): 10 | return x & 3 11 | 12 | def position(self, p, mul=1): 13 | x,y = 0,0 14 | 15 | for n in [2**i for i in range(1, self.size)]: 16 | 17 | t = Hilbert.last2bits(p) 18 | if t == 0: 19 | x,y = y,x 20 | elif t == 1: 21 | y += n/2 22 | elif t == 2: 23 | x += (n/2) 24 | y += (n/2) 25 | else: 26 | x,y = (n/2)-1-y+(n/2), (n/2)-1-x 27 | 28 | p = p >> 2 29 | 30 | return x*mul, y*mul 31 | -------------------------------------------------------------------------------- /data/function_pane.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {calls_rows} 10 | 11 |
Calls from {id}
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {xref_rows} 20 | 21 |
XREFs to {id}
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
Data operations (mov ...)
Floating point operations
Arthmetic operations (add, xor, shr ...)
Dataflow operations (call, cmp, jmp ...)
Other operations
47 |
48 |
49 | {img} 50 |
51 |
52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BinaryNinja Keyhole 2 | ==================== 3 | Plugin for [Binary Ninja](https://binary.ninja/) platform 4 | 5 | ## General 6 | This plugin iterates over all recognized functions in a binary and present short report about findings. 7 | Such findings might help reverser to determine _interesting_ functions and where to start looking. 8 | 9 | ## Features 10 | ### Overview 11 | In main view user is presented with following information about all functions: 12 | - number of instructions 13 | - number of basic blocks 14 | - number of function calls 15 | - number of times given function is being called 16 | 17 | ![Main View](https://i.imgur.com/4z1B2jF.png) 18 | 19 | ### Function Details 20 | In side pane user is presented with detailed list of functions given function call and with all the cross references to a given function. 21 | 22 | ![Function Pane](https://i.imgur.com/Acvqktp.png) 23 | 24 | ### Binary fingerprint 25 | Ueer is presented with an image draw using hilberts curve that displays type of instructions in a given function. Such view might help reverser to spot certain characteristics like dense clusters of arthemetic or data operations. 26 | 27 | ![Fingerprint](https://i.imgur.com/nMxP8AP.png) 28 | 29 | ## TODO 30 | - [x] Basic report 31 | - [x] Report about given function 32 | - [x] Basic binary fingerprint 33 | - [ ] More instructions recognized by binary fingerprint 34 | - [ ] Enchance basic report by adding additional characteristics 35 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin": { 3 | "name": "Keyhole", 4 | "type": ["ui"], 5 | "api": "python2", 6 | "description": "Report about functions in given binary.", 7 | "longdescription": "Plugin iterates through all identified functions in a given binary counting instructions, basic blocks, function calls and code references. Gathered information are then printed out in a tabular form.", 8 | "license": { 9 | "name": "MIT", 10 | "text": "Copyright (c) \n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." 11 | }, 12 | "dependencies": { 13 | "pip": [], 14 | "apt": [], 15 | "installers": [], 16 | "other": [] 17 | }, 18 | "version": "0.2", 19 | "author": "Michal Melewski ", 20 | "minimumBinaryNinjaVersion": { 21 | "dev": "1.0.dev-175", 22 | "release": "1.1.1038" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/fingerprint.py: -------------------------------------------------------------------------------- 1 | # Author: carstein 2 | # Binary Fingerprint 3 | 4 | import io 5 | import base64 6 | 7 | from hilbert import Hilbert 8 | from PIL import Image, ImageDraw 9 | 10 | scale = [(4, 2, 160), # elements, N-hilbert, size 11 | (16, 4, 80), 12 | (64, 8, 40), 13 | (256, 16, 20), 14 | (1024, 32, 10), 15 | (4096, 64, 5)] 16 | 17 | color = {0:'#F9DEC9', 18 | 1:'#33ADFF', 19 | 2:'#56E39F', 20 | 3:'#383D3B', 21 | 4:'#E3170A'} 22 | 23 | 24 | class FingerprintReport: 25 | t = [ 26 | (['mov', 'lea', 'pop','push', 'movzx', 'movsxd', 'cmovne'], 1), 27 | ([],2), 28 | (['or', 'and','xor','add', 'sub','mul','div', 'sar', 'shr', 'not', 'cmp', 'test'], 3), 29 | (['call', 'jle', 'ja', 'jg', 'jbe', 'je', 'jne', 'jmp', 'jl', 'jge', 'ret', 'retn'], 4), 30 | ] 31 | 32 | def __init__(self): 33 | self.fingerprint = [] 34 | self.hilbert = None 35 | 36 | def reset(self): 37 | self.fingerprint = [] 38 | 39 | def add(self, i): 40 | # Processing instruction 41 | for group, color in self.t: 42 | if i in group: 43 | self.fingerprint.append(color) 44 | return 45 | 46 | print 'unmatched instruction {}'.format(i) 47 | self.fingerprint.append(0) 48 | 49 | def create_image(self): 50 | img = Image.new("RGB", (320,320), '#FFFFFF') 51 | draw = ImageDraw.Draw(img) 52 | 53 | # Calculate sizes - box size and elements 54 | l = len(self.fingerprint) 55 | if 4 <= l <= 4096: 56 | for el, n, size in scale: 57 | if el >= l: 58 | self.size = size 59 | self.hilbert = Hilbert(n) 60 | break 61 | else: 62 | return None 63 | 64 | ## Create Image 65 | for idx, val in enumerate(self.fingerprint): 66 | x1,y1 = self.hilbert.position(idx, self.size) 67 | x2 = x1 + self.size - 1 68 | y2 = y1 + self.size - 1 69 | 70 | draw.rectangle([x1, y1, x2, y2], fill=color[val]) 71 | 72 | ## Save image 73 | b = io.BytesIO() 74 | img.save(b, 'PNG') 75 | 76 | return base64.b64encode(b.getvalue()) 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,pycharm 3 | 4 | ### PyCharm ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff: 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/dictionaries 12 | 13 | # Sensitive or high-churn files: 14 | .idea/**/dataSources/ 15 | .idea/**/dataSources.ids 16 | .idea/**/dataSources.xml 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | 22 | # Gradle: 23 | .idea/**/gradle.xml 24 | .idea/**/libraries 25 | 26 | # CMake 27 | cmake-build-debug/ 28 | 29 | # Mongo Explorer plugin: 30 | .idea/**/mongoSettings.xml 31 | 32 | ## File-based project format: 33 | *.iws 34 | 35 | ## Plugin-specific files: 36 | 37 | # IntelliJ 38 | /out/ 39 | 40 | # mpeltonen/sbt-idea plugin 41 | .idea_modules/ 42 | 43 | # JIRA plugin 44 | atlassian-ide-plugin.xml 45 | 46 | # Cursive Clojure plugin 47 | .idea/replstate.xml 48 | 49 | # Ruby plugin and RubyMine 50 | /.rakeTasks 51 | 52 | # Crashlytics plugin (for Android Studio and IntelliJ) 53 | com_crashlytics_export_strings.xml 54 | crashlytics.properties 55 | crashlytics-build.properties 56 | fabric.properties 57 | 58 | ### PyCharm Patch ### 59 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 60 | 61 | # *.iml 62 | # modules.xml 63 | # .idea/misc.xml 64 | # *.ipr 65 | 66 | # Sonarlint plugin 67 | .idea/sonarlint 68 | 69 | ### Python ### 70 | # Byte-compiled / optimized / DLL files 71 | __pycache__/ 72 | *.py[cod] 73 | *$py.class 74 | 75 | # C extensions 76 | *.so 77 | 78 | # Distribution / packaging 79 | .Python 80 | build/ 81 | develop-eggs/ 82 | dist/ 83 | downloads/ 84 | eggs/ 85 | .eggs/ 86 | lib/ 87 | lib64/ 88 | parts/ 89 | sdist/ 90 | var/ 91 | wheels/ 92 | *.egg-info/ 93 | .installed.cfg 94 | *.egg 95 | 96 | # PyInstaller 97 | # Usually these files are written by a python script from a template 98 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 99 | *.manifest 100 | *.spec 101 | 102 | # Installer logs 103 | pip-log.txt 104 | pip-delete-this-directory.txt 105 | 106 | # Unit test / coverage reports 107 | htmlcov/ 108 | .tox/ 109 | .coverage 110 | .coverage.* 111 | .cache 112 | .pytest_cache/ 113 | nosetests.xml 114 | coverage.xml 115 | *.cover 116 | .hypothesis/ 117 | 118 | # Translations 119 | *.mo 120 | *.pot 121 | 122 | # Flask stuff: 123 | instance/ 124 | .webassets-cache 125 | 126 | # Scrapy stuff: 127 | .scrapy 128 | 129 | # Sphinx documentation 130 | docs/_build/ 131 | 132 | # PyBuilder 133 | target/ 134 | 135 | # Jupyter Notebook 136 | .ipynb_checkpoints 137 | 138 | # pyenv 139 | .python-version 140 | 141 | # celery beat schedule file 142 | celerybeat-schedule.* 143 | 144 | # SageMath parsed files 145 | *.sage.py 146 | 147 | # Environments 148 | .env 149 | .venv 150 | env/ 151 | venv/ 152 | ENV/ 153 | env.bak/ 154 | venv.bak/ 155 | 156 | # Spyder project settings 157 | .spyderproject 158 | .spyproject 159 | 160 | # Rope project settings 161 | .ropeproject 162 | 163 | # mkdocs documentation 164 | /site 165 | 166 | # mypy 167 | .mypy_cache/ 168 | 169 | 170 | # End of https://www.gitignore.io/api/python,pycharm -------------------------------------------------------------------------------- /data/functions_report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 137 | 138 | 139 |
140 |

141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | {f_table} 153 | 154 |
Function nameInstructionsBlocksFunction callsXREFS
155 |

156 |
157 |
158 |

159 | {f_panes} 160 |

161 |
162 | 163 | 164 | -------------------------------------------------------------------------------- /modules/function_report.py: -------------------------------------------------------------------------------- 1 | # Author: carstein 2 | # Function report - functions with block size and instructions 3 | 4 | import os 5 | import binaryninja as bn 6 | 7 | from fingerprint import FingerprintReport 8 | 9 | 10 | # It does not end with / 11 | PLUGINDIR_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 12 | 13 | supported_arch = [ 14 | 'linux-x86', 15 | 'linux-x86_64' 16 | ] 17 | 18 | class Report: 19 | def __init__(self, bv): 20 | self.bv = bv 21 | self.templates = {} 22 | self.function_data = { 23 | # 'function_name': {'start':0x08040000, 24 | # 'blocks':10, 25 | # 'instructions': 50, 26 | # 'calls': [], // how many other functions are being called 27 | # 'xrefs': [], // how many functions call this function 28 | # 'fingerprint: ,' //Image of binary fingerprint 29 | # 'size': 'small'} 30 | } 31 | 32 | def __get_function_row(self, name, data): 33 | template = self.templates['function_row'] 34 | return template.format(start = data['start'], 35 | name = name, 36 | instructions = data['instructions'], 37 | blocks = data['blocks'], 38 | calls = len(data['calls']), 39 | xrefs = len(data['xrefs']), 40 | size = data['size'], 41 | w = self.bv.arch.address_size*2) 42 | 43 | def __extract_call_target(self, instr): 44 | if instr.dest.operation == bn.LowLevelILOperation.LLIL_CONST_PTR: 45 | addr = instr.dest.constant 46 | return addr, self.bv.get_function_at(addr).name 47 | else: 48 | return 0, "[{}]".format(instr.dest) 49 | 50 | def __get_function_pane(self, name, data): 51 | template = self.templates['function_pane'] 52 | xref_rows = '' 53 | 54 | for xref in data['xrefs']: 55 | xref_rows += self.templates['function_xref_row'].format(addr = xref.address, 56 | name = xref.function.name, 57 | w = self.bv.arch.address_size*2) 58 | calls_rows = '' 59 | for call in data['calls']: 60 | call_addr, call_name = self.__extract_call_target(call) 61 | calls_rows += self.templates['function_call_row'].format(addr = call_addr, 62 | name = call_name, 63 | w = self.bv.arch.address_size*2) 64 | 65 | fingerprint_image = self.templates['fingerprint'].format(img = data['fingerprint']) 66 | return template.format(id = name, 67 | calls_rows = calls_rows, 68 | xref_rows = xref_rows, 69 | img = fingerprint_image) 70 | 71 | def add_function(self, f): 72 | b, i = 0, 0 73 | c = [] 74 | 75 | # Basic data 76 | for block in f.low_level_il: 77 | b += 1 78 | for inst in block: 79 | i += 1 80 | if inst.operation in [bn.LowLevelILOperation.LLIL_CALL, bn.LowLevelILOperation.LLIL_CALL_STACK_ADJUST, bn.LowLevelILOperation.LLIL_TAILCALL]: 81 | c.append(inst) 82 | 83 | # Binary fingerprint 84 | fingerprint = FingerprintReport() 85 | 86 | for inst in f.instructions: 87 | fingerprint.add(inst[0][0].text) 88 | 89 | # naivly determine function size 90 | if b == 1 or i < 10: 91 | size = 'small' 92 | elif i < 100: 93 | size = 'medium' 94 | else: 95 | size = 'large' 96 | 97 | self.function_data[f.name] = {'start': f.start, 98 | 'blocks': b, 99 | 'instructions': i, 100 | 'calls': c, 101 | 'xrefs': self.bv.get_code_refs(f.start), 102 | 'size': size, 103 | 'fingerprint': fingerprint.create_image() 104 | } 105 | 106 | def load_template(self, name, template): 107 | template_path = PLUGINDIR_PATH + "/data/" + template 108 | 109 | with open(template_path) as fh: 110 | self.templates[name] = fh.read() 111 | 112 | def generate_html(self): 113 | html = self.templates['main'] 114 | f_table = '' 115 | panes = '' 116 | 117 | for name, data in sorted(self.function_data.iteritems(), reverse=True, key=lambda x: x[1]['instructions']): 118 | f_table += self.__get_function_row(name, data) 119 | panes += self.__get_function_pane(name, data) 120 | 121 | return html.format(f_number = len(self.function_data.keys()), 122 | f_table = f_table, 123 | f_panes = panes) 124 | 125 | def run_plugin(bv, function): 126 | # Supported platform check 127 | if bv.platform.name not in supported_arch: 128 | log_error('[x] Right now this plugin supports only the following platforms: ' + str(supported_arch)) 129 | return -1 130 | 131 | r = Report(bv) 132 | r.load_template('main','functions_report.html') 133 | r.load_template('function_row', 'function_table_row.tpl') 134 | r.load_template('function_pane', 'function_pane.tpl') 135 | r.load_template('function_call_row', 'function_call_row.tpl') 136 | r.load_template('function_xref_row', 'function_xref_row.tpl') 137 | r.load_template('fingerprint', 'fingerprint_image.tpl') 138 | 139 | bn.log_info('[*] Scanning functions...') 140 | for function in bv.functions: 141 | if function.symbol.type != bn.SymbolType.ImportedFunctionSymbol: 142 | r.add_function(function) 143 | 144 | save_filename = bn.interaction.get_save_filename_input("Save report to ...") 145 | 146 | if save_filename: 147 | with open(save_filename, "w+") as fh: 148 | fh.write(r.generate_html()) 149 | --------------------------------------------------------------------------------