diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a599a52..9278af610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ Unlock powerful malware analysis with capa's new [VMRay sandbox](https://www.vmr - add .justfile @williballenthin #2325 - dynamic: add support for VMRay dynamic sandbox traces #2208 @mike-hunhoff @r-sm2024 @mr-tz - cli: use modern terminal features to hyperlink to the rules website #2337 @williballenthin - +- update IDAPython to IDA Pro 9.0 @mr-tz + ### Breaking Changes ### New Rules (0) diff --git a/capa/features/extractors/ida/file.py b/capa/features/extractors/ida/file.py index 8e53d2a84..78200e438 100644 --- a/capa/features/extractors/ida/file.py +++ b/capa/features/extractors/ida/file.py @@ -14,6 +14,7 @@ import idautils import ida_entry +import capa.ida.helpers import capa.features.extractors.common import capa.features.extractors.helpers import capa.features.extractors.strings @@ -177,17 +178,17 @@ def extract_file_function_names() -> Iterator[Tuple[Feature, Address]]: def extract_file_format() -> Iterator[Tuple[Feature, Address]]: - file_info = idaapi.get_inf_structure() + filetype = capa.ida.helpers.get_filetype() - if file_info.filetype in (idaapi.f_PE, idaapi.f_COFF): + if filetype in (idaapi.f_PE, idaapi.f_COFF): yield Format(FORMAT_PE), NO_ADDRESS - elif file_info.filetype == idaapi.f_ELF: + elif filetype == idaapi.f_ELF: yield Format(FORMAT_ELF), NO_ADDRESS - elif file_info.filetype == idaapi.f_BIN: + elif filetype == idaapi.f_BIN: # no file type to return when processing a binary file, but we want to continue processing return else: - raise NotImplementedError(f"unexpected file format: {file_info.filetype}") + raise NotImplementedError(f"unexpected file format: {filetype}") def extract_features() -> Iterator[Tuple[Feature, Address]]: diff --git a/capa/features/extractors/ida/global_.py b/capa/features/extractors/ida/global_.py index 047bfd0ad..3c5f4623e 100644 --- a/capa/features/extractors/ida/global_.py +++ b/capa/features/extractors/ida/global_.py @@ -9,7 +9,6 @@ import contextlib from typing import Tuple, Iterator -import idaapi import ida_loader import capa.ida.helpers @@ -48,12 +47,12 @@ def extract_os() -> Iterator[Tuple[Feature, Address]]: def extract_arch() -> Iterator[Tuple[Feature, Address]]: - info: idaapi.idainfo = idaapi.get_inf_structure() - if info.procname == "metapc" and info.is_64bit(): + procname = capa.ida.helpers.get_processor_name() + if procname == "metapc" and capa.ida.helpers.is_64bit(): yield Arch(ARCH_AMD64), NO_ADDRESS - elif info.procname == "metapc" and info.is_32bit(): + elif procname == "metapc" and capa.ida.helpers.is_32bit(): yield Arch(ARCH_I386), NO_ADDRESS - elif info.procname == "metapc": + elif procname == "metapc": logger.debug("unsupported architecture: non-32-bit nor non-64-bit intel") return else: @@ -61,5 +60,5 @@ def extract_arch() -> Iterator[Tuple[Feature, Address]]: # 1. handling a new architecture (e.g. aarch64) # # for (1), this logic will need to be updated as the format is implemented. - logger.debug("unsupported architecture: %s", info.procname) + logger.debug("unsupported architecture: %s", procname) return diff --git a/capa/features/extractors/ida/helpers.py b/capa/features/extractors/ida/helpers.py index 1c9cf29de..5ddc3c7d2 100644 --- a/capa/features/extractors/ida/helpers.py +++ b/capa/features/extractors/ida/helpers.py @@ -21,6 +21,8 @@ IDA_NALT_ENCODING = ida_nalt.get_default_encoding_idx(ida_nalt.BPU_1B) # use one byte-per-character encoding +# TODO (mr): use find_bytes +# https://github.com/mandiant/capa/issues/2339 def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]: """yield all ea of a given byte sequence @@ -38,7 +40,7 @@ def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]: return while True: - ea = ida_bytes.bin_search(start, end, patterns, ida_bytes.BIN_SEARCH_FORWARD) + ea, _ = ida_bytes.bin_search3(start, end, patterns, ida_bytes.BIN_SEARCH_FORWARD) if ea == idaapi.BADADDR: break start = ea + 1 diff --git a/capa/ida/helpers.py b/capa/ida/helpers.py index 02046ecf1..547099f47 100644 --- a/capa/ida/helpers.py +++ b/capa/ida/helpers.py @@ -13,6 +13,7 @@ import idc import idaapi +import ida_ida import idautils import ida_bytes import ida_loader @@ -45,6 +46,39 @@ NETNODE_RULES_CACHE_ID = "rules-cache-id" +# wrappers for IDA Pro (IDAPython) 7, 8 and 9 compability +version = float(idaapi.get_kernel_version()) +if version < 9.0: + + def get_filetype() -> "ida_ida.filetype_t": + return idaapi.get_inf_structure().filetype + + def get_processor_name() -> str: + return idaapi.get_inf_structure().procname + + def is_32bit() -> bool: + info: idaapi.idainfo = idaapi.get_inf_structure() + return info.is_32bit() + + def is_64bit() -> bool: + info: idaapi.idainfo = idaapi.get_inf_structure() + return info.is_64bit() + +else: + + def get_filetype() -> "ida_ida.filetype_t": + return ida_ida.inf_get_filetype() + + def get_processor_name() -> str: + return idc.get_processor_name() + + def is_32bit() -> bool: + return idaapi.inf_is_32bit_exactly() + + def is_64bit() -> bool: + return idaapi.inf_is_64bit() + + def inform_user_ida_ui(message): # this isn't a logger, this is IDA's logging facility idaapi.info(f"{message}. Please refer to IDA Output window for more information.") # noqa: G004 @@ -52,17 +86,16 @@ def inform_user_ida_ui(message): def is_supported_ida_version(): version = float(idaapi.get_kernel_version()) - if version < 7.4 or version >= 9: + if version < 7.4 or version >= 10: warning_msg = "This plugin does not support your IDA Pro version" logger.warning(warning_msg) - logger.warning("Your IDA Pro version is: %s. Supported versions are: IDA >= 7.4 and IDA < 9.0.", version) + logger.warning("Your IDA Pro version is: %s. Supported versions are: IDA >= 7.4 and IDA < 10.0.", version) return False return True def is_supported_file_type(): - file_info = idaapi.get_inf_structure() - if file_info.filetype not in SUPPORTED_FILE_TYPES: + if get_filetype() not in SUPPORTED_FILE_TYPES: logger.error("-" * 80) logger.error(" Input file does not appear to be a supported file type.") logger.error(" ") @@ -76,8 +109,7 @@ def is_supported_file_type(): def is_supported_arch_type(): - file_info = idaapi.get_inf_structure() - if file_info.procname not in SUPPORTED_ARCH_TYPES or not any((file_info.is_32bit(), file_info.is_64bit())): + if get_processor_name() not in SUPPORTED_ARCH_TYPES or not any((is_32bit(), is_64bit())): logger.error("-" * 80) logger.error(" Input file does not appear to target a supported architecture.") logger.error(" ") @@ -125,10 +157,10 @@ def collect_metadata(rules: List[Path]): md5 = get_file_md5() sha256 = get_file_sha256() - info: idaapi.idainfo = idaapi.get_inf_structure() - if info.procname == "metapc" and info.is_64bit(): + procname = get_processor_name() + if procname == "metapc" and is_64bit(): arch = "x86_64" - elif info.procname == "metapc" and info.is_32bit(): + elif procname == "metapc" and is_32bit(): arch = "x86" else: arch = "unknown arch" diff --git a/pyproject.toml b/pyproject.toml index 0b9567b14..b2ff70008 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,6 +183,7 @@ known_first_party = [ "binaryninja", "flirt", "ghidra", + "ida_ida", "ida_bytes", "ida_entry", "ida_funcs",