mirror of
https://github.com/mandiant/capa.git
synced 2025-12-12 23:59:48 -08:00
Compare commits
2 Commits
idalib-tes
...
update-ida
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10d3031093 | ||
|
|
e9b02b372a |
@@ -1,5 +1,5 @@
|
||||
[tool.bumpversion]
|
||||
current_version = "9.3.1"
|
||||
current_version = "9.2.1"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "capa/version.py"
|
||||
@@ -19,9 +19,4 @@ replace = '"flare-capa=={new_version}"'
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "CHANGELOG.md"
|
||||
search = "v{current_version}...master"
|
||||
replace = "v{current_version}...{new_version}"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "CHANGELOG.md"
|
||||
search = "master (unreleased)"
|
||||
replace = "v{new_version}"
|
||||
replace = "{current_version}...{new_version}"
|
||||
|
||||
@@ -138,7 +138,6 @@ repos:
|
||||
- "--ignore=tests/test_ghidra_features.py"
|
||||
- "--ignore=tests/test_ida_features.py"
|
||||
- "--ignore=tests/test_viv_features.py"
|
||||
- "--ignore=tests/test_idalib_features.py"
|
||||
- "--ignore=tests/test_main.py"
|
||||
- "--ignore=tests/test_scripts.py"
|
||||
always_run: true
|
||||
|
||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -3,55 +3,11 @@
|
||||
## master (unreleased)
|
||||
|
||||
### New Features
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### New Rules (0)
|
||||
|
||||
-
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
### capa Explorer Web
|
||||
|
||||
### capa Explorer IDA Pro plugin
|
||||
|
||||
### Development
|
||||
|
||||
### Raw diffs
|
||||
- [capa v9.3.1...master](https://github.com/mandiant/capa/compare/v9.3.1...master)
|
||||
- [capa-rules v9.3.1...master](https://github.com/mandiant/capa-rules/compare/v9.3.1...master)
|
||||
|
||||
## v9.3.1
|
||||
|
||||
This patch release fixes a missing import for the capa explorer plugin for IDA Pro.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add missing ida-netnode dependency to project.toml @mike-hunhoff #2765
|
||||
|
||||
### Development
|
||||
|
||||
- ci: bump binja min version @mike-hunhoff #2763
|
||||
|
||||
### Raw diffs
|
||||
- [capa v9.3.0...master](https://github.com/mandiant/capa/compare/v9.3.0...master)
|
||||
- [capa-rules v9.3.0...master](https://github.com/mandiant/capa-rules/compare/v9.3.0...master)
|
||||
|
||||
## v9.3.0
|
||||
|
||||
capa v9.3.0 comes with over 20 new and/or impoved rules.
|
||||
For IDA users the capa explorer plugin is now available via the IDA Pro plugin repository and contains Qt compatibility layer for PyQt5 and PySide6 support.
|
||||
Additionally a Binary Ninja bug has been fixed. Released binaries now include ARM64 binaries (Linux and macOS).
|
||||
|
||||
### New Features
|
||||
|
||||
- ci: add support for arm64 binary releases
|
||||
- tests: run tests against IDA via idalib @williballenthin #2742
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### New Rules (24)
|
||||
### New Rules (21)
|
||||
|
||||
- anti-analysis/anti-vm/vm-detection/detect-mouse-movement-via-activity-checks-on-windows tevajdr@gmail.com
|
||||
- nursery/create-executable-heap moritz.raabe@mandiant.com
|
||||
@@ -73,9 +29,7 @@ Additionally a Binary Ninja bug has been fixed. Released binaries now include AR
|
||||
- host-interaction/network/routing-table/get-routing-table michael.hunhoff@mandiant.com
|
||||
- host-interaction/file-system/use-io_uring-io-interface-on-linux jakubjozwiak@google.com
|
||||
- collection/keylog/log-keystrokes-via-direct-input zeze-zeze
|
||||
- nursery/compiled-from-fsharp mehunhoff@google.com
|
||||
- nursery/decrypt-data-using-aes-via-dotnet mehunhoff@google.com
|
||||
- nursery/get-dotnet-assembly-entry-point mehunhoff@google.com
|
||||
-
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -87,7 +41,7 @@ Additionally a Binary Ninja bug has been fixed. Released binaries now include AR
|
||||
|
||||
- add `ida-plugin.json` for inclusion in the IDA Pro plugin repository @williballenthin
|
||||
- ida plugin: add Qt compatibility layer for PyQt5 and PySide6 support @williballenthin #2707
|
||||
- delay import to not load Qt* when running under idalib @mr-tz #2752
|
||||
- ida plugin: update ida-settings API @mr-tz #2736
|
||||
|
||||
### Development
|
||||
|
||||
@@ -95,8 +49,8 @@ Additionally a Binary Ninja bug has been fixed. Released binaries now include AR
|
||||
- dev: add bumpmyversion to bump and sync versions across the project @mr-tz
|
||||
|
||||
### Raw diffs
|
||||
- [capa v9.2.1...9.3.0](https://github.com/mandiant/capa/compare/v9.2.1...9.3.0)
|
||||
- [capa-rules v9.2.1...9.3.0](https://github.com/mandiant/capa-rules/compare/v9.2.1...9.3.0)
|
||||
- [capa v9.2.1...master](https://github.com/mandiant/capa/compare/v9.2.1...master)
|
||||
- [capa-rules v9.2.1...master](https://github.com/mandiant/capa-rules/compare/v9.2.1...master)
|
||||
|
||||
## v9.2.1
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ import idaapi
|
||||
import idautils
|
||||
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features.file import FunctionName
|
||||
from capa.features.insn import API
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors import loops
|
||||
@@ -52,22 +50,10 @@ def extract_recursive_call(fh: FunctionHandle):
|
||||
yield Characteristic("recursive call"), fh.address
|
||||
|
||||
|
||||
def extract_function_alternative_names(fh: FunctionHandle):
|
||||
"""Get all alternative names for an address."""
|
||||
|
||||
for aname in capa.features.extractors.ida.helpers.get_function_alternative_names(fh.inner.start_ea):
|
||||
yield FunctionName(aname), fh.address
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for feature, addr in func_handler(fh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (
|
||||
extract_function_calls_to,
|
||||
extract_function_loop,
|
||||
extract_recursive_call,
|
||||
extract_function_alternative_names,
|
||||
)
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call)
|
||||
|
||||
@@ -20,7 +20,6 @@ import idaapi
|
||||
import ida_nalt
|
||||
import idautils
|
||||
import ida_bytes
|
||||
import ida_funcs
|
||||
import ida_segment
|
||||
|
||||
from capa.features.address import AbsoluteVirtualAddress
|
||||
@@ -437,23 +436,3 @@ def is_basic_block_return(bb: idaapi.BasicBlock) -> bool:
|
||||
def has_sib(oper: idaapi.op_t) -> bool:
|
||||
# via: https://reverseengineering.stackexchange.com/a/14300
|
||||
return oper.specflag1 == 1
|
||||
|
||||
|
||||
def get_function_alternative_names(fva: int):
|
||||
"""Get all alternative names for an address."""
|
||||
|
||||
# Check indented comment
|
||||
cmt = ida_bytes.get_cmt(fva, False) # False = non-repeatable
|
||||
if cmt:
|
||||
for line in cmt.split("\n"):
|
||||
if line.startswith("Alternative name is '") and line.endswith("'"):
|
||||
name = line[len("Alternative name is '") : -1] # Extract name between quotes
|
||||
yield name
|
||||
|
||||
# Check function comment
|
||||
func_cmt = ida_funcs.get_func_cmt(idaapi.get_func(fva), False)
|
||||
if func_cmt:
|
||||
for line in func_cmt.split("\n"):
|
||||
if line.startswith("Alternative name is '") and line.endswith("'"):
|
||||
name = line[len("Alternative name is '") : -1]
|
||||
yield name
|
||||
|
||||
@@ -22,11 +22,9 @@ import idautils
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features.file import FunctionName
|
||||
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.ida.function import extract_function_alternative_names
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
|
||||
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
|
||||
@@ -131,8 +129,8 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
|
||||
# not a function (start)
|
||||
return
|
||||
|
||||
name = idaapi.get_name(target_func.start_ea)
|
||||
if target_func.flags & idaapi.FUNC_LIB or not name.startswith("sub_"):
|
||||
if target_func.flags & idaapi.FUNC_LIB:
|
||||
name = idaapi.get_name(target_func.start_ea)
|
||||
yield API(name), ih.address
|
||||
if name.startswith("_"):
|
||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||
@@ -141,10 +139,6 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
|
||||
# see: https://stackoverflow.com/a/2628384/87207
|
||||
yield API(name[1:]), ih.address
|
||||
|
||||
for altname in capa.features.extractors.ida.helpers.get_function_alternative_names(target_func.start_ea):
|
||||
yield FunctionName(altname), ih.address
|
||||
yield API(altname), ih.address
|
||||
|
||||
|
||||
def extract_insn_number_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
|
||||
@@ -17,6 +17,7 @@ import logging
|
||||
import idaapi
|
||||
import ida_kernwin
|
||||
|
||||
from capa.ida.plugin.form import CapaExplorerForm
|
||||
from capa.ida.plugin.icon import ICON
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -73,9 +74,6 @@ class CapaExplorerPlugin(idaapi.plugin_t):
|
||||
arg (int): bitflag. Setting LSB enables automatic analysis upon
|
||||
loading. The other bits are currently undefined. See `form.Options`.
|
||||
"""
|
||||
# delay import to not trigger load of Qt components when not running in idaq, i.e., in idalib
|
||||
from capa.ida.plugin.form import CapaExplorerForm
|
||||
|
||||
if not self.form:
|
||||
self.form = CapaExplorerForm(self.PLUGIN_NAME, arg)
|
||||
else:
|
||||
|
||||
@@ -54,7 +54,6 @@ from capa.ida.plugin.qt_compat import QtGui, QtCore, QtWidgets
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = ida_settings.IDASettings("capa")
|
||||
|
||||
CAPA_SETTINGS_RULE_PATH = "rule_path"
|
||||
CAPA_SETTINGS_RULEGEN_AUTHOR = "rulegen_author"
|
||||
@@ -79,6 +78,13 @@ AnalyzeOptionsText = {
|
||||
}
|
||||
|
||||
|
||||
def get_setting(key: str, default=None):
|
||||
try:
|
||||
return ida_settings.get_current_plugin_setting(key)
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
|
||||
def write_file(path: Path, data):
|
||||
""" """
|
||||
path.write_bytes(data)
|
||||
@@ -107,7 +113,7 @@ class QLineEditClicked(QtWidgets.QLineEdit):
|
||||
old = self.text()
|
||||
new = str(
|
||||
QtWidgets.QFileDialog.getExistingDirectory(
|
||||
self.parent(), "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "")
|
||||
self.parent(), "Please select a capa rules directory", get_setting(CAPA_SETTINGS_RULE_PATH, "")
|
||||
)
|
||||
)
|
||||
if new:
|
||||
@@ -125,8 +131,8 @@ class CapaSettingsInputDialog(QtWidgets.QDialog):
|
||||
self.setMinimumWidth(500)
|
||||
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
|
||||
|
||||
self.edit_rule_path = QLineEditClicked(settings.user.get(CAPA_SETTINGS_RULE_PATH, ""))
|
||||
self.edit_rule_author = QtWidgets.QLineEdit(settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, ""))
|
||||
self.edit_rule_path = QLineEditClicked(get_setting(CAPA_SETTINGS_RULE_PATH, ""))
|
||||
self.edit_rule_author = QtWidgets.QLineEdit(get_setting(CAPA_SETTINGS_RULEGEN_AUTHOR, ""))
|
||||
self.edit_rule_scope = QtWidgets.QComboBox()
|
||||
self.edit_rules_link = QtWidgets.QLabel()
|
||||
self.edit_analyze = QtWidgets.QComboBox()
|
||||
@@ -141,11 +147,11 @@ class CapaSettingsInputDialog(QtWidgets.QDialog):
|
||||
|
||||
scopes = ("file", "function", "basic block", "instruction")
|
||||
self.edit_rule_scope.addItems(scopes)
|
||||
self.edit_rule_scope.setCurrentIndex(scopes.index(settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function")))
|
||||
self.edit_rule_scope.setCurrentIndex(scopes.index(get_setting(CAPA_SETTINGS_RULEGEN_SCOPE, "function")))
|
||||
|
||||
self.edit_analyze.addItems(AnalyzeOptionsText.values())
|
||||
# set the default analysis option here
|
||||
self.edit_analyze.setCurrentIndex(settings.user.get(CAPA_SETTINGS_ANALYZE, Options.NO_ANALYSIS))
|
||||
self.edit_analyze.setCurrentIndex(get_setting(CAPA_SETTINGS_ANALYZE, Options.NO_ANALYSIS))
|
||||
|
||||
buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, self)
|
||||
|
||||
@@ -235,7 +241,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
|
||||
self.Show()
|
||||
|
||||
analyze = settings.user.get(CAPA_SETTINGS_ANALYZE)
|
||||
analyze = get_setting(CAPA_SETTINGS_ANALYZE)
|
||||
if analyze != Options.NO_ANALYSIS or (option & Options.ANALYZE_AUTO) == Options.ANALYZE_AUTO:
|
||||
self.analyze_program(analyze=analyze)
|
||||
|
||||
@@ -581,7 +587,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
|
||||
def ensure_capa_settings_rule_path(self):
|
||||
try:
|
||||
path: str = settings.user.get(CAPA_SETTINGS_RULE_PATH, "")
|
||||
path: str = get_setting(CAPA_SETTINGS_RULE_PATH, "")
|
||||
|
||||
# resolve rules directory - check self and settings first, then ask user
|
||||
# pathlib.Path considers "" equivalent to "." so we first check if rule path is an empty string
|
||||
@@ -611,7 +617,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
logger.error("rule path %s does not exist or cannot be accessed", path)
|
||||
return False
|
||||
|
||||
settings.user[CAPA_SETTINGS_RULE_PATH] = path
|
||||
ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULE_PATH, path)
|
||||
except UserCancelledError:
|
||||
capa.ida.helpers.inform_user_ida_ui("Analysis requires capa rules")
|
||||
logger.warning(
|
||||
@@ -635,7 +641,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
if not self.ensure_capa_settings_rule_path():
|
||||
return False
|
||||
|
||||
rule_path: Path = Path(settings.user.get(CAPA_SETTINGS_RULE_PATH, ""))
|
||||
rule_path: Path = Path(get_setting(CAPA_SETTINGS_RULE_PATH, ""))
|
||||
try:
|
||||
|
||||
def on_load_rule(_, i, total):
|
||||
@@ -649,10 +655,10 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
return None
|
||||
except Exception as e:
|
||||
capa.ida.helpers.inform_user_ida_ui(
|
||||
f"Failed to load capa rules from {settings.user[CAPA_SETTINGS_RULE_PATH]}"
|
||||
f"Failed to load capa rules from {get_setting(CAPA_SETTINGS_RULE_PATH)}"
|
||||
)
|
||||
|
||||
logger.error("Failed to load capa rules from %s (error: %s).", settings.user[CAPA_SETTINGS_RULE_PATH], e)
|
||||
logger.error("Failed to load capa rules from %s (error: %s).", get_setting(CAPA_SETTINGS_RULE_PATH), e)
|
||||
logger.error(
|
||||
"Make sure your file directory contains properly " # noqa: G003 [logging statement uses +]
|
||||
+ "formatted capa rules. You can download and extract the official rules from %s. "
|
||||
@@ -661,7 +667,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
CAPA_RULESET_DOC_URL,
|
||||
)
|
||||
|
||||
settings.user[CAPA_SETTINGS_RULE_PATH] = ""
|
||||
ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULE_PATH, "")
|
||||
return None
|
||||
|
||||
def load_capa_results(self, new_analysis, from_cache):
|
||||
@@ -701,7 +707,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
update_wait_box("verifying cached results")
|
||||
|
||||
count_source_rules = self.program_analysis_ruleset_cache.source_rule_count
|
||||
user_settings = settings.user[CAPA_SETTINGS_RULE_PATH]
|
||||
user_settings = get_setting(CAPA_SETTINGS_RULE_PATH)
|
||||
view_status_rules: str = f"{user_settings} ({count_source_rules} rules)"
|
||||
|
||||
# warn user about potentially outdated rules, depending on the use-case this may be expected
|
||||
@@ -775,7 +781,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
update_wait_box("extracting features")
|
||||
|
||||
try:
|
||||
meta = capa.ida.helpers.collect_metadata([Path(settings.user[CAPA_SETTINGS_RULE_PATH])])
|
||||
meta = capa.ida.helpers.collect_metadata([Path(get_setting(CAPA_SETTINGS_RULE_PATH))])
|
||||
capabilities = capa.capabilities.common.find_capabilities(
|
||||
ruleset, self.feature_extractor, disable_progress=True
|
||||
)
|
||||
@@ -855,7 +861,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
except Exception as e:
|
||||
logger.exception("Failed to save results to database (error: %s)", e)
|
||||
return False
|
||||
user_settings = settings.user[CAPA_SETTINGS_RULE_PATH]
|
||||
user_settings = get_setting(CAPA_SETTINGS_RULE_PATH)
|
||||
count_source_rules = self.program_analysis_ruleset_cache.source_rule_count
|
||||
new_view_status = f"capa rules: {user_settings} ({count_source_rules} rules)"
|
||||
# regardless of new analysis, render results - e.g. we may only want to render results after checking
|
||||
@@ -1076,13 +1082,13 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
# load preview and feature tree
|
||||
self.view_rulegen_preview.load_preview_meta(
|
||||
self.rulegen_current_function.address if self.rulegen_current_function else None,
|
||||
settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, "<insert_author>"),
|
||||
settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function"),
|
||||
get_setting(CAPA_SETTINGS_RULEGEN_AUTHOR, "<insert_author>"),
|
||||
get_setting(CAPA_SETTINGS_RULEGEN_SCOPE, "function"),
|
||||
)
|
||||
|
||||
self.view_rulegen_features.load_features(all_file_features, all_function_features)
|
||||
|
||||
self.set_view_status_label(f"capa rules: {settings.user[CAPA_SETTINGS_RULE_PATH]}")
|
||||
self.set_view_status_label(f"capa rules: {get_setting(CAPA_SETTINGS_RULE_PATH)}")
|
||||
except Exception as e:
|
||||
logger.exception("Failed to render views (error: %s)", e)
|
||||
return False
|
||||
@@ -1303,12 +1309,11 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
""" """
|
||||
dialog = CapaSettingsInputDialog("capa explorer settings", parent=self.parent)
|
||||
if dialog.exec_():
|
||||
(
|
||||
settings.user[CAPA_SETTINGS_RULE_PATH],
|
||||
settings.user[CAPA_SETTINGS_RULEGEN_AUTHOR],
|
||||
settings.user[CAPA_SETTINGS_RULEGEN_SCOPE],
|
||||
settings.user[CAPA_SETTINGS_ANALYZE],
|
||||
) = dialog.get_values()
|
||||
rule_path, rule_author, rule_scope, analyze = dialog.get_values()
|
||||
ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULE_PATH, rule_path)
|
||||
ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULEGEN_AUTHOR, rule_author)
|
||||
ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULEGEN_SCOPE, rule_scope)
|
||||
ida_settings.set_current_plugin_setting(CAPA_SETTINGS_ANALYZE, analyze)
|
||||
|
||||
def save_program_analysis(self):
|
||||
""" """
|
||||
@@ -1408,7 +1413,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
"""create Qt dialog to ask user for a directory"""
|
||||
return str(
|
||||
QtWidgets.QFileDialog.getExistingDirectory(
|
||||
self.parent, "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "")
|
||||
self.parent, "Please select a capa rules directory", get_setting(CAPA_SETTINGS_RULE_PATH, "")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1417,7 +1422,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
return QtWidgets.QFileDialog.getSaveFileName(
|
||||
None,
|
||||
"Please select a location to save capa rule file",
|
||||
settings.user.get(CAPA_SETTINGS_RULE_PATH, ""),
|
||||
get_setting(CAPA_SETTINGS_RULE_PATH, ""),
|
||||
"*.yml",
|
||||
)[0]
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"plugin": {
|
||||
"name": "capa",
|
||||
"entryPoint": "capa_explorer.py",
|
||||
"version": "9.3.1",
|
||||
"version": "9.2.1",
|
||||
"idaVersions": ">=7.4",
|
||||
"description": "Identify capabilities in executable files using FLARE's capa framework",
|
||||
"license": "Apache-2.0",
|
||||
@@ -12,7 +12,7 @@
|
||||
"api-scripting-and-automation",
|
||||
"ui-ux-and-visualization"
|
||||
],
|
||||
"pythonDependencies": ["flare-capa==9.3.1"],
|
||||
"pythonDependencies": ["flare-capa==9.2.1"],
|
||||
"urls": {
|
||||
"repository": "https://github.com/mandiant/capa"
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__version__ = "9.3.1"
|
||||
__version__ = "9.2.1"
|
||||
|
||||
|
||||
def get_major_version():
|
||||
|
||||
@@ -74,7 +74,6 @@ dependencies = [
|
||||
# comments and context.
|
||||
"pyyaml>=6",
|
||||
"colorama>=0.4",
|
||||
"ida-netnode>=3.0",
|
||||
"ida-settings>=3.1.0",
|
||||
"ruamel.yaml>=0.18",
|
||||
"pefile>=2023.2.7",
|
||||
@@ -122,7 +121,7 @@ dev = [
|
||||
# we want all developer environments to be consistent.
|
||||
# These dependencies are not used in production environments
|
||||
# and should not conflict with other libraries/tooling.
|
||||
"pre-commit==4.5.0",
|
||||
"pre-commit==4.2.0",
|
||||
"pytest==8.0.0",
|
||||
"pytest-sugar==1.1.1",
|
||||
"pytest-instafail==0.5.0",
|
||||
@@ -138,7 +137,7 @@ dev = [
|
||||
"flake8-use-pathlib==0.3.0",
|
||||
"flake8-copyright==0.2.4",
|
||||
"ruff==0.12.0",
|
||||
"black==25.11.0",
|
||||
"black==25.1.0",
|
||||
"isort==6.0.0",
|
||||
"mypy==1.17.1",
|
||||
"mypy-protobuf==3.6.0",
|
||||
@@ -158,9 +157,9 @@ build = [
|
||||
# we want all developer environments to be consistent.
|
||||
# These dependencies are not used in production environments
|
||||
# and should not conflict with other libraries/tooling.
|
||||
"pyinstaller==6.16.0",
|
||||
"pyinstaller==6.14.1",
|
||||
"setuptools==80.9.0",
|
||||
"build==1.3.0"
|
||||
"build==1.2.2"
|
||||
]
|
||||
scripts = [
|
||||
# can (optionally) be more lenient on dependencies here
|
||||
|
||||
@@ -12,7 +12,7 @@ cxxfilt==0.3.0
|
||||
dncil==1.0.2
|
||||
dnfile==0.17.0
|
||||
funcy==2.0
|
||||
humanize==4.14.0
|
||||
humanize==4.13.0
|
||||
ida-netnode==3.0
|
||||
ida-settings==3.2.2
|
||||
intervaltree==3.1.0
|
||||
@@ -22,16 +22,16 @@ msgpack==1.0.8
|
||||
networkx==3.4.2
|
||||
pefile==2024.8.26
|
||||
pip==25.3
|
||||
protobuf==6.33.1
|
||||
protobuf==6.31.1
|
||||
pyasn1==0.5.1
|
||||
pyasn1-modules==0.3.0
|
||||
pycparser==2.23
|
||||
pydantic==2.12.4
|
||||
pycparser==2.22
|
||||
pydantic==2.11.4
|
||||
# pydantic pins pydantic-core,
|
||||
# but dependabot updates these separately (which is broken) and is annoying,
|
||||
# so we rely on pydantic to pull in the right version of pydantic-core.
|
||||
# pydantic-core==2.23.4
|
||||
xmltodict==1.0.2
|
||||
xmltodict==0.14.2
|
||||
pyelftools==0.32
|
||||
pygments==2.19.1
|
||||
python-flirt==0.9.2
|
||||
|
||||
2
rules
2
rules
Submodule rules updated: b0b486fe0c...9e4cc28265
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
import contextlib
|
||||
import collections
|
||||
from pathlib import Path
|
||||
@@ -21,7 +21,6 @@ from functools import lru_cache
|
||||
import pytest
|
||||
|
||||
import capa.main
|
||||
import capa.helpers
|
||||
import capa.features.file
|
||||
import capa.features.insn
|
||||
import capa.features.common
|
||||
@@ -54,7 +53,6 @@ from capa.features.extractors.base_extractor import (
|
||||
)
|
||||
from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
CD = Path(__file__).resolve().parent
|
||||
DOTNET_DIR = CD / "data" / "dotnet"
|
||||
DNFILE_TESTFILES = DOTNET_DIR / "dnfile-testfiles"
|
||||
@@ -202,65 +200,6 @@ def get_binja_extractor(path: Path):
|
||||
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 idapro
|
||||
import ida_auto
|
||||
|
||||
import capa.features.extractors.ida.extractor
|
||||
|
||||
logger.debug("idalib: opening database...")
|
||||
|
||||
idapro.enable_console_messages(False)
|
||||
# - 0 - Success (database not packed)
|
||||
# - 1 - Success (database was packed)
|
||||
# - 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)
|
||||
if ret not in (0, 1):
|
||||
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 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 (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.helpers import load_json_from_path
|
||||
@@ -955,8 +894,20 @@ FEATURE_PRESENCE_TESTS = sorted(
|
||||
("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), False),
|
||||
("mimikatz", "function=0x4556E5", capa.features.insn.API("LsaQueryInformationPolicy"), True),
|
||||
# insn/api: x64
|
||||
(
|
||||
"kernel32-64",
|
||||
"function=0x180001010",
|
||||
capa.features.insn.API("RtlVirtualUnwind"),
|
||||
True,
|
||||
),
|
||||
("kernel32-64", "function=0x180001010", capa.features.insn.API("RtlVirtualUnwind"), True),
|
||||
# insn/api: x64 thunk
|
||||
(
|
||||
"kernel32-64",
|
||||
"function=0x1800202B0",
|
||||
capa.features.insn.API("RtlCaptureContext"),
|
||||
True,
|
||||
),
|
||||
("kernel32-64", "function=0x1800202B0", capa.features.insn.API("RtlCaptureContext"), True),
|
||||
# insn/api: x64 nested thunk
|
||||
("al-khaser x64", "function=0x14004B4F0", capa.features.insn.API("__vcrt_GetModuleHandle"), True),
|
||||
@@ -1044,20 +995,20 @@ FEATURE_PRESENCE_TESTS = sorted(
|
||||
("pma16-01", "file", OS(OS_WINDOWS), True),
|
||||
("pma16-01", "file", OS(OS_LINUX), False),
|
||||
("mimikatz", "file", OS(OS_WINDOWS), True),
|
||||
("pma16-01", "function=0x401100", OS(OS_WINDOWS), True),
|
||||
("pma16-01", "function=0x401100,bb=0x401130", OS(OS_WINDOWS), True),
|
||||
("pma16-01", "function=0x404356", OS(OS_WINDOWS), True),
|
||||
("pma16-01", "function=0x404356,bb=0x4043B9", OS(OS_WINDOWS), True),
|
||||
("mimikatz", "function=0x40105D", OS(OS_WINDOWS), True),
|
||||
("pma16-01", "file", Arch(ARCH_I386), True),
|
||||
("pma16-01", "file", Arch(ARCH_AMD64), False),
|
||||
("mimikatz", "file", Arch(ARCH_I386), True),
|
||||
("pma16-01", "function=0x401100", Arch(ARCH_I386), True),
|
||||
("pma16-01", "function=0x401100,bb=0x401130", Arch(ARCH_I386), True),
|
||||
("pma16-01", "function=0x404356", Arch(ARCH_I386), True),
|
||||
("pma16-01", "function=0x404356,bb=0x4043B9", Arch(ARCH_I386), True),
|
||||
("mimikatz", "function=0x40105D", Arch(ARCH_I386), True),
|
||||
("pma16-01", "file", Format(FORMAT_PE), True),
|
||||
("pma16-01", "file", Format(FORMAT_ELF), False),
|
||||
("mimikatz", "file", Format(FORMAT_PE), True),
|
||||
# format is also a global feature
|
||||
("pma16-01", "function=0x401100", Format(FORMAT_PE), True),
|
||||
("pma16-01", "function=0x404356", Format(FORMAT_PE), True),
|
||||
("mimikatz", "function=0x456BB9", Format(FORMAT_PE), True),
|
||||
# elf support
|
||||
("7351f.elf", "file", OS(OS_LINUX), True),
|
||||
|
||||
@@ -70,4 +70,4 @@ def test_standalone_binja_backend():
|
||||
@pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed")
|
||||
def test_binja_version():
|
||||
version = binaryninja.core_version_info()
|
||||
assert version.major == 5 and version.minor == 2
|
||||
assert version.major == 5 and version.minor == 1
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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 logging
|
||||
|
||||
import pytest
|
||||
import fixtures
|
||||
|
||||
import capa.features.extractors.ida.idalib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
idalib_present = capa.features.extractors.ida.idalib.has_idalib()
|
||||
|
||||
|
||||
@pytest.mark.skipif(idalib_present is False, reason="Skip idalib tests if the idalib Python API is not installed")
|
||||
@fixtures.parametrize(
|
||||
"sample,scope,feature,expected",
|
||||
fixtures.FEATURE_PRESENCE_TESTS + fixtures.FEATURE_SYMTAB_FUNC_TESTS,
|
||||
indirect=["sample", "scope"],
|
||||
)
|
||||
def test_idalib_features(sample, scope, feature, expected):
|
||||
try:
|
||||
fixtures.do_test_feature_presence(fixtures.get_idalib_extractor, sample, scope, feature, expected)
|
||||
finally:
|
||||
logger.debug("closing database...")
|
||||
import idapro
|
||||
|
||||
idapro.close_database(save=False)
|
||||
logger.debug("opened database.")
|
||||
|
||||
|
||||
@pytest.mark.skipif(idalib_present is False, reason="Skip idalib tests if the idalib Python API is not installed")
|
||||
@fixtures.parametrize(
|
||||
"sample,scope,feature,expected",
|
||||
fixtures.FEATURE_COUNT_TESTS,
|
||||
indirect=["sample", "scope"],
|
||||
)
|
||||
def test_idalib_feature_counts(sample, scope, feature, expected):
|
||||
try:
|
||||
fixtures.do_test_feature_count(fixtures.get_idalib_extractor, sample, scope, feature, expected)
|
||||
finally:
|
||||
logger.debug("closing database...")
|
||||
import idapro
|
||||
|
||||
idapro.close_database(save=False)
|
||||
logger.debug("closed database.")
|
||||
17
web/explorer/package-lock.json
generated
17
web/explorer/package-lock.json
generated
@@ -2272,11 +2272,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz",
|
||||
"integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
@@ -2288,6 +2287,9 @@
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
@@ -2639,11 +2641,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
|
||||
@@ -212,18 +212,6 @@
|
||||
|
||||
<h2 class="mt-3">Tool Updates</h2>
|
||||
|
||||
<h3 class="mt-2">v9.3.1 (<em>2025-11-19</em>)</h3>
|
||||
<p class="mt-0">
|
||||
This patch release fixes a missing import for the capa explorer plugin for IDA Pro.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-2">v9.3.0 (<em>2025-11-12</em>)</h3>
|
||||
<p class="mt-0">
|
||||
capa v9.3.0 comes with over 20 new and/or impoved rules.
|
||||
For IDA users the capa explorer plugin is now available via the IDA Pro plugin repository and contains Qt compatibility layer for PyQt5 and PySide6 support.
|
||||
Additionally a Binary Ninja bug has been fixed. Released binaries now include ARM64 binaries (Linux and macOS).
|
||||
</p>
|
||||
|
||||
<h3 class="mt-2">v9.2.1 (<em>2025-06-06</em>)</h3>
|
||||
<p class="mt-0">
|
||||
This point release fixes bugs including removing an unnecessary PyInstaller warning message and enabling the standalone binary to execute on systems running older versions of glibc.
|
||||
|
||||
Reference in New Issue
Block a user