diff --git a/capa/features/extractors/ida/extractor.py b/capa/features/extractors/ida/extractor.py index 463f4876..4e702ac8 100644 --- a/capa/features/extractors/ida/extractor.py +++ b/capa/features/extractors/ida/extractor.py @@ -16,19 +16,19 @@ from typing import Iterator import idaapi -import capa.ida.helpers -import capa.features.extractors.ida.file -import capa.features.extractors.ida.insn -import capa.features.extractors.ida.global_ -import capa.features.extractors.ida.function import capa.features.extractors.ida.basicblock +import capa.features.extractors.ida.file +import capa.features.extractors.ida.function +import capa.features.extractors.ida.global_ +import capa.features.extractors.ida.insn +import capa.ida.helpers +from capa.features.address import AbsoluteVirtualAddress, Address from capa.features.common import Feature -from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.extractors.base_extractor import ( BBHandle, + FunctionHandle, InsnHandle, SampleHashes, - FunctionHandle, StaticFeatureExtractor, ) @@ -43,7 +43,9 @@ class IdaFeatureExtractor(StaticFeatureExtractor): ) ) self.global_features: list[tuple[Feature, Address]] = [] - self.global_features.extend(capa.features.extractors.ida.file.extract_file_format()) + self.global_features.extend( + capa.features.extractors.ida.file.extract_file_format() + ) self.global_features.extend(capa.features.extractors.ida.global_.extract_os()) self.global_features.extend(capa.features.extractors.ida.global_.extract_arch()) @@ -67,7 +69,9 @@ class IdaFeatureExtractor(StaticFeatureExtractor): f = idaapi.get_func(ea) return FunctionHandle(address=AbsoluteVirtualAddress(f.start_ea), inner=f) - def extract_function_features(self, fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: + def extract_function_features( + self, fh: FunctionHandle + ) -> Iterator[tuple[Feature, Address]]: yield from capa.features.extractors.ida.function.extract_features(fh) def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]: @@ -76,13 +80,19 @@ class IdaFeatureExtractor(StaticFeatureExtractor): for bb in ida_helpers.get_function_blocks(fh.inner): yield BBHandle(address=AbsoluteVirtualAddress(bb.start_ea), inner=bb) - def extract_basic_block_features(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[tuple[Feature, Address]]: + def extract_basic_block_features( + self, fh: FunctionHandle, bbh: BBHandle + ) -> Iterator[tuple[Feature, Address]]: yield from capa.features.extractors.ida.basicblock.extract_features(fh, bbh) - def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]: + def get_instructions( + self, fh: FunctionHandle, bbh: BBHandle + ) -> Iterator[InsnHandle]: import capa.features.extractors.ida.helpers as ida_helpers - for insn in ida_helpers.get_instructions_in_range(bbh.inner.start_ea, bbh.inner.end_ea): + for insn in ida_helpers.get_instructions_in_range( + bbh.inner.start_ea, bbh.inner.end_ea + ): yield InsnHandle(address=AbsoluteVirtualAddress(insn.ea), inner=insn) def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle): diff --git a/tests/conftest.py b/tests/conftest.py index 9637ed87..8e17413a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,3 @@ # https://www.revsys.com/tidbits/pytest-fixtures-are-magic/ # https://lobste.rs/s/j8xgym/pytest_fixtures_are_magic from fixtures import * # noqa: F403 [unable to detect undefined names] -from fixtures import _692f_dotnetfile_extractor # noqa: F401 [imported but unused] -from fixtures import _1c444_dotnetfile_extractor # noqa: F401 [imported but unused] -from fixtures import _039a6_dotnetfile_extractor # noqa: F401 [imported but unused] -from fixtures import _0953c_dotnetfile_extractor # noqa: F401 [imported but unused] diff --git a/tests/fixtures.py b/tests/fixtures.py index 9ba85d51..1b32b4cb 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -14,49 +14,29 @@ import collections import contextlib +import functools import json import logging from dataclasses import dataclass, field -from functools import lru_cache from pathlib import Path -from typing import Callable, Optional, Union +from typing import Literal, Optional, Union import pytest import capa.engine as ceng -import capa.features.basicblock -import capa.features.common -import capa.features.file -import capa.features.insn -import capa.features.common -import capa.features.basicblock import capa.loader import capa.rules import capa.render.result_document from capa.features.address import Address -from capa.features.common import ( - ARCH_AMD64, - ARCH_I386, - FORMAT_AUTO, - FORMAT_DOTNET, - FORMAT_ELF, - FORMAT_PE, - OS, - OS_ANY, - OS_AUTO, - OS_LINUX, - OS_WINDOWS, - Arch, - Feature, - FeatureAccess, - Format, -) +from capa.features.common import FORMAT_AUTO, OS_AUTO, Feature from capa.features.extractors.base_extractor import ( BBHandle, CallHandle, + DynamicFeatureExtractor, FunctionHandle, InsnHandle, ProcessHandle, + StaticFeatureExtractor, ThreadHandle, ) from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor @@ -70,171 +50,110 @@ DNFILE_TESTFILES = DOTNET_DIR / "dnfile-testfiles" def parse_feature_string(s: str) -> Feature | ceng.Range | ceng.Statement: """ - parse a fixture feature string into a Feature, Range, or Statement. + parse a feature from a single string + no extra description is assigned. - count(...) fixtures have a string integer value in the JSON - (e.g. `count(basic blocks): 7`). translate that to an int so - `build_feature` returns a Range rather than raising on an - unrecognized range expression. + examples: + "mnemonic: mov" + "string: /foo/" + "count(basic blocks): 7" + + returns: Range if the feature is a count, and generated Statement for COM features, otherwise Feature. """ key, _, value = s.partition(": ") - initial_value: str | int = value - if key.startswith("count(") and key.endswith(")"): - try: - initial_value = int(value) - except ValueError: - # leave as string so that `build_feature` can handle - # "N or more"/"N or fewer"/"(N, M)" range expressions. - initial_value = value - return capa.rules.build_feature(key, initial_value, initial_description=None) + return capa.rules.build_feature(key, value, initial_description=None) -# scope-kind tags are derived from the fixture location and inserted -# into the fixture's tag set. backends that only support a subset of -# scopes (e.g., pefile is file-only) can exclude the others via tags. -SCOPE_KIND_TAGS: frozenset[str] = frozenset( +KNOWN_FEATURE_NAMES = { + "api", + "arch", + "basic blocks", + "bytes", + "characteristic", + "class", + "export", + "format", + "function-name", + "import", + "mnemonic", + "namespace", + "number", + "offset", + "operand[0].number", + "operand[0].offset", + "operand[1].number", + "operand[1].offset", + "operand[2].offset", + "os", + "property", + "property/read", + "property/write", + "section", + "string", + "substring", +} + +KNOWN_SCOPE_NAMES = capa.rules.STATIC_SCOPES | capa.rules.DYNAMIC_SCOPES + +KNOWN_FIXTURE_TAGS: set[str] = ( { - "file", - "function", - "basic-block", - "instruction", - "process", - "thread", - "call", + "static", # static analysis test, PE/ELF format. + "dynamic", # dynamic analysis test + "dotnet", # .NET format + "elf", # ELF format + "flirt", # requires FLIRT signature matching + "symtab", # requires ELF symbol table parsing TODO: can we remove this? + "binja-db", # Binary Ninja database format + "binexport", # BinExport2 format + "aarch64", # AArch64 architecture + "cape", # CAPE analysis + "drakvuf", # Drakvuf analysis + "vmray", # VMRay analysis } -) - -# feature-type tags are derived from the fixture feature string's key -# and inserted into the fixture's tag set. backends that don't support -# a feature type (e.g., pefile has no function-name features) can -# exclude by tag rather than by Python class. -# -# values come from `capa.rules.parse_feature` keys so the tag names -# align with the textual rule syntax. -FEATURE_TYPE_TAGS: frozenset[str] = frozenset( - { - "api", - "string", - "substring", - "bytes", - "number", - "offset", - "mnemonic", - "basic blocks", - "characteristic", - "export", - "import", - "section", - "match", - "function-name", - "os", - "format", - "arch", - "class", - "namespace", - "property", - # operand[N].X is collapsed to operand.X (index-independent) - "operand.number", - "operand.offset", - } -) - -# known fixture tags used for backend selection. -# merged tags that are not listed here will fail collection, to catch typos. -KNOWN_FIXTURE_TAGS: frozenset[str] = ( - frozenset( - { - "static", - "dynamic", - "dotnet", - "elf", - "flirt", - "symtab", - "ghidra", - "binja-db", - "binexport", - "aarch64", - "cape", - "drakvuf", - "vmray", - } - ) - | SCOPE_KIND_TAGS - | FEATURE_TYPE_TAGS + | KNOWN_SCOPE_NAMES + | KNOWN_FEATURE_NAMES ) -def get_scope_kind(location: str) -> str: +def get_scope_from_location(location: str) -> capa.rules.Scope: """ classify a fixture location string into a scope kind. reuses the same location grammar handled by `resolve_scope()`. """ if location == "file": - return "file" + return capa.rules.Scope.FILE if "insn=" in location: - return "instruction" + return capa.rules.Scope.INSTRUCTION if "bb=" in location: - return "basic-block" + return capa.rules.Scope.BASIC_BLOCK if "call=" in location: - return "call" + return capa.rules.Scope.CALL if "thread=" in location: - return "thread" + return capa.rules.Scope.THREAD if "process=" in location: - return "process" + return capa.rules.Scope.PROCESS if location.startswith(("function", "token")): - return "function" + return capa.rules.Scope.FUNCTION raise ValueError(f"unexpected scope location: {location}") -def get_feature_type_tag(feature_str: str) -> str: - """ - extract the feature-type tag from a fixture feature string. - - examples: - `api: CryptSetHashParam` -> `api` - `function-name: __aulldiv` -> `function-name` - `count(basic blocks): 7` -> `basic blocks` - `count(mnemonic(mov)): 3` -> `mnemonic` - `count(characteristic(nzxor))` -> `characteristic` - `operand[1].number: 0xFF` -> `operand.number` - `property/read: Foo.Bar` -> `property` - """ - if feature_str.startswith("count("): - # find the matching close-paren for the outer `count(` so that - # nested parens and colons inside the argument (e.g. strings with - # `:` or `(`) don't confuse a naive partition. - depth = 0 - for i, c in enumerate(feature_str): - if c == "(": - depth += 1 - elif c == ")": - depth -= 1 - if depth == 0: - inner = feature_str[len("count(") : i] - # collapse nested arg: `mnemonic(mov)` -> `mnemonic` - inner, _, _ = inner.partition("(") - return _normalize_feature_key(inner.strip()) - raise ValueError(f"unbalanced parentheses in feature string: {feature_str!r}") - key, _, _ = feature_str.partition(":") - return _normalize_feature_key(key.strip()) - - -def _normalize_feature_key(key: str) -> str: - # collapse `operand[N].X` -> `operand.X` so the tag is index-independent - if key.startswith("operand[") and "]." in key: - _, _, suffix = key.partition("].") - return f"operand.{suffix}" - # collapse `property/read` and `property/write` -> `property` - if key.startswith("property/"): - return "property" - return key - - @dataclass(frozen=True) class FixtureMark: - backend: str - mark: str + backend: ( + Literal["vivisect"] + | Literal["dotnet"] + | Literal["binja"] + | Literal["pefile"] + | Literal["cape"] + | Literal["drakvuf"] + | Literal["vmray"] + | Literal["freeze"] + | Literal["binexport2"] + | Literal["ida"] + | Literal["ghidra"] + ) + mark: Literal["skip"] | Literal["xfail"] reason: str @@ -250,29 +169,22 @@ class FeatureFixture: sample_key: str sample_path: Path location: str - scope_kind: str + scope: capa.rules.Scope statement: Union[Feature, ceng.Range, ceng.Statement] expected: bool = True tags: frozenset[str] = frozenset() marks: tuple[FixtureMark, ...] = () explanation: Optional[str] = None - comment: Optional[str] = None @dataclass(frozen=True) class BackendFeaturePolicy: name: str - get_extractor: Callable[[Path], object] - include_tags: frozenset[str] = field(default_factory=frozenset) - exclude_tags: frozenset[str] = field(default_factory=frozenset) - - def __post_init__(self): - object.__setattr__(self, "include_tags", frozenset(self.include_tags)) - object.__setattr__(self, "exclude_tags", frozenset(self.exclude_tags)) + include_tags: set[str] = field(default_factory=set) + exclude_tags: set[str] = field(default_factory=set) -@lru_cache(maxsize=1) -def _load_feature_fixture_manifests() -> tuple[tuple[Path, dict], ...]: +def get_fixture_files() -> tuple[tuple[Path, dict], ...]: manifests = [] for path in sorted(FIXTURE_MANIFEST_DIR.glob("*.json")): with path.open("r") as f: @@ -282,20 +194,7 @@ def _load_feature_fixture_manifests() -> tuple[tuple[Path, dict], ...]: return tuple(manifests) -@lru_cache(maxsize=1) -def _load_fixture_file_paths() -> dict[str, Path]: - return {key: file.path for key, file in load_feature_fixture_files().items()} - - -def get_fixture_file_path(key: str) -> Path: - paths = _load_fixture_file_paths() - if key not in paths: - raise ValueError(f"unknown fixture file key: {key}") - return paths[key] - - -@lru_cache(maxsize=1) -def load_feature_fixture_files() -> dict[str, FixtureFile]: +def load_fixture_file_references() -> dict[str, FixtureFile]: """ load the combined `files` tables from `tests/fixtures/features/*.json`. @@ -304,7 +203,7 @@ def load_feature_fixture_files() -> dict[str, FixtureFile]: """ files: dict[str, FixtureFile] = {} file_sources: dict[str, Path] = {} - for manifest_path, data in _load_feature_fixture_manifests(): + for manifest_path, data in get_fixture_files(): for entry in data["files"]: key = entry["key"] if key in files: @@ -327,7 +226,6 @@ def load_feature_fixture_files() -> dict[str, FixtureFile]: return files -@lru_cache(maxsize=1) def load_feature_fixtures() -> tuple[FeatureFixture, ...]: """ load the full list of feature fixtures from `tests/fixtures/features/*.json`. @@ -336,53 +234,55 @@ def load_feature_fixtures() -> tuple[FeatureFixture, ...]: the known registry, parses the statement (including `count(...)`), and defaults `expected` to True. """ - files = load_feature_fixture_files() + fixture_file_references = load_fixture_file_references() fixtures_: list[FeatureFixture] = [] - for manifest_path, data in _load_feature_fixture_manifests(): - for entry in data["features"]: - key = entry["file"] - if key not in files: + for fixture_file_path, fixture_file_data in get_fixture_files(): + for fixture_file_entry in fixture_file_data["features"]: + fixture_file_reference = fixture_file_entry["file"] + if fixture_file_reference not in fixture_file_references: raise ValueError( - f"unknown fixture file key referenced by feature in {manifest_path}: {key!r}" + f"unknown fixture file key referenced by feature in {fixture_file_path}: {fixture_file_reference!r}" ) - file = files[key] + fixture_file = fixture_file_references[fixture_file_reference] - feature_str: str = entry["feature"] - feature_tags = frozenset(entry.get("tags", [])) - merged_tags = file.tags | feature_tags - unknown = merged_tags - KNOWN_FIXTURE_TAGS + feature_str: str = fixture_file_entry["feature"] + tags = frozenset(fixture_file_entry.get("tags", [])) | fixture_file.tags + unknown = tags - KNOWN_FIXTURE_TAGS if unknown: raise ValueError( - f"unknown fixture tag(s) on feature {feature_str!r} for file {key!r} in {manifest_path}: {sorted(unknown)}" + f"unknown fixture tag(s) on feature {feature_str!r} for file {fixture_file_reference!r} in {fixture_file_path}: {sorted(unknown)}" ) - location = entry["location"] + location = fixture_file_entry["location"] statement = parse_feature_string(feature_str) - scope_kind = get_scope_kind(location) - feature_type_tag = get_feature_type_tag(feature_str) + scope = get_scope_from_location(location) # scope-kind and feature-type tags are auto-derived so that # backend policies can include/exclude scopes and feature types # purely via `include_tags`/`exclude_tags`. they're drawn from # the known-tag registry so no re-validation is needed here. - merged_tags = merged_tags | {scope_kind, feature_type_tag} - expected = entry.get("expected", True) + tags = tags | {scope.value} + if isinstance(statement, Feature): + tags = tags | {statement.name} + # technically we're not extracting the feature name for COM and count features + # but i think thats ok for now, since no tests rely on include/excluding those. + + expected = fixture_file_entry.get("expected", True) marks = tuple( FixtureMark(backend=m["backend"], mark=m["mark"], reason=m["reason"]) - for m in entry.get("marks", []) + for m in fixture_file_entry.get("marks", []) ) fixtures_.append( FeatureFixture( - sample_key=key, - sample_path=file.path, + sample_key=fixture_file_reference, + sample_path=fixture_file.path, location=location, - scope_kind=scope_kind, + scope=scope, statement=statement, expected=expected, - tags=merged_tags, + tags=tags, marks=marks, - explanation=entry.get("explanation"), - comment=entry.get("comment"), + explanation=fixture_file_entry.get("explanation"), ) ) @@ -390,14 +290,6 @@ def load_feature_fixtures() -> tuple[FeatureFixture, ...]: return tuple(fixtures_) -@dataclass(frozen=True) -class FixtureSelectionSummary: - total: int - selected: int - excluded: int - excluded_by_tag: dict[str, int] - - def _fixture_is_included(policy: BackendFeaturePolicy, fixture: FeatureFixture) -> bool: """decide whether a fixture is selected by a policy.""" if policy.include_tags and not (fixture.tags & policy.include_tags): @@ -422,31 +314,6 @@ def select_feature_fixtures(policy: BackendFeaturePolicy) -> list[FeatureFixture return [f for f in load_feature_fixtures() if _fixture_is_included(policy, f)] -def summarize_feature_selection( - policy: BackendFeaturePolicy, -) -> FixtureSelectionSummary: - """ - summarize the effect of a policy's fixture selection. - - useful for debug output and maintenance scripts. - """ - all_fixtures = load_feature_fixtures() - excluded_by_tag: dict[str, int] = collections.defaultdict(int) - selected = 0 - for fixture in all_fixtures: - if _fixture_is_included(policy, fixture): - selected += 1 - continue - for tag in sorted(fixture.tags): - excluded_by_tag[tag] += 1 - return FixtureSelectionSummary( - total=len(all_fixtures), - selected=selected, - excluded=len(all_fixtures) - selected, - excluded_by_tag=dict(excluded_by_tag), - ) - - def _fixture_test_id(fixture: FeatureFixture) -> str: """ build a readable pytest parameter id for a fixture. @@ -489,14 +356,13 @@ def parametrize_backend_feature_fixtures(policy: BackendFeaturePolicy): return pytest.mark.parametrize("feature_fixture", params) -def run_feature_fixture(policy: BackendFeaturePolicy, fixture: FeatureFixture) -> None: +def run_feature_fixture( + extractor: StaticFeatureExtractor | DynamicFeatureExtractor, + fixture: FeatureFixture, +) -> None: """ generic runner that evaluates a feature fixture against a backend. - - handles both plain features and `count(...)` statements via one - `evaluate` path, comparing the boolean result to `fixture.expected`. """ - extractor = policy.get_extractor(fixture.sample_path) scope = resolve_scope(fixture.location) features = scope(extractor) result = fixture.statement.evaluate(features) @@ -531,7 +397,7 @@ def xfail(condition, reason: str = ""): except Exception: if condition: # we expected the test to fail, so raise and register this via pytest - pytest.xfail(reason) + pytest.xfail(reason or "") else: # we don't expect an exception, so the test should fail raise @@ -546,262 +412,6 @@ def xfail(condition, reason: str = ""): raise RuntimeError("expected to fail, but didn't") -# need to limit cache size so GitHub Actions doesn't run out of memory, see #545 -@lru_cache(maxsize=1) -def get_viv_extractor(path: Path): - import capa.loader - import capa.features.extractors.viv.extractor - import capa.main - - sigpaths = [ - CD / "data" / "sigs" / "test_aulldiv.pat", - CD / "data" / "sigs" / "test_aullrem.pat.gz", - CD.parent / "sigs" / "1_flare_msvc_rtf_32_64.sig", - CD.parent / "sigs" / "2_flare_msvc_atlmfc_32_64.sig", - CD.parent / "sigs" / "3_flare_common_libs.sig", - ] - - if "raw32" in path.name: - vw = capa.loader.get_workspace(path, "sc32", sigpaths=sigpaths) - elif "raw64" in path.name: - vw = capa.loader.get_workspace(path, "sc64", sigpaths=sigpaths) - else: - vw = capa.loader.get_workspace(path, FORMAT_AUTO, sigpaths=sigpaths) - vw.saveWorkspace() - extractor = capa.features.extractors.viv.extractor.VivisectFeatureExtractor( - vw, path, OS_AUTO - ) - fixup_viv(path, extractor) - return extractor - - -def fixup_viv(path: Path, extractor): - """ - vivisect fixups to overcome differences between backends - """ - if "3b13b" in path.name: - # vivisect only recognizes calling thunk function at 0x10001573 - extractor.vw.makeFunction(0x10006860) - if "294b8d" in path.name: - # see vivisect/#561 - extractor.vw.makeFunction(0x404970) - - -@lru_cache(maxsize=1) -def get_pefile_extractor(path: Path): - import capa.features.extractors.pefile - - extractor = capa.features.extractors.pefile.PefileFeatureExtractor(path) - - # overload the extractor so that the fixture exposes `extractor.path` - setattr(extractor, "path", path.as_posix()) - - return extractor - - -@lru_cache(maxsize=1) -def get_dnfile_extractor(path: Path): - import capa.features.extractors.dnfile.extractor - - extractor = capa.features.extractors.dnfile.extractor.DnfileFeatureExtractor(path) - - # overload the extractor so that the fixture exposes `extractor.path` - setattr(extractor, "path", path.as_posix()) - - return extractor - - -@lru_cache(maxsize=1) -def get_dotnetfile_extractor(path: Path): - import capa.features.extractors.dotnetfile - - extractor = capa.features.extractors.dotnetfile.DotnetFileFeatureExtractor(path) - - # overload the extractor so that the fixture exposes `extractor.path` - setattr(extractor, "path", path.as_posix()) - - return extractor - - -@lru_cache(maxsize=1) -def get_binja_extractor(path: Path): - import binaryninja - from binaryninja import Settings - - import capa.features.extractors.binja.extractor - - # Workaround for a BN bug: https://github.com/Vector35/binaryninja-api/issues/4051 - settings = Settings() - if path.name.endswith("kernel32-64.dll_"): - old_pdb = settings.get_bool("pdb.loadGlobalSymbols") - settings.set_bool("pdb.loadGlobalSymbols", False) - bv = binaryninja.load(str(path)) - settings.set_bool("pdb.loadGlobalSymbols", old_pdb) - else: - bv = binaryninja.load(str(path)) - - # TODO(xusheng6): Temporary fix for https://github.com/mandiant/capa/issues/2507. Remove this once it is fixed in - # binja - if "al-khaser_x64.exe_" in path.name: - bv.create_user_function(0x14004B4F0) - bv.update_analysis_and_wait() - - extractor = capa.features.extractors.binja.extractor.BinjaFeatureExtractor(bv) - - # overload the extractor so that the fixture exposes `extractor.path` - setattr(extractor, "path", path.as_posix()) - - return extractor - - -# we can't easily cache this because the extractor relies on global state (the opened database) -# which also has to be closed elsewhere. so, the idalib tests will just take a little bit to run. -def get_idalib_extractor(path: Path): - import capa.features.extractors.ida.idalib as idalib - - if not idalib.has_idalib(): - raise RuntimeError("cannot find IDA idalib module.") - - if not idalib.load_idalib(): - raise RuntimeError("failed to load IDA idalib module.") - - import ida_auto - import idapro - - import capa.features.extractors.ida.extractor - - logger.debug("idalib: opening database...") - - idapro.enable_console_messages(False) - - # we set the primary and secondary Lumina servers to 0.0.0.0 to disable Lumina, - # which sometimes provides bad names, including overwriting names from debug info. - # - # use -R to load resources, which can help us embedded PE files. - # - # return values from open_database: - # 0 - Success - # 2 - User cancelled or 32-64 bit conversion failed - # 4 - Database initialization failed - # -1 - Generic errors (database already open, auto-analysis failed, etc.) - # -2 - User cancelled operation - ret = idapro.open_database( - str(path), - run_auto_analysis=True, - args="-Olumina:host=0.0.0.0 -Osecondary_lumina:host=0.0.0.0 -R", - ) - if ret != 0: - raise RuntimeError("failed to analyze input file") - - logger.debug("idalib: waiting for analysis...") - ida_auto.auto_wait() - logger.debug("idalib: opened database.") - - extractor = capa.features.extractors.ida.extractor.IdaFeatureExtractor() - fixup_idalib(path, extractor) - return extractor - - -def fixup_idalib(path: Path, extractor): - """ - IDA fixups to overcome differences between backends - """ - import ida_funcs - import idaapi - - def remove_library_id_flag(fva): - f = idaapi.get_func(fva) - f.flags &= ~ida_funcs.FUNC_LIB - ida_funcs.update_func(f) - - if "kernel32-64" in path.name: - # remove (correct) library function id, so we can test x64 thunk - remove_library_id_flag(0x1800202B0) - - if "al-khaser_x64" in path.name: - # remove (correct) library function id, so we can test x64 nested thunk - remove_library_id_flag(0x14004B4F0) - - -@lru_cache(maxsize=1) -def get_cape_extractor(path): - from capa.features.extractors.cape.extractor import CapeExtractor - from capa.helpers import load_json_from_path - - report = load_json_from_path(path) - - return CapeExtractor.from_report(report) - - -@lru_cache(maxsize=1) -def get_drakvuf_extractor(path): - from capa.features.extractors.drakvuf.extractor import DrakvufExtractor - from capa.helpers import load_jsonl_from_path - - report = load_jsonl_from_path(path) - - return DrakvufExtractor.from_report(report) - - -@lru_cache(maxsize=1) -def get_vmray_extractor(path): - from capa.features.extractors.vmray.extractor import VMRayExtractor - - return VMRayExtractor.from_zipfile(path) - - -GHIDRA_CACHE: dict[Path, tuple] = {} - - -def get_ghidra_extractor(path: Path): - # we need to start PyGhidra before importing the extractor - # because the extractor imports Ghidra modules that are only available after PyGhidra is started - import pyghidra - - if not pyghidra.started(): - pyghidra.start() - - import capa.loader - import capa.features.extractors.ghidra.context - - if path in GHIDRA_CACHE: - extractor, program, flat_api, monitor = GHIDRA_CACHE[path] - capa.features.extractors.ghidra.context.set_context(program, flat_api, monitor) - return extractor - - # We use a larger cache size to avoid re-opening the same file multiple times - # which is very slow with Ghidra. - extractor = capa.loader.get_extractor( - path, - FORMAT_AUTO, - OS_AUTO, - capa.loader.BACKEND_GHIDRA, - [], - disable_progress=True, - ) - - ctx = capa.features.extractors.ghidra.context.get_context() - GHIDRA_CACHE[path] = (extractor, ctx.program, ctx.flat_api, ctx.monitor) - return extractor - - -@lru_cache(maxsize=1) -def get_binexport_extractor(path): - import capa.features.extractors.binexport2 - import capa.features.extractors.binexport2.extractor - - be2 = capa.features.extractors.binexport2.get_binexport2(path) - search_paths = [CD / "data", CD / "data" / "aarch64"] - path = capa.features.extractors.binexport2.get_sample_from_binexport2( - path, be2, search_paths - ) - buf = path.read_bytes() - - return capa.features.extractors.binexport2.extractor.BinExport2FeatureExtractor( - be2, buf - ) - - def extract_global_features(extractor): features = collections.defaultdict(set) for feature, va in extractor.extract_global_features(): @@ -809,7 +419,7 @@ def extract_global_features(extractor): return features -@lru_cache +@functools.lru_cache() def extract_file_features(extractor): features = collections.defaultdict(set) for feature, va in extractor.extract_file_features(): @@ -847,7 +457,7 @@ def extract_call_features(extractor, ph, th, ch): return features -# f may not be hashable (e.g. ida func_t) so cannot @lru_cache this +# f may not be hashable (e.g. ida func_t) so cannot @functools.lru_cache this def extract_function_features(extractor, fh): features = collections.defaultdict(set) for bb in extractor.get_basic_blocks(fh): @@ -861,7 +471,7 @@ def extract_function_features(extractor, fh): return features -# f may not be hashable (e.g. ida func_t) so cannot @lru_cache this +# f may not be hashable (e.g. ida func_t) so cannot @functools.lru_cache this def extract_basic_block_features(extractor, fh, bbh): features = collections.defaultdict(set) for insn in extractor.get_instructions(fh, bbh): @@ -872,7 +482,7 @@ def extract_basic_block_features(extractor, fh, bbh): return features -# f may not be hashable (e.g. ida func_t) so cannot @lru_cache this +# f may not be hashable (e.g. ida func_t) so cannot @functools.lru_cache this def extract_instruction_features(extractor, fh, bbh, ih) -> dict[Feature, set[Address]]: features = collections.defaultdict(set) for feature, addr in extractor.extract_insn_features(fh, bbh, ih): @@ -880,301 +490,6 @@ def extract_instruction_features(extractor, fh, bbh, ih) -> dict[Feature, set[Ad return features -# note: to reduce the testing time it's recommended to reuse already existing test samples, if possible -def get_data_path_by_name(name) -> Path: - # prefer the fixture manifest registry; fall back to the legacy hard-coded - # branches below for any keys not yet migrated. - lookup_key = name[:-3] if name.endswith("...") else name - json_paths = _load_fixture_file_paths() - if lookup_key in json_paths: - return json_paths[lookup_key] - - if name == "mimikatz": - return CD / "data" / "mimikatz.exe_" - elif name == "kernel32": - return CD / "data" / "kernel32.dll_" - elif name == "kernel32-64": - return CD / "data" / "kernel32-64.dll_" - elif name == "pma01-01": - return CD / "data" / "Practical Malware Analysis Lab 01-01.dll_" - elif name == "pma01-01-rd": - return CD / "data" / "rd" / "Practical Malware Analysis Lab 01-01.dll_.json" - elif name == "pma12-04": - return CD / "data" / "Practical Malware Analysis Lab 12-04.exe_" - elif name == "pma16-01": - return CD / "data" / "Practical Malware Analysis Lab 16-01.exe_" - elif name == "pma16-01_binja_db": - return CD / "data" / "Practical Malware Analysis Lab 16-01.exe_.bndb" - elif name == "pma21-01": - return CD / "data" / "Practical Malware Analysis Lab 21-01.exe_" - elif name == "al-khaser x86": - return CD / "data" / "al-khaser_x86.exe_" - elif name == "al-khaser x64": - return CD / "data" / "al-khaser_x64.exe_" - elif name.startswith("39c05"): - return ( - CD - / "data" - / "39c05b15e9834ac93f206bc114d0a00c357c888db567ba8f5345da0529cbed41.dll_" - ) - elif name.startswith("499c2"): - return CD / "data" / "499c2a85f6e8142c3f48d4251c9c7cd6.raw32" - elif name.startswith("9324d"): - return CD / "data" / "9324d1a8ae37a36ae560c37448c9705a.exe_" - elif name.startswith("395eb"): - return CD / "data" / "395eb0ddd99d2c9e37b6d0b73485ee9c.exe_" - elif name.startswith("a1982"): - return CD / "data" / "a198216798ca38f280dc413f8c57f2c2.exe_" - elif name.startswith("a933a"): - return CD / "data" / "a933a1a402775cfa94b6bee0963f4b46.dll_" - elif name.startswith("bfb9b"): - return CD / "data" / "bfb9b5391a13d0afd787e87ab90f14f5.dll_" - elif name.startswith("c9188"): - return CD / "data" / "c91887d861d9bd4a5872249b641bc9f9.exe_" - elif name.startswith("64d9f"): - return CD / "data" / "64d9f7d96b99467f36e22fada623c3bb.dll_" - elif name.startswith("82bf6"): - return CD / "data" / "82BF6347ACF15E5D883715DC289D8A2B.exe_" - elif name.startswith("pingtaest"): - return CD / "data" / "ping_täst.exe_" - elif name.startswith("77329"): - return CD / "data" / "773290480d5445f11d3dc1b800728966.exe_" - elif name.startswith("3b13b"): - return ( - CD - / "data" - / "3b13b6f1d7cd14dc4a097a12e2e505c0a4cff495262261e2bfc991df238b9b04.dll_" - ) - elif name == "7351f.elf": - return CD / "data" / "7351f8a40c5450557b24622417fc478d.elf_" - elif name == "055da8e6.elf": - return CD / "data" / "055da8e6ccfe5a9380231ea04b850e18.elf_" - elif name == "bb38149.elf": - return CD / "data" / "bb38149ff4b5c95722b83f24ca27a42b.elf_" - elif name.startswith("79abd"): - return CD / "data" / "79abd17391adc6251ecdc58d13d76baf.dll_" - elif name.startswith("946a9"): - return CD / "data" / "946a99f36a46d335dec080d9a4371940.dll_" - elif name.startswith("2f7f5f"): - return CD / "data" / "2f7f5fb5de175e770d7eae87666f9831.elf_" - elif name.startswith("b9f5b"): - return CD / "data" / "b9f5bd514485fb06da39beff051b9fdc.exe_" - elif name.startswith("mixed-mode-64"): - return ( - DNFILE_TESTFILES - / "mixed-mode" - / "ModuleCode" - / "bin" - / "ModuleCode_amd64.exe" - ) - elif name.startswith("hello-world"): - return DNFILE_TESTFILES / "hello-world" / "hello-world.exe" - elif name.startswith("_1c444"): - return DOTNET_DIR / "1c444ebeba24dcba8628b7dfe5fec7c6.exe_" - elif name.startswith("_387f15"): - return ( - DOTNET_DIR - / "387f15043f0198fd3a637b0758c2b6dde9ead795c3ed70803426fc355731b173.dll_" - ) - elif name.startswith("_692f"): - return DOTNET_DIR / "692f7fd6d198e804d6af98eb9e390d61.exe_" - elif name.startswith("_0953c"): - return ( - CD - / "data" - / "0953cc3b77ed2974b09e3a00708f88de931d681e2d0cb64afbaf714610beabe6.exe_" - ) - elif name.startswith("_039a6"): - return ( - CD - / "data" - / "039a6336d0802a2255669e6867a5679c7eb83313dbc61fb1c7232147379bd304.exe_" - ) - elif name.startswith("b5f052"): - return ( - CD - / "data" - / "b5f0524e69b3a3cf636c7ac366ca57bf5e3a8fdc8a9f01caf196c611a7918a87.elf_" - ) - elif name.startswith("bf7a9c"): - return ( - CD - / "data" - / "bf7a9c8bdfa6d47e01ad2b056264acc3fd90cf43fe0ed8deec93ab46b47d76cb.elf_" - ) - elif name.startswith("294b8d"): - return ( - CD - / "data" - / "294b8db1f2702b60fb2e42fdc50c2cee6a5046112da9a5703a548a4fa50477bc.elf_" - ) - elif name.startswith("2bf18d"): - return CD / "data" / "2bf18d0403677378adad9001b1243211.elf_" - elif name.startswith("2d3edc"): - return CD / "data" / "2d3edc218a90f03089cc01715a9f047f.exe_" - elif name.startswith("0000a657"): - return ( - CD - / "data" - / "dynamic" - / "cape" - / "v2.2" - / "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz" - ) - elif name.startswith("d46900"): - return ( - CD - / "data" - / "dynamic" - / "cape" - / "v2.2" - / "d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json.gz" - ) - elif name.startswith("93b2d1-drakvuf"): - return ( - CD - / "data" - / "dynamic" - / "drakvuf" - / "93b2d1840566f45fab674ebc79a9d19c88993bcb645e0357f3cb584d16e7c795.log.gz" - ) - elif name.startswith("93b2d1-vmray"): - return ( - CD - / "data" - / "dynamic" - / "vmray" - / "93b2d1840566f45fab674ebc79a9d19c88993bcb645e0357f3cb584d16e7c795_min_archive.zip" - ) - elif name.startswith("2f8a79-vmray"): - return ( - CD - / "data" - / "dynamic" - / "vmray" - / "2f8a79b12a7a989ac7e5f6ec65050036588a92e65aeb6841e08dc228ff0e21b4_min_archive.zip" - ) - elif name.startswith("eb1287-vmray"): - return ( - CD - / "data" - / "dynamic" - / "vmray" - / "eb12873c0ce3e9ea109c2a447956cbd10ca2c3e86936e526b2c6e28764999f21_min_archive.zip" - ) - elif name.startswith("ea2876"): - return ( - CD - / "data" - / "ea2876e9175410b6f6719f80ee44b9553960758c7d0f7bed73c0fe9a78d8e669.dll_" - ) - elif name.startswith("1038a2"): - return ( - CD - / "data" - / "1038a23daad86042c66bfe6c9d052d27048de9653bde5750dc0f240c792d9ac8.elf_" - ) - elif name.startswith("3da7c"): - return ( - CD - / "data" - / "3da7c2c70a2d93ac4643f20339d5c7d61388bddd77a4a5fd732311efad78e535.elf_" - ) - elif name.startswith("nested_typedef"): - return CD / "data" / "dotnet" / "dd9098ff91717f4906afe9dafdfa2f52.exe_" - elif name.startswith("nested_typeref"): - return CD / "data" / "dotnet" / "2c7d60f77812607dec5085973ff76cea.dll_" - elif name.startswith("687e79.ghidra.be2"): - return ( - CD - / "data" - / "binexport2" - / "687e79cde5b0ced75ac229465835054931f9ec438816f2827a8be5f3bd474929.elf_.ghidra.BinExport" - ) - elif name.startswith("d1e650.ghidra.be2"): - return ( - CD - / "data" - / "binexport2" - / "d1e6506964edbfffb08c0dd32e1486b11fbced7a4bd870ffe79f110298f0efb8.elf_.ghidra.BinExport" - ) - else: - raise ValueError(f"unexpected sample fixture: {name}") - - -def get_sample_md5_by_name(name): - """used by IDA tests to ensure the correct IDB is loaded""" - if name == "mimikatz": - return "5f66b82558ca92e54e77f216ef4c066c" - elif name == "kernel32": - return "e80758cf485db142fca1ee03a34ead05" - elif name == "kernel32-64": - return "a8565440629ac87f6fef7d588fe3ff0f" - elif name == "pma12-04": - return "56bed8249e7c2982a90e54e1e55391a2" - elif name == "pma16-01": - return "7faafc7e4a5c736ebfee6abbbc812d80" - elif name == "pma01-01": - return "290934c61de9176ad682ffdd65f0a669" - elif name == "pma21-01": - return "c8403fb05244e23a7931c766409b5e22" - elif name == "al-khaser x86": - return "db648cd247281954344f1d810c6fd590" - elif name == "al-khaser x64": - return "3cb21ae76ff3da4b7e02d77ff76e82be" - elif name.startswith("39c05"): - return "b7841b9d5dc1f511a93cc7576672ec0c" - elif name.startswith("499c2"): - return "499c2a85f6e8142c3f48d4251c9c7cd6" - elif name.startswith("9324d"): - return "9324d1a8ae37a36ae560c37448c9705a" - elif name.startswith("a1982"): - return "a198216798ca38f280dc413f8c57f2c2" - elif name.startswith("a933a"): - return "a933a1a402775cfa94b6bee0963f4b46" - elif name.startswith("bfb9b"): - return "bfb9b5391a13d0afd787e87ab90f14f5" - elif name.startswith("c9188"): - return "c91887d861d9bd4a5872249b641bc9f9" - elif name.startswith("64d9f"): - return "64d9f7d96b99467f36e22fada623c3bb" - elif name.startswith("82bf6"): - return "82bf6347acf15e5d883715dc289d8a2b" - elif name.startswith("77329"): - return "773290480d5445f11d3dc1b800728966" - elif name.startswith("3b13b"): - # file name is SHA256 hash - return "56a6ffe6a02941028cc8235204eef31d" - elif name.startswith("7351f"): - return "7351f8a40c5450557b24622417fc478d" - elif name.startswith("79abd"): - return "79abd17391adc6251ecdc58d13d76baf" - elif name.startswith("946a9"): - return "946a99f36a46d335dec080d9a4371940" - elif name.startswith("b9f5b"): - return "b9f5bd514485fb06da39beff051b9fdc" - elif name.startswith("294b8d"): - # file name is SHA256 hash - return "3db3e55b16a7b1b1afb970d5e77c5d98" - elif name.startswith("2bf18d"): - return "2bf18d0403677378adad9001b1243211" - elif name.startswith("2d3edc"): - return "2d3edc218a90f03089cc01715a9f047f" - elif name.startswith("ea2876"): - return "76fa734236daa023444dec26863401dc" - else: - raise ValueError(f"unexpected sample fixture: {name}") - - -def resolve_sample(sample): - return get_data_path_by_name(sample) - - -@pytest.fixture -def sample(request): - return resolve_sample(request.param) - - def get_process(extractor, ppid: int, pid: int) -> ProcessHandle: for ph in extractor.get_processes(): if ph.address.ppid == ppid and ph.address.pid == pid: @@ -1387,33 +702,6 @@ def parametrize(params, values, **kwargs): return pytest.mark.parametrize(params, values, ids=ids, **kwargs) -# legacy tuple-of-tuples lists still needed by `test_binexport_features.py`, -# which rewrites a mimikatz sample path to its `.ghidra.BinExport` counterpart -# at test time. -# -# built from the new `load_feature_fixtures()` so the JSON manifests remain the -# single source of truth for fixture data. -FEATURE_PRESENCE_TESTS: list[tuple] = sorted( - ( - (f.sample_key, f.location, f.statement, f.expected) - for f in load_feature_fixtures() - if not isinstance(f.statement, ceng.Range) - and not (f.tags & frozenset({"dotnet", "symtab"})) - ), - key=lambda t: (t[0], t[1]), -) - -FEATURE_COUNT_TESTS_GHIDRA: list[tuple] = sorted( - ( - (f.sample_key, f.location, f.statement.child, f.statement.min) - for f in load_feature_fixtures() - if isinstance(f.statement, ceng.Range) and "ghidra" in f.tags - ), - key=lambda t: (t[0], t[1]), -) - - - FEATURE_COUNT_TESTS_BE2_INTEL = [ ( "mimikatz", @@ -1430,139 +718,11 @@ FEATURE_COUNT_TESTS_BE2_INTEL = [ ] - -def do_test_feature_presence(get_extractor, sample, scope, feature, expected): - extractor = get_extractor(sample) - features = scope(extractor) - if expected: - msg = f"{str(feature)} should be found in {scope.__name__}" - else: - msg = f"{str(feature)} should not be found in {scope.__name__}" - assert feature.evaluate(features) == expected, msg - - def do_test_feature_count(get_extractor, sample, scope, feature, expected): extractor = get_extractor(sample) features = scope(extractor) - msg = f"{str(feature)} should be found {expected} times in {scope.__name__}, found: {len(features[feature])}" - assert len(features[feature]) == expected, msg - - -def get_extractor(path: Path): - extractor = get_viv_extractor(path) - # overload the extractor so that the fixture exposes `extractor.path` - setattr(extractor, "path", path.as_posix()) - return extractor - - -@pytest.fixture -def mimikatz_extractor(): - return get_extractor(get_data_path_by_name("mimikatz")) - - -@pytest.fixture -def a933a_extractor(): - return get_extractor(get_data_path_by_name("a933a...")) - - -@pytest.fixture -def kernel32_extractor(): - return get_extractor(get_data_path_by_name("kernel32")) - - -@pytest.fixture -def a1982_extractor(): - return get_extractor(get_data_path_by_name("a1982...")) - - -@pytest.fixture -def z9324d_extractor(): - return get_extractor(get_data_path_by_name("9324d...")) - - -@pytest.fixture -def z395eb_extractor(): - return get_extractor(get_data_path_by_name("395eb...")) - - -@pytest.fixture -def pma12_04_extractor(): - return get_extractor(get_data_path_by_name("pma12-04")) - - -@pytest.fixture -def pma16_01_extractor(): - return get_extractor(get_data_path_by_name("pma16-01")) - - -@pytest.fixture -def bfb9b_extractor(): - return get_extractor(get_data_path_by_name("bfb9b...")) - - -@pytest.fixture -def pma21_01_extractor(): - return get_extractor(get_data_path_by_name("pma21-01")) - - -@pytest.fixture -def c9188_extractor(): - return get_extractor(get_data_path_by_name("c9188...")) - - -@pytest.fixture -def z39c05_extractor(): - return get_extractor(get_data_path_by_name("39c05...")) - - -@pytest.fixture -def z499c2_extractor(): - return get_extractor(get_data_path_by_name("499c2...")) - - -@pytest.fixture -def al_khaser_x86_extractor(): - return get_extractor(get_data_path_by_name("al-khaser x86")) - - -@pytest.fixture -def pingtaest_extractor(): - return get_extractor(get_data_path_by_name("pingtaest")) - - -@pytest.fixture -def b9f5b_dotnetfile_extractor(): - return get_dotnetfile_extractor(get_data_path_by_name("b9f5b")) - - -@pytest.fixture -def mixed_mode_64_dotnetfile_extractor(): - return get_dotnetfile_extractor(get_data_path_by_name("mixed-mode-64")) - - -@pytest.fixture -def hello_world_dotnetfile_extractor(): - return get_dnfile_extractor(get_data_path_by_name("hello-world")) - - -@pytest.fixture -def _1c444_dotnetfile_extractor(): - return get_dnfile_extractor(get_data_path_by_name("_1c444")) - - -@pytest.fixture -def _692f_dotnetfile_extractor(): - return get_dnfile_extractor(get_data_path_by_name("_692f")) - - -@pytest.fixture -def _0953c_dotnetfile_extractor(): - return get_dnfile_extractor(get_data_path_by_name("_0953c")) - - -@pytest.fixture -def _039a6_dotnetfile_extractor(): - return get_dnfile_extractor(get_data_path_by_name("_039a6")) + assert features.get(feature, set()) != set(), f"{feature} should be found in {scope.__name__}" + assert len(features[feature]) == expected, f"{feature} should be found {expected} times in {scope.__name__}" def get_result_doc(path: Path): @@ -1624,3 +784,232 @@ def dynamic_a0000a6_rd(): / "rd" / "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz" ) + + +PMA1601 = CD / "data" / "Practical Malware Analysis Lab 16-01.exe_" +z9324 = CD / "data" / "9324d1a8ae37a36ae560c37448c9705a.exe_" + + +# used by test_viv_features +# as well as some fixtures below +@functools.lru_cache(maxsize=1) +def get_viv_extractor(path: Path): + import capa.features.extractors.viv.extractor + import capa.main + + sigpaths = [ + CD / "data" / "sigs" / "test_aulldiv.pat", + CD / "data" / "sigs" / "test_aullrem.pat.gz", + CD.parent / "sigs" / "1_flare_msvc_rtf_32_64.sig", + CD.parent / "sigs" / "2_flare_msvc_atlmfc_32_64.sig", + CD.parent / "sigs" / "3_flare_common_libs.sig", + ] + + if "raw32" in path.name: + vw = capa.loader.get_workspace(path, "sc32", sigpaths=sigpaths) + elif "raw64" in path.name: + vw = capa.loader.get_workspace(path, "sc64", sigpaths=sigpaths) + else: + vw = capa.loader.get_workspace(path, FORMAT_AUTO, sigpaths=sigpaths) + vw.saveWorkspace() + + extractor = capa.features.extractors.viv.extractor.VivisectFeatureExtractor( + vw, path, OS_AUTO + ) + + # + # fixups to overcome differences between backends + # + if "3b13b" in path.name: + # vivisect only recognizes calling thunk function at 0x10001573 + extractor.vw.makeFunction(0x10006860) + if "294b8d" in path.name: + # see vivisect/#561 + extractor.vw.makeFunction(0x404970) + + return extractor + + +@pytest.fixture +def z9324d_extractor(): + return get_viv_extractor(z9324) + + +@pytest.fixture +def pma16_01_extractor(): + return get_viv_extractor(PMA1601) + + +@functools.lru_cache(maxsize=1) +def get_pefile_extractor(path: Path): + import capa.features.extractors.pefile + + extractor = capa.features.extractors.pefile.PefileFeatureExtractor(path) + setattr(extractor, "path", path.as_posix()) + return extractor + + +@functools.lru_cache(maxsize=1) +def get_dnfile_extractor(path: Path): + extractor = DnfileFeatureExtractor(path) + setattr(extractor, "path", path.as_posix()) + return extractor + + +@functools.lru_cache(maxsize=1) +def get_dotnetfile_extractor(path: Path): + import capa.features.extractors.dotnetfile + + extractor = capa.features.extractors.dotnetfile.DotnetFileFeatureExtractor(path) + setattr(extractor, "path", path.as_posix()) + return extractor + + +@functools.lru_cache(maxsize=1) +def get_cape_extractor(path): + from capa.helpers import load_json_from_path + from capa.features.extractors.cape.extractor import CapeExtractor + + report = load_json_from_path(path) + return CapeExtractor.from_report(report) + + +@functools.lru_cache(maxsize=1) +def get_drakvuf_extractor(path): + from capa.helpers import load_jsonl_from_path + from capa.features.extractors.drakvuf.extractor import DrakvufExtractor + + report = load_jsonl_from_path(path) + return DrakvufExtractor.from_report(report) + + +@functools.lru_cache(maxsize=1) +def get_vmray_extractor(path): + from capa.features.extractors.vmray.extractor import VMRayExtractor + + return VMRayExtractor.from_zipfile(path) + + +@functools.lru_cache(maxsize=1) +def get_binja_extractor(path: Path): + import binaryninja + from binaryninja import Settings + + import capa.features.extractors.binja.extractor + + settings = Settings() + if path.name.endswith("kernel32-64.dll_"): + old_pdb = settings.get_bool("pdb.loadGlobalSymbols") + settings.set_bool("pdb.loadGlobalSymbols", False) + else: + old_pdb = False + bv = binaryninja.load(str(path)) + if path.name.endswith("kernel32-64.dll_"): + settings.set_bool("pdb.loadGlobalSymbols", old_pdb) + + if "al-khaser_x64.exe_" in path.name: + bv.create_user_function(0x14004B4F0) + bv.update_analysis_and_wait() + + extractor = capa.features.extractors.binja.extractor.BinjaFeatureExtractor(bv) + setattr(extractor, "path", path.as_posix()) + return extractor + + +GHIDRA_CACHE: dict[Path, tuple] = {} + + +def get_ghidra_extractor(path: Path): + import pyghidra + + if not pyghidra.started(): + pyghidra.start() + + import capa.features.extractors.ghidra.context + + if path in GHIDRA_CACHE: + extractor, program, flat_api, monitor = GHIDRA_CACHE[path] + capa.features.extractors.ghidra.context.set_context(program, flat_api, monitor) + return extractor + + extractor = capa.loader.get_extractor( + path, + FORMAT_AUTO, + OS_AUTO, + capa.loader.BACKEND_GHIDRA, + [], + disable_progress=True, + ) + + ctx = capa.features.extractors.ghidra.context.get_context() + GHIDRA_CACHE[path] = (extractor, ctx.program, ctx.flat_api, ctx.monitor) + return extractor + + +def _fixup_idalib(path: Path, extractor): + import idaapi + import ida_funcs + + def remove_library_id_flag(fva): + f = idaapi.get_func(fva) + f.flags &= ~ida_funcs.FUNC_LIB + ida_funcs.update_func(f) + + if "kernel32-64" in path.name: + remove_library_id_flag(0x1800202B0) + + if "al-khaser_x64" in path.name: + remove_library_id_flag(0x14004B4F0) + + +def get_idalib_extractor(path: Path): + import capa.features.extractors.ida.extractor + import capa.features.extractors.ida.idalib as idalib + + if not idalib.has_idalib(): + raise RuntimeError("cannot find IDA idalib module.") + + if not idalib.load_idalib(): + raise RuntimeError("failed to load IDA idalib module.") + + import idapro + import ida_auto + + logger.debug("idalib: opening database...") + idapro.enable_console_messages(False) + + ret = idapro.open_database( + str(path), + run_auto_analysis=True, + args="-Olumina:host=0.0.0.0 -Osecondary_lumina:host=0.0.0.0 -R", + ) + if ret != 0: + raise RuntimeError("failed to analyze input file") + + logger.debug("idalib: waiting for analysis...") + ida_auto.auto_wait() + logger.debug("idalib: opened database.") + + extractor = capa.features.extractors.ida.extractor.IdaFeatureExtractor() + _fixup_idalib(path, extractor) + return extractor + + +# used by both: +# - test_binexport_features +# - test_binexport_accessors +@functools.lru_cache(maxsize=1) +def get_binexport_extractor(path): + import capa.features.extractors.binexport2 + import capa.features.extractors.binexport2.extractor + + be2 = capa.features.extractors.binexport2.get_binexport2(path) + search_paths = [CD / "data", CD / "data" / "aarch64"] + path = capa.features.extractors.binexport2.get_sample_from_binexport2( + path, be2, search_paths + ) + buf = path.read_bytes() + + return capa.features.extractors.binexport2.extractor.BinExport2FeatureExtractor( + be2, buf + ) diff --git a/tests/fixtures/features/README.md b/tests/fixtures/features/README.md index 748a26e8..a164229a 100644 --- a/tests/fixtures/features/README.md +++ b/tests/fixtures/features/README.md @@ -126,16 +126,17 @@ For example: ```python import fixtures -BACKEND = fixtures.BackendFeaturePolicy( - name="viv", - get_extractor=fixtures.get_viv_extractor, - exclude_tags={"dotnet"}, + +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="viv", + include_tags={"static"}, + exclude_tags={"dotnet", "ghidra"}, + ) ) - - -@fixtures.parametrize_backend_feature_fixtures(BACKEND) def test_viv_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_viv_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) ``` Module-level availability checks are still allowed. runtime-specific hooks are allowed only when they depend on the installed backend or tool version and cannot be represented declaratively in the fixture manifests. diff --git a/tests/fixtures/features/binexport.json b/tests/fixtures/features/binexport.json index a9067a56..1a2a08c6 100644 --- a/tests/fixtures/features/binexport.json +++ b/tests/fixtures/features/binexport.json @@ -3,28 +3,24 @@ { "key": "687e79.ghidra.be2", "path": "data/binexport2/687e79cde5b0ced75ac229465835054931f9ec438816f2827a8be5f3bd474929.elf_.ghidra.BinExport", - "tags": [ - "binexport", - "elf", - "aarch64" - ] + "tags": ["binexport", "elf", "aarch64"] }, { "key": "d1e650.ghidra.be2", "path": "data/binexport2/d1e6506964edbfffb08c0dd32e1486b11fbced7a4bd870ffe79f110298f0efb8.elf_.ghidra.BinExport", - "tags": [ - "binexport", - "elf", - "aarch64" - ] + "tags": ["binexport", "elf", "aarch64"] + }, + { + "key": "mimikatz.ghidra.be2", + "path": "data/binexport2/mimikatz.exe_.ghidra.BinExport", + "tags": ["binexport"] } ], "features": [ { "file": "687e79.ghidra.be2", "location": "file", - "feature": "string: AppDataService start", - "expected": true + "feature": "string: AppDataService start" }, { "file": "687e79.ghidra.be2", @@ -35,8 +31,7 @@ { "file": "687e79.ghidra.be2", "location": "file", - "feature": "section: .text", - "expected": true + "feature": "section: .text" }, { "file": "687e79.ghidra.be2", @@ -48,7 +43,6 @@ "file": "687e79.ghidra.be2", "location": "file", "feature": "export: android::clearDir", - "expected": true, "marks": [ { "backend": "binexport", @@ -66,20 +60,17 @@ { "file": "687e79.ghidra.be2", "location": "file", - "feature": "import: fopen", - "expected": true + "feature": "import: fopen" }, { "file": "687e79.ghidra.be2", "location": "file", - "feature": "import: exit", - "expected": true + "feature": "import: exit" }, { "file": "687e79.ghidra.be2", "location": "file", - "feature": "import: _ZN7android10IInterfaceD0Ev", - "expected": true + "feature": "import: _ZN7android10IInterfaceD0Ev" }, { "file": "687e79.ghidra.be2", @@ -90,8 +81,7 @@ { "file": "687e79.ghidra.be2", "location": "function=0x1056c0", - "feature": "characteristic: loop", - "expected": true + "feature": "characteristic: loop" }, { "file": "687e79.ghidra.be2", @@ -102,14 +92,12 @@ { "file": "d1e650.ghidra.be2", "location": "function=0x114af4", - "feature": "characteristic: tight loop", - "expected": true + "feature": "characteristic: tight loop" }, { "file": "d1e650.ghidra.be2", "location": "function=0x118F1C", - "feature": "characteristic: tight loop", - "expected": true + "feature": "characteristic: tight loop" }, { "file": "d1e650.ghidra.be2", @@ -121,7 +109,6 @@ "file": "687e79.ghidra.be2", "location": "function=0x0", "feature": "characteristic: stack string", - "expected": true, "marks": [ { "backend": "binexport", @@ -133,20 +120,17 @@ { "file": "687e79.ghidra.be2", "location": "function=0x107588", - "feature": "mnemonic: stp", - "expected": true + "feature": "mnemonic: stp" }, { "file": "687e79.ghidra.be2", "location": "function=0x107588", - "feature": "mnemonic: adrp", - "expected": true + "feature": "mnemonic: adrp" }, { "file": "687e79.ghidra.be2", "location": "function=0x107588", - "feature": "mnemonic: bl", - "expected": true + "feature": "mnemonic: bl" }, { "file": "687e79.ghidra.be2", @@ -177,32 +161,27 @@ { "file": "687e79.ghidra.be2", "location": "function=0x105128,bb=0x1051e4", - "feature": "operand[1].number: 0xFFFFFFFF", - "expected": true + "feature": "operand[1].number: 0xFFFFFFFF" }, { "file": "687e79.ghidra.be2", "location": "function=0x107588,bb=0x107588", - "feature": "operand[1].number: 0x8", - "expected": true + "feature": "operand[1].number: 0x8" }, { "file": "687e79.ghidra.be2", "location": "function=0x107588,bb=0x107588,insn=0x1075a4", - "feature": "operand[1].number: 0x8", - "expected": true + "feature": "operand[1].number: 0x8" }, { "file": "687e79.ghidra.be2", "location": "function=0x105128,bb=0x105450", - "feature": "operand[2].offset: 0x10", - "expected": true + "feature": "operand[2].offset: 0x10" }, { "file": "d1e650.ghidra.be2", "location": "function=0x124854,bb=0x1248AC,insn=0x1248B4", - "feature": "operand[2].offset: -0x48", - "expected": true + "feature": "operand[2].offset: -0x48" }, { "file": "d1e650.ghidra.be2", @@ -213,26 +192,22 @@ { "file": "687e79.ghidra.be2", "location": "function=0x105C88", - "feature": "number: 0xF000", - "expected": true + "feature": "number: 0xF000" }, { "file": "687e79.ghidra.be2", "location": "function=0x1057f8,bb=0x1057f8", - "feature": "number: 0xFFFFFFFFFFFFFFFF", - "expected": true + "feature": "number: 0xFFFFFFFFFFFFFFFF" }, { "file": "687e79.ghidra.be2", "location": "function=0x1066e0,bb=0x1068c4", - "feature": "number: 0xFFFFFFFF", - "expected": true + "feature": "number: 0xFFFFFFFF" }, { "file": "687e79.ghidra.be2", "location": "function=0x105128,bb=0x105450", - "feature": "offset: 0x10", - "expected": true + "feature": "offset: 0x10" }, { "file": "d1e650.ghidra.be2", @@ -245,21 +220,18 @@ "file": "d1e650.ghidra.be2", "location": "function=0x1183e0,bb=0x11849c,insn=0x1184b0", "feature": "offset: 0x8", - "expected": true, "comment": "stp x20,x0,[x19, #0x8]" }, { "file": "d1e650.ghidra.be2", "location": "function=0x138688,bb=0x138994,insn=0x1389a8", "feature": "offset: 0x8", - "expected": true, "comment": "str xzr,[x8, #0x8]!" }, { "file": "d1e650.ghidra.be2", "location": "function=0x138688,bb=0x138978,insn=0x138984", "feature": "offset: 0x8", - "expected": true, "comment": "ldr x9,[x8, #0x8]!" }, { @@ -273,27 +245,23 @@ "file": "d1e650.ghidra.be2", "location": "function=0x138a9c,bb=0x138b00,insn=0x138b00", "feature": "offset: 0x1", - "expected": true, "comment": "ldrb w9,[x8, #0x1]" }, { "file": "d1e650.ghidra.be2", "location": "function=0x124854,bb=0x1248AC,insn=0x1248B4", - "feature": "offset: -0x48", - "expected": true + "feature": "offset: -0x48" }, { "file": "687e79.ghidra.be2", "location": "function=0x105128,bb=0x105128,insn=0x10514c", "feature": "offset: 0x8", - "expected": true, "comment": "0010514c add x23,param_1,#0x8" }, { "file": "687e79.ghidra.be2", "location": "function=0x105c88", - "feature": "api: memset", - "expected": true + "feature": "api: memset" }, { "file": "687e79.ghidra.be2", @@ -304,14 +272,12 @@ { "file": "687e79.ghidra.be2", "location": "function=0x107588", - "feature": "string: AppDataService start", - "expected": true + "feature": "string: AppDataService start" }, { "file": "687e79.ghidra.be2", "location": "function=0x1075c0", - "feature": "string: AppDataService", - "expected": true + "feature": "string: AppDataService" }, { "file": "687e79.ghidra.be2", @@ -322,32 +288,27 @@ { "file": "687e79.ghidra.be2", "location": "function=0x106d58", - "feature": "string: /data/misc/wifi/wpa_supplicant.conf", - "expected": true + "feature": "string: /data/misc/wifi/wpa_supplicant.conf" }, { "file": "687e79.ghidra.be2", "location": "function=0x105c88", - "feature": "string: /innerRename/", - "expected": true + "feature": "string: /innerRename/" }, { "file": "687e79.ghidra.be2", "location": "function=0x106d58", - "feature": "string: /\\/data\\/misc/", - "expected": true + "feature": "string: /\\/data\\/misc/" }, { "file": "687e79.ghidra.be2", "location": "function=0x106d58", - "feature": "substring: /data/misc", - "expected": true + "feature": "substring: /data/misc" }, { "file": "d1e650.ghidra.be2", "location": "function=0x1165a4", - "feature": "bytes: E4 05 B8 93 70 BA 6B 41 9C D7 92 52 75 BF 6F CC 1E 83 60 CC", - "expected": true + "feature": "bytes: E4 05 B8 93 70 BA 6B 41 9C D7 92 52 75 BF 6F CC 1E 83 60 CC" }, { "file": "687e79.ghidra.be2", @@ -359,32 +320,27 @@ { "file": "d1e650.ghidra.be2", "location": "function=0x114af4", - "feature": "characteristic: nzxor", - "expected": true + "feature": "characteristic: nzxor" }, { "file": "d1e650.ghidra.be2", "location": "function=0x117988", - "feature": "characteristic: nzxor", - "expected": true + "feature": "characteristic: nzxor" }, { "file": "687e79.ghidra.be2", "location": "function=0x105b38", - "feature": "characteristic: recursive call", - "expected": true + "feature": "characteristic: recursive call" }, { "file": "687e79.ghidra.be2", "location": "function=0x106530", - "feature": "characteristic: recursive call", - "expected": true + "feature": "characteristic: recursive call" }, { "file": "d1e650.ghidra.be2", "location": "function=0x118620", - "feature": "characteristic: indirect call", - "expected": true + "feature": "characteristic: indirect call" }, { "file": "d1e650.ghidra.be2", @@ -395,14 +351,12 @@ { "file": "d1e650.ghidra.be2", "location": "function=0x11451c", - "feature": "characteristic: indirect call", - "expected": true + "feature": "characteristic: indirect call" }, { "file": "687e79.ghidra.be2", "location": "function=0x105080", - "feature": "characteristic: calls from", - "expected": true + "feature": "characteristic: calls from" }, { "file": "687e79.ghidra.be2", @@ -413,14 +367,12 @@ { "file": "687e79.ghidra.be2", "location": "function=0x1075c0", - "feature": "characteristic: calls to", - "expected": true + "feature": "characteristic: calls to" }, { "file": "687e79.ghidra.be2", "location": "file", "feature": "function-name: __libc_init", - "expected": true, "marks": [ { "backend": "binexport", @@ -432,8 +384,7 @@ { "file": "687e79.ghidra.be2", "location": "file", - "feature": "os: android", - "expected": true + "feature": "os: android" }, { "file": "687e79.ghidra.be2", @@ -450,14 +401,12 @@ { "file": "687e79.ghidra.be2", "location": "function=0x107588", - "feature": "os: android", - "expected": true + "feature": "os: android" }, { "file": "687e79.ghidra.be2", "location": "function=0x1075c0,bb=0x1076c0", - "feature": "os: android", - "expected": true + "feature": "os: android" }, { "file": "687e79.ghidra.be2", @@ -474,26 +423,22 @@ { "file": "687e79.ghidra.be2", "location": "file", - "feature": "arch: aarch64", - "expected": true + "feature": "arch: aarch64" }, { "file": "687e79.ghidra.be2", "location": "function=0x107588", - "feature": "arch: aarch64", - "expected": true + "feature": "arch: aarch64" }, { "file": "687e79.ghidra.be2", "location": "function=0x1075c0,bb=0x1076c0", - "feature": "arch: aarch64", - "expected": true + "feature": "arch: aarch64" }, { "file": "687e79.ghidra.be2", "location": "file", - "feature": "format: elf", - "expected": true + "feature": "format: elf" }, { "file": "687e79.ghidra.be2", @@ -504,14 +449,612 @@ { "file": "687e79.ghidra.be2", "location": "function=0x107588", - "feature": "format: elf", - "expected": true + "feature": "format: elf" }, { "file": "687e79.ghidra.be2", "location": "function=0x107588", "feature": "format: pe", "expected": false + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "string: SCardControl", + "explanation": "basic UTF-16LE string" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "string: ACR > ", + "explanation": "UTF-16LE encoded strings with unusual characters and trailing spaces" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "string: nope", + "expected": false, + "explanation": "non-existant string" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "section: .text", + "explanation": "basic section name" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "section: .nope", + "expected": false, + "explanation": "non-existant section" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "import: advapi32.CryptSetHashParam", + "explanation": "import with DLL prefix" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "import: CryptSetHashParam", + "explanation": "import with no DLL prefix" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "import: cabinet.#11", + "explanation": "import by ordinal" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "import: #11", + "expected": false, + "explanation": "non-existant ordinal import" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "import: #nope", + "expected": false, + "explanation": "non-existant ordinal import" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "import: nope", + "expected": false, + "explanation": "non-existant import" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401517", + "feature": "characteristic: loop", + "explanation": "loop" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401000", + "feature": "characteristic: loop", + "expected": false, + "explanation": "non-existant loop" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x402EC4", + "feature": "characteristic: tight loop", + "explanation": "tight-loop" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401000", + "feature": "characteristic: tight loop", + "expected": false, + "explanation": "non-existant tight-loop" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x402EC4,bb=0x402F8E", + "feature": "characteristic: tight loop", + "explanation": "tight-loop at basic block scope" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401000,bb=0x401000", + "feature": "characteristic: tight loop", + "expected": false, + "explanation": "non-existant tight-loop at basic block scope" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4556E5", + "feature": "characteristic: stack string", + "explanation": "stack string (but capa doesn't extract it as a string yet)", + "marks": [ + { + "backend": "binexport", + "mark": "xfail", + "reason": "stack string detection not implemented for binexport" + } + ] + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401000", + "feature": "characteristic: stack string", + "expected": false, + "explanation": "non-existant stack string" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "mnemonic: push", + "explanation": "basic mnemonic" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "mnemonic: in", + "expected": false, + "explanation": "non-existant mnemonic" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D,bb=0x401073,insn=0x401073", + "feature": "number: 0xFF", + "explanation": "number" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D,bb=0x401073,insn=0x401073", + "feature": "operand[1].number: 0xFF", + "explanation": "mov eax, 0FFh; instruction operand number" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D,bb=0x401073,insn=0x401073", + "feature": "operand[0].number: 0xFF", + "expected": false, + "explanation": "mov eax, 0FFh; non-existant instruction operand number" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D,bb=0x4010B0,insn=0x4010B4", + "feature": "operand[0].offset: 4", + "explanation": "cmp [esi+4], ebx; instruction operand offset" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D,bb=0x4010B0,insn=0x4010B4", + "feature": "operand[1].offset: 4", + "expected": false, + "explanation": "cmp [esi+4], ebx; non-existant instruction operand offset" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "number: 0xFF", + "explanation": "small number" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "number: 0x3136B0", + "explanation": "large number" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401000", + "feature": "number: 0x0", + "explanation": "zero number" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "number: 0xC", + "expected": false, + "explanation": "non-existant number" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401553", + "feature": "number: 0xFFFFFFFF", + "explanation": "max u32 number" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x43e543", + "feature": "number: 0xFFFFFFF0", + "explanation": "large u32 number" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "offset: 0x0", + "explanation": "cmp [esi], ebx; zero offset" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "offset: 0x4", + "explanation": "cmp [esi+4], ebx; simple offset" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "offset: 0x8", + "expected": false, + "explanation": "no instruction in the function references [reg+8]" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4011FB", + "feature": "offset: -0x1", + "explanation": "movzx ecx, [eax-1]; negative offset" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4011FB", + "feature": "offset: -0x2", + "explanation": "cmp [eax-2], cx; negative offset -2" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4011FB", + "feature": "number: -0x2", + "expected": false, + "explanation": "cmp [eax-2], cx; negative offset shouldn't emit a number too" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401D64,bb=0x401D73,insn=0x401D85", + "feature": "offset: 0x80000000", + "expected": false, + "explanation": "add ecx, 80000000h; too-large immediate should not be considered an offset" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401CC7,bb=0x401CDE,insn=0x401CF6", + "feature": "offset: 0x10", + "expected": false, + "explanation": "add esp, 10h; stack-relative ADD should not be considered an offset" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x402203,bb=0x402221,insn=0x40223C", + "feature": "offset: 0x4", + "explanation": "add eax, 4; non-stack register ADD should emit an offset feature, treating eax as a pointer" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x471EAB,bb=0x471ED8,insn=0x471EE6", + "feature": "number: 0x4", + "expected": false, + "explanation": "lea ebx, [ecx+eax*4]; should not emit Number feature for the scale" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x47153B,bb=0x4717AB,insn=0x4717B1", + "feature": "number: -0x30", + "expected": false, + "explanation": "lea ecx, [ecx+esi-30h]; should not emit Number feature for the displacement" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401873,bb=0x4018B2,insn=0x4018C0", + "feature": "number: 0x2", + "explanation": "lea ecx, [ebx+2]; should emit Number feature, treating ebx as zero" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x403BAC", + "feature": "api: CryptAcquireContextW", + "explanation": "basic API feature with trailing W" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x403BAC", + "feature": "api: CryptAcquireContext", + "explanation": "basic API feature with stripped W" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x403BAC", + "feature": "api: Nope", + "expected": false, + "explanation": "non-existent API" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4556E5", + "feature": "api: LsaQueryInformationPolicy" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40B3C6", + "feature": "api: LocalFree", + "explanation": "tail call to API via jmp" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "string: SCardControl", + "explanation": "basic string" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "string: ACR > ", + "explanation": "basic string with trailing whitespace" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "string: nope", + "expected": false, + "explanation": "basic string not present" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x44EDEF", + "feature": "string: INPUTEVENT", + "explanation": "string referenced via a pointer" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x46D6CE", + "feature": "string: (null)", + "explanation": "string referenced via direct memory reference" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401517", + "feature": "bytes: CA 3B 0E 00 00 00 F8 AF 47", + "explanation": "basic bytes" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x404414", + "feature": "bytes: 01 80 00 00 40 EA 47 00", + "explanation": "basic bytes, which are a pointer" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "bytes: 53 00 43 00 61 00 72 00 64 00 43 00 6F 00 6E 00 74 00 72 00 6F 00 6C 00", + "expected": false, + "explanation": "should not extract bytes feature for an obvious string (here: UTF-16LE 'SCardControl')" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401000", + "feature": "bytes: FD FF 59 F6 47", + "expected": false, + "explanation": "push offset aAcsAcr1220 ('ACS...') where ACS == 41 00 43 00 happens to be a valid pointer to the middle of an instruction; should not be misinterpreted as bytes feature" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x44570F", + "feature": "bytes: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF", + "expected": false, + "explanation": "regression test for issue #409: should not extract bytes feature from byte sequences read from invalid memory" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x44EDEF", + "feature": "bytes: 49 00 4E 00 50 00 55 00 54 00 45 00 56 00 45 00 4E 00 54 00", + "expected": false, + "explanation": "should not extract bytes feature when instruction references it as a pointer to string bytes" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x410DFC", + "feature": "characteristic: nzxor", + "explanation": "should extract nzxor characteristic, including from xorps SSE instructions" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "characteristic: nzxor", + "expected": false, + "explanation": "non-existant nzxor" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x46D534", + "feature": "characteristic: nzxor", + "expected": false, + "explanation": "should not extract nzxor characteristic for security cookie xors" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4556E5", + "feature": "characteristic: peb access", + "expected": false + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4556E5", + "feature": "characteristic: gs access", + "expected": false + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x410DFC,bb=0x410F05,insn=0x410F0B", + "feature": "characteristic: nzxor" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x410DFC,bb=0x410F05,insn=0x410F12", + "feature": "characteristic: nzxor" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4556E5", + "feature": "characteristic: cross section flow", + "expected": false + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40640e", + "feature": "characteristic: recursive call" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4175FF", + "feature": "characteristic: recursive call", + "expected": false, + "explanation": "issue #386: 0x4175FF makes indirect calls (via dword_4B821C) but never calls itself, directly or via a function-pointer table" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4175FF", + "feature": "characteristic: indirect call" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4556E5", + "feature": "characteristic: indirect call", + "expected": false + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4556E5", + "feature": "characteristic: calls from" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4702FD", + "feature": "characteristic: calls from", + "expected": false + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D", + "feature": "characteristic: calls to" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x456BB9", + "feature": "characteristic: calls to", + "expected": false, + "explanation": "issue #386: 0x456BB9 is only referenced from a function-pointer table at 0x475834, never via a direct call" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40105D,bb=0x401089,insn=0x40108E", + "feature": "characteristic: calls from" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4175FF,bb=0x41761B,insn=0x417620", + "feature": "characteristic: indirect call" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "os: windows" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "arch: i386" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "format: pe" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401000,bb=0x401000", + "feature": "basic blocks: x", + "explanation": "basic block feature emitted" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "file", + "feature": "basic blocks: 1", + "expected": false, + "explanation": "non-existant basic block feature" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40E5C2", + "feature": "count(basic blocks): 7", + "explanation": "7 basic blocks in function", + "marks": [ + { + "backend": "binexport", + "mark": "xfail", + "reason": "Ghidra identifies different function boundaries; see ghidra-tagged count variant" + } + ] + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4702FD", + "feature": "count(characteristic(calls from)): 0", + "explanation": "function has no calls" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40E5C2", + "feature": "count(characteristic(calls from)): 3", + "explanation": "function has 3 calls", + "marks": [ + { + "backend": "binexport", + "mark": "xfail", + "reason": "Ghidra identifies different function boundaries; see ghidra-tagged count variant" + } + ] + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4556E5", + "feature": "count(characteristic(calls to)): 0", + "explanation": "function has no callers", + "marks": [ + { + "backend": "binexport", + "mark": "xfail", + "reason": "Ghidra identifies different function boundaries; see ghidra-tagged count variant" + } + ] + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x40B1F1", + "feature": "count(characteristic(calls to)): 3", + "explanation": "function has 3 callers", + "marks": [ + { + "backend": "binexport", + "mark": "xfail", + "reason": "Ghidra identifies different function boundaries; see ghidra-tagged count variant" + } + ] + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x4702FD", + "feature": "count(characteristic(calls from)): 0", + "explanation": "Ghidra: function has no calls" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401bf1", + "feature": "count(characteristic(calls to)): 2", + "explanation": "Ghidra: function has 2 callers" + }, + { + "file": "mimikatz.ghidra.be2", + "location": "function=0x401000", + "feature": "count(basic blocks): 3", + "explanation": "Ghidra: 3 basic blocks in function" } ] } diff --git a/tests/fixtures/features/static.json b/tests/fixtures/features/static.json index 669e9ea9..e274d64e 100644 --- a/tests/fixtures/features/static.json +++ b/tests/fixtures/features/static.json @@ -3,141 +3,97 @@ { "key": "mimikatz", "path": "data/mimikatz.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "kernel32", "path": "data/kernel32.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "kernel32-64", "path": "data/kernel32-64.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "pma12-04", "path": "data/Practical Malware Analysis Lab 12-04.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "pma16-01", "path": "data/Practical Malware Analysis Lab 16-01.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "7351f.elf", "path": "data/7351f8a40c5450557b24622417fc478d.elf_", - "tags": [ - "elf", - "static" - ] + "tags": ["elf", "static"] }, { "key": "055da8e6.elf", "path": "data/055da8e6ccfe5a9380231ea04b850e18.elf_", - "tags": [ - "elf", - "static" - ] + "tags": ["elf", "static"] }, { "key": "bb38149.elf", "path": "data/bb38149ff4b5c95722b83f24ca27a42b.elf_", - "tags": [ - "elf", - "static" - ] + "tags": ["elf", "static"] }, { "key": "al-khaser x64", "path": "data/al-khaser_x64.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "64d9f", "path": "data/64d9f7d96b99467f36e22fada623c3bb.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "79abd", "path": "data/79abd17391adc6251ecdc58d13d76baf.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "946a9", "path": "data/946a99f36a46d335dec080d9a4371940.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "773290", "path": "data/773290480d5445f11d3dc1b800728966.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "294b8d", "path": "data/294b8db1f2702b60fb2e42fdc50c2cee6a5046112da9a5703a548a4fa50477bc.elf_", - "tags": [ - "elf", - "static" - ] + "tags": ["elf", "static"] }, { "key": "a1982", "path": "data/a198216798ca38f280dc413f8c57f2c2.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "c91887", "path": "data/c91887d861d9bd4a5872249b641bc9f9.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "2bf18d", "path": "data/2bf18d0403677378adad9001b1243211.elf_", - "tags": [ - "elf", - "static", - "symtab" - ] + "tags": ["elf", "static", "symtab"] }, { "key": "2d3edc", "path": "data/2d3edc218a90f03089cc01715a9f047f.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "ea2876", "path": "data/ea2876e9175410b6f6719f80ee44b9553960758c7d0f7bed73c0fe9a78d8e669.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "pma01-01.frz", @@ -158,79 +114,57 @@ { "key": "b9f5b", "path": "data/b9f5bd514485fb06da39beff051b9fdc.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "mixed-mode-64", "path": "data/dotnet/dnfile-testfiles/mixed-mode/ModuleCode/bin/ModuleCode_amd64.exe", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "hello-world", "path": "data/dotnet/dnfile-testfiles/hello-world/hello-world.exe", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "_1c444", "path": "data/dotnet/1c444ebeba24dcba8628b7dfe5fec7c6.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "_692f", "path": "data/dotnet/692f7fd6d198e804d6af98eb9e390d61.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "_0953c", "path": "data/0953cc3b77ed2974b09e3a00708f88de931d681e2d0cb64afbaf714610beabe6.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "_039a6", "path": "data/039a6336d0802a2255669e6867a5679c7eb83313dbc61fb1c7232147379bd304.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "_387f15", "path": "data/dotnet/387f15043f0198fd3a637b0758c2b6dde9ead795c3ed70803426fc355731b173.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "nested_typedef", "path": "data/dotnet/dd9098ff91717f4906afe9dafdfa2f52.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "nested_typeref", "path": "data/dotnet/2c7d60f77812607dec5085973ff76cea.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "pma01-01", "path": "data/Practical Malware Analysis Lab 01-01.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "pma01-01-rd", @@ -239,119 +173,82 @@ { "key": "pma21-01", "path": "data/Practical Malware Analysis Lab 21-01.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "al-khaser x86", "path": "data/al-khaser_x86.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "39c05", "path": "data/39c05b15e9834ac93f206bc114d0a00c357c888db567ba8f5345da0529cbed41.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "499c2", "path": "data/499c2a85f6e8142c3f48d4251c9c7cd6.raw32", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "9324d", "path": "data/9324d1a8ae37a36ae560c37448c9705a.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "395eb", "path": "data/395eb0ddd99d2c9e37b6d0b73485ee9c.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "a933a", "path": "data/a933a1a402775cfa94b6bee0963f4b46.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "bfb9b", "path": "data/bfb9b5391a13d0afd787e87ab90f14f5.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "82bf6", "path": "data/82BF6347ACF15E5D883715DC289D8A2B.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "pingtaest", "path": "data/ping_t\u00e4st.exe_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "3b13b", "path": "data/3b13b6f1d7cd14dc4a097a12e2e505c0a4cff495262261e2bfc991df238b9b04.dll_", - "tags": [ - "static" - ] + "tags": ["static"] }, { "key": "2f7f5f", "path": "data/2f7f5fb5de175e770d7eae87666f9831.elf_", - "tags": [ - "elf", - "static" - ] + "tags": ["elf", "static"] }, { "key": "b5f052", "path": "data/b5f0524e69b3a3cf636c7ac366ca57bf5e3a8fdc8a9f01caf196c611a7918a87.elf_", - "tags": [ - "elf", - "static" - ] + "tags": ["elf", "static"] }, { "key": "bf7a9c", "path": "data/bf7a9c8bdfa6d47e01ad2b056264acc3fd90cf43fe0ed8deec93ab46b47d76cb.elf_", - "tags": [ - "elf", - "static" - ] + "tags": ["elf", "static"] }, { "key": "1038a2", "path": "data/1038a23daad86042c66bfe6c9d052d27048de9653bde5750dc0f240c792d9ac8.elf_", - "tags": [ - "elf", - "static" - ] + "tags": ["elf", "static"] }, { "key": "3da7c", "path": "data/3da7c2c70a2d93ac4643f20339d5c7d61388bddd77a4a5fd732311efad78e535.elf_", - "tags": [ - "elf", - "static" - ] + "tags": ["elf", "static"] } ], "features": [ @@ -359,7 +256,6 @@ "file": "pma12-04", "location": "file", "feature": "characteristic: embedded pe", - "expected": true, "explanation": "embedded PE file in resource section", "marks": [ { @@ -378,7 +274,6 @@ "file": "2d3edc", "location": "file", "feature": "characteristic: embedded pe", - "expected": true, "explanation": "embedded PE file at file scope using file offset addresses", "marks": [ { @@ -386,28 +281,24 @@ "mark": "skip", "reason": "Python capa has bug extracting embedded PE files at absolute offsets" } - ], - "comment": "Embedded PE at file offset 0x7FB0. Note: vivisect freeze has 0x4091b0 but that's a virtual address mislabeled as file offset." + ] }, { "file": "mimikatz", "location": "file", "feature": "string: SCardControl", - "expected": true, "explanation": "basic UTF-16LE string" }, { "file": "mimikatz", "location": "file", "feature": "string: ACR > ", - "expected": true, "explanation": "UTF-16LE encoded strings with unusual characters and trailing spaces" }, { "file": "pma12-04", "location": "file", "feature": "string: winlogon.exe", - "expected": true, "explanation": "basic ASCII string" }, { @@ -421,7 +312,6 @@ "file": "mimikatz", "location": "file", "feature": "section: .text", - "expected": true, "explanation": "basic section name" }, { @@ -435,7 +325,6 @@ "file": "kernel32", "location": "file", "feature": "export: BaseThreadInitThunk", - "expected": true, "explanation": "basic export name" }, { @@ -449,28 +338,24 @@ "file": "ea2876", "location": "file", "feature": "export: vresion.GetFileVersionInfoA", - "expected": true, "explanation": "forwarded export" }, { "file": "mimikatz", "location": "file", "feature": "import: advapi32.CryptSetHashParam", - "expected": true, "explanation": "import with DLL prefix" }, { "file": "mimikatz", "location": "file", "feature": "import: CryptSetHashParam", - "expected": true, "explanation": "import with no DLL prefix" }, { "file": "mimikatz", "location": "file", "feature": "import: cabinet.#11", - "expected": true, "explanation": "import by ordinal" }, { @@ -498,7 +383,6 @@ "file": "mimikatz", "location": "function=0x401517", "feature": "characteristic: loop", - "expected": true, "explanation": "loop" }, { @@ -512,7 +396,6 @@ "file": "mimikatz", "location": "function=0x402EC4", "feature": "characteristic: tight loop", - "expected": true, "explanation": "tight-loop" }, { @@ -526,7 +409,6 @@ "file": "mimikatz", "location": "function=0x402EC4,bb=0x402F8E", "feature": "characteristic: tight loop", - "expected": true, "explanation": "tight-loop at basic block scope" }, { @@ -540,7 +422,6 @@ "file": "mimikatz", "location": "function=0x4556E5", "feature": "characteristic: stack string", - "expected": true, "explanation": "stack string (but capa doesn't extract it as a string yet)" }, { @@ -554,8 +435,7 @@ "file": "mimikatz", "location": "function=0x40105D", "feature": "mnemonic: push", - "explanation": "basic mnemonic", - "expected": true + "explanation": "basic mnemonic" }, { "file": "mimikatz", @@ -568,14 +448,12 @@ "file": "mimikatz", "location": "function=0x40105D,bb=0x401073,insn=0x401073", "feature": "number: 0xFF", - "expected": true, "explanation": "number" }, { "file": "mimikatz", "location": "function=0x40105D,bb=0x401073,insn=0x401073", "feature": "operand[1].number: 0xFF", - "expected": true, "explanation": "mov eax, 0FFh; instruction operand number" }, { @@ -589,7 +467,6 @@ "file": "mimikatz", "location": "function=0x40105D,bb=0x4010B0,insn=0x4010B4", "feature": "operand[0].offset: 4", - "expected": true, "explanation": "cmp [esi+4], ebx; instruction operand offset" }, { @@ -603,21 +480,18 @@ "file": "mimikatz", "location": "function=0x40105D", "feature": "number: 0xFF", - "expected": true, "explanation": "small number" }, { "file": "mimikatz", "location": "function=0x40105D", "feature": "number: 0x3136B0", - "expected": true, "explanation": "large number" }, { "file": "mimikatz", "location": "function=0x401000", "feature": "number: 0x0", - "expected": true, "explanation": "zero number" }, { @@ -631,35 +505,30 @@ "file": "mimikatz", "location": "function=0x401553", "feature": "number: 0xFFFFFFFF", - "expected": true, "explanation": "max u32 number" }, { "file": "mimikatz", "location": "function=0x43e543", "feature": "number: 0xFFFFFFF0", - "expected": true, "explanation": "large u32 number" }, { "file": "mimikatz", "location": "function=0x40105D", "feature": "offset: 0x0", - "explanation": "cmp [esi], ebx; zero offset", - "expected": true + "explanation": "cmp [esi], ebx; zero offset" }, { "file": "mimikatz", "location": "function=0x40105D", "feature": "offset: 0x4", - "explanation": "cmp [esi+4], ebx; simple offset", - "expected": true + "explanation": "cmp [esi+4], ebx; simple offset" }, { "file": "64d9f", "location": "function=0x10001510,bb=0x100015B0", "feature": "offset: 0x4000", - "expected": true, "explanation": "regression test for issue #276" }, { @@ -673,14 +542,12 @@ "file": "mimikatz", "location": "function=0x4011FB", "feature": "offset: -0x1", - "expected": true, "explanation": "movzx ecx, [eax-1]; negative offset" }, { "file": "mimikatz", "location": "function=0x4011FB", "feature": "offset: -0x2", - "expected": true, "explanation": "cmp [eax-2], cx; negative offset -2" }, { @@ -708,7 +575,6 @@ "file": "mimikatz", "location": "function=0x402203,bb=0x402221,insn=0x40223C", "feature": "offset: 0x4", - "expected": true, "explanation": "add eax, 4; non-stack register ADD should emit an offset feature, treating eax as a pointer" }, { @@ -729,21 +595,18 @@ "file": "mimikatz", "location": "function=0x401873,bb=0x4018B2,insn=0x4018C0", "feature": "number: 0x2", - "expected": true, "explanation": "lea ecx, [ebx+2]; should emit Number feature, treating ebx as zero" }, { "file": "mimikatz", "location": "function=0x403BAC", "feature": "api: CryptAcquireContextW", - "expected": true, "explanation": "basic API feature with trailing W" }, { "file": "mimikatz", "location": "function=0x403BAC", "feature": "api: CryptAcquireContext", - "expected": true, "explanation": "basic API feature with stripped W" }, { @@ -756,14 +619,12 @@ { "file": "mimikatz", "location": "function=0x4556E5", - "feature": "api: LsaQueryInformationPolicy", - "expected": true + "feature": "api: LsaQueryInformationPolicy" }, { "file": "kernel32-64", "location": "function=0x180001010", "feature": "api: RtlVirtualUnwind", - "expected": true, "marks": [ { "backend": "idalib", @@ -776,7 +637,6 @@ "file": "kernel32-64", "location": "function=0x1800202B0", "feature": "api: RtlCaptureContext", - "expected": true, "explanation": "API called via thunk", "marks": [ { @@ -795,7 +655,6 @@ "file": "al-khaser x64", "location": "function=0x14004B4F0", "feature": "api: __vcrt_GetModuleHandle", - "expected": true, "explanation": "API called via nested thunks", "marks": [ { @@ -814,49 +673,42 @@ "file": "mimikatz", "location": "function=0x40B3C6", "feature": "api: LocalFree", - "expected": true, "explanation": "tail call to API via jmp" }, { "file": "c91887", "location": "function=0x40156F", "feature": "api: CloseClipboard", - "expected": true, "explanation": "tail call to API via jmp" }, { "file": "c91887", "location": "function=0x401A77", "feature": "api: CreatePipe", - "expected": true, "explanation": "API is present" }, { "file": "c91887", "location": "function=0x401A77", "feature": "api: kernel32.CreatePipe", - "expected": true, "explanation": "API is present, and DLL name is ignored" }, { "file": "c91887", "location": "function=0x401A77", "feature": "api: CreatePipe", - "expected": true, "explanation": "API resolved from call to GetProcAddress" }, { "file": "mimikatz", "location": "function=0x40105D", "feature": "string: SCardControl", - "expected": true, "explanation": "basic string" }, { "file": "mimikatz", "location": "function=0x40105D", "feature": "string: ACR > ", - "expected": true, "explanation": "basic string with trailing whitespace" }, { @@ -870,7 +722,6 @@ "file": "773290", "location": "function=0x140001140", "feature": "string: %s:\\\\OfficePackagesForWDAG", - "expected": true, "explanation": "string with escaping characters" }, { @@ -884,55 +735,47 @@ "file": "pma16-01", "location": "function=0x4021B0", "feature": "substring: HTTP/1.0", - "expected": true, "explanation": "basic substring" }, { "file": "pma16-01", "location": "function=0x402F40", "feature": "string: /PRACTICALmalwareANALYSIS/i", - "expected": true, "explanation": "case-insensitive regex" }, { "file": "pma16-01", "location": "function=0x402F40", "feature": "string: /www.*/", - "expected": true, "explanation": "simple regex prefix match" }, { "file": "pma16-01", "location": "function=0x402F40", - "feature": "substring: practicalmalwareanalysis.com", - "expected": true + "feature": "substring: practicalmalwareanalysis.com" }, { "file": "mimikatz", "location": "function=0x44EDEF", "feature": "string: INPUTEVENT", - "expected": true, "explanation": "string referenced via a pointer" }, { "file": "mimikatz", "location": "function=0x46D6CE", "feature": "string: (null)", - "expected": true, "explanation": "string referenced via direct memory reference" }, { "file": "mimikatz", "location": "function=0x401517", "feature": "bytes: CA 3B 0E 00 00 00 F8 AF 47", - "expected": true, "explanation": "basic bytes" }, { "file": "mimikatz", "location": "function=0x404414", "feature": "bytes: 01 80 00 00 40 EA 47 00", - "expected": true, "explanation": "basic bytes, which are a pointer" }, { @@ -967,7 +810,6 @@ "file": "mimikatz", "location": "function=0x410DFC", "feature": "characteristic: nzxor", - "expected": true, "explanation": "should extract nzxor characteristic, including from xorps SSE instructions" }, { @@ -987,8 +829,7 @@ { "file": "kernel32-64", "location": "function=0x1800017D0", - "feature": "characteristic: peb access", - "expected": true + "feature": "characteristic: peb access" }, { "file": "mimikatz", @@ -999,8 +840,7 @@ { "file": "kernel32-64", "location": "function=0x180001068", - "feature": "characteristic: gs access", - "expected": true + "feature": "characteristic: gs access" }, { "file": "mimikatz", @@ -1011,56 +851,47 @@ { "file": "mimikatz", "location": "function=0x410DFC,bb=0x410F05,insn=0x410F0B", - "feature": "characteristic: nzxor", - "expected": true + "feature": "characteristic: nzxor" }, { "file": "mimikatz", "location": "function=0x410DFC,bb=0x410F05,insn=0x410F12", - "feature": "characteristic: nzxor", - "expected": true + "feature": "characteristic: nzxor" }, { "file": "kernel32-64", "location": "function=0x1800017D0,bb=0x1800018AD,insn=0x1800018AD", - "feature": "characteristic: peb access", - "expected": true + "feature": "characteristic: peb access" }, { "file": "kernel32-64", "location": "function=0x180001068,bb=0x18000118D,insn=0x180001197", - "feature": "characteristic: gs access", - "expected": true + "feature": "characteristic: gs access" }, { "file": "kernel32-64", "location": "function=0x180001068,bb=0x180001269,insn=0x18000127F", - "feature": "characteristic: gs access", - "expected": true + "feature": "characteristic: gs access" }, { "file": "kernel32", "location": "function=0x7DD70E00,bb=0x7DD70E00,insn=0x7DD70E05", - "feature": "characteristic: fs access", - "expected": true + "feature": "characteristic: fs access" }, { "file": "kernel32", "location": "function=0x7DD70E00,bb=0x7DD70E25,insn=0x7DD70E2D", - "feature": "characteristic: fs access", - "expected": true + "feature": "characteristic: fs access" }, { "file": "kernel32", "location": "function=0x7DD70E00,bb=0x7DD70FCB,insn=0x7DD70FCB", - "feature": "characteristic: fs access", - "expected": true + "feature": "characteristic: fs access" }, { "file": "a1982", "location": "function=0x4014D0", - "feature": "characteristic: cross section flow", - "expected": true + "feature": "characteristic: cross section flow" }, { "file": "kernel32-64", @@ -1078,8 +909,7 @@ { "file": "mimikatz", "location": "function=0x40640e", - "feature": "characteristic: recursive call", - "expected": true + "feature": "characteristic: recursive call" }, { "file": "mimikatz", @@ -1091,8 +921,7 @@ { "file": "mimikatz", "location": "function=0x4175FF", - "feature": "characteristic: indirect call", - "expected": true + "feature": "characteristic: indirect call" }, { "file": "mimikatz", @@ -1103,8 +932,7 @@ { "file": "mimikatz", "location": "function=0x4556E5", - "feature": "characteristic: calls from", - "expected": true + "feature": "characteristic: calls from" }, { "file": "mimikatz", @@ -1115,14 +943,12 @@ { "file": "mimikatz", "location": "function=0x40105D", - "feature": "characteristic: calls to", - "expected": true + "feature": "characteristic: calls to" }, { "file": "ea2876", "location": "file", - "feature": "characteristic: forwarded export", - "expected": true + "feature": "characteristic: forwarded export" }, { "file": "mimikatz", @@ -1134,27 +960,23 @@ { "file": "mimikatz", "location": "function=0x40105D,bb=0x401089,insn=0x40108E", - "feature": "characteristic: calls from", - "expected": true + "feature": "characteristic: calls from" }, { "file": "mimikatz", "location": "function=0x4175FF,bb=0x41761B,insn=0x417620", - "feature": "characteristic: indirect call", - "expected": true + "feature": "characteristic: indirect call" }, { "file": "pma16-01", "location": "file", "feature": "function-name: __aulldiv", - "expected": true, "explanation": "recognize function name via FLIRT signatures" }, { "file": "pma16-01", "location": "file", - "feature": "os: windows", - "expected": true + "feature": "os: windows" }, { "file": "pma16-01", @@ -1165,28 +987,24 @@ { "file": "mimikatz", "location": "file", - "feature": "os: windows", - "expected": true + "feature": "os: windows" }, { "file": "pma16-01", "location": "function=0x401100", "feature": "os: windows", - "expected": true, "explanation": "OS available at function scope" }, { "file": "pma16-01", "location": "function=0x401100,bb=0x401130", "feature": "os: windows", - "expected": true, "explanation": "OS available at basic block scope" }, { "file": "pma16-01", "location": "file", - "feature": "arch: i386", - "expected": true + "feature": "arch: i386" }, { "file": "pma16-01", @@ -1197,28 +1015,24 @@ { "file": "mimikatz", "location": "file", - "feature": "arch: i386", - "expected": true + "feature": "arch: i386" }, { "file": "pma16-01", "location": "function=0x401100", "feature": "arch: i386", - "expected": true, "explanation": "arch available at function scope" }, { "file": "pma16-01", "location": "function=0x401100,bb=0x401130", "feature": "arch: i386", - "expected": true, "explanation": "arch available at basic blockscope" }, { "file": "pma16-01", "location": "file", - "feature": "format: pe", - "expected": true + "feature": "format: pe" }, { "file": "pma16-01", @@ -1229,21 +1043,18 @@ { "file": "mimikatz", "location": "file", - "feature": "format: pe", - "expected": true + "feature": "format: pe" }, { "file": "pma16-01", "location": "function=0x401100", "feature": "format: pe", - "expected": true, "explanation": "format available at function scope" }, { "file": "7351f.elf", "location": "file", - "feature": "os: linux", - "expected": true + "feature": "os: linux" }, { "file": "7351f.elf", @@ -1254,8 +1065,7 @@ { "file": "7351f.elf", "location": "file", - "feature": "format: elf", - "expected": true + "feature": "format: elf" }, { "file": "7351f.elf", @@ -1272,116 +1082,106 @@ { "file": "7351f.elf", "location": "file", - "feature": "arch: amd64", - "expected": true + "feature": "arch: amd64" }, { "file": "7351f.elf", "location": "function=0x408753", - "feature": "string: /dev/null", - "expected": true + "feature": "string: /dev/null" }, { "file": "7351f.elf", "location": "function=0x408753,bb=0x408781", "feature": "api: open", - "expected": true, "explanation": "API from ELF import" }, { "file": "055da8e6.elf", "location": "file", "feature": "import: puts", - "expected": true, "explanation": "ELF import promoted from elffile feature tests" }, { "file": "055da8e6.elf", "location": "file", "feature": "section: .text", - "expected": true, "explanation": "ELF section promoted from primary presence fixture" }, { "file": "bb38149.elf", "location": "file", "feature": "import: __android_log_print", - "expected": true, "explanation": "stripped ELF import promoted from elffile feature tests" }, { "file": "bb38149.elf", "location": "file", "feature": "export: Java_o_ac_a", - "expected": true, "explanation": "stripped ELF export promoted from elffile feature tests" }, { "file": "bb38149.elf", "location": "file", "feature": "section: .dynamic", - "expected": true, - "explanation": "stripped ELF section promoted into the shared presence fixture" + "explanation": "stripped ELF section promoted into the shared presence fixture", + "marks": [ + { + "backend": "idalib", + "mark": "xfail", + "reason": "IDA maps this stripped ELF from program headers, not section headers, so .dynamic is subsumed into a LOAD segment" + } + ] }, { "file": "79abd", "location": "function=0x10002385,bb=0x10002385", - "feature": "characteristic: call $+5", - "expected": true + "feature": "characteristic: call $+5" }, { "file": "946a9", "location": "function=0x10001510,bb=0x100015c0", - "feature": "characteristic: call $+5", - "expected": true + "feature": "characteristic: call $+5" }, { "file": "2bf18d", "location": "function=0x4027b3,bb=0x402861,insn=0x40286d", "feature": "api: __GI_connect", - "expected": true, "explanation": "API from symbol table alternative name" }, { "file": "2bf18d", "location": "function=0x4027b3,bb=0x402861,insn=0x40286d", "feature": "api: connect", - "expected": true, "explanation": "API from symbol table alternative name" }, { "file": "2bf18d", "location": "function=0x4027b3,bb=0x402861,insn=0x40286d", "feature": "api: __libc_connect", - "expected": true, "explanation": "API from symbol table alternative name" }, { "file": "2bf18d", "location": "function=0x4088a4", "feature": "function-name: __GI_connect", - "expected": true, "explanation": "function name from symbol table alternative name" }, { "file": "2bf18d", "location": "function=0x4088a4", "feature": "function-name: connect", - "expected": true, "explanation": "function name from symbol table alternative name" }, { "file": "2bf18d", "location": "function=0x4088a4", "feature": "function-name: __libc_connect", - "expected": true, "explanation": "function name from symbol table alternative name" }, { "file": "mimikatz", "location": "function=0x401000,bb=0x401000", "feature": "basic blocks: x", - "expected": true, "explanation": "basic block feature emitted" }, { @@ -1395,7 +1195,6 @@ "file": "mimikatz", "location": "function=0x40E5C2", "feature": "count(basic blocks): 7", - "expected": true, "explanation": "7 basic blocks in function", "marks": [ { @@ -1409,14 +1208,12 @@ "file": "mimikatz", "location": "function=0x4702FD", "feature": "count(characteristic(calls from)): 0", - "expected": true, "explanation": "function has no calls" }, { "file": "mimikatz", "location": "function=0x40E5C2", "feature": "count(characteristic(calls from)): 3", - "expected": true, "explanation": "function has 3 calls", "marks": [ { @@ -1430,7 +1227,6 @@ "file": "mimikatz", "location": "function=0x4556E5", "feature": "count(characteristic(calls to)): 0", - "expected": true, "explanation": "function has no callers", "marks": [ { @@ -1444,7 +1240,6 @@ "file": "mimikatz", "location": "function=0x40B1F1", "feature": "count(characteristic(calls to)): 3", - "expected": true, "explanation": "function has 3 callers", "marks": [ { @@ -1458,355 +1253,243 @@ "file": "mimikatz", "location": "function=0x4702FD", "feature": "count(characteristic(calls from)): 0", - "expected": true, - "tags": [ - "ghidra" - ], "explanation": "Ghidra: function has no calls" }, { "file": "mimikatz", "location": "function=0x401bf1", "feature": "count(characteristic(calls to)): 2", - "expected": true, - "tags": [ - "ghidra" - ], "explanation": "Ghidra: function has 2 callers" }, { "file": "mimikatz", "location": "function=0x401000", "feature": "count(basic blocks): 3", - "expected": true, - "tags": [ - "ghidra" - ], "explanation": "Ghidra: 3 basic blocks in function" }, { "file": "b9f5b", "location": "file", "feature": "arch: i386", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "b9f5b", "location": "file", "feature": "arch: amd64", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "mixed-mode-64", "location": "file", "feature": "arch: amd64", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "mixed-mode-64", "location": "file", "feature": "arch: i386", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "mixed-mode-64", "location": "file", "feature": "characteristic: mixed mode", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "file", "feature": "characteristic: mixed mode", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "b9f5b", "location": "file", "feature": "os: any", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "b9f5b", "location": "file", "feature": "format: pe", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "b9f5b", "location": "file", "feature": "format: dotnet", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "file", "feature": "function-name: HelloWorld::Main", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "file", "feature": "function-name: HelloWorld::ctor", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "file", "feature": "function-name: HelloWorld::cctor", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "file", "feature": "string: Hello World!", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "file", "feature": "class: HelloWorld", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "file", "feature": "class: System.Console", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "file", "feature": "namespace: System.Diagnostics", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "function=0x250", "feature": "string: Hello World!", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "function=0x250,bb=0x250,insn=0x252", "feature": "string: Hello World!", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "function=0x250,bb=0x250,insn=0x257", "feature": "class: System.Console", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "function=0x250,bb=0x250,insn=0x257", "feature": "namespace: System", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "function=0x250", "feature": "api: System.Console::WriteLine", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "hello-world", "location": "file", "feature": "import: System.Console::WriteLine", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "file", "feature": "string: SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "file", "feature": "string: get_IsAlive", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "file", "feature": "import: gdi32.CreateCompatibleBitmap", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "file", "feature": "import: CreateCompatibleBitmap", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "file", "feature": "import: gdi32::CreateCompatibleBitmap", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "function=0x1F68", "feature": "api: GetWindowDC", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "function=0x1F68", "feature": "number: 0xCC0020", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "token=0x600001D", "feature": "characteristic: calls to", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "token=0x6000018", "feature": "characteristic: calls to", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "token=0x600001D", "feature": "characteristic: calls from", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "token=0x600000F", "feature": "characteristic: calls from", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "function=0x1F68", "feature": "number: 0x0", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "function=0x1F68", "feature": "number: 0x1", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_692f", "location": "token=0x6000004", "feature": "api: System.Linq.Enumerable::First", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "generic method" }, { @@ -1814,235 +1497,167 @@ "location": "token=0x6000004", "feature": "property: System.Linq.Enumerable::First", "expected": false, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "generic method" }, { "file": "_692f", "location": "token=0x6000004", "feature": "namespace: System.Linq", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "generic method" }, { "file": "_692f", "location": "token=0x6000004", "feature": "class: System.Linq.Enumerable", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "generic method" }, { "file": "_1c444", "location": "token=0x6000020", "feature": "namespace: Reqss", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "ldftn" }, { "file": "_1c444", "location": "token=0x6000020", "feature": "class: Reqss.Reqss", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "ldftn" }, { "file": "_1c444", "location": "function=0x1F59,bb=0x1F59,insn=0x1F5B", "feature": "characteristic: unmanaged call", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "function=0x2544", "feature": "characteristic: unmanaged call", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "token=0x6000088", "feature": "characteristic: unmanaged call", "expected": false, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "same as above but using token instead of function" }, { "file": "_1c444", "location": "function=0x1F68,bb=0x1F68,insn=0x1FF9", "feature": "api: System.Drawing.Image::FromHbitmap", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "function=0x1F68,bb=0x1F68,insn=0x1FF9", "feature": "api: FromHbitmap", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "token=0x600002B", "feature": "property/read: System.IO.FileInfo::Length", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { "file": "_1c444", "location": "token=0x600002B", "feature": "property: System.IO.FileInfo::Length", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { "file": "_1c444", "location": "token=0x6000081", "feature": "api: System.Diagnostics.Process::Start", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { "file": "_1c444", "location": "token=0x6000081", "feature": "property/write: System.Diagnostics.ProcessStartInfo::UseShellExecute", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { "file": "_1c444", "location": "token=0x6000081", "feature": "property/write: System.Diagnostics.ProcessStartInfo::WorkingDirectory", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { "file": "_1c444", "location": "token=0x6000081", "feature": "property/write: System.Diagnostics.ProcessStartInfo::FileName", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { "file": "_1c444", "location": "token=0x6000087", "feature": "property/write: Sockets.MySocket::reConnectionDelay", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "Field property access" }, { "file": "_1c444", "location": "token=0x600008A", "feature": "property/write: Sockets.MySocket::isConnected", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "Field property access" }, { "file": "_1c444", "location": "token=0x600008A", "feature": "class: Sockets.MySocket", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "Field property access" }, { "file": "_1c444", "location": "token=0x600008A", "feature": "namespace: Sockets", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "Field property access" }, { "file": "_1c444", "location": "token=0x600008A", "feature": "property/read: Sockets.MySocket::onConnected", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "Field property access" }, { "file": "_0953c", "location": "token=0x6000004", "feature": "property/read: System.Diagnostics.Debugger::IsAttached", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { "file": "_0953c", "location": "token=0x6000004", "feature": "class: System.Diagnostics.Debugger", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { "file": "_0953c", "location": "token=0x6000004", "feature": "namespace: System.Diagnostics", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { @@ -2050,58 +1665,41 @@ "location": "token=0x6000006", "feature": "property/read: System.Management.Automation.PowerShell::Streams", "expected": false, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef property access" }, { "file": "_387f15", "location": "token=0x600009E", "feature": "property/read: Modulo.IqQzcRDvSTulAhyLtZHqyeYGgaXGbuLwhxUKXYmhtnOmgpnPJDTSIPhYPpnE::geoplugin_countryCode", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MethodDef property access" }, { "file": "_387f15", "location": "token=0x600009E", "feature": "class: Modulo.IqQzcRDvSTulAhyLtZHqyeYGgaXGbuLwhxUKXYmhtnOmgpnPJDTSIPhYPpnE", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MethodDef property access" }, { "file": "_387f15", "location": "token=0x600009E", "feature": "namespace: Modulo", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MethodDef property access" }, { "file": "_039a6", "location": "token=0x6000007", "feature": "api: System.Reflection.Assembly::Load", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_039a6", "location": "token=0x600001D", "feature": "property/read: StagelessHollow.Arac::Marka", - "expected": true, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MethodDef method" }, { @@ -2109,9 +1707,7 @@ "location": "token=0x600001C", "feature": "property/read: StagelessHollow.Arac::Marka", "expected": false, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MethodDef method" }, { @@ -2119,190 +1715,136 @@ "location": "token=0x6000023", "feature": "property/read: System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Task", "expected": false, - "tags": [ - "dotnet" - ], + "tags": ["dotnet"], "explanation": "MemberRef method" }, { "file": "nested_typedef", "location": "file", "feature": "class: mynamespace.myclass_outer0", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: mynamespace.myclass_outer1", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: mynamespace.myclass_outer0/myclass_inner0_0", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: mynamespace.myclass_outer0/myclass_inner0_1", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: mynamespace.myclass_outer1/myclass_inner1_0", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: mynamespace.myclass_outer1/myclass_inner1_1", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: mynamespace.myclass_outer1/myclass_inner1_0/myclass_inner_inner", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: myclass_inner_inner", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: myclass_inner1_0", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: myclass_inner1_1", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: myclass_inner0_0", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typedef", "location": "file", "feature": "class: myclass_inner0_1", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typeref", "location": "file", "feature": "import: Android.OS.Build/VERSION::SdkInt", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typeref", "location": "file", "feature": "import: Android.Media.Image/Plane::Buffer", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typeref", "location": "file", "feature": "import: Android.Provider.Telephony/Sent/Sent::ContentUri", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typeref", "location": "file", "feature": "import: Android.OS.Build::SdkInt", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typeref", "location": "file", "feature": "import: Plane::Buffer", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "nested_typeref", "location": "file", "feature": "import: Sent::ContentUri", "expected": false, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "token=0x600001D", "feature": "count(characteristic(calls to)): 1", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] }, { "file": "_1c444", "location": "token=0x600001D", "feature": "count(characteristic(calls from)): 9", - "expected": true, - "tags": [ - "dotnet" - ] + "tags": ["dotnet"] } ] } diff --git a/tests/test_binexport_accessors.py b/tests/test_binexport_accessors.py index 73645f87..2ea1d90c 100644 --- a/tests/test_binexport_accessors.py +++ b/tests/test_binexport_accessors.py @@ -15,11 +15,13 @@ import re import logging -from typing import Any from pathlib import Path +from typing import Any import pytest import fixtures + +CD = Path(__file__).resolve().parent from google.protobuf.json_format import ParseDict import capa.features.extractors.binexport2.helpers @@ -57,7 +59,6 @@ from capa.features.extractors.binexport2.arch.intel.helpers import get_operand_p logger = logging.getLogger(__name__) -CD = Path(__file__).resolve().parent # found via https://www.virustotal.com/gui/search/type%253Aelf%2520and%2520size%253A1.2kb%252B%2520and%2520size%253A1.4kb-%2520and%2520tag%253Aarm%2520and%2520not%2520tag%253Arelocatable%2520and%2520tag%253A64bits/files @@ -621,7 +622,7 @@ def test_pattern_matching_not_stack(): assert match_address_with_be2(BE2_EXTRACTOR_687, queries, 0x107918) is None -BE2_EXTRACTOR_MIMI = fixtures.get_binexport_extractor(CD / "data" / "binexport2" / "mimikatz.exe_.ghidra.BinExport") +BE2_EXTRACTOR_MIMI = fixtures.get_binexport_extractor(fixtures.CD / "data" / "binexport2" / "mimikatz.exe_.ghidra.BinExport") def test_pattern_matching_x86(): diff --git a/tests/test_binexport_features.py b/tests/test_binexport_features.py index cb90e1e5..2172954f 100644 --- a/tests/test_binexport_features.py +++ b/tests/test_binexport_features.py @@ -11,60 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from typing import cast - import fixtures -import pytest - -import capa.features.common - -BACKEND = fixtures.BackendFeaturePolicy( - name="binexport", - get_extractor=fixtures.get_binexport_extractor, - include_tags={"binexport"}, -) -@fixtures.parametrize_backend_feature_fixtures(BACKEND) -def test_binexport_features_elf_aarch64(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) - - -@fixtures.parametrize( - "sample,scope,feature,expected", - fixtures.FEATURE_PRESENCE_TESTS, - indirect=["sample", "scope"], -) -def test_binexport_features_pe_x86(sample, scope, feature, expected): - if "mimikatz.exe_" not in sample.name: - pytest.skip("for now only testing mimikatz.exe_ Ghidra BinExport file") - - if isinstance( - feature, capa.features.common.Characteristic - ) and "stack string" in cast(str, feature.value): - pytest.skip("for now only testing basic features") - - sample = sample.parent / "binexport2" / (sample.name + ".ghidra.BinExport") - assert sample.exists() - fixtures.do_test_feature_presence( - fixtures.get_binexport_extractor, sample, scope, feature, expected +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="binexport", + include_tags={"binexport"}, ) - - -@fixtures.parametrize( - "sample,scope,feature,expected", - fixtures.FEATURE_COUNT_TESTS_GHIDRA, - indirect=["sample", "scope"], ) -def test_binexport_feature_counts_ghidra(sample, scope, feature, expected): - if "mimikatz.exe_" not in sample.name: - pytest.skip("for now only testing mimikatz.exe_ Ghidra BinExport file") - sample = sample.parent / "binexport2" / (sample.name + ".ghidra.BinExport") - assert sample.exists() - fixtures.do_test_feature_count( - fixtures.get_binexport_extractor, sample, scope, feature, expected - ) +def test_binexport_features(feature_fixture): + extractor = fixtures.get_binexport_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) @fixtures.parametrize( diff --git a/tests/test_binja_features.py b/tests/test_binja_features.py index b2052787..d1a6d9c4 100644 --- a/tests/test_binja_features.py +++ b/tests/test_binja_features.py @@ -13,12 +13,10 @@ # limitations under the License. import logging -from pathlib import Path import fixtures import pytest -import capa.features.common import capa.main logger = logging.getLogger(__file__) @@ -41,23 +39,22 @@ except ImportError: pass -BACKEND = fixtures.BackendFeaturePolicy( - name="binja", - # binja also loads .bndb database files natively, so include `binja-db` - # alongside the regular static-binary fixtures. - get_extractor=fixtures.get_binja_extractor, - include_tags={"static", "binja-db"}, - exclude_tags={"dotnet", "ghidra"}, -) - - @pytest.mark.skipif( binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed", ) -@fixtures.parametrize_backend_feature_fixtures(BACKEND) +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="binja", + # binja also loads .bndb database files natively, so include `binja-db` + # alongside the regular static-binary fixtures. + include_tags={"static", "binja-db"}, + exclude_tags={"dotnet", "ghidra"}, + ) +) def test_binja_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_binja_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) @pytest.mark.skipif( @@ -65,10 +62,7 @@ def test_binja_features(feature_fixture): reason="Skip binja tests if the binaryninja Python API is not installed", ) def test_standalone_binja_backend(): - CD = Path(__file__).resolve().parent - test_path = ( - CD / ".." / "tests" / "data" / "Practical Malware Analysis Lab 01-01.exe_" - ) + test_path = fixtures.CD / "data" / "Practical Malware Analysis Lab 01-01.exe_" assert capa.main.main([str(test_path), "-b", capa.main.BACKEND_BINJA]) == 0 diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 912be1cf..e3328e07 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -16,6 +16,7 @@ import textwrap import capa.rules import capa.features.common +import fixtures import capa.capabilities.common import capa.features.extractors.null from capa.features.address import AbsoluteVirtualAddress @@ -212,7 +213,8 @@ def test_byte_matching(z9324d_extractor): assert "byte match test" in capabilities.matches -def test_com_feature_matching(z395eb_extractor): +def test_com_feature_matching(): + extractor = fixtures.get_viv_extractor(fixtures.CD / "data" / "395eb0ddd99d2c9e37b6d0b73485ee9c.exe_") rules = capa.rules.RuleSet([ capa.rules.Rule.from_yaml( textwrap.dedent(""" @@ -230,7 +232,7 @@ def test_com_feature_matching(z395eb_extractor): """) ) ]) - capabilities = capa.capabilities.common.find_capabilities(rules, z395eb_extractor) + capabilities = capa.capabilities.common.find_capabilities(rules, extractor) assert "initialize IWebBrowser2" in capabilities.matches diff --git a/tests/test_cape_features.py b/tests/test_cape_features.py index 226f1cf0..59de29ee 100644 --- a/tests/test_cape_features.py +++ b/tests/test_cape_features.py @@ -11,17 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - - import fixtures -BACKEND = fixtures.BackendFeaturePolicy( - name="cape", - get_extractor=fixtures.get_cape_extractor, - include_tags={"cape"}, + +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="cape", + include_tags={"cape"}, + ) ) - - -@fixtures.parametrize_backend_feature_fixtures(BACKEND) def test_cape_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_cape_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) diff --git a/tests/test_cape_model.py b/tests/test_cape_model.py index 23fa24a5..c11e4903 100644 --- a/tests/test_cape_model.py +++ b/tests/test_cape_model.py @@ -14,7 +14,6 @@ import gzip from typing import Type -from pathlib import Path import pytest import fixtures @@ -25,8 +24,7 @@ from capa.features.extractors.cape.models import Call, CapeReport from capa.features.extractors.cape.thread import get_calls from capa.features.extractors.base_extractor import ThreadHandle, ProcessHandle -CD = Path(__file__).resolve().parent -CAPE_DIR = CD / "data" / "dynamic" / "cape" +CAPE_DIR = fixtures.CD / "data" / "dynamic" / "cape" @fixtures.parametrize( diff --git a/tests/test_dnfile_features.py b/tests/test_dnfile_features.py index 5828b68d..8e9f12b6 100644 --- a/tests/test_dnfile_features.py +++ b/tests/test_dnfile_features.py @@ -31,16 +31,16 @@ CD = Path(__file__).resolve().parent DOTNET_DIR = Path(__file__).resolve().parent / "data" / "dotnet" -BACKEND = fixtures.BackendFeaturePolicy( - name="dnfile", - get_extractor=fixtures.get_dnfile_extractor, - include_tags={"dotnet"}, + +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="dnfile", + include_tags={"dotnet"}, + ) ) - - -@fixtures.parametrize_backend_feature_fixtures(BACKEND) def test_dnfile_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_dnfile_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) def test_get_dotnet_table_row_first_row(): diff --git a/tests/test_dotnetfile_features.py b/tests/test_dotnetfile_features.py index abd67bcc..143f7d5a 100644 --- a/tests/test_dotnetfile_features.py +++ b/tests/test_dotnetfile_features.py @@ -14,37 +14,53 @@ import fixtures -BACKEND = fixtures.BackendFeaturePolicy( - name="dotnetfile", - get_extractor=fixtures.get_dotnetfile_extractor, - include_tags={"dotnet"}, - exclude_tags={ - # dotnetfile is a file-scope extractor; drop non-file scopes - "function", - "basic-block", - "instruction", - # and drop feature types dotnetfile doesn't produce - "function-name", - }, + +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="dotnetfile", + include_tags={"dotnet"}, + exclude_tags={ + # dotnetfile is a file-scope extractor; drop non-file scopes + "function", + "basic-block", + "instruction", + # and drop feature types dotnetfile doesn't produce + "function-name", + }, + ) ) - - -@fixtures.parametrize_backend_feature_fixtures(BACKEND) def test_dotnetfile_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_dotnetfile_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) -@fixtures.parametrize( - "extractor,function,expected", - [ - ("b9f5b_dotnetfile_extractor", "is_dotnet_file", True), - ("b9f5b_dotnetfile_extractor", "is_mixed_mode", False), - ("mixed_mode_64_dotnetfile_extractor", "is_mixed_mode", True), - ("b9f5b_dotnetfile_extractor", "get_entry_point", 0x6000007), - ("b9f5b_dotnetfile_extractor", "get_runtime_version", (2, 5)), - ("b9f5b_dotnetfile_extractor", "get_meta_version_string", "v2.0.50727"), - ], -) -def test_dotnetfile_extractor(request, extractor, function, expected): - extractor_function = getattr(request.getfixturevalue(extractor), function) - assert extractor_function() == expected +def test_dotnetfile_extractor_is_dotnet_file(): + extractor = fixtures.get_dotnetfile_extractor(fixtures.CD / "data" / "b9f5bd514485fb06da39beff051b9fdc.exe_") + assert extractor.is_dotnet_file() is True + + +def test_dotnetfile_extractor_is_not_mixed_mode(): + extractor = fixtures.get_dotnetfile_extractor(fixtures.CD / "data" / "b9f5bd514485fb06da39beff051b9fdc.exe_") + assert extractor.is_mixed_mode() is False + + +def test_dotnetfile_extractor_mixed_mode_64_is_mixed_mode(): + extractor = fixtures.get_dotnetfile_extractor( + fixtures.DNFILE_TESTFILES / "mixed-mode" / "ModuleCode" / "bin" / "ModuleCode_amd64.exe" + ) + assert extractor.is_mixed_mode() is True + + +def test_dotnetfile_extractor_get_entry_point(): + extractor = fixtures.get_dotnetfile_extractor(fixtures.CD / "data" / "b9f5bd514485fb06da39beff051b9fdc.exe_") + assert extractor.get_entry_point() == 0x6000007 + + +def test_dotnetfile_extractor_get_runtime_version(): + extractor = fixtures.get_dotnetfile_extractor(fixtures.CD / "data" / "b9f5bd514485fb06da39beff051b9fdc.exe_") + assert extractor.get_runtime_version() == (2, 5) + + +def test_dotnetfile_extractor_get_meta_version_string(): + extractor = fixtures.get_dotnetfile_extractor(fixtures.CD / "data" / "b9f5bd514485fb06da39beff051b9fdc.exe_") + assert extractor.get_meta_version_string() == "v2.0.50727" diff --git a/tests/test_drakvuf_features.py b/tests/test_drakvuf_features.py index 3cb1e9af..4247c6be 100644 --- a/tests/test_drakvuf_features.py +++ b/tests/test_drakvuf_features.py @@ -11,17 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - - import fixtures -BACKEND = fixtures.BackendFeaturePolicy( - name="drakvuf", - get_extractor=fixtures.get_drakvuf_extractor, - include_tags={"drakvuf"}, + +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="drakvuf", + include_tags={"drakvuf"}, + ) ) - - -@fixtures.parametrize_backend_feature_fixtures(BACKEND) def test_drakvuf_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_drakvuf_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) diff --git a/tests/test_dynamic_span_of_calls_scope.py b/tests/test_dynamic_span_of_calls_scope.py index a9d906e3..6e816144 100644 --- a/tests/test_dynamic_span_of_calls_scope.py +++ b/tests/test_dynamic_span_of_calls_scope.py @@ -64,7 +64,7 @@ def filter_threads(extractor: DynamicFeatureExtractor, ppid: int, pid: int, tid: @lru_cache(maxsize=1) def get_0000a657_thread3064(): - extractor = fixtures.get_cape_extractor(fixtures.get_data_path_by_name("0000a657")) + extractor = fixtures.get_cape_extractor(fixtures.CD / "data" / "dynamic" / "cape" / "v2.2" / "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz") extractor = filter_threads(extractor, 2456, 3052, 3064) return extractor diff --git a/tests/test_elffile_features.py b/tests/test_elffile_features.py index 014d87b2..bebacbb4 100644 --- a/tests/test_elffile_features.py +++ b/tests/test_elffile_features.py @@ -15,13 +15,14 @@ import io from pathlib import Path +import fixtures + from elftools.elf.elffile import ELFFile from capa.features.extractors.elffile import extract_file_export_names, extract_file_import_names -CD = Path(__file__).resolve().parent -SAMPLE_PATH = CD / "data" / "055da8e6ccfe5a9380231ea04b850e18.elf_" -STRIPPED_SAMPLE_PATH = CD / "data" / "bb38149ff4b5c95722b83f24ca27a42b.elf_" +SAMPLE_PATH = fixtures.CD / "data" / "055da8e6ccfe5a9380231ea04b850e18.elf_" +STRIPPED_SAMPLE_PATH = fixtures.CD / "data" / "bb38149ff4b5c95722b83f24ca27a42b.elf_" def check_import_features(sample_path, expected_imports): diff --git a/tests/test_extractor_hashing.py b/tests/test_extractor_hashing.py index dd436f20..e0296ae1 100644 --- a/tests/test_extractor_hashing.py +++ b/tests/test_extractor_hashing.py @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) def test_viv_hash_extraction(): - assert fixtures.get_viv_extractor(fixtures.get_data_path_by_name("mimikatz")).get_sample_hashes() == SampleHashes( + assert fixtures.get_viv_extractor(fixtures.CD / "data" / "mimikatz.exe_").get_sample_hashes() == SampleHashes( md5="5f66b82558ca92e54e77f216ef4c066c", sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38", sha256="131314a6f6d1d263c75b9909586b3e1bd837036329ace5e69241749e861ac01d", @@ -33,7 +33,7 @@ def test_viv_hash_extraction(): def test_pefile_hash_extraction(): assert fixtures.get_pefile_extractor( - fixtures.get_data_path_by_name("mimikatz") + fixtures.CD / "data" / "mimikatz.exe_" ).get_sample_hashes() == SampleHashes( md5="5f66b82558ca92e54e77f216ef4c066c", sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38", @@ -42,7 +42,7 @@ def test_pefile_hash_extraction(): def test_dnfile_hash_extraction(): - assert fixtures.get_dnfile_extractor(fixtures.get_data_path_by_name("b9f5b")).get_sample_hashes() == SampleHashes( + assert fixtures.get_dnfile_extractor(fixtures.CD / "data" / "b9f5bd514485fb06da39beff051b9fdc.exe_").get_sample_hashes() == SampleHashes( md5="b9f5bd514485fb06da39beff051b9fdc", sha1="c72a2e50410475a51d897d29ffbbaf2103754d53", sha256="34acc4c0b61b5ce0b37c3589f97d1f23e6d84011a241e6f85683ee517ce786f1", @@ -51,7 +51,7 @@ def test_dnfile_hash_extraction(): def test_dotnetfile_hash_extraction(): assert fixtures.get_dotnetfile_extractor( - fixtures.get_data_path_by_name("b9f5b") + fixtures.CD / "data" / "b9f5bd514485fb06da39beff051b9fdc.exe_" ).get_sample_hashes() == SampleHashes( md5="b9f5bd514485fb06da39beff051b9fdc", sha1="c72a2e50410475a51d897d29ffbbaf2103754d53", @@ -60,7 +60,7 @@ def test_dotnetfile_hash_extraction(): def test_cape_hash_extraction(): - assert fixtures.get_cape_extractor(fixtures.get_data_path_by_name("0000a657")).get_sample_hashes() == SampleHashes( + assert fixtures.get_cape_extractor(fixtures.CD / "data" / "dynamic" / "cape" / "v2.2" / "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz").get_sample_hashes() == SampleHashes( md5="e2147b5333879f98d515cd9aa905d489", sha1="ad4d520fb7792b4a5701df973d6bd8a6cbfbb57f", sha256="0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82", @@ -84,7 +84,7 @@ except ImportError: @pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed") def test_binja_hash_extraction(): - extractor = fixtures.get_binja_extractor(fixtures.get_data_path_by_name("mimikatz")) + extractor = fixtures.get_binja_extractor(fixtures.CD / "data" / "mimikatz.exe_") hashes = SampleHashes( md5="5f66b82558ca92e54e77f216ef4c066c", sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38", diff --git a/tests/test_freeze_dynamic.py b/tests/test_freeze_dynamic.py index 1afb6e48..c2db45ca 100644 --- a/tests/test_freeze_dynamic.py +++ b/tests/test_freeze_dynamic.py @@ -156,7 +156,7 @@ def test_freeze_bytes_roundtrip(): def test_freeze_load_sample(tmpdir): o = tmpdir.mkdir("capa").join("test.frz") - extractor = fixtures.get_cape_extractor(fixtures.get_data_path_by_name("d46900")) + extractor = fixtures.get_cape_extractor(fixtures.CD / "data" / "dynamic" / "cape" / "v2.2" / "d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json.gz") Path(o.strpath).write_bytes(capa.features.freeze.dump(extractor)) diff --git a/tests/test_freeze_static.py b/tests/test_freeze_static.py index 173cc1ef..57ee6f89 100644 --- a/tests/test_freeze_static.py +++ b/tests/test_freeze_static.py @@ -15,7 +15,7 @@ import textwrap from pathlib import Path -import pytest +import fixtures import capa.main import capa.rules @@ -190,26 +190,15 @@ def test_no_address_lt_irreflexivity(): assert not (no_addr < no_addr) -def test_freeze_sample(tmpdir, z9324d_extractor): +def test_freeze_sample(tmpdir): # tmpdir fixture handles cleanup o = tmpdir.mkdir("capa").join("test.frz").strpath - path = z9324d_extractor.path + path = str(fixtures.CD / "data" / "9324d1a8ae37a36ae560c37448c9705a.exe_") assert capa.features.freeze.main([path, o, "-v"]) == 0 -@pytest.mark.parametrize( - "extractor", - [ - pytest.param("z9324d_extractor"), - ], -) -def test_freeze_load_sample(tmpdir, request, extractor): +def test_freeze_load_sample(tmpdir, z9324d_extractor): o = tmpdir.mkdir("capa").join("test.frz") - - extractor = request.getfixturevalue(extractor) - - Path(o.strpath).write_bytes(capa.features.freeze.dump(extractor)) - + Path(o.strpath).write_bytes(capa.features.freeze.dump(z9324d_extractor)) null_extractor = capa.features.freeze.load(Path(o.strpath).read_bytes()) - - compare_extractors(extractor, null_extractor) + compare_extractors(z9324d_extractor, null_extractor) diff --git a/tests/test_ghidra_features.py b/tests/test_ghidra_features.py index 6b9fd7a1..0b04d9c2 100644 --- a/tests/test_ghidra_features.py +++ b/tests/test_ghidra_features.py @@ -23,15 +23,14 @@ ghidra_present = ( ) -BACKEND = fixtures.BackendFeaturePolicy( - name="ghidra", - get_extractor=fixtures.get_ghidra_extractor, - include_tags={"static"}, - exclude_tags={"dotnet"}, -) - - @pytest.mark.skipif(ghidra_present is False, reason="PyGhidra not installed") -@fixtures.parametrize_backend_feature_fixtures(BACKEND) +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="ghidra", + include_tags={"static"}, + exclude_tags={"dotnet"}, + ) +) def test_ghidra_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_ghidra_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) diff --git a/tests/test_idalib_features.py b/tests/test_idalib_features.py index 7cd4ee14..0a4a58d9 100644 --- a/tests/test_idalib_features.py +++ b/tests/test_idalib_features.py @@ -16,18 +16,19 @@ import logging import fixtures import pytest -import capa.features.extractors.ida.idalib +import capa.features.extractors.ida.idalib as idalib from capa.features.common import Characteristic from capa.features.file import FunctionName from capa.features.insn import API logger = logging.getLogger(__name__) -idalib_present = capa.features.extractors.ida.idalib.has_idalib() +idalib_present = idalib.has_idalib() if idalib_present: try: + if True: + import idapro # noqa: F401 [imported but unused] import ida_kernwin - import idapro # noqa: F401 [imported but unused] kernel_version: str = ida_kernwin.get_kernel_version() except ImportError: @@ -37,19 +38,17 @@ else: kernel_version = "0.0" -BACKEND = fixtures.BackendFeaturePolicy( - name="idalib", - get_extractor=fixtures.get_idalib_extractor, - include_tags={"static"}, - exclude_tags={"dotnet", "ghidra"}, -) - - @pytest.mark.skipif( idalib_present is False, reason="Skip idalib tests if the idalib Python API is not installed", ) -@fixtures.parametrize_backend_feature_fixtures(BACKEND) +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="idalib", + include_tags={"static"}, + exclude_tags={"dotnet", "ghidra"}, + ) +) def test_idalib_features(feature_fixture): # apply runtime-conditional xfails for specific IDA versions. # version-specific behavior stays in the test body because it @@ -79,7 +78,8 @@ def test_idalib_features(feature_fixture): pytest.xfail("idalib 9.0 does not support loading resource segments") try: - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_idalib_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) finally: import idapro diff --git a/tests/test_main.py b/tests/test_main.py index 5f3ac7b6..80ad5c0c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -23,9 +23,9 @@ import capa.main import capa.rules -def test_main(z9324d_extractor): +def test_main(): # tests rules can be loaded successfully and all output modes - path = z9324d_extractor.path + path = str(fixtures.PMA1601) assert capa.main.main([path, "-vv"]) == 0 assert capa.main.main([path, "-v"]) == 0 assert capa.main.main([path, "-j"]) == 0 @@ -33,7 +33,7 @@ def test_main(z9324d_extractor): assert capa.main.main([path]) == 0 -def test_main_single_rule(z9324d_extractor, tmpdir): +def test_main_single_rule(tmpdir): # tests a single rule can be loaded successfully RULE_CONTENT = textwrap.dedent(""" rule: @@ -47,12 +47,11 @@ def test_main_single_rule(z9324d_extractor, tmpdir): features: - string: test """) - path = z9324d_extractor.path rule_file = tmpdir.mkdir("capa").join("rule.yml") rule_file.write(RULE_CONTENT) assert ( capa.main.main([ - path, + str(fixtures.PMA1601), "-v", "-r", rule_file.strpath, @@ -61,16 +60,17 @@ def test_main_single_rule(z9324d_extractor, tmpdir): ) -def test_main_non_ascii_filename(pingtaest_extractor, tmpdir, capsys): +def test_main_non_ascii_filename(tmpdir, capsys): # here we print a string with unicode characters in it # (specifically, a byte string with utf-8 bytes in it, see file encoding) # only use one rule to speed up analysis - assert capa.main.main(["-q", pingtaest_extractor.path, "-r", "rules/communication/icmp"]) == 0 + path = str(fixtures.CD / "./data/ping_täst.exe_") + assert capa.main.main(["-q", path, "-r", "rules/communication/icmp"]) == 0 std = capsys.readouterr() # but here, we have to use a unicode instance, # because capsys has decoded the output for us. - assert pingtaest_extractor.path in std.out + assert path in std.out def test_main_non_ascii_filename_nonexistent(tmpdir, caplog): @@ -80,8 +80,9 @@ def test_main_non_ascii_filename_nonexistent(tmpdir, caplog): assert NON_ASCII_FILENAME in caplog.text -def test_main_shellcode(z499c2_extractor): - path = z499c2_extractor.path +def test_main_shellcode(): + path = str(fixtures.CD / "./data/499c2a85f6e8142c3f48d4251c9c7cd6.raw32") + assert capa.main.main([path, "-vv", "-f", "sc32"]) == 0 assert capa.main.main([path, "-v", "-f", "sc32"]) == 0 assert capa.main.main([path, "-j", "-f", "sc32"]) == 0 @@ -198,8 +199,8 @@ def test_ruleset(): assert len(rules.call_rules) == 2 -def test_fix262(pma16_01_extractor, capsys): - path = pma16_01_extractor.path +def test_fix262(capsys): + path = str(fixtures.CD / "./data/Practical Malware Analysis Lab 16-01.exe_") assert capa.main.main([path, "-vv", "-t", "send HTTP request", "-q"]) == 0 std = capsys.readouterr() @@ -207,11 +208,11 @@ def test_fix262(pma16_01_extractor, capsys): assert "www.practicalmalwareanalysis.com" not in std.out -def test_not_render_rules_also_matched(z9324d_extractor, capsys): +def test_not_render_rules_also_matched(capsys): # rules that are also matched by other rules should not get rendered by default. # this cuts down on the amount of output while giving approx the same detail. # see #224 - path = z9324d_extractor.path + path = str(fixtures.CD / "./data/9324d1a8ae37a36ae560c37448c9705a.exe_") # `act as TCP client` matches on # `connect TCP client` matches on @@ -234,7 +235,7 @@ def test_not_render_rules_also_matched(z9324d_extractor, capsys): def test_json_meta(capsys): - path = str(fixtures.get_data_path_by_name("pma01-01")) + path = str(fixtures.CD / "./data/Practical Malware Analysis Lab 01-01.dll_") assert capa.main.main([path, "-j"]) == 0 std = capsys.readouterr() std_json = json.loads(std.out) @@ -248,9 +249,9 @@ def test_json_meta(capsys): assert {"address": {"type": "absolute", "value": 0x1000108C}} in func["matched_basic_blocks"] -def test_main_dotnet(_1c444_dotnetfile_extractor): +def test_main_dotnet(): # tests successful execution and all output modes - path = _1c444_dotnetfile_extractor.path + path = str(fixtures.CD / "./data/dotnet/1c444ebeba24dcba8628b7dfe5fec7c6.exe_") assert capa.main.main([path, "-vv"]) == 0 assert capa.main.main([path, "-v"]) == 0 assert capa.main.main([path, "-j"]) == 0 @@ -258,27 +259,27 @@ def test_main_dotnet(_1c444_dotnetfile_extractor): assert capa.main.main([path]) == 0 -def test_main_dotnet2(_692f_dotnetfile_extractor): +def test_main_dotnet2(): # tests successful execution and one rendering # above covers all output modes - path = _692f_dotnetfile_extractor.path + path = str(fixtures.CD / "./data/dotnet/692f7fd6d198e804d6af98eb9e390d61.exe_") assert capa.main.main([path, "-vv"]) == 0 -def test_main_dotnet3(_0953c_dotnetfile_extractor): +def test_main_dotnet3(): # tests successful execution and one rendering - path = _0953c_dotnetfile_extractor.path + path = str(fixtures.CD / "./data/0953cc3b77ed2974b09e3a00708f88de931d681e2d0cb64afbaf714610beabe6.exe_") assert capa.main.main([path, "-vv"]) == 0 -def test_main_dotnet4(_039a6_dotnetfile_extractor): +def test_main_dotnet4(): # tests successful execution and one rendering - path = _039a6_dotnetfile_extractor.path + path = str(fixtures.CD / "./data/039a6336d0802a2255669e6867a5679c7eb83313dbc61fb1c7232147379bd304.exe_") assert capa.main.main([path, "-vv"]) == 0 def test_main_rd(): - path = str(fixtures.get_data_path_by_name("pma01-01-rd")) + path = str(fixtures.CD / "./data/rd/Practical Malware Analysis Lab 01-01.dll_.json") assert capa.main.main([path, "-vv"]) == 0 assert capa.main.main([path, "-v"]) == 0 assert capa.main.main([path, "-j"]) == 0 @@ -293,7 +294,7 @@ def extract_cape_report(tmp_path: Path, gz: Path) -> Path: def test_main_cape1(tmp_path): - path = extract_cape_report(tmp_path, fixtures.get_data_path_by_name("0000a657")) + path = extract_cape_report(tmp_path, fixtures.CD / "./data/dynamic/cape/v2.2/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz") # TODO(williballenthin): use default rules set # https://github.com/mandiant/capa/pull/1696 @@ -342,5 +343,5 @@ def test_main_cape1(tmp_path): def test_main_cape_gzip(): # tests successful execution of .json.gz - path = str(fixtures.get_data_path_by_name("0000a657")) + path = str(fixtures.CD / "./data/dynamic/cape/v2.2/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz") assert capa.main.main([path]) == 0 diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 0f3ee0f9..8c3f0794 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -63,7 +63,7 @@ def test_optimizer_order(): - arch: amd64 - mnemonic: cmp - and: - - bytes: 3 + - bytes: 03 04 05 - offset: 2 - or: - number: 1 diff --git a/tests/test_os_detection.py b/tests/test_os_detection.py index 9dc6832d..7f1d03d0 100644 --- a/tests/test_os_detection.py +++ b/tests/test_os_detection.py @@ -14,9 +14,8 @@ import io import zlib -from pathlib import Path -from fixtures import get_data_path_by_name +import fixtures import capa.features.extractors.elf import capa.features.extractors.common @@ -31,8 +30,8 @@ def test_elf_sh_notes(): # guess: ABI versions needed: None # guess: symtab: None # guess: needed dependencies: None - path = get_data_path_by_name("2f7f5f") - with Path(path).open("rb") as f: + path = fixtures.CD / "data" / "2f7f5fb5de175e770d7eae87666f9831.elf_" + with path.open("rb") as f: assert capa.features.extractors.elf.detect_elf_os(f) == "linux" @@ -44,8 +43,8 @@ def test_elf_pt_notes(): # guess: ABI versions needed: OS.LINUX # guess: symtab: None # guess: needed dependencies: None - path = get_data_path_by_name("7351f.elf") - with Path(path).open("rb") as f: + path = fixtures.CD / "data" / "7351f8a40c5450557b24622417fc478d.elf_" + with path.open("rb") as f: assert capa.features.extractors.elf.detect_elf_os(f) == "linux" @@ -57,8 +56,8 @@ def test_elf_so_needed(): # guess: ABI versions needed: OS.HURD # guess: symtab: None # guess: needed dependencies: OS.HURD - path = get_data_path_by_name("b5f052") - with Path(path).open("rb") as f: + path = fixtures.CD / "data" / "b5f0524e69b3a3cf636c7ac366ca57bf5e3a8fdc8a9f01caf196c611a7918a87.elf_" + with path.open("rb") as f: assert capa.features.extractors.elf.detect_elf_os(f) == "hurd" @@ -70,8 +69,8 @@ def test_elf_abi_version_hurd(): # guess: ABI versions needed: OS.HURD # guess: symtab: None # guess: needed dependencies: None - path = get_data_path_by_name("bf7a9c") - with Path(path).open("rb") as f: + path = fixtures.CD / "data" / "bf7a9c8bdfa6d47e01ad2b056264acc3fd90cf43fe0ed8deec93ab46b47d76cb.elf_" + with path.open("rb") as f: assert capa.features.extractors.elf.detect_elf_os(f) == "hurd" @@ -83,8 +82,8 @@ def test_elf_symbol_table(): # guess: ABI versions needed: None # guess: symtab: OS.LINUX # guess: needed dependencies: None - path = get_data_path_by_name("2bf18d") - with Path(path).open("rb") as f: + path = fixtures.CD / "data" / "2bf18d0403677378adad9001b1243211.elf_" + with path.open("rb") as f: assert capa.features.extractors.elf.detect_elf_os(f) == "linux" @@ -95,14 +94,14 @@ def test_elf_android_notes(): # DEBUG:capa.features.extractors.elf:guess: linker: None # DEBUG:capa.features.extractors.elf:guess: ABI versions needed: None # DEBUG:capa.features.extractors.elf:guess: needed dependencies: OS.ANDROID - path = get_data_path_by_name("1038a2") - with Path(path).open("rb") as f: + path = fixtures.CD / "data" / "1038a23daad86042c66bfe6c9d052d27048de9653bde5750dc0f240c792d9ac8.elf_" + with path.open("rb") as f: assert capa.features.extractors.elf.detect_elf_os(f) == "android" def test_elf_go_buildinfo(): - path = get_data_path_by_name("3da7c") - with Path(path).open("rb") as f: + path = fixtures.CD / "data" / "3da7c2c70a2d93ac4643f20339d5c7d61388bddd77a4a5fd732311efad78e535.elf_" + with path.open("rb") as f: assert capa.features.extractors.elf.detect_elf_os(f) == "linux" diff --git a/tests/test_pefile_features.py b/tests/test_pefile_features.py index c331d449..592bd405 100644 --- a/tests/test_pefile_features.py +++ b/tests/test_pefile_features.py @@ -11,26 +11,25 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import fixtures -BACKEND = fixtures.BackendFeaturePolicy( - name="pefile", - get_extractor=fixtures.get_pefile_extractor, - include_tags={"static"}, - exclude_tags={ - "dotnet", - "elf", - # pefile is a file-scope extractor; drop non-file scopes - "function", - "basic-block", - "instruction", - # and drop feature types pefile doesn't produce - "function-name", - }, + +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="pefile", + include_tags={"static"}, + exclude_tags={ + "dotnet", + "elf", + # pefile is a file-scope extractor; drop non-file scopes + "function", + "basic block", + "instruction", + # and drop feature types pefile doesn't produce + "function-name", + }, + ) ) - - -@fixtures.parametrize_backend_feature_fixtures(BACKEND) def test_pefile_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_pefile_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) diff --git a/tests/test_result_document.py b/tests/test_result_document.py index e4468494..b18763ab 100644 --- a/tests/test_result_document.py +++ b/tests/test_result_document.py @@ -284,12 +284,12 @@ def test_round_trip(request, rd_file): def test_json_to_rdoc(): - path = fixtures.get_data_path_by_name("pma01-01-rd") + path = fixtures.CD / "data" / "rd" / "Practical Malware Analysis Lab 01-01.dll_.json" assert isinstance(rdoc.ResultDocument.from_file(path), rdoc.ResultDocument) def test_rdoc_to_capa(): - path = fixtures.get_data_path_by_name("pma01-01-rd") + path = fixtures.CD / "data" / "rd" / "Practical Malware Analysis Lab 01-01.dll_.json" rd = rdoc.ResultDocument.from_file(path) diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 0a30dee0..632c164b 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -23,23 +23,22 @@ from pathlib import Path import pytest import capa.rules +import fixtures logger = logging.getLogger(__name__) -CD = Path(__file__).resolve().parent - def get_script_path(s: str): - return str(CD / ".." / "scripts" / s) + return str(fixtures.CD /".." / "scripts" / s) def get_binary_file_path(): - return str(CD / "data" / "9324d1a8ae37a36ae560c37448c9705a.exe_") + return str(fixtures.CD /"data" / "9324d1a8ae37a36ae560c37448c9705a.exe_") def get_cape_report_file_path(): return str( - CD + fixtures.CD / "data" / "dynamic" / "cape" @@ -49,11 +48,11 @@ def get_cape_report_file_path(): def get_binexport2_file_path(): - return str(CD / "data" / "binexport2" / "mimikatz.exe_.ghidra.BinExport") + return str(fixtures.CD /"data" / "binexport2" / "mimikatz.exe_.ghidra.BinExport") def get_rules_path(): - return str(CD / ".." / "rules") + return str(fixtures.CD /".." / "rules") def get_rule_path(): @@ -67,7 +66,7 @@ def get_rule_path(): pytest.param("capafmt.py", [get_rule_path()]), pytest.param( "capa2sarif.py", - [Path(__file__).resolve().parent / "data" / "rd" / "Practical Malware Analysis Lab 01-01.dll_.json"], + [fixtures.CD /"data" / "rd" / "Practical Malware Analysis Lab 01-01.dll_.json"], ), # testing some variations of linter script pytest.param("lint.py", ["-t", "create directory", get_rules_path()]), @@ -98,7 +97,7 @@ def test_scripts(script, args): ) def test_binexport_scripts(script, args): # define sample bytes location - os.environ["CAPA_SAMPLES_DIR"] = str(Path(CD / "data")) + os.environ["CAPA_SAMPLES_DIR"] = str(fixtures.CD / "data") script_path = get_script_path(script) p = run_program(script_path, args) @@ -110,7 +109,7 @@ def test_bulk_process(tmp_path): t = tmp_path / "test" t.mkdir() - source_file = Path(__file__).resolve().parent / "data" / "ping_täst.exe_" + source_file = fixtures.CD /"data" / "ping_täst.exe_" dest_file = t / "test.exe_" dest_file.write_bytes(source_file.read_bytes()) @@ -149,7 +148,7 @@ def run_program(script_path, args): def test_proto_conversion(tmp_path): t = tmp_path / "proto-test" t.mkdir() - json_file = Path(__file__).resolve().parent / "data" / "rd" / "Practical Malware Analysis Lab 01-01.dll_.json" + json_file = fixtures.CD /"data" / "rd" / "Practical Malware Analysis Lab 01-01.dll_.json" p = run_program(get_script_path("proto-from-results.py"), [json_file]) assert p.returncode == 0 diff --git a/tests/test_viv_features.py b/tests/test_viv_features.py index d0873ce5..cb078f28 100644 --- a/tests/test_viv_features.py +++ b/tests/test_viv_features.py @@ -11,17 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import fixtures -BACKEND = fixtures.BackendFeaturePolicy( - name="viv", - get_extractor=fixtures.get_viv_extractor, - include_tags={"static"}, - exclude_tags={"dotnet", "ghidra"}, + + +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="viv", + include_tags={"static"}, + exclude_tags={"dotnet", "ghidra"}, + ) ) - - -@fixtures.parametrize_backend_feature_fixtures(BACKEND) def test_viv_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_viv_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) diff --git a/tests/test_vmray_features.py b/tests/test_vmray_features.py index a5e23df6..ec7ae471 100644 --- a/tests/test_vmray_features.py +++ b/tests/test_vmray_features.py @@ -11,24 +11,22 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - - import fixtures -BACKEND = fixtures.BackendFeaturePolicy( - name="vmray", - get_extractor=fixtures.get_vmray_extractor, - include_tags={"vmray"}, + +@fixtures.parametrize_backend_feature_fixtures( + fixtures.BackendFeaturePolicy( + name="vmray", + include_tags={"vmray"}, + ) ) - - -@fixtures.parametrize_backend_feature_fixtures(BACKEND) def test_vmray_features(feature_fixture): - fixtures.run_feature_fixture(BACKEND, feature_fixture) + extractor = fixtures.get_vmray_extractor(feature_fixture.sample_path) + fixtures.run_feature_fixture(extractor, feature_fixture) def test_vmray_processes(): # see #2394 - path = fixtures.get_data_path_by_name("2f8a79-vmray") + path = fixtures.CD / "data" / "dynamic" / "vmray" / "2f8a79b12a7a989ac7e5f6ec65050036588a92e65aeb6841e08dc228ff0e21b4_min_archive.zip" vmre = fixtures.get_vmray_extractor(path) assert len(vmre.analysis.monitor_processes) == 9