diff --git a/CHANGELOG.md b/CHANGELOG.md index 020ad343..2214d323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### New Features - ghidra: add Ghidra feature extractor and supporting code #1770 @colton-gabertan - ghidra: add entry script helping users run capa against a loaded Ghidra database #1767 @mike-hunhoff +- binja: add support for forwarded exports #1646 @xusheng6 - binja: add support for symtab names #1504 @xusheng6 ### Breaking Changes diff --git a/capa/features/extractors/binja/file.py b/capa/features/extractors/binja/file.py index 034b1636..84b25348 100644 --- a/capa/features/extractors/binja/file.py +++ b/capa/features/extractors/binja/file.py @@ -17,7 +17,7 @@ import capa.features.extractors.strings from capa.features.file import Export, Import, Section, FunctionName from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress -from capa.features.extractors.binja.helpers import unmangle_c_name +from capa.features.extractors.binja.helpers import read_c_string, unmangle_c_name def check_segment_for_pe(bv: BinaryView, seg: Segment) -> Iterator[Tuple[int, int]]: @@ -82,6 +82,24 @@ def extract_file_export_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address if name != unmangled_name: yield Export(unmangled_name), AbsoluteVirtualAddress(sym.address) + for sym in bv.get_symbols_of_type(SymbolType.DataSymbol): + if sym.binding not in [SymbolBinding.GlobalBinding]: + continue + + name = sym.short_name + if not name.startswith("__forwarder_name"): + continue + + # Due to https://github.com/Vector35/binaryninja-api/issues/4641, in binja version 3.5, the symbol's name + # does not contain the DLL name. As a workaround, we read the C string at the symbol's address, which contains + # both the DLL name and the function name. + # Once the above issue is closed in the next binjs stable release, we can update the code here to use the + # symbol name directly. + name = read_c_string(bv, sym.address, 1024) + forwarded_name = capa.features.extractors.helpers.reformat_forwarded_export_name(name) + yield Export(forwarded_name), AbsoluteVirtualAddress(sym.address) + yield Characteristic("forwarded export"), AbsoluteVirtualAddress(sym.address) + def extract_file_import_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]: """extract function imports diff --git a/capa/features/extractors/binja/helpers.py b/capa/features/extractors/binja/helpers.py index a96f64da..0ce0f073 100644 --- a/capa/features/extractors/binja/helpers.py +++ b/capa/features/extractors/binja/helpers.py @@ -9,7 +9,7 @@ import re from typing import List, Callable from dataclasses import dataclass -from binaryninja import LowLevelILInstruction +from binaryninja import BinaryView, LowLevelILInstruction from binaryninja.architecture import InstructionTextToken @@ -51,3 +51,19 @@ def unmangle_c_name(name: str) -> str: return match.group(1) return name + + +def read_c_string(bv: BinaryView, offset: int, max_len: int) -> str: + s: List[str] = [] + while len(s) < max_len: + try: + c = bv.read(offset + len(s), 1)[0] + except Exception: + break + + if c == 0: + break + + s.append(chr(c)) + + return "".join(s) diff --git a/tests/test_binja_features.py b/tests/test_binja_features.py index 3d51886d..78addff7 100644 --- a/tests/test_binja_features.py +++ b/tests/test_binja_features.py @@ -40,12 +40,6 @@ except ImportError: indirect=["sample", "scope"], ) def test_binja_features(sample, scope, feature, expected): - if isinstance(feature, capa.features.file.Export) and "." in str(feature.value): - pytest.xfail("skip Binja unsupported forwarded export feature, see #1646") - - if feature == capa.features.common.Characteristic("forwarded export"): - pytest.xfail("skip Binja unsupported forwarded export feature, see #1646") - fixtures.do_test_feature_presence(fixtures.get_binja_extractor, sample, scope, feature, expected)