From f60e84330c8fc94f9eaeed2b8abd069e73133eb5 Mon Sep 17 00:00:00 2001 From: Brandon Miller Date: Tue, 1 Jul 2025 13:22:06 -0400 Subject: Python bindings for custom basic block analysis --- python/architecture.py | 361 +++++++++++++++++++++++++++++++++++++++++++------ python/basicblock.py | 2 - python/function.py | 164 +--------------------- 3 files changed, 324 insertions(+), 203 deletions(-) (limited to 'python') diff --git a/python/architecture.py b/python/architecture.py index 827d4bef..099b56f1 100644 --- a/python/architecture.py +++ b/python/architecture.py @@ -20,7 +20,7 @@ import traceback import ctypes -from typing import Generator, Union, List, Optional, Mapping, Tuple, NewType, Dict +from typing import Generator, Union, List, Optional, Mapping, Tuple, NewType, Dict, Set from dataclasses import dataclass, field # Binary Ninja components @@ -39,8 +39,8 @@ from . import callingconvention from . import typelibrary from . import function from . import binaryview -from . import deprecation -from .variable import IndirectBranchInfo +from . import variable +from . import basicblock RegisterIndex = NewType('RegisterIndex', int) RegisterStackIndex = NewType('RegisterStackIndex', int) @@ -66,14 +66,324 @@ SemanticGroupType = Union[SemanticGroupName, 'lowlevelil.ILSemanticFlagGroup', S IntrinsicType = Union[IntrinsicName, 'lowlevelil.ILIntrinsic', IntrinsicIndex] -@dataclass(frozen=True) +@dataclass class BasicBlockAnalysisContext: - """Used by ``analyze_basic_blocks`` and contains analysis settings and other contextual information.""" - indirect_branches: List[IndirectBranchInfo] - analysis_skip_override: core.FunctionAnalysisSkipOverrideEnum - translate_tail_calls: bool - disallow_branch_to_string: bool - max_function_size: int + """Used by ``analyze_basic_blocks`` and contains analysis settings and other contextual information. + + .. note:: This class is meant to be used by Architecture plugins only + """ + + _handle: core.BNBasicBlockAnalysisContext + _function: "function.Function" + _contextual_returns_dirty: bool + + # In + _indirect_branches: List["variable.IndirectBranchInfo"] + _indirect_no_return_calls: Set["function.ArchAndAddr"] + _analysis_skip_override: core.FunctionAnalysisSkipOverride + _translate_tail_calls: bool + _disallow_branch_to_string: bool + _max_function_size: int + _halt_on_invalid_instruction: bool + _max_size_reached: bool + + # In/Out + _contextual_returns: Dict["function.ArchAndAddr", bool] + + # Out + _direct_code_references: Dict[int, "function.ArchAndAddr"] + _direct_no_return_calls: Set["function.ArchAndAddr"] + _halted_disassembly_addresses: Set["function.ArchAndAddr"] + + @staticmethod + def from_core_struct(bn_bb_context: core.BNBasicBlockAnalysisContext) -> "BasicBlockAnalysisContext": + """Create a BasicBlockAnalysisContext from a core.BNBasicBlockAnalysisContext structure.""" + + indirect_branches = [] + for i in range(0, bn_bb_context.indirectBranchesCount): + ibi = variable.IndirectBranchInfo( + source_arch=CoreArchitecture._from_cache(bn_bb_context.indirectBranches[i].sourceArch), + source_addr=bn_bb_context.indirectBranches[i].sourceAddr, + dest_arch=CoreArchitecture._from_cache(bn_bb_context.indirectBranches[i].destArch), + dest_addr=bn_bb_context.indirectBranches[i].destAddr, + auto_defined=bn_bb_context.indirectBranches[i].autoDefined, + ) + indirect_branches.append(ibi) + + indirect_no_return_calls = set() + for i in range(0, bn_bb_context.indirectNoReturnCallsCount): + loc = function.ArchAndAddr( + CoreArchitecture._from_cache(bn_bb_context.indirectNoReturnCalls[i].arch), + bn_bb_context.indirectNoReturnCalls[i].address, + ) + indirect_no_return_calls.add(loc) + + contextual_returns = {} + for i in range(0, bn_bb_context.contextualFunctionReturnCount): + loc = function.ArchAndAddr( + CoreArchitecture._from_cache(bn_bb_context.contextualFunctionReturnLocations[i].arch), + bn_bb_context.contextualFunctionReturnLocations[i].address, + ) + contextual_returns[loc] = bn_bb_context._contextualFunctionReturnValues[i] + + direct_code_references = {} + for i in range(0, bn_bb_context.directRefCount): + src = function.ArchAndAddr( + CoreArchitecture._from_cache(bn_bb_context.directRefSources[i].arch), + bn_bb_context.directRefSources[i].address, + ) + direct_code_references[bn_bb_context.directRefTargets[i]] = src + + direct_no_return_calls = set() + for i in range(0, bn_bb_context.directNoReturnCallsCount): + loc = function.ArchAndAddr( + CoreArchitecture._from_cache(bn_bb_context.directNoReturnCallLocations[i].arch), + bn_bb_context.directNoReturnCallLocations[i].address, + ) + direct_no_return_calls.add(loc) + + halted_disassembly_addresses = set() + for i in range(0, bn_bb_context.haltedDisassemblyAddressesCount): + addr = function.ArchAndAddr( + CoreArchitecture._from_cache(bn_bb_context.haltedDisassemblyAddresses[i].arch), + bn_bb_context.haltedDisassemblyAddresses[i].address, + ) + halted_disassembly_addresses.add(addr) + + view = binaryview.BinaryView(handle=core.BNGetFunctionData(bn_bb_context.function)) + return BasicBlockAnalysisContext( + _handle=bn_bb_context, + _function=function.Function(view, core.BNNewFunctionReference(bn_bb_context.function)), + _indirect_branches=indirect_branches, + _indirect_no_return_calls=indirect_no_return_calls, + _analysis_skip_override=bn_bb_context.analysisSkipOverride, + _translate_tail_calls=bn_bb_context.translateTailCalls, + _disallow_branch_to_string=bn_bb_context.disallowBranchToString, + _max_function_size=bn_bb_context.maxFunctionSize, + _halt_on_invalid_instruction=bn_bb_context.haltOnInvalidInstructions, + _max_size_reached=bn_bb_context.maxSizeReached, + _contextual_returns=contextual_returns, + _contextual_returns_dirty=False, + _direct_code_references=direct_code_references, + _direct_no_return_calls=direct_no_return_calls, + _halted_disassembly_addresses=halted_disassembly_addresses, + ) + + @property + def indirect_branches(self) -> List["variable.IndirectBranchInfo"]: + """Get the list of indirect branches in this context.""" + + return self._indirect_branches + + @property + def indirect_no_return_calls(self) -> Set["function.ArchAndAddr"]: + """Get the set of indirect no-return calls in this context.""" + + return self._indirect_no_return_calls + + @property + def analysis_skip_override(self) -> core.FunctionAnalysisSkipOverride: + """Get the analysis skip override setting for this context.""" + + return self._analysis_skip_override + + @property + def translate_tail_calls(self) -> bool: + """Get setting from context that determines if tail calls should be translated.""" + + return self._translate_tail_calls + + @property + def disallow_branch_to_string(self) -> bool: + """Get setting from context that determines if branches to string addresses should be disallowed.""" + + return self._disallow_branch_to_string + + @property + def max_function_size(self) -> int: + """Get the maximum function size setting for this context.""" + + return self._max_function_size + + @property + def halt_on_invalid_instruction(self) -> bool: + """Get the setting from context that determines if analysis should halt on invalid instructions.""" + + return self._halt_on_invalid_instruction + + @property + def max_size_reached(self) -> bool: + """Get boolean that indicates if the maximum function size has been reached.""" + + return self._max_size_reached + + @property + def contextual_returns(self) -> Dict["function.ArchAndAddr", bool]: + """Get the mapping of contextual function return locations to their values.""" + + return self._contextual_returns + + def add_contextual_return(self, loc: "function.ArchAndAddr", value: bool) -> None: + """ + ``add_contextual_return`` adds a contextual function return location and its value to the current function. + + :param function.ArchAndAddr loc: The location of the contextual function return + :param bool value: The value of the contextual function return + """ + if not isinstance(value, bool): + raise TypeError("value must be a boolean") + + if not isinstance(loc, function.ArchAndAddr): + raise TypeError("loc must be an instance of function.ArchAndAddr") + + # Update existing value if it exists + if loc in self._contextual_returns: + if self._contextual_returns[loc] == value: + return + + self._contextual_returns[loc] = value + self._contextual_returns_dirty = True + + @property + def direct_code_references(self) -> Dict[int, "function.ArchAndAddr"]: + """Get the mapping of direct code reference targets to their source locations.""" + + return self._direct_code_references + + def add_direct_code_reference(self, target: int, source: "function.ArchAndAddr") -> None: + """ + ``add_direct_code_reference`` adds a direct code reference to the current function. + + :param int target: The target address of the direct code reference + :param function.ArchAndAddr source: The source location of the direct code reference + """ + + if not isinstance(target, int): + raise TypeError("target must be an integer") + + if not isinstance(source, function.ArchAndAddr): + raise TypeError("source must be an instance of function.ArchAndAddr") + + self._direct_code_references[target] = source + + @property + def direct_no_return_calls(self) -> Set["function.ArchAndAddr"]: + """Get the set of direct no-return call locations in this context.""" + + return self._direct_no_return_calls + + def add_direct_no_return_call(self, loc: "function.ArchAndAddr") -> None: + """ + ``add_direct_no_return_call`` adds a direct no-return call location to the current function. + + :param function.ArchAndAddr loc: The location of the direct no-return call + """ + if not isinstance(loc, function.ArchAndAddr): + raise TypeError("loc must be an instance of function.ArchAndAddr") + + self._direct_no_return_calls.add(loc) + + @property + def halted_disassembly_addresses(self) -> Set["function.ArchAndAddr"]: + """Get the set of addresses where disassembly has been halted.""" + + return self._halted_disassembly_addresses + + def add_halted_disassembly_address(self, loc: "function.ArchAndAddr") -> None: + """ + ``add_halted_disassembly_address`` adds an address to the set of halted disassembly addresses. + + :param function.ArchAndAddr loc: The location of the halted disassembly address + """ + if not isinstance(loc, function.ArchAndAddr): + raise TypeError("loc must be an instance of function.ArchAndAddr") + + self._halted_disassembly_addresses.add(loc) + + def create_basic_block(self, arch: "Architecture", start: int) -> Optional["basicblock.BasicBlock"]: + """ + ``create_basic_block`` creates a new BasicBlock at the specified address for the given Architecture. + + :param Architecture arch: Architecture of the BasicBlock to create + :param int start: Address of the BasicBlock to create + """ + + if not isinstance(arch, Architecture): + raise TypeError("arch must be an instance of architecture.Architecture") + + bnblock = core.BNAnalyzeBasicBlocksContextCreateBasicBlock(self._handle, arch.handle, start) + if not bnblock: + return None + + view = binaryview.BinaryView(handle=core.BNGetFunctionData(self._function.handle)) + return basicblock.BasicBlock(bnblock, view) + + def add_basic_block(self, block: "basicblock.BasicBlock") -> None: + """ + ``add_basic_block`` adds a BasicBlock to the current function. + + :param basicblock.BasicBlock block: The BasicBlock to add + """ + if not isinstance(block, basicblock.BasicBlock): + raise TypeError("block must be an instance of basicblock.BasicBlock") + + core.BNAnalyzeBasicBlocksContextAddBasicBlockToFunction(self._handle, block.handle) + + def add_temp_outgoing_reference(self, target: "function.Function") -> None: + """ + ``add_temp_outgoing_reference`` adds a temporary outgoing reference to the specified function. + + :param function.Function target: The target function to add a temporary outgoing reference to + """ + if not isinstance(target, function.Function): + raise TypeError("target must be an instance of function.Function") + + core.BNAnalyzeBasicBlocksContextAddTempReference(self._handle, target.handle) + + def finalize(self) -> None: + """ + ``finalize`` finalizes the function's basic block analysis + """ + + if self._direct_code_references: + total = len(self._direct_code_references) + sources = (core.BNArchitectureAndAddress * total)() + targets = (ctypes.c_ulonglong * total)() + for i, (target, src) in enumerate(self._direct_code_references.items()): + sources[i].arch = src.arch.handle + sources[i].address = src.addr + targets[i] = target + + core.BNAnalyzeBasicBlocksContextSetDirectCodeReferences(self._handle, sources, targets, total) + + if self._direct_no_return_calls: + total = len(self._direct_no_return_calls) + direct_no_return_calls = (core.BNArchitectureAndAddress * total)() + for i, loc in enumerate(self._direct_no_return_calls): + direct_no_return_calls[i].arch = loc.arch.handle + direct_no_return_calls[i].address = loc.addr + core.BNAnalyzeBasicBlocksContextSetDirectNoReturnCalls(self._handle, direct_no_return_calls, total) + + self._halted_disassembly_addresses.add(function.ArchAndAddr(self._function.arch, 0)) + if self._halted_disassembly_addresses: + total = len(self._halted_disassembly_addresses) + halted_addresses = (core.BNArchitectureAndAddress * total)() + for i, loc in enumerate(self._halted_disassembly_addresses): + halted_addresses[i].arch = loc.arch.handle + halted_addresses[i].address = loc.addr + core.BNAnalyzeBasicBlocksContextSetHaltedDisassemblyAddresses(self._handle, halted_addresses, total) + + if self._contextual_returns_dirty: + total = len(self._contextual_returns) + values = (ctypes.c_bool * total)() + returns = (core.BNArchitectureAndAddress * total)() + for i, (loc, value) in enumerate(self._contextual_returns.items()): + returns[i].arch = loc.arch.handle + returns[i].address = loc.addr + values[i] = value + core.BNAnalyzeBasicBlocksContextSetContextualFunctionReturns(self._handle, returns, values, total) + + core.BNAnalyzeBasicBlocksContextFinalize(self._handle) @dataclass(frozen=True) @@ -725,20 +1035,8 @@ class Architecture(metaclass=_ArchitectureMetaClass): def _analyze_basic_blocks(self, ctx, func, ptr_bn_bb_context): try: - return core.BNArchitectureDefaultAnalyzeBasicBlocks(func, ptr_bn_bb_context) bn_bb_context = ptr_bn_bb_context.contents - indirect_branches = [] - for i in range(0, bn_bb_context.indirectBranchesCount): - ibi = IndirectBranchInfo() - ibi.source_arch = CoreArchitecture._from_cache(bn_bb_context.indirectBranches[i].sourceArch) - ibi.source_addr = bn_bb_context.indirectBranches[i].sourceAddr - ibi.dest_arch = CoreArchitecture._from_cache(bn_bb_context.indirectBranches[i].destArch) - ibi.dest_addr = bn_bb_context.indirectBranches[i].destAddr - ibi.auto_defined = bn_bb_context.indirectBranches[i].autoDefined - indirect_branches.append(ibi) - - context = BasicBlockAnalysisContext(indirect_branches, bn_bb_context.analysisSkipOverride, - bn_bb_context.translateTailCalls, bn_bb_context.disallowBranchToString, bn_bb_context.maxFunctionSize) + context = BasicBlockAnalysisContext.from_core_struct(bn_bb_context) self.analyze_basic_blocks(function.Function(handle=core.BNNewFunctionReference(func)), context) except: log_error(traceback.format_exc()) @@ -1465,24 +1763,11 @@ class Architecture(metaclass=_ArchitectureMetaClass): .. note:: Architecture subclasses should only implement this method if function-level analysis is required :param Function func: the function to analyze - :param BNBasicBlockAnalysisContext context: the analysis context + :param BasicBlockAnalysisContext context: the analysis context """ try: - bn_bb_context = core.BNBasicBlockAnalysisContext() - bn_bb_context.indirectBranchesCount = len(context.indirect_branches) - bn_bb_context.analysisSkipOverride = context.analysis_skip_override - bn_bb_context.translateTailCalls = context.translate_tail_calls - bn_bb_context.disallowBranchToString = context.disallow_branch_to_string - bn_bb_context.maxFunctionSize = context.max_function_size - bn_bb_context.indirectBranches = (core.BNIndirectBranchInfo * len(context.indirect_branches))() - for i in range(0, len(context.indirect_branches)): - bn_bb_context.indirectBranches[i].sourceArch = context.indirect_branches[i].source_arch.handle - bn_bb_context.indirectBranches[i].sourceAddr = context.indirect_branches[i].source_addr - bn_bb_context.indirectBranches[i].destArch = context.indirect_branches[i].dest_arch.handle - bn_bb_context.indirectBranches[i].destAddr = context.indirect_branches[i].dest_addr - bn_bb_context.indirectBranches[i].autoDefined = context.indirect_branches[i].auto_defined - core.BNArchitectureDefaultAnalyzeBasicBlocks(func.handle, ctypes.byref(bn_bb_context)) + core.BNArchitectureDefaultAnalyzeBasicBlocks(func.handle, context._handle) except: log_error(traceback.format_exc()) diff --git a/python/basicblock.py b/python/basicblock.py index 1148d887..9f02f2a9 100644 --- a/python/basicblock.py +++ b/python/basicblock.py @@ -478,7 +478,6 @@ class BasicBlock: .. note:: This method is intended for use by architecture plugins only. :return: List of PendingBasicBlockEdge objects. - :rtype: list[PendingBasicBlockEdge] """ count = ctypes.c_ulonglong(0) pending_edges = core.BNGetBasicBlockPendingOutgoingEdges(self.handle, ctypes.byref(count)) @@ -513,7 +512,6 @@ class BasicBlock: .. note:: This method is intended for use by architecture plugins only. :return: Raw instruction data as bytes. - :rtype: bytes """ size = ctypes.c_ulonglong(0) diff --git a/python/function.py b/python/function.py index 45863f21..f290bebd 100644 --- a/python/function.py +++ b/python/function.py @@ -2658,96 +2658,7 @@ class Function: return None return basicblock.BasicBlock(block, self._view) - def create_basic_block(self, arch: 'architecture.Architecture', addr: int) -> 'basicblock.BasicBlock': - """ - ``create_basic_block`` creates a new BasicBlock at the specified address for the given Architecture. - - .. note:: This method is intended for use by architecture plugins only. - - :param Architecture arch: Architecture of the BasicBlock to create - :param int addr: Address of the BasicBlock to create - :rtype: basicblock.BasicBlock - """ - if not isinstance(arch, architecture.Architecture): - raise TypeError("arch must be an instance of architecture.Architecture") - - bnblock = core.BNCreateFunctionBasicBlock(self.handle, arch.handle, addr) - if not bnblock: - return None - - return basicblock.BasicBlock(bnblock, self._view) - - def add_basic_block(self, block: 'basicblock.BasicBlock') -> None: - """ - ``add_basic_block`` adds the specified BasicBlock to the Function. - - .. note:: This method is intended for use by architecture plugins only. - - :param basicblock.BasicBlock block: BasicBlock to add to the Function - :rtype: None - """ - if not isinstance(block, basicblock.BasicBlock): - raise TypeError("block must be an instance of basicblock.BasicBlock") - - core.BNAddFunctionBasicBlock(self.handle, block.handle) - - def finalize_basic_blocks(self) -> None: - """ - ``finalize_basic_blocks`` finalizes the BasicBlocks in the Function, ensuring they are committed to analysis. - - .. note:: This method is intended for use by architecture plugins only. - - :rtype: None - """ - core.BNFinalizeFunctionBasicBlocks(self.handle) - - def add_direct_code_reference(self, source: ArchAndAddr, target: int) -> None: - """ - ``add_direct_code_reference`` adds a direct code reference from the source to the target address. - - .. note:: This method is intended for use by architecture plugins only. - - :param ArchAndAddr source: Source address and architecture of the reference - :param int target: Target address of the reference - :rtype: None - """ - bn_arch_and_addr = core.BNArchitectureAndAddress() - bn_arch_and_addr.arch = source.arch.handle - bn_arch_and_addr.address = source.addr - core.BNFunctionAddDirectCodeReference( - self.handle, bn_arch_and_addr, target - ) - - def add_direct_no_return_call(self, location: ArchAndAddr) -> None: - """ - ``add_direct_no_return_call`` adds a direct no return call site to the function at the specified location. - - .. note:: This method is intended for use by architecture plugins only. - - :param ArchAndAddr location: Location of the call site - :rtype: None - """ - bn_arch_and_addr = core.BNArchitectureAndAddress() - bn_arch_and_addr.arch = location.arch.handle - bn_arch_and_addr.address = location.addr - core.BNFunctionAddDirectNoReturnCall(self.handle, bn_arch_and_addr) - - def location_has_no_return_calls(self, location: ArchAndAddr) -> bool: - """ - ``location_has_no_return_calls`` checks if the specified location has any no return calls sites. - - .. note:: This method is intended for use by architecture plugins only. - - :param ArchAndAddr location: Location to check for no return calls - :return: True if there are no return calls at the specified location, False otherwise - :rtype: bool - """ - bn_arch_and_addr = core.BNArchitectureAndAddress() - bn_arch_and_addr.arch = location.arch.handle - bn_arch_and_addr.address = location.addr - return core.BNFunctionLocationHasNoReturnCalls(self.handle, bn_arch_and_addr) - - def get_callee_for_analysis(self, platform: 'platform.Platform', addr: int, exact: bool = False) -> Optional['Function']: + def get_callee_for_analysis(self, platform: '_platform.Platform', addr: int, exact: bool = False) -> Optional['Function']: """ ``get_callee_for_analysis`` retrieves the callee function for the specified address and platform. @@ -2764,79 +2675,6 @@ class Function: return None return Function(func, self._view) - def add_temp_outgoing_reference(self, target: 'Function') -> None: - """ - ``add_temp_outgoing_reference`` adds a temporary outgoing reference to the specified target function. - - .. note:: This method is intended for use by architecture plugins only. - - :param Function target: Target function to add a temporary outgoing reference to - :rtype: None - """ - if not isinstance(target, Function): - raise TypeError("target must be an instance of Function") - core.BNFunctionAddTempOutgoingReference(self.handle, target.handle) - - def has_temp_outgoing_reference(self, target: 'Function') -> bool: - """ - ``has_temp_outgoing_reference`` checks if the function has a temporary outgoing reference to the specified target function. - - .. note:: This method is intended for use by architecture plugins only. - - :param Function target: Target function to check for a temporary outgoing reference - :return: True if there is a temporary outgoing reference to the target function, False otherwise - :rtype: bool - """ - if not isinstance(target, Function): - raise TypeError("target must be an instance of Function") - return core.BNFunctionHasTempOutgoingReference(self.handle, target.handle) - - def add_temp_incoming_reference(self, source: 'Function') -> None: - """ - ``add_temp_incoming_reference`` adds a temporary incoming reference from the specified source function. - - .. note:: This method is intended for use by architecture plugins only. - - :param Function source: Source function to add a temporary incoming reference from - :rtype: None - """ - if not isinstance(source, Function): - raise TypeError("source must be an instance of Function") - core.BNFunctionAddTempIncomingReference(self.handle, source.handle) - - def get_contextual_function_return(self, location: ArchAndAddr) -> tuple[bool, bool]: - """ - ``get_contextual_function_return`` checks if the specified location has a contextual function return. - - .. note:: This method is intended for use by architecture plugins only. - - :param ArchAndAddr location: Location to check for a contextual function return - :return: A tuple containing a bool indicating whether the location has a contextual function return, and a bool containing the value that was set - :rtype: tuple[bool, bool] - """ - bn_arch_and_addr = core.BNArchitectureAndAddress() - bn_arch_and_addr.arch = location.arch.handle - bn_arch_and_addr.address = location.addr - result = ctypes.c_bool() - if not core.BNFunctionGetContextualFunctionReturn(self.handle, bn_arch_and_addr, ctypes.byref(result)): - return (False, False) - return (True, result.value) - - def set_contextual_function_return(self, location: ArchAndAddr, value: bool) -> None: - """ - ``set_contextual_function_return`` sets a contextual function return for the specified location. - - .. note:: This method is intended for use by architecture plugins only. - - :param ArchAndAddr location: Location to set the contextual function return for - :param bool value: Value to set for the contextual function return - :rtype: None - """ - bn_arch_and_addr = core.BNArchitectureAndAddress() - bn_arch_and_addr.arch = location.arch.handle - bn_arch_and_addr.address = location.addr - core.BNFunctionSetContextualFunctionReturn(self.handle, bn_arch_and_addr, value) - def get_instr_highlight( self, addr: int, arch: Optional['architecture.Architecture'] = None ) -> '_highlight.HighlightColor': -- cgit v1.3.1