Skip to content

Instantly share code, notes, and snippets.

@psifertex
Last active April 9, 2024 11:10
Show Gist options
  • Star 58 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save psifertex/6fbc7532f536775194edd26290892ef7 to your computer and use it in GitHub Desktop.
Save psifertex/6fbc7532f536775194edd26290892ef7 to your computer and use it in GitHub Desktop.
my current collection of snippets
Welcome to Jordan's grab-bag of common Binary Ninja Snippets.
These snippest are meant to run with the Binary Ninja Snippets Plugin
(http://github.com/Vector35/snippets) though they can all also be pasted
directly into the python console or turned into stand-alone plugins if needed.
To install the entire collection at once, just install the Snippets plugin via
the plugin manager (CMD/CTL-SHIFT-M), confirm the Snippet Editor works
(Tool/Snippets/Snippet Editor), and unzip this bundle (Download ZIP above) into
your Snippets folder.
Alternatively, the `update_snippets` snippet can be used to automatically
download/update this entire collection.
You can access the snippets folder by using the "Browse Snippets" button in the
Snippet Editor itself.
Also, appologies for the weird file name, trying to fight gist's auto-sorting.
Since these are starting to get a bit more complex and there's a lot of code gathered I figured I should be explicit that these are released under a public domain license (CC0):
To the extent possible under law, Jordan Wiens (@psifertex) has waived all copyright and related or neighboring rights to this Binary Ninja Snippets Collection (https://gist.github.com/psifertex/6fbc7532f536775194edd26290892ef7). This work is published from: United States.
For more information on a CC0 license, see: https://creativecommons.org/publicdomain/zero/1.0/deed.en
# append file contents to view
#
# Will append a file's raw contents to the existing view (useful for working with firmware or memory dumps). Thanks to @mariomain for the snippet.
def load_file(infilename):
f = open(infilename, "rb")
g = f.read()
f.close()
faddr = None
while (faddr is not None):
faddr = interaction.get_int_input("Load address", "Load address")
fsize = len(g)
return g, faddr, fsize
def overlap(reg1, reg1sz, reg2, reg2sz):
if ((reg1 >= reg2) and (reg1 < (reg2 + reg2sz))):
return True
if (((reg1 + reg1sz) > reg2) and ((reg1 + reg1sz) < (reg2 + reg2sz))):
return True
if ((reg1 < reg2) and ((reg1 + reg1sz) >= (reg2 + reg2sz))):
return True
return False
def add_some_file_sec(file_name):
rv = bv.get_view_of_type("Raw")
fblob, faddr, fsize = load_file(file_name)
prev_sec = bv.get_segment_at(bv.start)
succ_sec = None
new_succ_blob = None
new_succ_blob_addr = 0
if (overlap(faddr, fsize, prev_sec.start, len(prev_sec)) == False) and (faddr < prev_sec.start):
succ_sec = prev_sec
else:
for i in bv.sections:
i = bv.get_section_by_name(i)
if overlap(faddr, fsize, i.start, len(i)) == True:
log_info("overlapping with region {}".format(hex(i.start)))
return False
if prev_sec.start != i.start:
if (faddr >= (prev_sec.start + len(prev_sec))) and (faddr < i.start):
succ_sec = i
break
prev_sec = i
if succ_sec != None:
follow_seg = bv.get_segment_at(succ_sec.start)
the_rest = rv.read(follow_seg.data_offset, rv.length - follow_seg.data_offset)
new_succ_blob = fblob + the_rest
new_succ_blob_addr = follow_seg.data_offset
else:
new_succ_blob = fblob
new_succ_blob_addr = rv.length
rv.write(new_succ_blob_addr, new_succ_blob)
if succ_sec != None:
for i in bv.segments:
if i.start > faddr:
temp_start = i.start
temp_off = i.data_offset
temp_len = i.data_length
bv.remove_auto_segment(i.start, len(i))
temp_off += fsize
bv.add_auto_segment(start=temp_start, length=temp_len, data_offset=temp_off, data_length=temp_len,
flags=(SegmentFlag.SegmentReadable | SegmentFlag.SegmentExecutable))
bv.add_auto_section(os.path.basename(file_name), start=faddr, length=fsize,
semantics=SectionSemantics.ReadOnlyCodeSectionSemantics)
bv.add_auto_segment(start=faddr, length=fsize, data_offset=new_succ_blob_addr, data_length=fsize,
flags=(SegmentFlag.SegmentReadable | SegmentFlag.SegmentExecutable))
# auto strings
#
# automatically create string types at all detected strings
count = 0
for s in bv.strings:
bv.define_user_data_var(s.start, Type.array(Type.char(), s.length))
if bv.get_symbol_at(s.start) is None:
sym = Symbol(types.SymbolType.DataSymbol, s.start, "str_{}".format(s.value))
bv.define_user_symbol(sym)
count += 1
interaction.show_message_box(
"Auto-Rename strings",
f"Completed renaming variables based {count} strings!"
)
# batch example
#
# Intended to be run from the CLI, not as a snippet
# Remove these top lines to run headlessly (requires a commercial license)
#!/usr/bin/env python3
import binaryninja
import glob
import filetype
import os
from tabulate import tabulate
from collections import defaultdict
binaryninja.disable_default_log()
for file in glob.glob("./**/*", recursive=True):
if os.path.isfile(file):
guess = filetype.guess(file)
if guess and guess.extension == "elf":
print(f"Filename: {file}")
try:
with binaryninja.load(file, update_analysis=False) as bv:
# Imports don't require analysis, otherwise remove update_analysis param
syms = []
for sym in bv.get_symbols_of_type(binaryninja.SymbolType.ImportAddressSymbol):
syms.append([sym.name, hex(sym.address)])
print(tabulate(syms, headers=["Import Name", "Offset"]))
except:
print("Invalid architecture.")
print("\n")
# copy location as offset
#
# copies the currently selected value as an offset from the base to the clipboard
# Was used to include offset as a module name, not needed now
# import os
# modulename=os.path.basename(bv.file.original_filename)
s = bv.get_segment_at(here)
fileoffset=here - s.start + s.data_offset
# Alternate implementation that copies as a file offset (requires above lines
#offset=s.data_offset + offset
clip = PySide6.QtGui.QGuiApplication.clipboard()
clip.setText('%x' % (fileoffset))
# componentize cpp
#
# Using the components API to sort CPP symbols into class hierarchies
def ___sortCPPSymbols():
bv.begin_undo_actions()
cpp_comp = bv.get_component_by_path("C++ Classes")
if not cpp_comp:
cpp_comp = bv.create_component("C++ Classes")
for func in bv.functions:
if len(demangle_gnu3(bv.arch, func.name)[1]) < 2 or isinstance(demangle_gnu3(bv.arch, func.name)[1], str):
continue
namespace = demangle_gnu3(bv.arch, func.name)[1][0]
comp = bv.get_component_by_path("C++ Classes/" + namespace)
if comp is None:
comp = bv.create_component(namespace, cpp_comp)
comp.add_function(func)
bv.commit_undo_actions()
___sortCPPSymbols()
# componentize sections
#
# Using the components API to sort symbols in different sections into folders
def ___sortSymbolsBySectionName():
bv.begin_undo_actions()
for func in bv.functions:
sects = bv.get_sections_at(func.start)
for sect in sects:
comp = bv.get_component_by_path(sect.name)
if comp is None:
comp = bv.create_component(sect.name)
comp.add_function(func)
bv.commit_undo_actions()
___sortSymbolsBySectionName()
# IL instruction counts for a given function
#
# Walks all IL instructions at different layers, counting unique operations
from binaryninja.lowlevelil import LowLevelILInstruction
from binaryninja.mediumlevelil import MediumLevelILInstruction
from binaryninja.highlevelil import HighLevelILInstruction
def visit(illest, expr, operations):
for field in operations[expr.operation]:
if field[1] == "expr":
visit(illest, getattr(expr, field[0]), operations)
illest.add(expr.operation)
llillest = set(())
mlillest = set(())
hlillest = set(())
if current_llil:
fnname = current_function.name
for ins in current_llil.instructions:
visit(llillest, ins, LowLevelILInstruction.ILOperations)
if current_mlil:
for ins in current_mlil.instructions:
visit(mlillest, ins, MediumLevelILInstruction.ILOperations)
if current_hlil:
for ins in current_hlil.instructions:
visit(hlillest, ins, HighLevelILInstruction.ILOperations)
log_info("%s LLIL (%d): " % (fnname, len(llillest)))
log_info(str(llillest))
log_info("%s MLIL (%d): " % (fnname, len(mlillest)))
log_info(str(mlillest))
log_info("%s HLIL (%d): " % (fnname, len(hlillest)))
log_info(str(hlillest))
# creates executable sections
#
# Useful snippet for binaries without sections but executable (and writable)
# segments. Also demonstrates triggering an analysis module after load.
#
# Note that the last line is unnecessary in the python console since the UI
# triggers it after each command entered anyway.
counter=0
for seg in bv.segments:
if seg.executable and seg.writable:
bv.add_user_section("section_%d"%counter, seg.start, seg.end-seg.start, SectionSemantics.ReadOnlyCodeSectionSemantics)
bv.add_analysis_option("linearsweep")
counter += 1
bv.update_analysis()
# create a symbol
#
# Used to create a symbol when you don't want to define a type at a particular location
sym_name = get_text_line_input("Symbol name:", "Symbol Name")
if sym_name not in [None, b'']:
bv.define_user_symbol(Symbol(SymbolType.DataSymbol, here, sym_name))
# demangle gnu pe
#
# Will attempt to demangle GNU mangled C++ names in a PE file since that is not automatically done
for func_sym in bv.get_symbols_of_type(SymbolType.FunctionSymbol):
log_debug(f"Attempting to demangle {func_sym}")
symtype, symname = demangle_gnu3(Architecture['x86'], func_sym.name)
if symname != func_sym.name:
log_info(f"Successfully demangled {func_sym.name}")
if type(symname) == str:
full_name = symname
else:
full_name = '::'.join(map(str, symname))
new_sym = binaryninja.types.Symbol(SymbolType.FunctionSymbol, func_sym.address, short_name=symname[-1], full_name=full_name, raw_name=func_sym.name)
bv.define_user_symbol(new_sym)
# generate official settings documentation
#
# Snippet that generates https://docs.binary.ninja/getting-started.html#all-settings
import json
from PySide6.QtGui import QGuiApplication
settings = json.loads(binaryninja.Settings().serialize_schema())
table = """|Category|Setting|Description|Type|Default|Scope|Key|
|---|---|---|---|---|---|---|
"""
excludeEnum = [ "analysis.unicode.blocks", "python.interpreter", "ui.theme"]
allscope = set(["SettingsProjectScope", "SettingsUserScope", "SettingsResourceScope"])
for category in sorted(settings):
for setting in sorted(settings[category]['settings']):
title = settings[category]['settings'][setting]['title']
description = settings[category]['settings'][setting]['description']
typ = settings[category]['settings'][setting]['type']
key = settings[category]['settings'][setting]['key']
default = settings[category]['settings'][setting]['default']
if isinstance(default, list):
default = "[" + ', '.join(["`%s`" % x for x in sorted(default)]) + "]"
else:
default = f"`{str(default)}`"
if default == "``":
default = " ";
print(settings[category]['settings'][setting])
if 'ignore' in settings[category]['settings'][setting].keys():
scope = allscope - set(settings[category]['settings'][setting]['ignore'])
else:
scope = allscope
scope = "[" + ', '.join(["`%s`" % x for x in sorted(scope)]) + "]"
table += f"|{category}|{title}|{description}|`{typ}`|{default}|{scope}|<a id='{key}'>{key}</a>|\n"
if settings[category]['settings'][setting].get("enum") and key not in excludeEnum:
for idx, enum in enumerate(settings[category]['settings'][setting]["enum"]):
if settings[category]['settings'][setting].get("enumDescriptions"):
description = " enum: " + settings[category]['settings'][setting]["enumDescriptions"][idx]
else:
description = " "
table += f"| | |{description}|`enum`|`{enum}`| | |\n"
show_markdown_report("Settings Documentation", "Below table added to the clipboard:\n\n"+table)
log_info("Saving result to the clipboard.")
clip = QGuiApplication.clipboard()
clip.setText(table)
# export to text
#
# Can export IL and assembly forms for the current function or the entire binary
import os
import io
def valid_filename(s):
s = s.strip().replace(' ', '_')
return re.sub(r'(?u)[^-\w.]', '', s)
def filtername(name):
return "".join(x for x in name if x.isalnum() or x in ["_", "-"])
def fnsource(fn, form):
if form == "HLIL":
return ''.join(["\t" + x + "\n" for x in map(str, fn.hlil.root.lines)])
if form == "MLIL":
return ''.join(["\t" + x + "\n" for x in map(str, fn.mlil.instructions)])
if form == "LLIL":
return ''.join(["\t" + x + "\n" for x in map(str, fn.llil.instructions)])
if form == "Assembly":
return ''.join(["\t" + "".join(map(str, x[0])) + "\n" for x in fn.instructions])
if form == "Assembly with offset":
return ''.join([f'\t{x[1]:#04x}: {"".join(map(str, x[0]))}\n' for x in fn.instructions])
def overwrite(fname):
if show_message_box("File exists", "File exists, delete and overwrite?", buttons=MessageBoxButtonSet.YesNoButtonSet) == MessageBoxButtonResult.YesButton:
os.unlink(fname)
return True
else:
return False
def main():
#Maybe eventually add "Whole Binary" to include data from linear view
all_or_func = ChoiceField("Scope?", ["All functions", "Current function"])
asm_or_il = ChoiceField("Which form?", ["Assembly with offset", "Assembly", "LLIL", "MLIL", "HLIL"])
folder = DirectoryNameField("Folder to save result", default_name=os.path.dirname(bv.file.filename))
choices = get_form_input(["Which would you like to export?\n\nNote that \"whole binary\" will only dump IL contained in functions when IL is selected", all_or_func, asm_or_il, folder], "Export to text")
if choices:
current_only = all_or_func.result == 1
form = asm_or_il.choices[asm_or_il.result]
fname = os.path.splitext(os.path.basename(bv.file.filename))[0]
if folder.result:
if current_only:
outputname = f"{os.path.join(folder.result, fname)}.{valid_filename(current_function.name)}.txt"
if os.path.isfile(outputname):
if not overwrite(outputname):
log_warn("Stopping export to text due to existing file.")
return
log_info(f"Dumping {current_function.name} to {outputname}")
with io.open(outputname, mode='w', encoding="utf-8") as f:
f.write(fnsource(current_function, form))
else:
outputname = f"{os.path.join(folder.result, fname)}.txt"
if os.path.isfile(outputname):
if not overwrite(outputname):
log_warn("Stopping export to text due to existing file.")
return
with io.open(outputname, mode='w', encoding="utf-8") as f:
for fn in bv.functions:
log_info(f"Writing {fn.name}")
f.write(f"\n{fn.name}: \n")
f.write(fnsource(fn, form))
log_info(f"Done dumping whole binary to {outputname}")
main()
# Find a variable definition from current selection
#
if uicontext.token.localVarValid:
log_info("Found a localvar")
varname = uicontext.token.token.text
log_info(str(dir(uicontext)))
log_info("-----\n")
instrIndex = 0
else:
log_warn("No variable selected")
# Basic sample flowgrpah
#
# Creates a flow graph, showing some basic functionality
graph = FlowGraph()
node_a = FlowGraphNode(graph)
node_a.lines = ["Node A"]
node_b = FlowGraphNode(graph)
node_b.lines = ["Node B"]
node_c = FlowGraphNode(graph)
node_c.lines = ["Node C"]
graph.append(node_a)
graph.append(node_b)
graph.append(node_c)
node_a.add_outgoing_edge(BranchType.UnconditionalBranch, node_b)
node_a.add_outgoing_edge(BranchType.UnconditionalBranch, node_c)
show_graph_report("In order", graph)
graph2 = FlowGraph()
node2_a = FlowGraphNode(graph)
node2_a.lines = ["Node A"]
node2_b = FlowGraphNode(graph)
node2_b.lines = ["Node B"]
node2_c = FlowGraphNode(graph)
node2_c.lines = ["Node C"]
graph2.append(node2_b)
graph2.append(node2_c)
graph2.append(node2_a)
node2_a.add_outgoing_edge(BranchType.UnconditionalBranch, node2_b)
node2_a.add_outgoing_edge(BranchType.UnconditionalBranch, node2_c)
show_graph_report("Out of order", graph)
# function comment
#
# The UI currently lacks a way to add a "function" based comment that will be repeated when the function is called
newcomment = get_text_line_input("Current value: " + current_function.comment, "Function plate comment")
if (newcomment):
current_function.comment = newcomment
# create common tags for function attributes like leaf, loop, stub, etc
#
# Snippet replaced with a plugin, will be removed in the future: https://github.com/psifertex/tagteam
large = 40
complex = 40
def init_tags():
for tagType in tags:
if tagType['name'] not in bv.tag_types.keys():
bv.create_tag_type(tagType['name'], tagType['emoji'])
def cc(fn):
nodes = len(fn.basic_blocks)
edges = sum(len(x.outgoing_edges) for x in fn.basic_blocks)
connected = 1 #always 1 for binary control flow graphs, kinda the whole point
return edges - nodes + 2 * connected
def iscomplex(fn):
return cc(fn) > complex
def isleaf(fn):
return len(fn.callees) == 0
def islarge(fn):
return len(fn.basic_blocks) >= large
def isstub(fn):
"""Returns true if a function is likely only a stub"""
if len(fn.basic_blocks) > 1 or len(fn.llil.basic_blocks) > 1:
return False
if fn.llil.basic_blocks[0].has_undetermined_outgoing_edges or len(fn.callees) == 1:
return True
return False
def hasloop(fn):
"""Returns true if a function has a 'strange loop' (ignore this, inside joke)"""
for bb in fn.basic_blocks:
if bb in bb.dominance_frontier:
return True
return False
tags = [ \
{'emoji': '🍃', 'name': 'Leaf Function', 'description': 'Leaf function (does not call anything else)', 'fn': isleaf},
{'emoji': '🔄', 'name': 'Loop Function', 'description': 'Function contains a loop', 'fn': hasloop},
{'emoji': '🥾', 'name': 'Stub Function', 'description': 'Function is likely a stub (only contains one basic block and one call or indirect jump)', 'fn': isstub},
{'emoji': '🐘', 'name': 'Large Function', 'description': 'Function is "large" (IE, it has more than the blocks defined above)', 'fn': islarge},
{'emoji': '🤯', 'name': 'Complex Function', 'description': 'Function is "complex" (IE, it has a cyclomatic complexity greater than a defined constant)', 'fn': iscomplex},
]
init_tags()
for fn in bv.functions:
for tagType in tags:
if tagType['fn'](fn):
fn.create_user_function_tag(bv.tag_types[tagType['name']], '', unique=True)
# get action context
#
# Useful for quering what is selected from a snippet or plugin instead of using various register_for APIs
def get_current_context() -> Optional[Any]:
ctx = UIContext.activeContext()
if not ctx:
ctx = UIContext.allContexts()[0]
if not ctx:
binaryninja.log_warn(f'No UI Context available')
return None
handler = ctx.contentActionHandler()
if not handler:
binaryninja.log_warn(f'No Action Context available')
return None
return handler.actionContext()
# Highlight with theme-aware colors any "dangerous" functions:
# Note that prefixed versions like "_system" will not be highlighted using this sample code.
dangerous = ["strcpy", "gets"]
sus = ["printf", "system", "exec"]
for fnname in dangerous + sus:
if fnname in dangerous:
color = HighlightStandardColor.RedHighlightColor
if fnname in sus:
color = HighlightStandardColor.OrangeHighlightColor
for sym in bv.get_symbols_by_name(fnname):
for ref in bv.get_code_refs(sym.address):
log_info(f"Highlighting dangerous call at {hex(ref.address)}")
ref.function.set_user_instr_highlight(ref.address, color)
# history dump
#
# Dump the entire history of the scripting console of this instance of Binary Ninja to the log
import binaryninjaui #needed to load PySide
from binaryninja import log_info
from PySide6.QtCore import QSettings
settings = QSettings()
history = settings.value("script/history")
for line in history:
log_info(line)
# debug HLIL decompilation
#
# trigger a debug report to show all intermediate stages of analysis creating HLIL
current_function.request_debug_report("high_level_il")
# interesting units
#
# Log interesting components like the function and basic blocks with most incoming or outgoing edges
log_info("Most connected function: " + repr(max(bv.functions, key=lambda x: len(x.callees) + len(x.callers))))
log_info("Most incoming callers: " + repr(max(bv.functions, key=lambda x: len(x.callers))))
log_info("Most connected bblock: " + repr(max(bv.basic_blocks, key=lambda x: len(x.incoming_edges) + len(x.outgoing_edges))))
log_info("Highest xrefs: " + repr(max(bv.functions, key=lambda x: len(x.callers))))
# list plugins
#
# Produce an HTML report showing how to use a sortable HTML table to make a slightly more useful UI. Appologies for the awful intermixing of code and HTMl, think it's actually more readable this way.
from binaryninjaui import getThemeColor, ThemeColor
r = RepositoryManager()
repos = ["community", "official"]
html = '''<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/jq-3.3.1/dt-1.10.22/datatables.min.css"/>
<script type="text/javascript" src="https://cdn.datatables.net/v/bs4/jq-3.3.1/dt-1.10.22/datatables.min.js"></script>
<script>'''
for repo in repos:
html += f'''$(document).ready( function () {{
$('#{repo}').DataTable({{
"paging": false,
"info": false,
"searching": false,
"order": [[2, "desc"], [0, "asc"]]
}});
}} );
'''
html += f''' </script>
<style>
tr[data-state="Update Available"]{{
color: {getThemeColor(ThemeColor.CyanStandardHighlightColor).name()};
}}
tr[data-state="Disabled"]{{
color: {getThemeColor(ThemeColor.FalseBranchColor).name()};
}}
tr[data-state="Enabled"]{{
color: {getThemeColor(ThemeColor.TrueBranchColor).name()};
}}
</style>
</head>
<body>
<div style="margin: 50px">
'''
for repo in ["community", "official"]:
html += f'''<h3>{repo.capitalize()} Plugins</h3>
<table id="{repo}" class="sortable" cellspacing="0" width="100%">
<thead>
<tr>
<th>Plugin Name</th>
<th>Version</th>
<th>Status</th>
<th>Short Description</th>
</tr>
</thead>
<tbody>
'''
for plugin in r.plugins[repo]:
if plugin.update_available:
status = "Update Available"
elif plugin.installed and not plugin.enabled:
status = "Disabled"
elif plugin.installed:
status = "Enabled"
else:
continue
html += f'<tr data-state="{status}"><td>{plugin.name}</td><td>{plugin.version}</td><td>{status}</td><td>{plugin.description}</td></tr>'
html += f'''</tbody>
</table>
<hr />
'''
html += '''
</div>
</body>
</html>
'''
show_html_report('Plugin List', html)
# load borland map
#
# loads function and data variable symbols from Borland MAP files (https://community.embarcadero.com/article/technical-articles/149-tools/14524-understanding-linker-generated-32bit-map-files)
import re
import os
#Load symbols from a MAP file
mapfile = get_open_filename_input("filename:", "All Files (*)")
if mapfile is not None and os.access(mapfile, os.R_OK):
with open(mapfile, "r+", encoding="utf-8") as f:
data = f.readlines()
else:
log_error("Unable to parse specified map file.")
data = []
mylog = log_debug
#uncomment to enable debugging even if BN debugging is off
#mylog = log_info
segments = {}
symcount = 0
for line in data:
line = line.strip()
if line.startswith("0"):
index = line.split(":")[0]
if index in segments.keys(): #this is a record for a segment we know about
offset = int(line.split(" ")[0].split(":")[1], 16)
addr = offset + segments[index][0]
symbol = line.split(" ")[-1]
symtype=SymbolType.DataSymbol
if symbol.endswith("()"):
symbol = symbol[0:-2]
if symbol.startswith("<-"):
symbol = symbol[2:]
symtype=SymbolType.FunctionSymbol
contain = bv.get_functions_containing(addr)
makenew = True
for fn in contain:
if fn.start == addr: #there should not be other functions around this
makenew = False
else:
mylog(f'Removed bogus prior function at {hex(fn.start)}')
bv.remove_user_function(fn)
if makenew:
mylog(f'Created function at {hex(addr)}')
bv.create_user_function(addr)
if symbol.startswith("->:"): #call to a function, name destination
symbol = symbol[3:]
symtype=SymbolType.FunctionSymbol
dest = bv.get_callees(addr)
if len(dest) == 0: #current function hasn't been analyzed yet, extract destination from disasssembly and create destination function and symbol
destaddr = int(bv.get_disassembly(addr).split(' ')[-1], 16)
bv.create_user_function(destaddr)
bv.define_user_symbol(Symbol(symtype, destaddr, symbol))
mylog(f'Created function at {hex(destaddr)}')
continue
else:
destfn = bv.get_function_at(dest[0])
destfn.name = symbol
mylog(f'Renamed function {symbol} as destination of call at {hex(addr)}')
if symbol.startswith("->"):
symbol = symbol[2:]
continue #just a pointer to an import, skip
if symbol.startswith("*"):
symbol = symbol[1:]
bv.define_user_symbol(Symbol(symtype, addr, symbol))
mylog(f'Creating symbol {symbol} at {hex(addr)}')
symcount += 1
else: #new memory segment
records = re.split('\s+', line[5:])
base = int(records[0], 16)
size = int(records[1][:-1], 16)
try:
name = records[2]
except IndexError:
name = 'name'
try:
cls = records[3]
except IndexError:
cls = 'class'
if name.endswith("END") or name.endswith("END_"):
continue
segments[index] = [ base, size, name, cls ]
log_info(f'Updated {symcount} total symbols')
# load ida map
#
# Bare-bones script with no error checking to load a .map file created by IDA
import re
pattern = re.compile(r"\d+:([0-9A-Fa-f]+)\s+(.+)$")
mapfile = get_open_filename_input("map file:", "All Files (*)")
if mapfile is not None and os.access(mapfile, os.R_OK):
with open(mapfile, "r+", encoding="utf-8") as f:
for line in f.readlines():
matching = pattern.search(line)
if not matching:
continue
addr = int(matching.group(1), 16)
fnname = matching.group(2)
if bv.get_function_at(addr):
bv.define_user_symbol(Symbol(SymbolType.FunctionSymbol, addr, fnname))
else:
bv.define_user_symbol(Symbol(SymbolType.DataSymbol, addr, fnname))
# make const
#
# Make the selected variable const
def make_const(token, fn):
if token.localVarValid:
var = Variable.from_core_variable(fn, token.localVar)
varType = var.type.mutable_copy()
if isinstance(varType, PointerBuilder):
print("got here")
varType.target.const = True
else:
varType.const = True
var.type = varType
print(f"Made {repr(var)} have type {repr(var.type)}")
elif current_token.type == InstructionTextTokenType.CodeSymbolToken:
#TODO
pass
else:
log_warn("Unhandled token")
if not current_token or not current_function:
log_warn("No valid variable token selected")
else:
make_const(current_token, current_function)
# make functions in selection
#
# Will create multiple functions in a background thread across the selected region using the default architecture
class FunctionTask(BackgroundTaskThread):
def __init__(self, bv, selection):
BackgroundTaskThread.__init__(self, "Finding functions...", False)
self.bv = bv
self.selection = selection
def run(self):
self.bv.begin_undo_actions()
for addr in range(self.selection[0], self.selection[1]):
if len(self.bv.get_functions_containing(addr)) == 0:
self.bv.create_user_function(addr)
self.bv.update_analysis_and_wait()
self.bv.commit_undo_actions()
FunctionTask(bv, current_selection).start()
# navigate to a file offset
#
# Takes a file offset, converts it to a virtual address and navigates there. Note that the relative checkbox wont work but we use this dialog to get calculation feature
while True:
offset=get_address_input("File offset: ", "Offset")
if not offset:
break
if offset_to_vaddr(offset):
vaddr = bv.get_address_for_data_offset(offset)
log_info("Navigating to file offset %x" % vaddr)
bv.navigate(bv.view, vaddr)
break
# new file from selection
#
# Opens the current selections contents into a new file / new tab
# To modify it for a new tab
import tempfile
from binaryninjaui import UIContext
def get_selected_data():
#remove when snippets implements this first-class
return bv.read(current_selection[0], current_selection[1]-current_selection[0])
def openFile(filename):
ctx = UIContext.activeContext()
ctx.openFilename(filename)
temp = tempfile.NamedTemporaryFile(delete=False)
buf = get_selected_data()
log_info(f"Writing {len(buf)} bytes to {temp.name}")
temp.write(get_selected_data())
temp.close()
execute_on_main_thread_and_wait(lambda: openFile(temp.name))
# print current function to pseudo c
#
# Adapt as necessary to save to file for example, though File / Export will also work
def c_source(bv, func):
lines = ''
settings = DisassemblySettings()
settings.set_option(DisassemblyOption.ShowAddress, False)
settings.set_option(DisassemblyOption.WaitForIL, True)
obj = lineardisassembly.LinearViewObject.language_representation(bv, settings)
cursor_end = lineardisassembly.LinearViewCursor(obj)
cursor_end.seek_to_address(func.highest_address)
body = bv.get_next_linear_disassembly_lines(cursor_end)
cursor_end.seek_to_address(func.highest_address)
header= bv.get_previous_linear_disassembly_lines(cursor_end)
for line in header:
lines += str(line) + '\n'
for line in body:
lines += str(line) + '\n'
return lines
print(c_source(bv, current_function))
# PDB PTR Downloader
#
# Can be adapted to work together with the built in PDB support to fetch UNC paths from debug server file.ptr files. Also a good example of how to access GlobalArea widgets.
import binaryninjaui
import os
USER="myusername"
PASS="mypassword" #change to an interactive popup for more security but be aware os.system may leave history
DOMAIN="mydomain"
globalArea=uicontext.context.globalArea()
logWidget=globalArea.widget("Log")
for child in logWidget.children():
if isinstance(child, binaryninjaui.LogListModel):
if child.hasSelectedItems():
items = child.getSelectedItems()
if items:
lastLine = items[0].text
lineSplit = lastLine.split(" -> ")
if len(lineSplit) == 2:
unc = lineSplit[0]
pdbPath = lineSplit[1]
unc = unc.replace("\\","\/")
os.system(f"smbget -U {USER}%{PASS} -w {DOMAIN} -o {pdbPath} {unc}")
else:
log_warn("Invalid PTR log message.")
else:
log_info("No selected PTR log message.")
else:
log_info("No selected PTR log message.")
# random jump
#
# jump to a random function in the current binaryview
import random
randomIndex = random.randint(0, len(bv.functions)-1)
destination = bv.functions[randomIndex].start
log_info("Jumping to: 0x%x" % destination)
here = destination
# read dword
#
# Simple example showing how to read a dword at the current location
dword = int.from_bytes(bv.read(here, 4), "big" if bv.endianness == Endianness.BigEndian else "little")
clip = PySide6.QtGui.QGuiApplication.clipboard()
clip.setText('%x' % (dword))
# run python file
#
# Run a file from disk with the current python context
filename = get_open_filename_input("Python File:")
if filename:
exec(open(filename).read())
else:
show_message_box("Invalid python file", "Invalid python file selected.", MessageBoxButtonSet.OKButtonSet, MessageBoxIcon.ErrorIcon)
# save HLIL
#
# DEPRECATED IN FAVOR OF "export to text" snippet
def filtername(name):
return "".join(x for x in name if x.isalnum() or x in ["_", "-"])
choice = get_choice_input("Save all functions or just current?", "choices", ["All", "Current"])
if (choice == 0):
import os
fname = os.path.splitext(os.path.basename(bv.file.filename))[0]
folder = get_directory_name_input("Where to save decompilation:").decode('utf-8')
for fn in bv.functions:
source = '\n'.join(map(str, fn.hlil.root.lines))
output = os.path.join(folder, filtername(fn.name) + ".txt")
try:
with open(output, 'w') as f:
f.write(source)
log_info(f"Dumped {fn.name} to {output}")
except:
log_error(f"Unable to save {output}")
if (choice == 1):
source = '\n'.join(map(str, current_function.hlil.root.lines))
while True:
output = get_save_filename_input("Source filename:", "txt", "%s.txt" % filtername(current_function.name))
if output == None:
msg = "No file specified."
interaction.show_message_box(msg, msg)
break
try:
with open(output, "w") as f:
f.write(source)
except:
msg = "Save failed. Try again?"
if not interaction.show_message_box(msg, msg, buttons=MessageBoxButtonSet.YesNoButtonSet):
break
# save search
#
# Save search results to a specified file
targets = [FunctionGraphType.NormalFunctionGraph, FunctionGraphType.LowLevelILFunctionGraph, FunctionGraphType.MediumLevelILFunctionGraph, FunctionGraphType.HighLevelILFunctionGraph, FunctionGraphType.HighLevelLanguageRepresentationFunctionGraph]
search_text = TextLineField("Search text")
search_target = ChoiceField("Search target?", ["Assembly", "LLIL", "MLIL", "HLIL", "Pseudo C"])
search_addresses = ChoiceField("Include addresses in output?", ["No", "Yes"])
output_file = SaveFileNameField("Output filename: ", default_name=bv.file.filename + ".txt")
choices = get_form_input(["Saved Search Plugin", search_text, search_target, output_file, search_addresses], "Saved Search")
if choices and search_text.result and search_target.result and output_file.result:
with open(output_file.result, 'wb') as f:
target = targets[search_target.result]
for result in bv.find_all_text(bv.start, bv.end, search_text.result, graph_type=target):
if search_addresses.result and search_addresses.result == 1:
addr = bytes(hex(result[0]) + "\t", 'utf8')
else:
addr = b""
f.write(addr + bytes(str(result[2]), 'utf8') + b"\n")
log_info(f"Search results saved to {output_file.result}")
else:
log_warn("Search not saved, dialog cancelled or missing selection.")
# get the selected variable
#
# Uses the UIContext to get the variable the user has currently selected, copied from https://twitter.com/josh_watson/status/1352319354663178240
ctx = UIContext.activeContext()
h = ctx.contentActionHandler()
a = h.actionContext()
token_state = a.token
selectedVar = Variable.from_identifier(current_function, token_state.token.value)
# show exports
#
# Open a new tab with a list of all exports, can be slow for extremely large files, modify to dump an external html for a perf improvement
import time
def genRow(sym, exportType):
return f"<tr><td><a href=\"binaryninja:?expr={hex(sym.address)}\">{hex(sym.address)}</a></td><td>{sym.short_name}</td><td>{exportType}</td></tr>\n"
begin = time.time()
# [ Offset, Symbol, Type]
html = "<h1>Exports</h1>\n\n"
html += "<table><tr><th>Offset</th><th>Name</th><th>Type</th></tr>\n"
html += f"<tr><td>{hex(bv.entry_point)}</td><td>{bv.entry_function.name}</td><td>Entry</td></tr>\n"
for sym in bv.get_symbols_of_type(SymbolType.FunctionSymbol):
if sym.binding == SymbolBinding.GlobalBinding:
html += genRow(sym, "Export Function")
for sym in bv.get_symbols_of_type(SymbolType.DataSymbol):
if sym.binding == SymbolBinding.GlobalBinding:
html += genRow(sym, "Export Data Var")
html += "</table>"
lines = len(html.split("\n"))
generated=time.time()
log_info(f"Lines: {lines}")
log_info(f"Time: {generated - begin}")
bv.show_html_report("Exports", html)
render = time.time()
log_info(f"Render: {render - generated }")
# show file modifications
#
# Use the get_modification API to show all places where bytes were modified in the file
for offset in range(bv.start, len(bv) + bv.start):
mod = bv.get_modification(offset,1)[0]
if mod != ModificationStatus.Original:
b = "0x" + bv.read(offset, 1).hex()
if mod == ModificationStatus.Inserted:
print(f"Looks like {b} was inserted at {hex(offset)}")
else:
print(f"Looks like {b} was written at {hex(offset)}")
# Simple PySide UI elements/testing
#
import binaryninjaui
from PySide6 import QtWidgets, QtGui, QtWidgets, QtCore
def basic():
popout = QtWidgets.QDialog()
popout.setWindowTitle("test popout1")
popout.exec_()
def qpixmap():
pixmap = QtGui.QPixmap("play-and-pause-button.png")
mask = pixmap.createMaskFromColor(QtGui.QColor('black'), QtGui.Qt.MaskOutColor)
pixmap.fill((QtGui.QColor('red')))
pixmap.setMask(mask)
window = QtWidgets.QDialog()
window.setWindowTitle("View Image")
label = QtWidgets.QLabel(window)
label.setPixmap(pixmap)
label.setGeometry(QtCore.QRect(10, 40, pixmap.width(), pixmap.height()))
window.resize(pixmap.width()+20, pixmap.height()+100)
window.exec_()
def qpixmap2():
icon = QtGui.QIcon("play-and-pause-button.svg")
button = QtWidgets.QPushButton(icon)
msg_box = QtWidgets.QMessageBox()
msg_box.setWindowTitle("Testing Icons")
msg_box.exec_()
def colorize(img, color):
pixmap = QtGui.QPixmap(img)
mask = pixmap.createMaskFromColor(QtGui.QColor('black'), QtGui.Qt.MaskOutColor)
pixmap.fill(color)
pixmap.setMask(mask)
return pixmap
def qicon():
ico = QtGui.QIcon("play-and-pause-button.svg")
ico2 = QtGui.QIcon(colorize(ico.pixmap(ico.actualSize(QtCore.QSize(1024, 1024))), QtGui.QColor('red')))
msg_box = QtWidgets.QMessageBox()
msg_box.setWindowTitle("Show Icon")
button = QtWidgets.QPushButton(msg_box)
button.setIcon(ico2)
button.setText("PlayPause")
msg_box.exec_()
# Since Snippets now run on background threads, UI code needs to explicitly ensure that it is run on the main thread.
#execute_on_main_thread(qpixmap2)
#execute_on_main_thread(qpixmap)
#execute_on_main_thread(basic)
#execute_on_main_thread(qicon)
from binaryninjaui import (UIAction, UIActionHandler)
from binaryninja import log_info
from PySide6.QtWidgets import QDialog, QLabel, QVBoxLayout
def qd(context):
dialog = QDialog(parent=context.widget)
dialog.setWindowTitle("Hello Dialog")
label = QLabel("Hello text")
layout = QVBoxLayout()
layout.addWidget(label)
dialog.setLayout(layout)
dialog.show()
execute_on_main_thread_and_wait(lambda: qd(uicontext))
# Note: outside of snippets you would use:
# UIAction.registerAction("Simple")
# UIActionHandler.globalActions().bindAction("Simple", UIAction(qd))
# but snippets can provide the same UI context necessary to parent a QDialog
# annotate inline strings that are assembled via one byte moves/pushes
#
annotation=""
for instruction in current_basic_block.get_disassembly_text():
if instruction.address >= current_selection[0] and instruction.address < current_selection[1]:
address = instruction.address
value = instruction.tokens[-1].value
operand = instruction.tokens[-1].operand
type = IntegerDisplayType.CharacterConstantDisplayType
current_function.set_int_display_type(address, value, operand, type)
while (value > 0):
annotation += chr(value % 256)
value = value >> 8
log_info("Adding comment for string: %s" % annotation)
current_function.set_comment_at(current_selection[0], annotation)
# apply function types to structure parameters
#
# Lets you select a struct to automatically search type libraries for function signatures to apply type information
# Thanks @d0mnik! Original code: https://gist.github.com/d0mnik/1ee595605fc7c9a39364fdbfb660f268
from binaryninja import *
api_call_protos = {}
type_to_parse = get_text_line_input("Enter structure name to parse:", "Struct Parser")
if type_to_parse is None:
exit()
struct_name = type_to_parse.decode()
if bv.get_type_by_name(struct_name) is None:
show_message_box(f"Struct Parser", "Struct with specified name not found!")
exit()
for typelib in bv.type_libraries:
for name, obj in typelib.named_objects.items():
if not isinstance(obj, FunctionType): # filter for function calls
continue
api_call_protos[name.name[0]] = obj
mut: StructureBuilder = bv.get_type_by_name(struct_name).mutable_copy()
for member_idx, member in enumerate(mut.members):
for name, proto in api_call_protos.items():
if name.lower() not in member.name.lower():
continue
# replace the prototype
proto_pointer = PointerType.create(bv.arch, proto)
mut.replace(member_idx, proto_pointer, member.name)
break
bv.define_user_type(struct_name, mut)
# shell out to swift-demangle to handle swift symbol names
#
import subprocess
result = subprocess.run(['/usr/bin/xcrun', '--find', 'swift-demangle'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
if result.returncode == 0:
demangle_str = result.stdout.decode('utf-8')
for f in bv.functions:
result = subprocess.run([demangle_str, '-simplified', '-compact', symbol], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
if result.returncode == 0:
f.name = demangle(f.name)
else:
log_error('Unable to find swift-demangle.')
# toggle arm thumb
#
# ARM/Thumb have an odd quirk where the same architecture module handles both and
# uses the lowest bit of the address to determine the correct architecture, so
# the normal API usage of specifing the optional platform will not work. Here's a
# quick snippet to show how to toggle the architecture of the current function by
# removing/creating it
func = current_function
if not func and (current_address & 1) == 0:
address = current_address + 1
funcs = bv.get_functions_at(address)
func = funcs[0] if funcs else None
if not func:
log_error(f'Cannot find a function at current_address {current_address:#x}')
else:
address = func.start
if func.arch == Architecture['armv7']:
new_platform = Platform['thumb2']
address |= 1
elif func.arch == Architecture['thumb2']:
new_platform = Platform['armv7']
address &= ~3
else:
raise AttributeError("This snippet only works on thumb or armv7 functions")
bv.remove_user_function(func)
bv.create_user_function(address, new_platform)
platform_name = str(new_platform)
article = 'an' if platform_name[0].lower() in 'aeiou' else 'a'
log_info(f"Creating {article} {str(new_platform)} function at {(address - (address % 2)):#x}")
# trigger actions
#
# Trigger actions in the UI Action system via a plugin or snippet
# Use the command-palette (CMD/CTL-P) to find action descriptions
# Not compatible with headless of course
from binaryninjaui import UIContext
action = "About..."
execute_on_main_thread_and_wait(lambda: UIContext.activeContext().getCurrentActionHandler().executeAction(action))
# uidf example
#
# This snippet graciously provided by https://twitter.com/xmppwocky as an
# example of a workflow leveraging the User-Informed-Data-Flow system
# (https://binary.ninja/2020/09/10/user-informed-dataflow.html).
#
# Used to handle C++ classes with virtual methods but no subclasses
vt_ty_field = TextLineField("vtable type")
vt_addr_field = AddressField("vtable address")
assert get_form_input([vt_ty_field, vt_addr_field], "hello")
vt_type = bv.get_type_by_name(vt_ty_field.result)
vt_addr = vt_addr_field.result
assert vt_type is not None
def proc_fn(fn):
candidates = []
for var in fn.vars:
if var.type.target == vt_type:
candidates.append((fn, var))
return candidates
candidates = []
for fn in bv.functions:
candidates += proc_fn(fn)
if get_choice_input(
"Set value of {} variables?".format(len(candidates)),
"Confirm", ["Yes", "No"]) == 1:
raise Exception("cancelled")
for fn, var in candidates:
for defn in fn.mlil.get_var_definitions(var):
# could double-check that this comes from a vtable
# field here, if we wanted...
fn.set_user_var_value(var, defn.address,
PossibleValueSet.constant_ptr(vt_addr))
# unimplemented instructions
#
# Create a report showing unimplemented instructions
from collections import defaultdict
insts = defaultdict(lambda:0)
addrs = {}
for ins in bv.llil_instructions:
if ins.operation == LowLevelILOperation.LLIL_UNIMPL:
mnem = bv.get_disassembly(ins.address, ins.il_basic_block.arch).split()[0]
insts[mnem + '-' + ins.il_basic_block.arch.name] = insts[mnem + '-' + ins.il_basic_block.arch.name] + 1
addrs[mnem + '-' + ins.il_basic_block.arch.name] = ins.address
lines = list(insts.items())
lines = sorted(lines, key=lambda x: x[1], reverse=True)
contents = "| Memonic-Arch | Count | Example Address |\n|---|---|---|\n"
for mnem, count in lines:
contents += f"|{mnem}|{count}|[{hex(addrs[mnem])}](binaryninja://?expr={hex(addrs[mnem])})|\n"
bv.show_markdown_report("Unimplemented counts", contents, contents)
# unlifted opcodes
#
# Script to summarize frequency of unlifted opcodes for a given binary
# build list of all unimplemented opcodes
opcodes={}
for f in bv.functions:
for t in f.tags: # tuple of (arch, address, tag)
data = t[2].data
if "unimplemented" in data:
opcode = data.split('"')[1]
if opcode in opcodes.keys():
opcodes[opcode].append(t[1])
else:
opcodes[opcode] = [t[1]]
# print them out, sorted by number of occurrences
keys = list(opcodes.keys())
for key in sorted(opcodes, key=lambda x: len(opcodes[x]), reverse=True):
if len(opcodes[key]) <= 5:
print(f"{len(opcodes[key])}: {key}: [{', '.join(hex(x) for x in opcodes[key])}]")
else:
print(f"{len(opcodes[key])}: {key}: [{', '.join(hex(x) for x in opcodes[key][0:5])}, ...]")
# update snippets
#
# Automatically download and update this collection of snippets to your local snippet folder
from zipfile import ZipFile
from tempfile import TemporaryFile
import os
#TODO: Merge remote with local description or hotkey changes (AKA: if filename matches, skip the first two lines, truncate, re-write the rest)
domain = b'https://gist.github.com'
path = b'/psifertex/6fbc7532f536775194edd26290892ef7' # Feel free to adapt to your own setup
subfolder = 'default' # Change to save the snippets to a different sub-folder
tab2space = False
width = 4
def download(url):
# Can also use 'CoreDownloadProvider' or 'PythonDownloadProvider' as keys here
provider = DownloadProvider[Settings().get_string('network.downloadProviderName')].create_instance()
code, data = provider.get_response(url)
if code == 0:
return data
else:
raise ConnectionError("Unsuccessful download of %s" % url)
def update_snippets():
if not interaction.show_message_box('Warning', "Use at your own risk. Do you want to automatically overwrite local snippets from gist?", buttons=MessageBoxButtonSet.YesNoButtonSet):
return
snippetPath = os.path.realpath(os.path.join(user_plugin_path(), '..', 'snippets', subfolder))
if not os.path.isdir(snippetPath):
os.makedirs(snippetPath)
url = domain + path
log_info("Downloading from: %s" % url)
source = download(url)
zipPath = [s for s in source.split(b'\"') if s.endswith(b'.zip')]
if len(zipPath) != 1:
log_error("Update failed.")
return
url = domain + zipPath[0]
log_info("Downloading from: %s" % url)
zip = download(url)
with TemporaryFile() as f:
f.write(zip)
with ZipFile(f, 'r') as zip:
for item in zip.infolist():
if item.filename[-1] == '/':
continue
basename = os.path.basename(item.filename)
with open(os.path.join(snippetPath, basename), 'wb') as f:
if tab2space:
f.write(zip.read(item).replace(b'\t', b' ' * width))
else:
f.write(zip.read(item))
log_info("Extracting %s" % item.filename)
update_snippets()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment