diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/__init__.py | 1 | ||||
| -rw-r--r-- | python/basicblock.py | 105 | ||||
| -rw-r--r-- | python/binaryview.py | 17 | ||||
| -rw-r--r-- | python/examples/args_render_layer.py | 213 | ||||
| -rw-r--r-- | python/examples/follow_reg_render_layer.py | 209 | ||||
| -rw-r--r-- | python/flowgraph.py | 97 | ||||
| -rw-r--r-- | python/function.py | 82 | ||||
| -rw-r--r-- | python/lineardisassembly.py | 105 | ||||
| -rw-r--r-- | python/renderlayer.py | 450 |
9 files changed, 1219 insertions, 60 deletions
diff --git a/python/__init__.py b/python/__init__.py index 1cdc541e..db4e8e69 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -81,6 +81,7 @@ from .undo import * from .fileaccessor import * from .languagerepresentation import * from .lineformatter import * +from .renderlayer import * # We import each of these by name to prevent conflicts between # log.py and the function 'log' which we don't import below from .log import ( diff --git a/python/basicblock.py b/python/basicblock.py index 6c03fe32..a8dc85a8 100644 --- a/python/basicblock.py +++ b/python/basicblock.py @@ -23,6 +23,7 @@ from dataclasses import dataclass from typing import Generator, Optional, List, Tuple # Binary Ninja components +import binaryninja from . import _binaryninjacore as core from .enums import BranchType, HighlightStandardColor from . import binaryview @@ -88,6 +89,33 @@ class BasicBlock: if core is not None: core.BNFreeBasicBlock(self.handle) + @classmethod + def _from_core_block(cls, block: core.BNBasicBlockHandle) -> Optional['BasicBlock']: + """From a BNBasicBlockHandle, get a BasicBlock or one of the IL subclasses (takes ref)""" + func_handle = core.BNGetBasicBlockFunction(block) + if not func_handle: + core.BNFreeBasicBlock(block) + return None + + view = binaryview.BinaryView(handle=core.BNGetFunctionData(func_handle)) + func = _function.Function(view, func_handle) + + if core.BNIsLowLevelILBasicBlock(block): + return binaryninja.lowlevelil.LowLevelILBasicBlock( + block, binaryninja.lowlevelil.LowLevelILFunction(func.arch, core.BNGetBasicBlockLowLevelILFunction(block), func), + view + ) + elif core.BNIsMediumLevelILBasicBlock(block): + mlil_func = binaryninja.mediumlevelil.MediumLevelILFunction( + func.arch, core.BNGetBasicBlockMediumLevelILFunction(block), func + ) + return binaryninja.mediumlevelil.MediumLevelILBasicBlock(block, mlil_func, view) + elif core.BNIsHighLevelILBasicBlock(block): + hlil_func = binaryninja.highlevelil.HighLevelILFunction(func.arch, core.BNGetBasicBlockHighLevelILFunction(block), func) + return binaryninja.highlevelil.HighLevelILBasicBlock(block, hlil_func, view) + else: + return BasicBlock(block, view) + def __repr__(self): arch = self.arch if arch: @@ -211,6 +239,78 @@ class BasicBlock: return self._func @property + def il_function(self) -> Optional['_function.ILFunctionType']: + """IL Function of which this block is a part, if the block is part of an IL Function.""" + func = self.function + if func is None: + return None + il_type = self.function_graph_type + if il_type == _function.FunctionGraphType.NormalFunctionGraph: + return None + elif il_type == _function.FunctionGraphType.LowLevelILFunctionGraph: + return func.low_level_il + elif il_type == _function.FunctionGraphType.LiftedILFunctionGraph: + return func.lifted_il + elif il_type == _function.FunctionGraphType.LowLevelILSSAFormFunctionGraph: + return func.low_level_il.ssa_form + elif il_type == _function.FunctionGraphType.MediumLevelILFunctionGraph: + return func.medium_level_il + elif il_type == _function.FunctionGraphType.MediumLevelILSSAFormFunctionGraph: + return func.medium_level_il.ssa_form + elif il_type == _function.FunctionGraphType.MappedMediumLevelILFunctionGraph: + return func.mapped_medium_level_il + elif il_type == _function.FunctionGraphType.MappedMediumLevelILSSAFormFunctionGraph: + return func.mapped_medium_level_il.ssa_form + elif il_type == _function.FunctionGraphType.HighLevelILFunctionGraph: + return func.high_level_il + elif il_type == _function.FunctionGraphType.HighLevelILSSAFormFunctionGraph: + return func.high_level_il.ssa_form + elif il_type == _function.FunctionGraphType.HighLevelLanguageRepresentationFunctionGraph: + return func.high_level_il + else: + return None + + @property + def il_function_if_available(self) -> Optional['_function.ILFunctionType']: + """IL Function of which this block is a part, if the block is part of an IL Function, and if the function has generated IL already.""" + func = self.function + if func is None: + return None + il_type = self.function_graph_type + if il_type == _function.FunctionGraphType.NormalFunctionGraph: + return None + elif il_type == _function.FunctionGraphType.LowLevelILFunctionGraph: + return func.llil_if_available + elif il_type == _function.FunctionGraphType.LiftedILFunctionGraph: + return func.lifted_il_if_available + elif il_type == _function.FunctionGraphType.LowLevelILSSAFormFunctionGraph: + if func.llil_if_available is None: + return None + return func.llil_if_available.ssa_form + elif il_type == _function.FunctionGraphType.MediumLevelILFunctionGraph: + return func.mlil_if_available + elif il_type == _function.FunctionGraphType.MediumLevelILSSAFormFunctionGraph: + if func.mlil_if_available is None: + return None + return func.mlil_if_available.ssa_form + elif il_type == _function.FunctionGraphType.MappedMediumLevelILFunctionGraph: + return func.mmlil_if_available + elif il_type == _function.FunctionGraphType.MappedMediumLevelILSSAFormFunctionGraph: + if func.mmlil_if_available is None: + return None + return func.mmlil_if_available.ssa_form + elif il_type == _function.FunctionGraphType.HighLevelILFunctionGraph: + return func.hlil_if_available + elif il_type == _function.FunctionGraphType.HighLevelILSSAFormFunctionGraph: + if func.hlil_if_available is None: + return None + return func.hlil_if_available.ssa_form + elif il_type == _function.FunctionGraphType.HighLevelLanguageRepresentationFunctionGraph: + return func.hlil_if_available + else: + return None + + @property def view(self) -> Optional['binaryview.BinaryView']: """BinaryView that contains the basic block (read-only)""" if self._view is not None: @@ -426,6 +526,11 @@ class BasicBlock: self.set_user_highlight(value) @property + def function_graph_type(self) -> '_function.FunctionGraphType': + """Type of function graph from which this block represents instructions""" + return _function.FunctionGraphType(core.BNGetBasicBlockFunctionGraphType(self.handle)) + + @property def is_il(self) -> bool: """Whether the basic block contains IL""" return core.BNIsILBasicBlock(self.handle) diff --git a/python/binaryview.py b/python/binaryview.py index 73ddfd39..c1a57765 100644 --- a/python/binaryview.py +++ b/python/binaryview.py @@ -9011,20 +9011,9 @@ to a the type "tagRECT" found in the typelibrary "winX64common" def _LinearDisassemblyLine_convertor( self, lines: core.BNLinearDisassemblyLineHandle ) -> 'lineardisassembly.LinearDisassemblyLine': - func = None - block = None - line = lines[0] - if line.function: - func = _function.Function(self, core.BNNewFunctionReference(line.function)) - if line.block: - block_handle = core.BNNewBasicBlockReference(line.block) - assert block_handle is not None, "core.BNNewBasicBlockReference returned None" - block = basicblock.BasicBlock(block_handle, self) - color = highlight.HighlightColor._from_core_struct(line.contents.highlight) - addr = line.contents.addr - tokens = _function.InstructionTextToken._from_core_struct(line.contents.tokens, line.contents.count) - contents = _function.DisassemblyTextLine(tokens, addr, color=color) - return lineardisassembly.LinearDisassemblyLine(line.type, func, block, contents) + line = lineardisassembly.LinearDisassemblyLine._from_core_struct(lines[0]) + core.BNFreeLinearDisassemblyLines(lines, 1) + return line def find_all_text( self, start: int, end: int, text: str, settings: Optional[_function.DisassemblySettings] = None, diff --git a/python/examples/args_render_layer.py b/python/examples/args_render_layer.py new file mode 100644 index 00000000..bac0889e --- /dev/null +++ b/python/examples/args_render_layer.py @@ -0,0 +1,213 @@ +import functools +from typing import List, Mapping, Tuple, Iterator + +from binaryninja import DisassemblyTextLine, LowLevelILInstruction, LowLevelILOperation, \ + TypeClass, DisassemblyTextRenderer, MediumLevelILFunction, \ + MediumLevelILCallSsa, MediumLevelILVarSsa, MediumLevelILConstBase, \ + MediumLevelILInstruction, MediumLevelILTailcallSsa, MediumLevelILOperation, \ + MediumLevelILVarPhi, log_debug, RenderLayer, BasicBlock, InstructionTextTokenType, \ + RenderLayerDefaultEnableState + +""" +Render Layer that shows you where the arguments to calls are set, for Disasm/LLIL. +- Adds "Argument '<arg>' for call at <callsite>" comments to lines that set up call args +- Adds "Call at <callsite>" comments to the call sites + +========================================================================================= + +But how do you determine the argument to a call? +What seems like it has worked: +- You can't determine that an instruction is a parameter, you have to go from the call to its parameters +- Since trying to look up the call for an instruction is impossible, instead go through every call at once for a function (and memoize it) +- LLIL is useless for looking this up, since it has no types and the call parameters often include a list of every register +- Using MLIL, we can find all of the parameters as MLIL instructions, but we need to map them to LLIL so we can use them in the LLIL/Disasm display +- How do we map them? Turns out that's rather inconvenient: + - Register arguments are generally pretty easy because they are just MLIL vars + - Stack arguments somehow also work out generally, the .llil on the MLIL points to the push() + - Constants are a mess and I just use the MLIL's address (this is often incorrect) + - Flags are completely unhandled for now + - Phis are handled by just looking up every var they use... probably not proper but sort of works +- This fails in a couple of scenarios though, notably __builtin_xxxxxx() functions + - Which instruction specifies the length of a group of `mov qword [rbx+8], rax {0}` calls? I think it just picks one? + - The `rep` instructions could actually have these params resolved (they use real registers) but in practice this doesn't work + - Thunks are unhandled +""" + + +@functools.lru_cache(maxsize=64) +def get_param_sites(mlil: MediumLevelILFunction) -> Mapping[LowLevelILInstruction, List[Tuple[MediumLevelILInstruction, int]]]: + """ + For a given function, find all LLIL instructions that are parameters to a call, + and return a mapping for each instruction with all the calls that it maps to, + their corresponding MLIL call instruction, and which numbered parameter they are + in the call. + + :param mlil: MLIL function to search + :return: Map of param sites as described above + """ + call_sites = {} + mlil = mlil.ssa_form + + # As a function to handle call and tailcall identically + def collect_call_params(call_site, dest, params): + def_sites = [] + for i, param in enumerate(params): + llil = param.llil + if llil is not None: + def_sites.append((param, llil)) + continue + + match param: + case MediumLevelILVarSsa(src=var_src): + def_site = mlil.get_ssa_var_definition(var_src) + if def_site is not None and def_site.llil is not None: + def_sites.append((i, def_site.llil)) + continue + # Handle phis by just looking up the def sites of all their sources + match def_site: + case MediumLevelILVarPhi(src=phis): + for phi in phis: + phi_def = mlil.get_ssa_var_definition(phi) + if phi_def is not None and phi_def.llil is not None: + def_sites.append((i, phi_def.llil)) + case MediumLevelILConstBase(): + # This is wrong, but it works (sometimes) + # Oh god, have I just quoted php.net + def_site_idx = mlil.llil.get_instruction_start(param.address) + if def_site_idx is not None: + def_sites.append((i, mlil.llil[def_site_idx].ssa_form)) + continue + + if len(def_sites) == 0: + log_debug(f"Could not find def site for param {i} in call at {call_site.address:#x}") + + call_sites[call_site] = def_sites + + for instr in mlil.instructions: + match instr: + case MediumLevelILCallSsa(dest=dest, params=params) as call_site: + collect_call_params(call_site, dest, params) + case MediumLevelILTailcallSsa(dest=dest, params=params) as call_site: + collect_call_params(call_site, dest, params) + + # Inverse args + all_def_sites = {} + for call_site, params in call_sites.items(): + for (param_idx, llil) in params: + if llil not in all_def_sites: + all_def_sites[llil] = [] + else: + print(f"got two at {llil.instr_index} @ {llil.address:#x} -> {call_site.address:#x}") + all_def_sites[llil].append((call_site, param_idx)) + + return all_def_sites + + +def get_llil_arg(llil: LowLevelILInstruction) -> Iterator[Tuple[str, MediumLevelILInstruction]]: + args = get_param_sites(llil.function.mlil) + + if llil.ssa_form in args: + for call_site, param_idx in args[llil.ssa_form]: + target_type = call_site.function.get_expr_type(call_site.dest.expr_index) + + # Try getting the param name from the call's type + if target_type is not None: + if target_type.type_class == TypeClass.PointerTypeClass: + target_type = target_type.target + if target_type.type_class == TypeClass.FunctionTypeClass: + target_params = target_type.parameters + if param_idx < len(target_params): + param_name = target_params[param_idx].name + if param_name == '': + param_name = f"arg{param_idx+1}" + yield param_name, call_site + continue + + # Some calls have extra params that aren't reflected in their type + yield f"arg{param_idx+1}", call_site + return + + +def apply_to_lines(lines, get_instr, renderer): + # So we don't process lines twice since we're iterating over a list as we modify it + skip_lines = [] + + # Tailcalls that don't return incorrectly mark the { Does not return } line as a call + ignore_calls = set() + + for i, line in enumerate(lines): + if len(line.tokens) == 0: + continue + if i in skip_lines: + continue + + llil_instr = get_instr(line) + if llil_instr is not None: + new_lines = [] + for (arg, call) in get_llil_arg(llil_instr): + if call.operation == MediumLevelILOperation.MLIL_TAILCALL_SSA: + comment = f"Argument '{arg}' for tailcall at {call.address:#x}" + else: + comment = f"Argument '{arg}' for call at {call.address:#x}" + renderer.wrap_comment(new_lines, line, comment, False, " ", "") + for j, token in enumerate(line.tokens): + if token.type == InstructionTextTokenType.AddressSeparatorToken: + line.tokens = line.tokens[:j] + break + + # Annotate calls too so we can see them easily next to their args + if llil_instr.address == line.address and llil_instr.address not in ignore_calls: + if llil_instr.operation in [ + LowLevelILOperation.LLIL_CALL, + LowLevelILOperation.LLIL_CALL_SSA, + LowLevelILOperation.LLIL_TAILCALL, + LowLevelILOperation.LLIL_TAILCALL_SSA + ]: + ignore_calls.add(llil_instr.address) + if llil_instr.operation in [ + LowLevelILOperation.LLIL_TAILCALL, + LowLevelILOperation.LLIL_TAILCALL_SSA + ]: + comment = f"Tailcall at {llil_instr.address:#x}" + else: + comment = f"Call at {llil_instr.address:#x}" + # Creating comments is a bit unwieldy at the moment + renderer.wrap_comment(new_lines, line, comment, False, " ", "") + for j, token in enumerate(line.tokens): + if token.type == InstructionTextTokenType.AddressSeparatorToken: + line.tokens = line.tokens[:j] + break + + # If any of our lines changed, swap out the existing lines with the new ones + if len(new_lines) > 0: + lines.pop(i) + for j, new_line in enumerate(new_lines): + lines.insert(i + j, new_line) + skip_lines.append(i + j) + return lines + + +class ArgumentsRenderLayer(RenderLayer): + name = "Annotate Call Parameters" + default_enable_state = RenderLayerDefaultEnableState.EnabledByDefaultRenderLayerDefaultEnableState + + def apply_to_disassembly_block( + self, + block: BasicBlock, + lines: List['DisassemblyTextLine'] + ): + # Break this out into a helper so we don't have to write it twice + renderer = DisassemblyTextRenderer(block.function) + return apply_to_lines(lines, lambda line: block.function.get_llil_at(line.address), renderer) + + def apply_to_low_level_il_block( + self, + block: BasicBlock, + lines: List['DisassemblyTextLine'] + ): + # Break this out into a helper so we don't have to write it twice + renderer = DisassemblyTextRenderer(block.function) + return apply_to_lines(lines, lambda line: line.il_instruction, renderer) + + +ArgumentsRenderLayer.register() diff --git a/python/examples/follow_reg_render_layer.py b/python/examples/follow_reg_render_layer.py new file mode 100644 index 00000000..36e66576 --- /dev/null +++ b/python/examples/follow_reg_render_layer.py @@ -0,0 +1,209 @@ +from typing import List + +from PySide6.QtCore import QSettings +from binaryninja import DisassemblyTextLine, LowLevelILOperation, DisassemblyTextRenderer, \ + MediumLevelILOperation, \ + RenderLayer, BasicBlock, InstructionTextTokenType, RenderLayerDefaultEnableState, \ + PluginCommand, BinaryView, interaction, InstructionTextToken, RegisterValueType + + +""" +Render layer that adds annotations to follow the value of a register, for Disasm/LLIL. +- Adds "<reg> after: <value>" comments to every line that modifies your register +- Adds "<reg> at block entry: <value>" comments to every block +- Adds "<reg> at block exit: <value>" comments to every block +You can switch registers with the plugin command "Set Followed Register" +""" + + +class FollowRegRenderLayer(RenderLayer): + name = "Follow Register" + default_enable_state = RenderLayerDefaultEnableState.EnabledByDefaultRenderLayerDefaultEnableState + + def __init__(self, handle=None): + super().__init__(handle) + self.followed_reg = QSettings().value("layer/followed_reg", None) + + def apply_to_lines( + self, + block: BasicBlock, + lines: List['DisassemblyTextLine'], + ): + if self.followed_reg is None: + return lines + if self.followed_reg not in block.arch.regs: + return lines + + func = block.function + arch = block.arch + + # Give our tokens a unique value so they don't highlight all the other comments + token_value = arch.get_reg_index(self.followed_reg) ^ 0x12345678 + + # =============================================================================== + # Render the "<reg> after" comments + + for i, line in enumerate(lines): + # Ignore non-disassembly lines + if not any(token.type == InstructionTextTokenType.AddressSeparatorToken for token in line.tokens): + continue + + # If the register has changed, we should render the update + written = False + for w in func.get_regs_written_by(line.address, arch): + # In case this instruction only modifies a sub register of our target + # Or in the case that we wanted to follow a sub register + if arch.regs[w].full_width_reg == arch.regs[self.followed_reg].full_width_reg: + written = True + break + + if written: + # Add comment tokens to existing line + line.tokens.extend([ + InstructionTextToken(InstructionTextTokenType.CommentToken, ' // ', token_value), + InstructionTextToken(InstructionTextTokenType.CommentToken, self.followed_reg, token_value), + InstructionTextToken(InstructionTextTokenType.CommentToken, ' after: ', token_value), + ]) + + after = func.get_reg_value_after(line.address, self.followed_reg, arch) + if after.type == RegisterValueType.UndeterminedValue: + if line.il_instruction is not None: + instr = line.il_instruction + else: + instr_start = func.low_level_il.get_instruction_start(line.address, arch) + instr = func.low_level_il[instr_start] + if instr is not None: + after_possible = instr.get_possible_reg_values_after(self.followed_reg) + line.tokens.append( + InstructionTextToken(InstructionTextTokenType.CommentToken, str(after_possible), token_value) + ) + else: + line.tokens.append( + InstructionTextToken(InstructionTextTokenType.CommentToken, str(after), token_value) + ) + else: + line.tokens.append( + InstructionTextToken(InstructionTextTokenType.CommentToken, str(after), token_value) + ) + + block_start = block.start + block_end = block.end + if block.is_low_level_il: + block_start = func.llil[block.start].address + block_end = func.llil[block.end - 1].address + + # =============================================================================== + # Render the before-block "<reg> at block entry" comment + + start_before = func.get_reg_value_at(block_start, self.followed_reg, arch) + line = [ + InstructionTextToken(InstructionTextTokenType.CommentToken, '// ', token_value), + InstructionTextToken(InstructionTextTokenType.CommentToken, self.followed_reg, token_value), + InstructionTextToken(InstructionTextTokenType.CommentToken, ' at block entry: ', token_value), + ] + + # Sometimes the first line is blank and we want to insert after it + first_line = 0 + if len(lines[0].tokens) == 0: + first_line = 1 + if start_before.type == RegisterValueType.UndeterminedValue: + if lines[first_line].il_instruction is not None: + instr = lines[first_line].il_instruction + else: + instr_start = func.low_level_il.get_instruction_start(block_start, arch) + instr = func.low_level_il[instr_start] + if instr is not None: + start_before_possible = instr.get_possible_reg_values(self.followed_reg) + line.append( + InstructionTextToken(InstructionTextTokenType.CommentToken, str(start_before_possible), token_value) + ) + else: + line.append( + InstructionTextToken(InstructionTextTokenType.CommentToken, str(start_before), token_value) + ) + else: + line.append( + InstructionTextToken(InstructionTextTokenType.CommentToken, str(start_before), token_value) + ) + + lines.insert(first_line, DisassemblyTextLine(line, block_start)) + + # =============================================================================== + # Render the after-block "<reg> at block exit" comment + + end_after = func.get_reg_value_after(block_end, self.followed_reg, arch) + line = [ + InstructionTextToken(InstructionTextTokenType.CommentToken, '// ', token_value), + InstructionTextToken(InstructionTextTokenType.CommentToken, self.followed_reg, token_value), + InstructionTextToken(InstructionTextTokenType.CommentToken, ' at block exit: ', token_value), + ] + + # Sometimes the last line is blank and we want to insert before it + if len(lines) > 1 and len(lines[-1].tokens) == 0: + last_line = len(lines) - 1 + else: + last_line = len(lines) + if end_after.type == RegisterValueType.UndeterminedValue: + if lines[last_line - 1].il_instruction is not None: + instr = lines[last_line - 1].il_instruction + else: + instr_start = func.low_level_il.get_instruction_start(block_end, arch) + instr = func.low_level_il[instr_start] + if instr is not None: + end_after_possible = instr.get_possible_reg_values_after(self.followed_reg) + line.append( + InstructionTextToken(InstructionTextTokenType.CommentToken, str(end_after_possible), token_value) + ) + else: + line.append( + InstructionTextToken(InstructionTextTokenType.CommentToken, str(end_after), token_value) + ) + else: + line.append( + InstructionTextToken(InstructionTextTokenType.CommentToken, str(end_after), token_value) + ) + + lines.insert(last_line, DisassemblyTextLine(line, block_end)) + + return lines + + def apply_to_disassembly_block( + self, + block: BasicBlock, + lines: List['DisassemblyTextLine'] + ): + # Break this out into a helper so we don't have to write it twice + return self.apply_to_lines(block, lines) + + def apply_to_low_level_il_block( + self, + block: BasicBlock, + lines: List['DisassemblyTextLine'] + ): + # Break this out into a helper so we don't have to write it twice + return self.apply_to_lines(block, lines) + + +def set_follow_reg(bv: BinaryView): + if bv.platform is not None: + regs = list(bv.platform.arch.regs.keys()) + idx = interaction.get_large_choice_input("Choose", "Choose Followed Register", regs) + + # Save choice both in QSettings and on the RenderLayer object instance + layer = RenderLayer[FollowRegRenderLayer.name] + if idx is None: + layer.followed_reg = None + QSettings().remove("layer/followed_reg") + else: + layer.followed_reg = regs[idx] + QSettings().setValue("layer/followed_reg", regs[idx]) + + # Trigger view refresh (gross) + for func in bv.get_functions_containing(bv.offset): + func.reanalyze() + + +FollowRegRenderLayer.register() + +# Using a command for this is kinda janky but currently the only option +PluginCommand.register("Set Followed Register", "Choose which register to follow for the Follow Register Render Layer", set_follow_reg) diff --git a/python/flowgraph.py b/python/flowgraph.py index e661fff8..1f5040c2 100644 --- a/python/flowgraph.py +++ b/python/flowgraph.py @@ -156,30 +156,7 @@ class FlowGraphNode: block = core.BNGetFlowGraphBasicBlock(self.handle) if not block: return None - func_handle = core.BNGetBasicBlockFunction(block) - if not func_handle: - core.BNFreeBasicBlock(block) - return None - - view = binaryview.BinaryView(handle=core.BNGetFunctionData(func_handle)) - func = function.Function(view, func_handle) - - if core.BNIsLowLevelILBasicBlock(block): - block = lowlevelil.LowLevelILBasicBlock( - block, lowlevelil.LowLevelILFunction(func.arch, core.BNGetBasicBlockLowLevelILFunction(block), func), - view - ) - elif core.BNIsMediumLevelILBasicBlock(block): - mlil_func = mediumlevelil.MediumLevelILFunction( - func.arch, core.BNGetBasicBlockMediumLevelILFunction(block), func - ) - block = mediumlevelil.MediumLevelILBasicBlock(block, mlil_func, view) - elif core.BNIsHighLevelILBasicBlock(block): - hlil_func = highlevelil.HighLevelILFunction(func.arch, core.BNGetBasicBlockHighLevelILFunction(block), func) - block = highlevelil.HighLevelILBasicBlock(block, hlil_func, view) - else: - block = basicblock.BasicBlock(block, view) - return block + return basicblock.BasicBlock._from_core_block(block) @basic_block.setter def basic_block(self, block): @@ -613,6 +590,11 @@ class FlowGraph: return result @property + def node_count(self) -> int: + """Number of nodes in graph (read-only)""" + return core.BNGetFlowGraphNodeCount(self.handle) + + @property def has_nodes(self): """Whether the flow graph has at least one node (read-only)""" return core.BNFlowGraphHasNodes(self.handle) @@ -835,12 +817,35 @@ class FlowGraph: """ ``append`` adds a node to a flow graph. + .. note:: After the graph has completed layout, this function has no effect. + :param FlowGraphNode node: Node to add :return: Index of node :rtype: int """ return core.BNAddFlowGraphNode(self.handle, node.handle) + def replace(self, index, node): + """ + ``replace`` replaces an existing node in the graph with a new node. + Any existing edges referencing the old node will be updated to point to + the new node. + + .. note:: After the graph has completed layout, this function has no effect. + + :param index: Index of the node to replace + :param node: New node with which to replace the old node + """ + core.BNReplaceFlowGraphNode(self.handle, index, node.handle) + + def clear(self): + """ + ``clear`` clears all the nodes in the graph + + .. note:: After the graph has completed layout, this function has no effect. + """ + core.BNClearFlowGraphNodes(self.handle) + def show(self, title): """ ``show`` displays the graph in a new tab in the UI. @@ -869,6 +874,50 @@ class FlowGraph: def is_option_set(self, option): return core.BNIsFlowGraphOptionSet(self.handle, option) + @property + def render_layers(self) -> List['binaryninja.RenderLayer']: + """ + Get the list of Render Layers which will be applied to this Flow Graph, + after it calls populate_nodes. + :return: List of Render Layers + """ + count = ctypes.c_size_t(0) + layers = core.BNGetFlowGraphRenderLayers(self.handle, count) + assert layers is not None, "core.BNGetFlowGraphRenderLayers returned None" + + try: + result = [] + for i in range(0, count.value): + result.append(binaryninja.RenderLayer(handle=layers[i])) + + return result + finally: + core.BNFreeRenderLayerList(layers) + + @render_layers.setter + def render_layers(self, render_layers: List['binaryninja.RenderLayer']): + for layer in self.render_layers: + self.remove_render_layer(layer) + for layer in render_layers: + self.add_render_layer(layer) + + def add_render_layer(self, layer: 'binaryninja.RenderLayer'): + """ + Add a Render Layer to be applied to this Flow Graph. Note that layers will + be applied in the order in which they are added. + + :param layer: Render Layer to add + """ + core.BNAddFlowGraphRenderLayer(self.handle, layer.handle) + + def remove_render_layer(self, layer: 'binaryninja.RenderLayer'): + """ + Remove a Render Layer from being applied to this Flow Graph + + :param layer: Render Layer to remove + """ + core.BNRemoveFlowGraphRenderLayer(self.handle, layer.handle) + class CoreFlowGraph(FlowGraph): def __init__(self, handle): diff --git a/python/function.py b/python/function.py index 80f0a37c..14cc5e43 100644 --- a/python/function.py +++ b/python/function.py @@ -3335,15 +3335,29 @@ class AdvancedFunctionAnalysisDataRequestor: @dataclass +class DisassemblyTextLineTypeInfo: + parent_type: Optional['types.Type'] + field_index: int + offset: int + + +@dataclass class DisassemblyTextLine: tokens: List['InstructionTextToken'] highlight: '_highlight.HighlightColor' address: Optional[int] il_instruction: Optional[ILInstructionType] + tags: List['binaryview.Tag'] + type_info: Optional[DisassemblyTextLineTypeInfo] def __init__( - self, tokens: List['InstructionTextToken'], address: Optional[int] = None, il_instr: Optional[ILInstructionType] = None, - color: Optional[Union['_highlight.HighlightColor', HighlightStandardColor]] = None + self, + tokens: List['InstructionTextToken'], + address: Optional[int] = None, + il_instr: Optional[ILInstructionType] = None, + color: Optional[Union['_highlight.HighlightColor', HighlightStandardColor]] = None, + tags: Optional[List['binaryview.Tag']] = None, + type_info: Optional[DisassemblyTextLineTypeInfo] = None, ): self.address = address self.tokens = tokens @@ -3358,6 +3372,10 @@ class DisassemblyTextLine: self.highlight = _highlight.HighlightColor(color) else: self.highlight = color + if tags is None: + tags = [] + self.tags = tags + self.type_info = type_info def __str__(self): return "".join(map(str, self.tokens)) @@ -3413,6 +3431,64 @@ class DisassemblyTextLine: self._find_address_and_indentation_tokens(collect_tokens) return result + @classmethod + def _from_core_struct(cls, struct: core.BNDisassemblyTextLine, il_func: Optional['ILFunctionType'] = None): + il_instr = None + if il_func is not None and struct.instrIndex < len(il_func): + try: + il_instr = il_func[struct.instrIndex] + except: + il_instr = None + tokens = InstructionTextToken._from_core_struct(struct.tokens, struct.count) + + tags = [] + for i in range(struct.tagCount): + tags.append(binaryview.Tag(handle=core.BNNewTagReference(struct.tags[i]))) + + type_info = None + if struct.typeInfo.hasTypeInfo: + parent_type = None + if struct.typeInfo.parentType: + parent_type = types.Type.create(core.BNNewTypeReference(struct.typeInfo.parentType)) + type_info = DisassemblyTextLineTypeInfo( + parent_type=parent_type, + field_index=struct.typeInfo.fieldIndex, + offset=struct.typeInfo.offset + ) + + return DisassemblyTextLine( + tokens, + struct.addr, + il_instr, + _highlight.HighlightColor._from_core_struct(struct.highlight), + tags, + type_info + ) + + def _to_core_struct(self) -> core.BNDisassemblyTextLine: + result = core.BNDisassemblyTextLine() + result.addr = self.address + if self.il_instruction is not None: + result.instrIndex = self.il_instruction.instr_index + else: + result.instrIndex = 0xffffffffffffffff + result.tokens = InstructionTextToken._get_core_struct(self.tokens) + result.count = len(self.tokens) + result.highlight = self.highlight._to_core_struct() + result.tagCount = len(self.tags) + result.tags = (ctypes.POINTER(core.BNTag) * len(self.tags))() + for i, tag in enumerate(self.tags): + result.tags[i] = tag.handle + if self.type_info is None: + result.typeInfo.hasTypeInfo = False + else: + result.typeInfo.hasTypeInfo = True + if self.type_info.parent_type is not None: + result.typeInfo.parentType = self.type_info.parent_type.handle + result.typeInfo.fieldIndex = self.type_info.field_index + result.typeInfo.offset = self.type_info.offset + return result + class DisassemblyTextRenderer: def __init__( @@ -3463,7 +3539,7 @@ class DisassemblyTextRenderer: def basic_block(self) -> Optional['basicblock.BasicBlock']: result = core.BNGetDisassemblyTextRendererBasicBlock(self.handle) if result: - return basicblock.BasicBlock(handle=result) + return basicblock.BasicBlock._from_core_block(handle=result) return None @basic_block.setter diff --git a/python/lineardisassembly.py b/python/lineardisassembly.py index 40a0d6c5..8c4b0eb5 100644 --- a/python/lineardisassembly.py +++ b/python/lineardisassembly.py @@ -27,7 +27,7 @@ from . import highlight from . import function as _function from . import basicblock from . import binaryview -from .enums import LinearViewObjectIdentifierType +from .enums import (LinearDisassemblyLineType, LinearViewObjectIdentifierType) class LinearDisassemblyLine: @@ -43,6 +43,41 @@ class LinearDisassemblyLine: def __str__(self): return str(self.contents) + @classmethod + def _from_core_struct(cls, struct: core.BNLinearDisassemblyLine, obj: Optional['LinearViewObject'] = None) -> 'LinearDisassemblyLine': + function = None + if struct.function: + function = _function.Function(handle=core.BNNewFunctionReference(struct.function)) + block = None + if struct.block: + block = basicblock.BasicBlock._from_core_block(core.BNNewBasicBlockReference(struct.block)) + il_func = None + if block is not None: + il_func = block.il_function + if obj is not None and function is not None: + # Hack: HLIL bodies don't have basic blocks + if obj.identifier.name == "HLIL Function Body": + il_func = function.hlil + elif obj.identifier.name == "HLIL SSA Function Body": + il_func = function.hlil.ssa_form + elif obj.identifier.name == "Language Representation Function Body": + il_func = function.hlil + + contents = _function.DisassemblyTextLine._from_core_struct(struct.contents, il_func) + return LinearDisassemblyLine(LinearDisassemblyLineType(struct.type), function, block, contents) + + def _to_core_struct(self) -> core.BNLinearDisassemblyLine: + result = core.BNLinearDisassemblyLine() + result.type = self.type + result.function = None + if self.function is not None: + result.function = self.function.handle + result.block = None + if self.block is not None: + result.block = self.block.handle + result.contents = _function.DisassemblyTextLine._to_core_struct(self.contents) + return result + class LinearViewObjectIdentifier: def __init__(self, name, start=None, end=None): @@ -247,7 +282,7 @@ class LinearViewObject: count = ctypes.c_ulonglong(0) return LinearViewCursor._make_lines( - core.BNGetLinearViewObjectLines(self.handle, prev_obj, next_obj, count), count.value + core.BNGetLinearViewObjectLines(self.handle, prev_obj, next_obj, count), count.value, self ) def ordering_index_for_child(self, child): @@ -602,34 +637,21 @@ class LinearViewCursor: return core.BNLinearViewCursorNext(self.handle) @staticmethod - def _make_lines(lines, count: int) -> List['LinearDisassemblyLine']: + def _make_lines(lines, count: int, object) -> List['LinearDisassemblyLine']: assert lines is not None, "core returned None for LinearDisassembly lines" try: result = [] for i in range(0, count): - func = None - block = None - if lines[i].function: - func = _function.Function(None, core.BNNewFunctionReference(lines[i].function)) - if lines[i].block: - core_block = core.BNNewBasicBlockReference(lines[i].block) - assert core_block is not None, "core.BNNewBasicBlockReference returned None" - block = basicblock.BasicBlock(core_block, None) - color = highlight.HighlightColor._from_core_struct(lines[i].contents.highlight) - addr = lines[i].contents.addr - tokens = _function.InstructionTextToken._from_core_struct( - lines[i].contents.tokens, lines[i].contents.count - ) - contents = _function.DisassemblyTextLine(tokens, addr, color=color) - result.append(LinearDisassemblyLine(lines[i].type, func, block, contents)) + result.append(LinearDisassemblyLine._from_core_struct(lines[i], obj=object)) return result finally: core.BNFreeLinearDisassemblyLines(lines, count) @property def lines(self): + current = self.current_object count = ctypes.c_ulonglong(0) - return LinearViewCursor._make_lines(core.BNGetLinearViewCursorLines(self.handle, count), count.value) + return LinearViewCursor._make_lines(core.BNGetLinearViewCursorLines(self.handle, count), count.value, current) def duplicate(self): return LinearViewCursor(None, handle=core.BNDuplicateLinearViewCursor(self.handle)) @@ -637,3 +659,48 @@ class LinearViewCursor: @staticmethod def compare(a, b): return core.BNCompareLinearViewCursors(a.handle, b.handle) + + @property + def render_layers(self) -> List['binaryninja.RenderLayer']: + """ + Get the list of Render Layers which will be applied to this cursor, at the + end of calls to lines(). + + :return: List of Render Layers + """ + count = ctypes.c_size_t(0) + layers = core.BNGetLinearViewCursorRenderLayers(self.handle, count) + assert layers is not None, "core.BNGetLinearViewCursorRenderLayers returned None" + + try: + result = [] + for i in range(0, count.value): + result.append(binaryninja.RenderLayer(handle=layers[i])) + + return result + finally: + core.BNFreeRenderLayerList(layers) + + @render_layers.setter + def render_layers(self, render_layers: List['binaryninja.RenderLayer']): + for layer in self.render_layers: + self.remove_render_layer(layer) + for layer in render_layers: + self.add_render_layer(layer) + + def add_render_layer(self, layer: 'binaryninja.RenderLayer'): + """ + Add a Render Layer to be applied to this cursor. Note that layers will + be applied in the order in which they are added. + + :param layer: Render Layer to add + """ + core.BNAddLinearViewCursorRenderLayer(self.handle, layer.handle) + + def remove_render_layer(self, layer: 'binaryninja.RenderLayer'): + """ + Remove a Render Layer from being applied to this cursor + + :param layer: Render Layer to remove + """ + core.BNRemoveLinearViewCursorRenderLayer(self.handle, layer.handle) diff --git a/python/renderlayer.py b/python/renderlayer.py new file mode 100644 index 00000000..6f5984e0 --- /dev/null +++ b/python/renderlayer.py @@ -0,0 +1,450 @@ +# Copyright (c) 2015-2024 Vector 35 Inc +# +# Permission 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: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE 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. + +import ctypes +import traceback + +# Binary Ninja components +import binaryninja +from . import _binaryninjacore as core, LinearDisassemblyLine +from .enums import LinearDisassemblyLineType, RenderLayerDefaultEnableState +from . import binaryview +from . import types +from .log import log_error +from typing import Iterable, List, Optional, Union, Tuple + + +class _RenderLayerMetaclass(type): + def __iter__(self): + binaryninja._init_plugins() + count = ctypes.c_ulonglong() + instances = core.BNGetRenderLayerList(count) + try: + for i in range(0, count.value): + yield self._handle_to_instance(instances[i]) + finally: + core.BNFreeRenderLayerList(instances) + + def __getitem__(self, value): + binaryninja._init_plugins() + handle = core.BNGetRenderLayerByName(str(value)) + if handle is None: + raise KeyError(f"'{value}' is not a valid RenderLayer") + return self._handle_to_instance(handle) + + def _handle_to_instance(self, handle): + handle_ptr = ctypes.cast(handle, ctypes.c_void_p) + if handle_ptr.value in RenderLayer._registered_instances: + return RenderLayer._registered_instances[handle_ptr.value] + return CoreRenderLayer(handle) + + +class RenderLayer(metaclass=_RenderLayerMetaclass): + """ + RenderLayer is a plugin class that allows you to customize the presentation of + Linear and Graph view output, adding, changing, or removing lines before they are + presented in the UI. + """ + + name = None + default_enable_state = RenderLayerDefaultEnableState.DisabledByDefaultRenderLayerDefaultEnableState + _registered_instances = {} + _pending_lines = {} + + def __init__(self, handle=None): + if handle is not None: + self.handle = core.handle_of_type(handle, core.BNRenderLayer) + self.__dict__["name"] = core.BNGetRenderLayerName(handle) + self.__dict__["default_enable_state"] = core.BNGetRenderLayerDefaultEnableState(handle) + else: + self.handle = None + + @classmethod + def register(cls): + """ + Register a custom Render Layer. + """ + layer = cls() + + assert layer.__class__.name is not None + assert layer.handle is None + + layer._cb = core.BNRenderLayerCallbacks() + layer._cb.context = 0 + layer._cb.applyToFlowGraph = layer._cb.applyToFlowGraph.__class__(layer._apply_to_flow_graph) + layer._cb.applyToLinearViewObject = layer._cb.applyToLinearViewObject.__class__(layer._apply_to_linear_view_object) + layer._cb.freeLines = layer._cb.freeLines.__class__(layer._free_lines) + layer.handle = core.BNRegisterRenderLayer(layer.__class__.name, layer._cb, layer.default_enable_state) + handle_ptr = ctypes.cast(layer.handle, ctypes.c_void_p) + cls._registered_instances[handle_ptr.value] = layer + + def __eq__(self, other): + if not isinstance(other, RenderLayer): + return False + return self.name == other.name + + def __str__(self): + return f'<RenderLayer: {self.name}>' + + def __repr__(self): + return f'<RenderLayer: {self.name}>' + + def _apply_to_flow_graph(self, ctxt, graph): + try: + self.apply_to_flow_graph(binaryninja.FlowGraph(handle=core.BNNewFlowGraphReference(graph))) + except: + log_error(traceback.format_exc()) + + def _apply_to_linear_view_object(self, ctxt, obj, prev, next, in_lines, in_line_count, out_lines, out_line_count): + try: + obj_obj = binaryninja.LinearViewObject(core.BNNewLinearViewObjectReference(obj)) + prev_obj = binaryninja.LinearViewObject(core.BNNewLinearViewObjectReference(prev)) if prev else None + next_obj = binaryninja.LinearViewObject(core.BNNewLinearViewObjectReference(next)) if next else None + + lines = [] + for i in range(in_line_count): + lines.append(LinearDisassemblyLine._from_core_struct(in_lines[i], obj=obj_obj)) + + lines = self.apply_to_linear_view_object(obj_obj, prev_obj, next_obj, lines) + + out_line_count[0] = len(lines) + out_lines_buf = (core.BNLinearDisassemblyLine * len(lines))() + for i, r in enumerate(lines): + out_lines_buf[i] = r._to_core_struct() + out_lines_ptr = ctypes.cast(out_lines_buf, ctypes.c_void_p) + out_lines[0] = out_lines_buf + self._pending_lines[out_lines_ptr.value] = (out_lines_ptr.value, out_lines_buf) + except: + log_error(traceback.format_exc()) + out_lines[0] = None + out_line_count[0] = 0 + + def _free_lines(self, ctxt, lines, count): + try: + buf = ctypes.cast(lines, ctypes.c_void_p) + if buf.value is not None: + if buf.value not in self._pending_lines: + raise ValueError("freeing lines list that wasn't allocated") + del self._pending_lines[buf.value] + except: + log_error(traceback.format_exc()) + + def apply_to_disassembly_block( + self, + block: 'binaryninja.BasicBlock', + lines: List['binaryninja.DisassemblyTextLine'] + ): + """ + Apply this Render Layer to a single Basic Block of Disassembly lines. + Subclasses should modify the input `lines` list to make modifications to + the presentation of the block. + + .. note:: This function will only handle Disassembly lines, and not any ILs. + + :param block: Basic Block containing those lines + :param lines: Original lines of text for the block + :return: Modified list of lines + """ + return lines + + def apply_to_low_level_il_block( + self, + block: 'binaryninja.LowLevelILBasicBlock', + lines: List['binaryninja.DisassemblyTextLine'] + ): + """ + Apply this Render Layer to a single Basic Block of Low Level IL lines. + Subclasses should modify the input `lines` list to make modifications to + the presentation of the block. + + .. note:: This function will only handle Lifted IL/LLIL/LLIL(SSA) lines. \ + You can use the block's `function_graph_type` property to determine which is being handled. + + :param block: Basic Block containing those lines + :param lines: Original lines of text for the block + :return: Modified list of lines + """ + return lines + + def apply_to_medium_level_il_block( + self, + block: 'binaryninja.MediumLevelILBasicBlock', + lines: List['binaryninja.DisassemblyTextLine'] + ): + """ + Apply this Render Layer to a single Basic Block of Medium Level IL lines. + Subclasses should modify the input `lines` list to make modifications to + the presentation of the block. + + .. note:: This function will only handle MLIL/MLIL(SSA)/Mapped MLIL/Mapped MLIL(SSA) lines. \ + You can use the block's `function_graph_type` property to determine which is being handled. + + :param block: Basic Block containing those lines + :param lines: Original lines of text for the block + :return: Modified list of lines + """ + return lines + + def apply_to_high_level_il_block( + self, + block: 'binaryninja.HighLevelILBasicBlock', + lines: List['binaryninja.DisassemblyTextLine'] + ): + """ + Apply this Render Layer to a single Basic Block of High Level IL lines. + Subclasses should modify the input `lines` list to make modifications to + the presentation of the block. + + .. note:: This function will only handle HLIL/HLIL(SSA)/Language Representation lines. \ + You can use the block's `function_graph_type` property to determine which is being handled. + + .. warning:: This function will NOT apply to High Level IL bodies as displayed \ + in Linear View! Those are handled by `apply_to_high_level_il_body` instead as they \ + do not have a Basic Block associated with them. + + :param block: Basic Block containing those lines + :param lines: Original lines of text for the block + :return: Modified list of lines + """ + return lines + + def apply_to_high_level_il_body( + self, + function: 'binaryninja.Function', + lines: List['binaryninja.LinearDisassemblyLine'] + ): + """ + Apply this Render Layer to the entire body of a High Level IL function. + Subclasses should modify the input `lines` list to make modifications to + the presentation of the function. + + .. warning:: This function only applies to Linear View, and not to Graph View! \ + If you want to handle Graph View too, you will need to use `apply_to_high_level_il_block` \ + and handle the lines one block at a time. + + :param function: Function containing those lines + :param lines: Original lines of text for the function + :return: Modified list of lines + """ + return lines + + def apply_to_misc_linear_lines( + self, + obj: 'binaryninja.LinearViewObject', + prev: Optional['binaryninja.LinearViewObject'], + next: Optional['binaryninja.LinearViewObject'], + lines: List['binaryninja.LinearDisassemblyLine'] + ): + """ + Apply to lines generated by Linear View that are not part of a function. + It is up to your implementation to figure out which type of Linear View Object + lines these are, and what to do with them. + + :param obj: Linear View Object being rendered + :param prev: Linear View Object located directly above this one + :param next: Linear View Object located directly below this one + :param lines: Original lines rendered by `obj` + :return: Modified list of lines + """ + return lines + + def apply_to_block( + self, + block: 'binaryninja.BasicBlock', + lines: List['binaryninja.DisassemblyTextLine'], + ): + """ + Apply to lines generated by a Basic Block, of any type. If not overridden, this + function will call the appropriate apply_to_X_level_il_block function. + + :param block: Basic Block containing those lines + :param lines: Original lines of text for the block + :return: Modified list of lines + """ + if not block.is_il: + return self.apply_to_disassembly_block(block, lines) + elif block.is_low_level_il: + return self.apply_to_low_level_il_block(block, lines) + elif block.is_medium_level_il: + return self.apply_to_medium_level_il_block(block, lines) + elif block.is_high_level_il: + return self.apply_to_high_level_il_block(block, lines) + else: + # ??? + return lines + + def apply_to_flow_graph(self, graph: 'binaryninja.FlowGraph') -> None: + """ + Apply this Render Layer to a Flow Graph, potentially modifying its nodes, + their edges, their lines, and their lines' content. + + .. note:: If you override this function, you will need to call the ``super()`` \ + implementation if you want to use the higher level ``apply_to_X_level_il_block`` \ + functionality. + + :param graph: Graph to modify + """ + pass + for i, node in enumerate(graph.nodes): + lines = node.lines + if node.basic_block is not None and isinstance(node.basic_block, binaryninja.BasicBlock): + lines = self.apply_to_block(node.basic_block, lines) + node.lines = lines + + def apply_to_linear_view_object( + self, + obj: 'binaryninja.LinearViewObject', + prev: Optional['binaryninja.LinearViewObject'], + next: Optional['binaryninja.LinearViewObject'], + lines: List['binaryninja.LinearDisassemblyLine'] + ) -> List['binaryninja.LinearDisassemblyLine']: + """ + Apply this Render Layer to the lines produced by a LinearViewObject for rendering + in Linear View, potentially modifying the lines and their contents. + + .. note:: If you override this function, you will need to call the ``super()`` \ + implementation if you want to use the higher level ``apply_to_X_level_il_block`` \ + functionality. + + :param obj: Linear View Object being rendered + :param prev: Linear View Object located directly above this one + :param next: Linear View Object located directly below this one + :param lines: Original lines rendered by the Linear View Object + :return: Modified list of lines to display in Linear View + """ + # Hack: HLIL bodies don't have basic blocks + if len(lines) > 0 and obj.identifier.name in [ + "HLIL Function Body", + "HLIL SSA Function Body", + "Language Representation Function Body" + ]: + return self.apply_to_high_level_il_body(lines[0].function, lines) + + block_lines = [] + final_lines = [] + last_block = None + + def finish_block(): + nonlocal block_lines + nonlocal final_lines + if len(block_lines) > 0: + if last_block is not None: + # Convert linear lines to disassembly lines for the apply() + # and then convert back for linear view + new_block_lines = [] + disasm_lines = [] + misc_lines = [] + + def process_disasm(): + nonlocal disasm_lines + + if len(disasm_lines) > 0: + disasm_lines = self.apply_to_block(last_block, disasm_lines) + func = block_lines[0].function + block = block_lines[0].block + for block_line in disasm_lines: + new_block_lines.append( + LinearDisassemblyLine( + LinearDisassemblyLineType.CodeDisassemblyLineType, + func, + block, + block_line + ) + ) + disasm_lines = [] + + def process_misc(): + nonlocal misc_lines + nonlocal new_block_lines + + if len(misc_lines) > 0: + misc_lines = self.apply_to_misc_linear_lines(obj, prev, next, misc_lines) + new_block_lines += misc_lines + misc_lines = [] + + for block_line in block_lines: + # Lines in the block get sent to process_disasm, anything else goes + # to process_misc so we preserve line information + if block_line.type == LinearDisassemblyLineType.CodeDisassemblyLineType: + process_misc() + disasm_lines.append(block_line.contents) + else: + process_disasm() + misc_lines.append(block_line) + + # At the end, zero or one of these has lines in it + process_misc() + process_disasm() + block_lines = new_block_lines + else: + block_lines = self.apply_to_misc_linear_lines(obj, prev, next, block_lines) + final_lines += block_lines + block_lines = [] + + for line in lines: + # Assume we've finished a block when the line's block changes + if line.block != last_block: + finish_block() + block_lines.append(line) + last_block = line.block + + # And we've finished a block when we're done with every line + finish_block() + return final_lines + + +class CoreRenderLayer(RenderLayer): + + def apply_to_flow_graph(self, graph: 'binaryninja.FlowGraph') -> None: + core.BNApplyRenderLayerToFlowGraph(self.handle, graph.handle) + + def apply_to_linear_view_object( + self, + obj: 'binaryninja.LinearViewObject', + prev: Optional['binaryninja.LinearViewObject'], + next: Optional['binaryninja.LinearViewObject'], + lines: List['binaryninja.LinearDisassemblyLine'] + ) -> List['binaryninja.LinearDisassemblyLine']: + + in_lines_buf = (core.BNLinearDisassemblyLine * len(lines))() + for i, r in enumerate(lines): + in_lines_buf[i] = r._to_core_struct() + + out_lines = ctypes.POINTER(core.BNLinearDisassemblyLine)() + out_line_count = ctypes.c_size_t(0) + + core.BNApplyRenderLayerToLinearViewObject( + self.handle, + obj.handle, + prev.handle if prev is not None else None, + next.handle if next is not None else None, + in_lines_buf, + len(lines), + out_lines, + out_line_count + ) + + result = [] + for i in range(out_line_count.value): + result.append(binaryninja.LinearDisassemblyLine._from_core_struct(out_lines[i], obj=obj)) + + core.BNFreeLinearDisassemblyLines(out_lines, out_line_count.value) + + return result |
