diff options
Diffstat (limited to 'python/basedetection.py')
| -rw-r--r-- | python/basedetection.py | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/python/basedetection.py b/python/basedetection.py new file mode 100644 index 00000000..655f017b --- /dev/null +++ b/python/basedetection.py @@ -0,0 +1,321 @@ +# coding=utf-8 +# 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 os +import ctypes +from typing import Optional, Union, Literal +from dataclasses import dataclass +from .enums import BaseAddressDetectionPOIType, BaseAddressDetectionConfidence, BaseAddressDetectionPOISetting +from .binaryview import BinaryView +from . import _binaryninjacore as core + + +@dataclass +class BaseAddressDetectionReason: + """``class BaseAddressDetectionReason`` is a class that stores information used to understand why a base address + is a candidate. It consists of a pointer, the offset of the point-of-interest that the pointer aligns with, and the + type of point-of-interest (string, function, or data variable)""" + + pointer: int + offset: int + type: BaseAddressDetectionPOIType + + +class BaseAddressDetection: + """ + ``class BaseAddressDetection`` is a class that is used to detect candidate base addresses for position-dependent + raw binaries + + :Example: + + >>> from binaryninja import * + >>> bad = BaseAddressDetection("firmware.bin") + >>> bad.detect_base_address() + True + >>> hex(bad.preferred_base_address) + '0x4000000' + """ + + def __init__(self, view: Union[str, os.PathLike, BinaryView]) -> None: + if isinstance(view, str) or isinstance(view, os.PathLike): + view = BinaryView.load(str(view), update_analysis=False) + + _handle = core.BNCreateBaseAddressDetection(view.handle) + assert _handle is not None, "core.BNCreateBaseAddressDetection returned None" + self._handle = _handle + self._view_arch = view.arch + + self._scores = list() + self._confidence = 0 + self._last_tested_base_address = None + + def __del__(self): + if core is not None: + core.BNFreeBaseAddressDetection(self._handle) + + @property + def scores(self) -> list[tuple[int, int]]: + """ + ``scores`` returns a list of candidate base addresses and their scores + + .. note:: The score is set to the number of times a pointer pointed to a point-of-interest at that base address + + :Example: + + >>> from binaryninja import * + >>> bad = BaseAddressDetection("firmware.bin") + >>> bad.detect_base_address() + True + >>> for addr, score in bad.scores: + ... print(f"0x{addr:x}: {score}") + ... + 0x4000000: 7 + 0x400dc00: 1 + 0x400d800: 1 + 0x400cc00: 1 + 0x400c400: 1 + 0x400bc00: 1 + 0x400b800: 1 + 0x3fffc00: 1 + + :return: list of tuples containing each base address and score + :rtype: list[tuple[int, int]] + """ + + return self._scores + + @property + def confidence(self) -> BaseAddressDetectionConfidence: + """ + ``confidence`` returns an enum that indicates confidence the preferred candidate base address is correct + + :return: confidence of the base address detection results + :rtype: BaseAddressDetectionConfidence + """ + + return self._confidence + + @property + def last_tested_base_address(self) -> int: + """ + ``last_tested_base_address`` returns the last candidate base address that was tested + + .. note:: This is useful for situations where the user aborts the analysis and wants to restart from the last \ + tested base address by setting the ``low_boundary`` parameter in :py:func:`BaseAddressDetection.detect_base_address` + + :return: last candidate base address tested + :rtype: int + """ + + return self._last_tested_base_address + + @property + def preferred_base_address(self) -> Optional[int]: + """ + ``preferred_base_address`` returns the candidate base address which contains the most amount of pointers that + align with discovered points-of-interest in the binary + + .. note:: :py:attr:`BaseAddressDetection.confidence` reports a confidence level that the preferred base is correct + + .. note:: :py:attr:`BaseAddressDetection.scores` returns a list of the top 10 candidate base addresses and their \ + scores and can be used to discover other potential candidates + + :return: preferred candidate base address + :rtype: int + """ + + if not self._scores: + return None + + return self._scores[0][0] + + @property + def aborted(self) -> bool: + """ + ``aborted`` indicates whether or not base address detection analysis was aborted early + + :return: True if the analysis was aborted, False otherwise + :rtype: bool + """ + + return core.BNIsBaseAddressDetectionAborted(self._handle) + + def detect_base_address( + self, + arch: Optional[str] = "", + analysis: Optional[str] = Literal["basic", "controlFlow", "full"], + min_strlen: Optional[int] = 10, + alignment: Optional[int] = 1024, + low_boundary: Optional[int] = 0, + high_boundary: Optional[int] = 0xFFFFFFFFFFFFFFFF, + poi_analysis: Optional[BaseAddressDetectionPOISetting] = BaseAddressDetectionPOISetting.POIAnalysisAll, + max_pointers: Optional[int] = 128, + ) -> bool: + """ + ``detect_base_address`` runs initial analysis and attempts to identify candidate base addresses + + .. note:: This operation can take a long time to complete depending on the size and complexity of the binary \ + and the settings used + + :param str arch: CPU architecture of the binary (defaults to using auto-detection) + :param str analysis: analysis mode (``basic``, ``controlFlow``, or ``full``) + :param int min_strlen: minimum length of a string to be considered a point-of-interest + :param int alignment: byte boundary to align the base address to while brute-forcing + :param int low_boundary: lower boundary of the base address range to test + :param int high_boundary: upper boundary of the base address range to test + :param BaseAddressDetectionPOISetting poi_analysis: specifies types of points-of-interest to use for analysis + :param int max_pointers: maximum number of candidate pointers to collect per pointer cluster + :return: True if initial analysis completed with results, False otherwise + :rtype: bool + """ + + if not arch and self._view_arch: + arch = str(self._view_arch) + + if analysis not in ["basic", "controlFlow", "full"]: + raise ValueError("invalid analysis setting") + + if alignment <= 0: + raise ValueError("alignment must be greater than 0") + + if max_pointers < 2: + raise ValueError("max pointers must be at least 2") + + if high_boundary < low_boundary: + raise ValueError("upper boundary must be greater than lower boundary") + + settings = core.BNBaseAddressDetectionSettings( + arch.encode(), + analysis.encode(), + min_strlen, + alignment, + low_boundary, + high_boundary, + poi_analysis, + max_pointers, + ) + + if not core.BNDetectBaseAddress(self._handle, settings): + return False + + max_candidates = 10 + scores = (core.BNBaseAddressDetectionScore * max_candidates)() + confidence = core.BaseAddressDetectionConfidenceEnum() + last_base = ctypes.c_ulonglong() + num_candidates = core.BNGetBaseAddressDetectionScores( + self._handle, scores, max_candidates, ctypes.byref(confidence), ctypes.byref(last_base) + ) + + if num_candidates == 0: + return False + + self._scores.clear() + for i in range(num_candidates): + self._scores.append((scores[i].BaseAddress, scores[i].Score)) + + self._confidence = confidence.value + self._last_tested_base_address = last_base.value + return True + + def abort(self) -> None: + """ + ``abort`` aborts base address detection analysis + + .. note:: ``abort`` does not stop base address detection until after initial analysis has completed and it is \ + in the base address enumeration phase + + :rtype: None + """ + + core.BNAbortBaseAddressDetection(self._handle) + + def get_reasons(self, base_address: int) -> list[BaseAddressDetectionReason]: + """ + ``get_reasons`` returns a list of reasons that can be used to determine why a base address is a candidate + + :param int base_address: base address to get reasons for + :return: list of reasons for the specified base address + :rtype: list[BaseAddressDetectionReason] + """ + + count = ctypes.c_size_t() + reasons = core.BNGetBaseAddressDetectionReasons(self._handle, base_address, ctypes.byref(count)) + if count.value == 0: + return [] + + try: + result = list() + for i in range(count.value): + result.append(BaseAddressDetectionReason(reasons[i].Pointer, reasons[i].POIOffset, reasons[i].POIType)) + return result + finally: + core.BNFreeBaseAddressDetectionReasons(reasons) + + def _get_data_hits_by_type(self, base_address: int, poi_type: int) -> int: + reasons = self.get_reasons(base_address) + if not reasons: + return 0 + + hits = 0 + for reason in reasons: + if reason.type == poi_type: + hits += 1 + + return hits + + def get_string_hits(self, base_address: int) -> int: + """ + ``get_string_hits`` returns the number of times a pointer pointed to a string at the specified + base address + + .. note:: Data variables are only used as points-of-interest if analysis doesn't discover enough strings and \ + functions + + :param int base_address: base address to get string hits for + :return: number of string hits for the specified base address + :rtype: int + """ + + return self._get_data_hits_by_type(base_address, BaseAddressDetectionPOIType.POIString) + + def get_function_hits(self, base_address: int) -> int: + """ + ``get_function_hits`` returns the number of times a pointer pointed to a function at the + specified base address + + :param int base_address: base address to get function hits for + :return: number of function hits for the specified base address + :rtype: int + """ + + return self._get_data_hits_by_type(base_address, BaseAddressDetectionPOIType.POIFunction) + + def get_data_hits(self, base_address: int) -> int: + """ + ``get_data_hits`` returns the number of times a pointer pointed to a data variable at the + specified base address + + :param int base_address: base address to get data hits for + :return: number of data hits for the specified base address + :rtype: int + """ + + return self._get_data_hits_by_type(base_address, BaseAddressDetectionPOIType.POIDataVariable) |
