diff options
| author | Brandon Miller <brandon@vector35.com> | 2024-11-22 08:20:14 -0500 |
|---|---|---|
| committer | Brandon Miller <brandon@vector35.com> | 2024-11-22 08:20:53 -0500 |
| commit | c74fa0d5f974efa4ccd569a821701956063a31e9 (patch) | |
| tree | ed20c1e13e22df9c5d3867294654a28931f4fbe7 /python/firmwareninja.py | |
| parent | 3617086969462676272786827089388047502883 (diff) | |
C++ and Python API for Firmware Ninja
Diffstat (limited to 'python/firmwareninja.py')
| -rw-r--r-- | python/firmwareninja.py | 468 |
1 files changed, 468 insertions, 0 deletions
diff --git a/python/firmwareninja.py b/python/firmwareninja.py new file mode 100644 index 00000000..b000ecc9 --- /dev/null +++ b/python/firmwareninja.py @@ -0,0 +1,468 @@ +# 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 ctypes +from dataclasses import dataclass +from typing import Callable +from .binaryview import BinaryView +from .variable import RegisterValue +from .enums import ( + FirmwareNinjaMemoryHeuristic, + FirmwareNinjaMemoryAccessType, + FirmwareNinjaSectionAnalysisMode, + FirmwareNinjaSectionType, +) +from .function import Function +from . import _binaryninjacore as core + + +@dataclass +class FirmwareNinjaDevice: + """ + ``class FirmwareNinjaDevice`` is a class that stores information about a hardware device, including the device + name, start address, size, and information about the device. + """ + + name: str + start: int + size: int + info: str + + +@dataclass +class FirmwareNinjaSection: + """ + ``class FirmwareNinjaSection`` is a class that stores information about a section identified with Firmware Ninja + analysis, including the section type, start address, size, and entropy of the section. + """ + + type: FirmwareNinjaSectionType + start: int + size: int + entropy: float + + +@dataclass +class FirmwareNinjaMemoryAccess: + """ + ``class FirmwareNinjaMemoryAccess`` is a class that stores information on instructions that access regions of + memory that are not file-backed, such as memory-mapped I/O and RAM. + """ + + instr_address: int + mem_address: RegisterValue + heuristic: FirmwareNinjaMemoryHeuristic + type: FirmwareNinjaMemoryAccessType + value: RegisterValue + + @classmethod + def from_BNFirmwareNinjaMemoryAccess(cls, access: core.BNFirmwareNinjaMemoryAccess) -> "FirmwareNinjaMemoryAccess": + return cls( + instr_address=access.instrAddress, + mem_address=RegisterValue.from_BNRegisterValue(access.memAddress), + heuristic=FirmwareNinjaMemoryHeuristic(access.heuristic), + type=FirmwareNinjaMemoryAccessType(access.type), + value=RegisterValue.from_BNRegisterValue(access.value), + ) + + @classmethod + def to_BNFirmwareNinjaMemoryAccess(cls, access: "FirmwareNinjaMemoryAccess") -> core.BNFirmwareNinjaMemoryAccess: + return core.BNFirmwareNinjaMemoryAccess( + instrAddress=access.instr_address, + memAddress=RegisterValue.to_BNRegisterValue(access.mem_address), + heuristic=access.heuristic, + type=access.type, + value=RegisterValue.to_BNRegisterValue(access.value), + ) + + +@dataclass +class FirmwareNinjaFunctionMemoryAccesses: + """ + ``class FirmwareNinjaFunctionMemoryAccesses`` is a class that stores information on accesses made by a function + to memory regions that are not file-backed, such as memory-mapped I/O and RAM. + """ + + function: Function + accesses: list[FirmwareNinjaMemoryAccess] + + @classmethod + def from_BNFirmwareNinjaFunctionMemoryAccesses( + cls, + info: core.BNFirmwareNinjaFunctionMemoryAccesses, + view: BinaryView, + ) -> "FirmwareNinjaFunctionMemoryAccesses": + accesses = [] + for i in range(info.count): + access = info.accesses[i] + accesses.append(FirmwareNinjaMemoryAccess.from_BNFirmwareNinjaMemoryAccess(access.contents)) + + return cls( + function=view.get_function_at(info.start), + accesses=accesses, + ) + + +@dataclass +class FirmwareNinjaDeviceAccesses: + """ + ``class FirmwareNinjaDeviceAccesses`` is a class that stores information on the number of accesses to hardware + devices for each board that is compatible with the current architecture. This information can be used to identify + a board based on the number of accesses to hardware devices. + """ + + board_name: str + total: int + unique: int + + +class FirmwareNinja: + """ + ``class FirmwareNinja`` is a class that aids in analysis of embedded firmware images. This class is only available + in the Ultimate Edition of Binary Ninja. + + :Example: + + >>> from binaryninja import * + >>> view = load("path/to/firmware.bin", options={"loader.imageBase": 0x100000}) + >>> fwn = FirmwareNinja(view) + >>> fwn.get_function_memory_accesses()[0].accesses[0].mem_address + <const ptr 0x40090028> + """ + + def __init__(self, view: BinaryView) -> None: + self._view = view + self._handle = core.BNCreateFirmwareNinja(view.handle) + + def __del__(self): + if core is not None: + core.BNFreeFirmwareNinja(self._handle) + + def store_custom_device(self, name: str, start: int, size: int, info: str) -> bool: + """ + ``store_custom_device`` store a user-defined Firmware Ninja device in the binary view metadata + + :param str name: Name of the device + :param int start: Start address of the device + :param int size: Size of the device memory region + :param str info: Information about the device + :return: True on success, False on failure + :rtype: bool + """ + + return core.BNFirmwareNinjaStoreCustomDevice(self._handle, name, start, start + size, info) + + def remove_custom_device(self, name: str) -> bool: + """ + ``remove_custom_device`` removes a user-defined Firmware Ninja device from the binary view metadata by device + name + + :param str name: Name of the device + :return: True on success, False on failure + :rtype: bool + """ + + return core.BNFirmwareNinjaRemoveCustomDevice(self._handle, name) + + def query_custom_devices(self) -> list[FirmwareNinjaDevice]: + """ + ``query_custom_devices`` queries user-defined Firmware Ninja devices from the binary view metadata + + :return: List of Firmware Ninja device objects + :rtype: list[FirmwareNinjaDevice] + """ + + devices = ctypes.POINTER(core.BNFirmwareNinjaDevice)() + count = core.BNFirmwareNinjaQueryCustomDevices(self._handle, ctypes.byref(devices)) + if count == -1: + raise RuntimeError("BNFirmwareNinjaQueryCustomDevices") + + try: + device_list = [] + for i in range(count): + device_list.append( + FirmwareNinjaDevice( + name=devices[i].name, + start=devices[i].start, + size=devices[i].end - devices[i].start, + info=devices[i].info, + ) + ) + + return device_list + finally: + core.BNFirmwareNinjaFreeDevices(devices, count) + + def query_board_names(self) -> list[str]: + """ + ``query_board_names`` queries the name of all boards that are compatible with the current architecture + + :return: List of board names + :rtype: list[str] + """ + + boards = ctypes.POINTER(ctypes.c_char_p)() + count = core.BNFirmwareNinjaQueryBoardNamesForArchitecture( + self._handle, self._view.arch.handle, ctypes.byref(boards) + ) + if count == -1: + raise RuntimeError("BNFirmwareNinjaQueryBoardNamesForArchitecture") + + try: + board_list = [] + for i in range(count): + board_list.append(boards[i].decode("utf-8")) + + return board_list + finally: + core.BNFirmwareNinjaFreeBoardNames(boards, count) + + def query_devices_by_board_name(self, name: str) -> list[FirmwareNinjaDevice]: + """ + ``query_devices_by_board_name`` queries the hardware device information for a specific board + + :Example: + + >>> fwn = FirmwareNinja(bv) + >>> fwn.query_devices_by_board_name(fwn.query_board_names()[0])[0] + FirmwareNinjaDevice(name='nand@12f', start=303, size=1024, info='marvell,orion-nand') + + :param str name: Name of the board + :return: List of Firmware Ninja device objects + :rtype: list[FirmwareNinjaDevice] + """ + + devices = ctypes.POINTER(core.BNFirmwareNinjaDevice)() + count = core.BNFirmwareNinjaQueryBoardDevices(self._handle, self._view.arch.handle, name, ctypes.byref(devices)) + if count == -1: + raise RuntimeError("BNFirmwareNinjaQueryBoardDevices") + + try: + device_list = [] + for i in range(count): + device_list.append( + FirmwareNinjaDevice( + name=devices[i].name, + start=devices[i].start, + size=devices[i].end - devices[i].start, + info=devices[i].info, + ) + ) + + return device_list + finally: + core.BNFirmwareNinjaFreeDevices(devices, count) + + def find_sections( + self, + high_code_entropy_threshold: float = 0.910, + low_code_entropy_threshold: float = 0.500, + block_size: int = 4096, + mode: FirmwareNinjaSectionAnalysisMode = FirmwareNinjaSectionAnalysisMode.DetectStringsSectionAnalysisMode, + ) -> list[FirmwareNinjaSection]: + """ + ``find_sections`` finds sections with Firmware Ninja entropy analysis and heuristics + + :Example: + + >>> fwn = FirmwareNinja(bv) + >>> fwn.find_sections(block_size=2048)[0].entropy + 0.48716872930526733 + >>> fwn.find_sections(block_size=2048)[0].type + <FirmwareNinjaSectionType.DataSectionType: 1> + + :param float high_code_entropy_threshold: High code entropy threshold + :param float low_code_entropy_threshold: Low code entropy threshold + :param int block_size: Block size + :param str mode: Analysis mode + :return: List of sections + :rtype: list[FirmwareNinjaSection] + """ + + sections = ctypes.POINTER(core.BNFirmwareNinjaSection)() + count = core.BNFirmwareNinjaFindSectionsWithEntropy( + self._handle, + ctypes.byref(sections), + high_code_entropy_threshold, + low_code_entropy_threshold, + block_size, + mode, + ) + if count == -1: + raise RuntimeError("BNFirmwareNinjaFindSectionsWithEntropy") + + try: + section_list = [] + for i in range(count): + section_list.append( + FirmwareNinjaSection( + type=FirmwareNinjaSectionType(sections[i].type), + start=sections[i].start, + size=sections[i].end - sections[i].start, + entropy=sections[i].entropy, + ) + ) + + return section_list + finally: + core.BNFirmwareNinjaFreeSections(sections, count) + + def get_function_memory_accesses(self, progress_func: Callable = None) -> list[FirmwareNinjaFunctionMemoryAccesses]: + """ + ``get_function_memory_accesses`` runs analysis to find accesses to memory regions that are not file-backed, such + as memory-mapped I/O and RAM. + + :param callback progress_func: optional function to be called with the current progress and total count. + :return: List of function memory accesses objects + :rtype: list[FirmwareNinjaFunctionMemoryAccesses] + """ + + fma_info = ctypes.POINTER((ctypes.POINTER(core.BNFirmwareNinjaFunctionMemoryAccesses)))() + if progress_func is None: + progress_cfunc = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_ulonglong, ctypes.c_ulonglong)( + lambda ctxt, cur, total: True + ) + else: + progress_cfunc = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_ulonglong, ctypes.c_ulonglong)( + lambda ctxt, cur, total: progress_func(cur, total) + ) + + count = core.BNFirmwareNinjaGetFunctionMemoryAccesses( + self._handle, ctypes.byref(fma_info), progress_cfunc, None + ) + if count == -1: + raise RuntimeError("BNFirmwareNinjaGetFunctionMemoryAccesses") + + try: + fma_info_list = [] + for i in range(count): + fma_info_list.append( + FirmwareNinjaFunctionMemoryAccesses.from_BNFirmwareNinjaFunctionMemoryAccesses( + fma_info[i].contents, self._view + ) + ) + + return fma_info_list + finally: + core.BNFirmwareNinjaFreeFunctionMemoryAccesses(fma_info, count) + + def _fma_info_list_to_array(self, fma: list[FirmwareNinjaFunctionMemoryAccesses]) -> ctypes.POINTER: + fma_info_ptr_array = (ctypes.POINTER(core.BNFirmwareNinjaFunctionMemoryAccesses) * len(fma))() + for i, info in enumerate(fma): + accesses_ptr_array = (ctypes.POINTER(core.BNFirmwareNinjaMemoryAccess) * len(info.accesses))() + for j, access in enumerate(info.accesses): + accesses_ptr_array[j] = ctypes.pointer(FirmwareNinjaMemoryAccess.to_BNFirmwareNinjaMemoryAccess(access)) + + fma_info_struct = core.BNFirmwareNinjaFunctionMemoryAccesses( + function=info.function.handle, + accesses=accesses_ptr_array, + count=len(info.accesses), + ) + + fma_info_ptr_array[i] = ctypes.pointer(fma_info_struct) + + return fma_info_ptr_array + + def store_function_memory_accesses(self, fma: list[FirmwareNinjaFunctionMemoryAccesses]) -> None: + """ + ``store_function_memory_accesses`` saves information on function memory accesses to binary view metadata + + :Example: + + >>> fwn = FirmwareNinja(bv) + >>> fma = fwn.get_function_memory_accesses() + >>> fwn.store_function_memory_accesses(fma) + + :param list[FirmwareNinjaFunctionMemoryAccesses] fma: List of function memory accesses objects + :return: None + :rtype: None + """ + + fma_info_ptr_array = self._fma_info_list_to_array(fma) + core.BNFirmwareNinjaStoreFunctionMemoryAccessesToMetadata(self._handle, fma_info_ptr_array, len(fma)) + + def query_function_memory_accesses(self) -> list[FirmwareNinjaFunctionMemoryAccesses]: + """ + ``query_function_memory_accesses`` queries information on function memory accesses from binary view metadata + + :return: List of function memory accesses objects + :rtype: list[FirmwareNinjaFunctionMemoryAccesses] + """ + + fma = ctypes.POINTER((ctypes.POINTER(core.BNFirmwareNinjaFunctionMemoryAccesses)))() + count = core.BNFirmwareNinjaQueryFunctionMemoryAccessesFromMetadata(self._handle, ctypes.byref(fma)) + if count == -1: + return None + + try: + fma_info_list = [] + for i in range(count): + fma_info_list.append( + FirmwareNinjaFunctionMemoryAccesses.from_BNFirmwareNinjaFunctionMemoryAccesses( + fma[i].contents, self._view + ) + ) + + return fma_info_list + finally: + core.BNFirmwareNinjaFreeFunctionMemoryAccesses(fma, count) + + def get_board_device_accesses( + self, fma: list[FirmwareNinjaFunctionMemoryAccesses] + ) -> list[FirmwareNinjaDeviceAccesses]: + """ + ``get_board_device_accesses`` counts accesses made to memory-mapped hardware devices for each board that is + compatible with the current architecture. This function can be used to help identify a board. + + :Example: + + >>> fwn = FirmwareNinja(bv) + >>> fma = fwn.get_function_memory_accesses() + >>> fwn.get_board_device_accesses(fma)[0] + FirmwareNinjaDeviceAccesses(board_name='stm32mp157c-dhcom-picoitx', total=414, unique=2) + + :param list[FirmwareNinjaFunctionMemoryAccesses] fma: List of function memory accesses objects + :return: List of device accesses objects + :rtype: list[FirmwareNinjaDeviceAccesses] + """ + + fma_info_ptr_array = self._fma_info_list_to_array(fma) + device_accesses = ctypes.POINTER(core.BNFirmwareNinjaDeviceAccesses)() + count = core.BNFirmwareNinjaGetBoardDeviceAccesses( + self._handle, fma_info_ptr_array, len(fma), ctypes.byref(device_accesses), self._view.arch.handle + ) + if count == -1: + raise RuntimeError("BNFirmwareNinjaGetBoardDeviceAccesses") + + try: + device_accesses_list = [] + for i in range(count): + device_accesses_list.append( + FirmwareNinjaDeviceAccesses( + board_name=device_accesses[i].name, + total=device_accesses[i].total, + unique=device_accesses[i].unique, + ) + ) + + return device_accesses_list + finally: + core.BNFirmwareNinjaFreeBoardDeviceAccesses(device_accesses, count) |
