github mandiant/capa v10.0.0

5 hours ago

This release fixes a security issue in how capa loads vivisect workspaces, a large batch of bug fixes, a new --reproducible flag for freeze, dependency updates, and 12 new rules. We'd like to thank the following contributors: Ange Albertini (@corkamig), @aryanyk, Luca Schrott (@1337Name), @eversinc33, Matt Williams (@mwilliams31), Rizky Mirzaviandy Priambodo (@Xavrir), Sukesh Kumar (@SkxOverKill), lakshit verma (@vee1e), @w3lld1, and others!

Security

Crafted .viv files could execute code when capa analyzed a sample: either the sample itself named *.viv, or a <sample>.viv next to it. capa now always parses samples as PE or ELF and only loads an existing <sample>.viv when CAPA_LOAD_VIV_WORKSPACE=1 is set. Only enable this for .viv files you trust.

Affected: v9.4.0 and earlier.

New Features

  • freeze: add --reproducible flag that zeros dynamic header metadata

Breaking Changes

  • Remove redundant os_ parameter from capa.loader.collect_metadata(). External scripts calling this function must be updated to remove this argument. OS metadata now relies solely on the extractor yielding it via extract_global_features(). @mike-hunhoff #3076
  • Existing <sample>.viv vivisect workspace files are no longer loaded by default because loading them uses pickle deserialization. Set CAPA_LOAD_VIV_WORKSPACE=1 to opt in. @mr-tz #3191

New Rules (12)

Bug Fixes

  • fix: loader: inline vivisect workspace loading and gate .viv pickle deserialization behind CAPA_LOAD_VIV_WORKSPACE @mr-tz #3191
  • fix: freeze: omit null description fields from freeze JSON @SkxOverKill #3100
  • fix lots of linter errors identified by pyright @williballenthin #3052
  • fix: render_default always returns empty string @williballenthin #3012
  • fix: elf.py vdso_guess exception handler clobbers symtab_guess @williballenthin #3013
  • fix: _NoAddress.eq unconditionally returns True @williballenthin #3014
  • fix: capabilities/common.py: if va: drops address 0x0 @williballenthin #3015
  • fix: vverbose.py: render_call variable assigned but never used @williballenthin #3016
  • fix: extract_os yields duplicate/contradictory OS values @williballenthin #3017
  • fix: load_one_jsonl_from_path: finally block runs on unrelated exceptions @williballenthin #3018
  • fix: find_dynamic_limitations_from_cli overwrites instead of OR-ing @williballenthin #3019
  • fix: optimizer doesn't recurse into And/Or/Some children @williballenthin #3020
  • fix: address classes eq and lt assert on type @williballenthin #3021
  • fix: DNTokenOffsetAddress.eq lacks type guard @williballenthin #3022
  • fix: elffile.py: get_base_address returns None instead of NO_ADDRESS @williballenthin #3023
  • fix: base_extractor.py: metaclass is Python 2 syntax, ignored in Py3 @williballenthin #3024
  • fix: freeze/init.py: NO_ADDRESS < NO_ADDRESS returns True @williballenthin #3025
  • fix: dotnetfile.py: missing import for capa.features.extractors.common @williballenthin #3026
  • fix: rules/init.py: duplicate bytes_features line @williballenthin #3027
  • fix: EXTENSIONS_DYNAMIC has inconsistent leading dots @williballenthin #3028
  • fix: loader.py reads entire file for magic byte check @williballenthin #3029
  • fix: freeze/init.py: logically impossible condition @williballenthin #3030
  • fix: EXTENSIONS_ELF never referenced @williballenthin #3031
  • fix: correct off-by-one in get_dotnet_table_row so row_index=1 (first valid .NET metadata row) is no longer rejected @williballenthin
  • fix: add missing import for assert_never in cape extractor.py to avoid NameError when call argument has unexpected type @williballenthin
  • fix: stop mutating call.api in cape thread.get_calls; yield one CallHandle per call so the original API name is preserved for all handles @williballenthin
  • fix: use instruction_indices in is_security_cookie to handle single-instruction basic blocks where end_index is omitted, preventing KeyError on -1 @williballenthin
  • fix: guard get_operand_expressions against empty expression tree so Ghidra-exported BinExport2 files with empty operands no longer raise IndexError @williballenthin
  • fix: add return after zero-offset yield in extract_insn_offset_features so Offset(0) is not emitted twice @williballenthin
  • fix: use f-string in binexport2 extractor so unexpected global feature value appears in ValueError message @williballenthin
  • fix: correct scale/displacement expressions in get_operand_phrase_info 5-expression branch (used expression3 operator instead of expression4 value) @williballenthin
  • fix: use HasField instead of truthiness in _index_vertex_edges so call-graph edges to/from vertex 0 are not silently dropped @williballenthin
  • fix: change bare if to elif in get_backend_from_cli DRAKVUF branch to complete the if/elif chain @williballenthin
  • fix: close file handle in get_file_taste using a with statement @williballenthin
  • fix: correct off-by-one in dynamic analysis call_count debug log (enumerate -> explicit counter) @williballenthin
  • fix: correct capa/subscope-rule key in RuleMetadata so is_subscope_rule is no longer always False @williballenthin
  • fix: remove unreachable backports.functools_lru_cache fallback and dead dependency @williballenthin
  • fix: Scopes.from_dict uses cls instead of self so subclasses return the correct type @williballenthin
  • fix: correct wrong dict key in VMRay _compute_monitor_threads assertion (used thread_id instead of process_id) @williballenthin
  • fix: replace assert with isinstance guard in get_callee for invalid MethodSpec tokens @williballenthin
  • fix: remove unused imports of capa.engine, capa.helpers, and capa.features from cache-ruleset.py, detect-binexport2-capabilities.py, and show-capabilities-by-function.py @williballenthin (SURF-92)
  • fix: remove dead except ValueError clause in capa2sarif.py so JSONDecodeError is caught by the specific handler @williballenthin (SURF-91)
  • fix: dedent bulk-process.py main() body so explicit argv argument is used instead of silently ignored @williballenthin (SURF-90)
  • fix: guard statistics.quantiles/mean in compare-backends.py report() against empty duration lists @williballenthin (SURF-89)
  • fix: replace zipfile with pyzipper in minimize_vmray_results.py so output archive is AES-encrypted @williballenthin (SURF-88)
  • fix: assign yara_strings/yara_condition to empty string when Some has cmin=0 to prevent UnboundLocalError @williballenthin (SURF-87)
  • fix: parenthesize s_type checks in capa2yara.py so kid.name != "Some" guard applies to And/Or/Not uniformly @williballenthin (SURF-86)
  • fix: correct operator precedence in FeatureRegexRegistryControlSetMatchIncomplete to avoid false positives on unrelated currentcontrolset patterns @williballenthin (SURF-85)
  • fix: FeatureRegexRegistryControlSetMatchIncomplete now checks all Regex features instead of returning after the first @williballenthin (SURF-84)
  • fix: MissingStaticScope and MissingDynamicScope lint checks guard against absent scopes dict to prevent TypeError @williballenthin (SURF-83)
  • fix: MissingExampleOffset lint now reads scopes.static instead of obsolete scope key @williballenthin (SURF-82)
  • fix: extend MissingExampleOffset lint to validate dynamic examples using (pid:N,tid:N,call:N) format @williballenthin #3058
  • fix: invert scope filter in import-to-ida.py so function-scope rules are annotated instead of skipped @williballenthin (SURF-81)
  • fix: remove dead string literal in test_detect_duplicate_features @williballenthin (SURF-80)
  • fix: remove duplicate Rule.from_yaml call in test_scope_instruction_description @williballenthin (SURF-79)
  • fix: remove unused imports of capa.helpers, capa.features.basicblock, and redundant bare capa.features.extractors.base_extractor from test_freeze_dynamic.py @williballenthin (SURF-78)
  • fix: replace capa.main.find_capabilities with capa.capabilities.common.find_capabilities in test_com_feature_matching to avoid implicit transitive import dependency @williballenthin (SURF-77)
  • fix: correct test_json_meta to iterate list of function dicts and use correct address format for matched_basic_blocks assertion @williballenthin (SURF-76)
  • fix: remove unreachable StaticAnalysis assert in assert_meta and add dynamic fixture to test_doc_to_pb2 @williballenthin (SURF-75)
  • fix: correct self-comparison sa.max == sa.max to sa.max == sb.max in test_proto assert_statement @williballenthin (SURF-74)
  • fix: guard parse_node against missing "type" key to avoid TypeError crash @williballenthin (SURF-73)
  • fix: allocate feat_dict per feature in parse_json to avoid shared-reference aliasing @williballenthin (SURF-72)
  • fix: add missing capa.features.extractors.elf import to ghidra/helpers.py and ida/helpers.py @williballenthin (SURF-71)
  • fix: remove dead view_tab_rulegen assignment from CapaExplorerForm that was never read @williballenthin (SURF-70)
  • fix: remove dead reset_query method from CapaExplorerSearchProxyModel that was never called @williballenthin (SURF-69)
  • fix: remove unused imports of capa.rules and capa.engine from view.py @williballenthin (SURF-68)
  • fix: remove unused imports of capa.main, capa.render.json, and capa.features.extractors.ida.extractor from form.py @williballenthin (SURF-67)
  • fix: remove dead trim_function_name function from form.py that was never called @williballenthin (SURF-66)
  • fix: replace get_file_md5/get_file_sha256 with retrieve_input_file_md5/sha256 shims so all callers use consistent IDA version-aware API @williballenthin (SURF-65)
  • fix: rename dragEventEnter to dragEnterEvent so Qt dispatches drag-enter events correctly in CapaExplorerRulegenEditor @williballenthin (SURF-64)
  • fix: guard against None in lessThan else-branch so sorting columns with empty cells does not raise AttributeError @williballenthin (SURF-63)
  • fix: add explicit import capa.loader in form.py to avoid fragile transitive import dependency @williballenthin (SURF-62)
  • fix: initialize f=None before try block in load_capa_function_results to prevent UnboundLocalError in except handler @williballenthin (SURF-61)
  • fix: fix unreachable elif for NOT CompoundStatement so NOT rules render children in IDA plugin tree view @williballenthin (SURF-60)
  • fix: use next(iter(addrs)) instead of addrs.pop() to avoid mutating the feature cache in parse_features_for_tree @williballenthin (SURF-59)
  • fix: use integer division in get_printable_len for UTF-16 LE operands @williballenthin (SURF-58)
  • fix: break thunk chain loop after resolving import to avoid duplicate API features @williballenthin (SURF-57)
  • fix: pass insn instead of oper to getOperValue/getOperAddr in viv insn extractor @williballenthin (SURF-56)
  • fix: implement extract_function_loop in dnfile extractor to detect backward branches as loops @williballenthin (SURF-55)
  • fix: remove dead find_process function and helpers.py from cape extractor @williballenthin (SURF-54)
  • fix: remove dead interface_extract_* stub functions from viv basicblock, function, and insn extractors @williballenthin (SURF-53)
  • fix: remove unused import of capa.features.extractors.strings from binexport2 intel insn.py @williballenthin (SURF-52)
  • fix: remove extract_file_format from FILE_HANDLERS in five extractors to prevent duplicate Format features @williballenthin (SURF-51)
  • fix: replace assert with guard so 2-operand ARM ADD/SUB instructions are skipped instead of crashing @williballenthin (SURF-50)
  • fix: omit trailing -> suffix in syscall names when there is no return value @williballenthin (SURF-49)
  • fix: use AbsoluteVirtualAddress instead of FileOffsetAddress for string addresses in Ghidra and IDA file extractors @williballenthin (SURF-48)
  • fix: use dest.value.value and indirect_src.value.value for LLIL_CONST call destinations in binja insn.py @williballenthin (SURF-47)
  • fix: remove duplicate getPrevLocation call and dead loc variable in get_previous_instructions @williballenthin (SURF-46)
  • fix: unpack getByteDef offset and slice buffer so ENDBRANCH check applies to target address, not segment start @williballenthin (SURF-45)
  • fix: guard getByteDef against None return for unmapped addresses in viv insn extractor @williballenthin #3057
  • fix: correct inverted loop structure in extract_function_loop so each block edge is recorded as (src, dest) @williballenthin (SURF-44)
  • fix: initialize addr to None in Ghidra import extractors to prevent UnboundLocalError when external functions have no data references @williballenthin (SURF-43)
  • fix: replace assert with isinstance guard in get_callee for invalid MethodSpec tokens @williballenthin
  • fix: assign ConfigDict to model_config in ConciseModel so extra="ignore" is actually applied @williballenthin (SURF-42)
  • fix: replace assert with isinstance guard in get_callee for invalid MethodSpec tokens @williballenthin (SURF-41)
  • fix: incorrect bytes() constructor usage in buf_filled_with @mike-hunhoff #3077
  • fix: remove redundant code related to cli loading @mike-hunhoff #3076
  • fix: optimize all_zeros using fast bytes comparison @mike-hunhoff #3078
  • fix: duplicate rule candidate evaluation in optimized matching engine @mike-hunhoff #3080

capa Explorer Web

capa Explorer IDA Pro plugin

  • ida: display file-scope feature addresses (file:0x...) in rule generator tree and safely handle file-offset navigation/filtering @vee1e #3009

Development

  • ci: key vivisect workspace cache on vivisect, viv-utils, python-flirt versions and FLIRT signatures, shared across Python versions @mr-tz #3195
  • ci: test macos-15-intel only on Python 3.10, matching the Intel build @mr-tz #3195
  • tests: update binja version to 5.3 @mr-tz #3011
  • ci: use explicit and per job permissions @mike-hunhoff #3002
  • replace black/isort/flake8 with ruff @mike-hunhoff #2992
  • ci: update GitHub Actions to support Node.js 24 (deprecate Node.js 20) @mr-tz #2984
  • tests: add snapshot tests for feature extraction @williballenthin #3069
  • tests: add more tests to exercise ELF OS detection @williballenthin #3098

Raw diffs

Don't miss a new capa release

NewReleases is sending notifications on new releases.