├── 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 |
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 | | Calls from {id} |
6 |
7 |
8 |
9 | {calls_rows}
10 |
11 |
12 |
13 |
14 |
15 | | XREFs to {id} |
16 |
17 |
18 |
19 | {xref_rows}
20 |
21 |
22 |
23 |
24 |
25 |
26 | |
27 | Data operations (mov ...) |
28 |
29 |
30 | |
31 | Floating point operations |
32 |
33 |
34 | |
35 | Arthmetic operations (add, xor, shr ...) |
36 |
37 |
38 | |
39 | Dataflow operations (call, cmp, jmp ...) |
40 |
41 |
42 | |
43 | Other operations |
44 |
45 |
46 |
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 | 
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 | 
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 | 
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 | | Function name |
145 | Instructions |
146 | Blocks |
147 | Function calls |
148 | XREFS |
149 |
150 |
151 |
152 | {f_table}
153 |
154 |
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 |
--------------------------------------------------------------------------------
]