├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── capa-explorer-logo.png ├── dependabot.yml ├── flake8.ini ├── icon.png ├── logo.pdf ├── logo.png ├── mypy │ └── mypy.ini ├── pull_request_template.md ├── pyinstaller │ ├── hooks │ │ └── hook-vivisect.py │ ├── logo.ico │ └── pyinstaller.spec ├── ruff.toml └── workflows │ ├── build.yml │ ├── changelog.yml │ ├── pip-audit.yml │ ├── publish.yml │ ├── scorecard.yml │ ├── tag.yml │ ├── tests.yml │ ├── web-deploy.yml │ ├── web-release.yml │ └── web-tests.yml ├── .gitignore ├── .gitmodules ├── .justfile ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CITATION.cff ├── LICENSE.txt ├── README.md ├── capa ├── __init__.py ├── capabilities │ ├── __init__.py │ ├── common.py │ ├── dynamic.py │ └── static.py ├── engine.py ├── exceptions.py ├── features │ ├── __init__.py │ ├── address.py │ ├── basicblock.py │ ├── com │ │ ├── __init__.py │ │ ├── classes.py │ │ └── interfaces.py │ ├── common.py │ ├── extractors │ │ ├── __init__.py │ │ ├── base_extractor.py │ │ ├── binexport2 │ │ │ ├── __init__.py │ │ │ ├── arch │ │ │ │ ├── __init__.py │ │ │ │ ├── arm │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── helpers.py │ │ │ │ │ └── insn.py │ │ │ │ └── intel │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── helpers.py │ │ │ │ │ └── insn.py │ │ │ ├── basicblock.py │ │ │ ├── binexport2_pb2.py │ │ │ ├── binexport2_pb2.pyi │ │ │ ├── extractor.py │ │ │ ├── file.py │ │ │ ├── function.py │ │ │ ├── helpers.py │ │ │ └── insn.py │ │ ├── binja │ │ │ ├── __init__.py │ │ │ ├── basicblock.py │ │ │ ├── extractor.py │ │ │ ├── file.py │ │ │ ├── find_binja_api.py │ │ │ ├── function.py │ │ │ ├── global_.py │ │ │ ├── helpers.py │ │ │ └── insn.py │ │ ├── cape │ │ │ ├── __init__.py │ │ │ ├── call.py │ │ │ ├── extractor.py │ │ │ ├── file.py │ │ │ ├── global_.py │ │ │ ├── helpers.py │ │ │ ├── models.py │ │ │ ├── process.py │ │ │ └── thread.py │ │ ├── common.py │ │ ├── dnfile │ │ │ ├── __init__.py │ │ │ ├── extractor.py │ │ │ ├── file.py │ │ │ ├── function.py │ │ │ ├── helpers.py │ │ │ ├── insn.py │ │ │ └── types.py │ │ ├── dotnetfile.py │ │ ├── drakvuf │ │ │ ├── call.py │ │ │ ├── extractor.py │ │ │ ├── file.py │ │ │ ├── global_.py │ │ │ ├── helpers.py │ │ │ ├── models.py │ │ │ ├── process.py │ │ │ └── thread.py │ │ ├── elf.py │ │ ├── elffile.py │ │ ├── ghidra │ │ │ ├── __init__.py │ │ │ ├── basicblock.py │ │ │ ├── extractor.py │ │ │ ├── file.py │ │ │ ├── function.py │ │ │ ├── global_.py │ │ │ ├── helpers.py │ │ │ └── insn.py │ │ ├── helpers.py │ │ ├── ida │ │ │ ├── __init__.py │ │ │ ├── basicblock.py │ │ │ ├── extractor.py │ │ │ ├── file.py │ │ │ ├── function.py │ │ │ ├── global_.py │ │ │ ├── helpers.py │ │ │ ├── idalib.py │ │ │ └── insn.py │ │ ├── loops.py │ │ ├── null.py │ │ ├── pefile.py │ │ ├── strings.py │ │ ├── viv │ │ │ ├── __init__.py │ │ │ ├── basicblock.py │ │ │ ├── extractor.py │ │ │ ├── file.py │ │ │ ├── function.py │ │ │ ├── global_.py │ │ │ ├── helpers.py │ │ │ ├── indirect_calls.py │ │ │ └── insn.py │ │ └── vmray │ │ │ ├── __init__.py │ │ │ ├── call.py │ │ │ ├── extractor.py │ │ │ ├── file.py │ │ │ ├── global_.py │ │ │ └── models.py │ ├── file.py │ ├── freeze │ │ ├── __init__.py │ │ └── features.py │ └── insn.py ├── ghidra │ ├── README.md │ ├── __init__.py │ ├── capa_explorer.py │ ├── capa_ghidra.py │ └── helpers.py ├── helpers.py ├── ida │ ├── __init__.py │ ├── helpers.py │ └── plugin │ │ ├── README.md │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── capa_explorer.py │ │ ├── error.py │ │ ├── extractor.py │ │ ├── form.py │ │ ├── hooks.py │ │ ├── icon.py │ │ ├── item.py │ │ ├── model.py │ │ ├── proxy.py │ │ └── view.py ├── loader.py ├── main.py ├── optimizer.py ├── perf.py ├── render │ ├── __init__.py │ ├── default.py │ ├── json.py │ ├── proto │ │ ├── __init__.py │ │ ├── capa.proto │ │ ├── capa_pb2.py │ │ └── capa_pb2.pyi │ ├── result_document.py │ ├── utils.py │ ├── verbose.py │ └── vverbose.py ├── rules │ ├── __init__.py │ └── cache.py └── version.py ├── doc ├── capa_quickstart.pdf ├── faq.md ├── img │ ├── approve.png │ ├── capa_web_explorer.png │ ├── changelog │ │ ├── flirt-ignore.png │ │ └── tab.gif │ ├── explorer_condensed.png │ ├── explorer_expanded.png │ ├── ghidra_backend_logo.png │ ├── ghidra_headless_analyzer.png │ ├── ghidra_script_mngr_output.png │ ├── ghidra_script_mngr_rules.png │ ├── ghidra_script_mngr_verbosity.png │ └── rulegen_expanded.png ├── installation.md ├── limitations.md ├── release.md ├── rules.md └── usage.md ├── pyproject.toml ├── requirements.txt ├── scripts ├── bulk-process.py ├── cache-ruleset.py ├── capa-as-library.py ├── capa2sarif.py ├── capa2yara.py ├── capafmt.py ├── compare-backends.py ├── detect-backends.py ├── detect-binexport2-capabilities.py ├── detect-elf-os.py ├── detect_duplicate_features.py ├── import-to-bn.py ├── import-to-ida.py ├── inspect-binexport2.py ├── lint.py ├── linter-data.json ├── match-function-id.py ├── minimize_vmray_results.py ├── profile-memory.py ├── profile-time.py ├── proto-from-results.py ├── proto-to-results.py ├── setup-linter-dependencies.py ├── show-capabilities-by-function.py ├── show-features.py └── show-unused-features.py ├── sigs ├── 1_flare_msvc_rtf_32_64.sig ├── 2_flare_msvc_atlmfc_32_64.sig ├── 3_flare_common_libs.sig └── README.md ├── tests ├── conftest.py ├── fixtures.py ├── test_binexport_accessors.py ├── test_binexport_features.py ├── test_binja_features.py ├── test_capabilities.py ├── test_cape_features.py ├── test_cape_model.py ├── test_dnfile_features.py ├── test_dotnetfile_features.py ├── test_drakvuf_features.py ├── test_drakvuf_models.py ├── test_dynamic_span_of_calls_scope.py ├── test_elffile_features.py ├── test_engine.py ├── test_extractor_hashing.py ├── test_fmt.py ├── test_freeze_dynamic.py ├── test_freeze_static.py ├── test_function_id.py ├── test_ghidra_features.py ├── test_helpers.py ├── test_ida_features.py ├── test_main.py ├── test_match.py ├── test_optimizer.py ├── test_os_detection.py ├── test_pefile_features.py ├── test_proto.py ├── test_render.py ├── test_result_document.py ├── test_rule_cache.py ├── test_rules.py ├── test_rules_insn_scope.py ├── test_scripts.py ├── test_strings.py ├── test_viv_features.py ├── test_vmray_features.py └── test_vmray_model.py └── web ├── explorer ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── DEVELOPMENT.md ├── README.md ├── index.html ├── jsconfig.json ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── releases │ ├── CHANGELOG.md │ └── capa-explorer-web-v1.0.0-6a2330c.zip ├── src │ ├── App.vue │ ├── assets │ │ ├── images │ │ │ ├── icon.png │ │ │ └── logo-full.png │ │ └── main.css │ ├── components │ │ ├── BannerHeader.vue │ │ ├── DescriptionPanel.vue │ │ ├── FunctionCapabilities.vue │ │ ├── MetadataPanel.vue │ │ ├── NamespaceChart.vue │ │ ├── NavBar.vue │ │ ├── ProcessCapabilities.vue │ │ ├── RuleMatchesTable.vue │ │ ├── SettingsPanel.vue │ │ ├── UploadOptions.vue │ │ ├── columns │ │ │ └── RuleColumn.vue │ │ └── misc │ │ │ ├── LibraryTag.vue │ │ │ └── VTIcon.vue │ ├── composables │ │ └── useRdocLoader.js │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ └── rdocStore.js │ ├── tests │ │ └── rdocParser.test.js │ ├── utils │ │ ├── fileUtils.js │ │ ├── rdocParser.js │ │ └── urlHelpers.js │ └── views │ │ ├── AnalysisView.vue │ │ ├── ImportView.vue │ │ └── NotFoundView.vue ├── vite.config.js └── vitest.config.js ├── public ├── .gitignore ├── css │ └── bootstrap-5.3.3.min.css ├── img │ ├── capa-default-pma0101.png │ ├── capa-rule-create-socket.png │ ├── capa-vv-pma0101.png │ ├── capa.gif │ ├── icon.ico │ ├── icon.png │ └── logo.png ├── index.html └── js │ └── bootstrap-5.3.3.bundle.min.js └── rules ├── .gitignore ├── README.md ├── justfile ├── public ├── css │ ├── bootstrap-5.3.3.min.css │ ├── pagefind-modular-ui.css │ ├── pagefind-ui.css │ ├── poppins.css │ └── style.css ├── img │ ├── favicon.ico │ ├── favicon.png │ └── logo.png └── js │ ├── bootstrap-5.3.3.bundle.min.js │ ├── jquery-3.5.1.slim.min.js │ └── popper-2.9.2.min.js ├── requirements.txt └── scripts ├── build_root.py ├── build_rules.py └── modified-dates.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/python-3/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3-bullseye, 3.10-bullseye, 3-buster, 3.10-buster, etc. 4 | ARG VARIANT="3.10-bullseye" 5 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 6 | 7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 8 | ARG NODE_VERSION="none" 9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 10 | 11 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. 12 | # COPY requirements.txt /tmp/pip-tmp/ 13 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 14 | # && rm -rf /tmp/pip-tmp 15 | 16 | # [Optional] Uncomment this section to install additional OS packages. 17 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 18 | # && apt-get -y install --no-install-recommends 19 | 20 | # [Optional] Uncomment this line to install global node packages. 21 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/python-3 3 | { 4 | "name": "Python 3", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "context": "..", 8 | "args": { 9 | // Update 'VARIANT' to pick a Python version: 3, 3.10, etc. 10 | // Append -bullseye or -buster to pin to an OS version. 11 | // Use -bullseye variants on local on arm64/Apple Silicon. 12 | "VARIANT": "3.10", 13 | // Options 14 | "NODE_VERSION": "none" 15 | } 16 | }, 17 | 18 | // Set *default* container specific settings.json values on container create. 19 | "settings": { 20 | "python.defaultInterpreterPath": "/usr/local/bin/python", 21 | "python.linting.enabled": true, 22 | "python.linting.pylintEnabled": true, 23 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", 24 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 25 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", 26 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", 27 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", 28 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", 29 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", 30 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", 31 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" 32 | }, 33 | 34 | // Add the IDs of extensions you want installed when the container is created. 35 | "extensions": [ 36 | "ms-python.python", 37 | "ms-python.vscode-pylance" 38 | ], 39 | 40 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 41 | // "forwardPorts": [], 42 | 43 | // Use 'postCreateCommand' to run commands after the container is created. 44 | "postCreateCommand": "git submodule update --init && pip3 install --user -e .[dev] && pre-commit install", 45 | 46 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 47 | "remoteUser": "vscode", 48 | "features": { 49 | "git": "latest" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.py text 7 | *.yml text 8 | *.md text 9 | *.txt text 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 19 | 20 | ### Description 21 | 22 | 23 | 24 | ### Steps to Reproduce 25 | 26 | 27 | 28 | 29 | 30 | **Expected behavior:** 31 | 32 | 33 | 34 | **Actual behavior:** 35 | 36 | 37 | 38 | ### Versions 39 | 40 | 43 | 44 | ### Additional Information 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for capa 4 | 5 | --- 6 | 19 | 20 | ### Summary 21 | 22 | 23 | 24 | ### Motivation 25 | 26 | 27 | 28 | ### Describe alternatives you've considered 29 | 30 | 31 | 32 | ## Additional context 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/capa-explorer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/.github/capa-explorer-logo.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | ignore: 8 | - dependency-name: "*" 9 | update-types: ["version-update:semver-patch"] 10 | -------------------------------------------------------------------------------- /.github/flake8.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | 4 | extend-ignore = 5 | # E203: whitespace before ':' (black does this) 6 | E203, 7 | # F401: `foo` imported but unused (prefer ruff) 8 | F401, 9 | # F811 Redefinition of unused `foo` (prefer ruff) 10 | F811, 11 | # E501 line too long (prefer black) 12 | E501, 13 | # E701 multiple statements on one line (colon) (prefer black, see https://github.com/psf/black/issues/4173) 14 | E701, 15 | # B010 Do not call setattr with a constant attribute value 16 | B010, 17 | # G200 Logging statement uses exception in arguments 18 | G200, 19 | # SIM102 Use a single if-statement instead of nested if-statements 20 | # doesn't provide a space for commenting or logical separation of conditions 21 | SIM102, 22 | # SIM114 Use logical or and a single body 23 | # makes logic trees too complex 24 | SIM114, 25 | # SIM117 Use 'with Foo, Bar:' instead of multiple with statements 26 | # makes lines too long 27 | SIM117 28 | 29 | per-file-ignores = 30 | # T201 print found. 31 | # 32 | # scripts are meant to print output 33 | scripts/*: T201 34 | # capa.exe is meant to print output 35 | capa/main.py: T201 36 | # IDA tests emit results to output window so need to print 37 | tests/test_ida_features.py: T201 38 | # utility used to find the Binary Ninja API via invoking python.exe 39 | capa/features/extractors/binja/find_binja_api.py: T201 40 | 41 | copyright-check = True 42 | copyright-min-file-size = 1 43 | copyright-regexp = Copyright \d{4} Google LLC 44 | -------------------------------------------------------------------------------- /.github/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/.github/icon.png -------------------------------------------------------------------------------- /.github/logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/.github/logo.pdf -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/.github/logo.png -------------------------------------------------------------------------------- /.github/mypy/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | [mypy-ruamel.*] 4 | ignore_missing_imports = True 5 | 6 | [mypy-networkx.*] 7 | ignore_missing_imports = True 8 | 9 | [mypy-pefile.*] 10 | ignore_missing_imports = True 11 | 12 | [mypy-viv_utils.*] 13 | ignore_missing_imports = True 14 | 15 | [mypy-flirt.*] 16 | ignore_missing_imports = True 17 | 18 | [mypy-lief.*] 19 | ignore_missing_imports = True 20 | 21 | [mypy-idc.*] 22 | ignore_missing_imports = True 23 | 24 | [mypy-vivisect.*] 25 | ignore_missing_imports = True 26 | 27 | [mypy-envi.*] 28 | ignore_missing_imports = True 29 | 30 | [mypy-PE.*] 31 | ignore_missing_imports = True 32 | 33 | [mypy-idaapi.*] 34 | ignore_missing_imports = True 35 | 36 | [mypy-idautils.*] 37 | ignore_missing_imports = True 38 | 39 | [mypy-ida_auto.*] 40 | ignore_missing_imports = True 41 | 42 | [mypy-ida_bytes.*] 43 | ignore_missing_imports = True 44 | 45 | [mypy-ida_nalt.*] 46 | ignore_missing_imports = True 47 | 48 | [mypy-ida_kernwin.*] 49 | ignore_missing_imports = True 50 | 51 | [mypy-ida_settings.*] 52 | ignore_missing_imports = True 53 | 54 | [mypy-ida_funcs.*] 55 | ignore_missing_imports = True 56 | 57 | [mypy-ida_loader.*] 58 | ignore_missing_imports = True 59 | 60 | [mypy-ida_segment.*] 61 | ignore_missing_imports = True 62 | 63 | [mypy-PyQt5.*] 64 | ignore_missing_imports = True 65 | 66 | [mypy-binaryninja.*] 67 | ignore_missing_imports = True 68 | 69 | [mypy-pytest.*] 70 | ignore_missing_imports = True 71 | 72 | [mypy-devtools.*] 73 | ignore_missing_imports = True 74 | 75 | [mypy-elftools.*] 76 | ignore_missing_imports = True 77 | 78 | [mypy-dncil.*] 79 | ignore_missing_imports = True 80 | 81 | [mypy-netnode.*] 82 | ignore_missing_imports = True 83 | 84 | [mypy-ghidra.*] 85 | ignore_missing_imports = True 86 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ### Checklist 16 | 17 | 18 | - [ ] No CHANGELOG update needed 19 | 20 | - [ ] No new tests needed 21 | 22 | - [ ] No documentation update needed 23 | -------------------------------------------------------------------------------- /.github/pyinstaller/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/.github/pyinstaller/logo.ico -------------------------------------------------------------------------------- /.github/pyinstaller/pyinstaller.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | # Copyright 2020 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys 17 | 18 | import capa.rules.cache 19 | 20 | from pathlib import Path 21 | 22 | # SPECPATH is a global variable which points to .spec file path 23 | capa_dir = Path(SPECPATH).parent.parent 24 | rules_dir = capa_dir / 'rules' 25 | cache_dir = capa_dir / 'cache' 26 | 27 | if not capa.rules.cache.generate_rule_cache(rules_dir, cache_dir): 28 | sys.exit(-1) 29 | 30 | a = Analysis( 31 | # when invoking pyinstaller from the project root, 32 | # this gets invoked from the directory of the spec file, 33 | # i.e. ./.github/pyinstaller 34 | ["../../capa/main.py"], 35 | pathex=["capa"], 36 | binaries=None, 37 | datas=[ 38 | # when invoking pyinstaller from the project root, 39 | # this gets invoked from the directory of the spec file, 40 | # i.e. ./.github/pyinstaller 41 | ("../../rules", "rules"), 42 | ("../../sigs", "sigs"), 43 | ("../../cache", "cache"), 44 | ], 45 | # when invoking pyinstaller from the project root, 46 | # this gets run from the project root. 47 | hookspath=[".github/pyinstaller/hooks"], 48 | runtime_hooks=None, 49 | excludes=[ 50 | # ignore packages that would otherwise be bundled with the .exe. 51 | # review: build/pyinstaller/xref-pyinstaller.html 52 | # we don't do any GUI stuff, so ignore these modules 53 | "tkinter", 54 | "_tkinter", 55 | "Tkinter", 56 | # these are pulled in by networkx 57 | # but we don't need to compute the strongly connected components. 58 | "numpy", 59 | "scipy", 60 | "matplotlib", 61 | "pandas", 62 | "pytest", 63 | # deps from viv that we don't use. 64 | # this duplicates the entries in `hook-vivisect`, 65 | # but works better this way. 66 | "vqt", 67 | "vdb.qt", 68 | "envi.qt", 69 | "PyQt5", 70 | "qt5", 71 | "pyqtwebengine", 72 | "pyasn1", 73 | # don't pull in Binary Ninja/IDA bindings that should 74 | # only be installed locally. 75 | "binaryninja", 76 | "ida", 77 | ], 78 | ) 79 | 80 | a.binaries = a.binaries - TOC([("tcl85.dll", None, None), ("tk85.dll", None, None), ("_tkinter", None, None)]) 81 | 82 | pyz = PYZ(a.pure, a.zipped_data) 83 | 84 | exe = EXE( 85 | pyz, 86 | a.scripts, 87 | a.binaries, 88 | a.zipfiles, 89 | a.datas, 90 | exclude_binaries=False, 91 | name="capa", 92 | icon="logo.ico", 93 | debug=False, 94 | strip=False, 95 | upx=True, 96 | console=True, 97 | ) 98 | 99 | # enable the following to debug the contents of the .exe 100 | # 101 | # coll = COLLECT(exe, 102 | # a.binaries, 103 | # a.zipfiles, 104 | # a.datas, 105 | # strip=None, 106 | # upx=True, 107 | # name='capa-dat') 108 | -------------------------------------------------------------------------------- /.github/ruff.toml: -------------------------------------------------------------------------------- 1 | # Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default. 2 | # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or 3 | # McCabe complexity (`C901`) by default. 4 | lint.select = ["E", "F"] 5 | 6 | # Allow autofix for all enabled rules (when `--fix`) is provided. 7 | lint.fixable = ["ALL"] 8 | lint.unfixable = [] 9 | 10 | # E402 module level import not at top of file 11 | # E722 do not use bare 'except' 12 | # E501 line too long 13 | lint.ignore = ["E402", "E722", "E501"] 14 | 15 | line-length = 120 16 | 17 | exclude = [ 18 | # Exclude a variety of commonly ignored directories. 19 | ".bzr", 20 | ".direnv", 21 | ".eggs", 22 | ".git", 23 | ".git-rewrite", 24 | ".hg", 25 | ".mypy_cache", 26 | ".nox", 27 | ".pants.d", 28 | ".pytype", 29 | ".ruff_cache", 30 | ".svn", 31 | ".tox", 32 | ".venv", 33 | "__pypackages__", 34 | "_build", 35 | "buck-out", 36 | "build", 37 | "dist", 38 | "node_modules", 39 | "venv", 40 | # protobuf generated files 41 | "*_pb2.py", 42 | "*_pb2.pyi" 43 | ] 44 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: changelog 2 | 3 | on: 4 | # We need pull_request_target instead of pull_request because a write 5 | # repository token is needed to add a review to a PR. DO NOT BUILD 6 | # OR RUN UNTRUSTED CODE FROM PRs IN THIS ACTION 7 | pull_request_target: 8 | types: [opened, edited, synchronize] 9 | 10 | permissions: 11 | pull-requests: write 12 | 13 | jobs: 14 | check_changelog: 15 | # no need to check for dependency updates via dependabot 16 | # github.event.pull_request.user.login refers to PR author 17 | if: | 18 | github.event.pull_request.user.login != 'dependabot[bot]' && 19 | github.event.pull_request.user.login != 'dependabot-preview[bot]' 20 | runs-on: ubuntu-latest 21 | env: 22 | NO_CHANGELOG: '[x] No CHANGELOG update needed' 23 | steps: 24 | - name: Get changed files 25 | id: files 26 | uses: Ana06/get-changed-files@25f79e676e7ea1868813e21465014798211fad8c # v2.3.0 27 | - name: check changelog updated 28 | id: changelog_updated 29 | env: 30 | PR_BODY: ${{ github.event.pull_request.body }} 31 | FILES: ${{ steps.files.outputs.modified }} 32 | run: | 33 | echo $FILES | grep -qF 'CHANGELOG.md' || echo $PR_BODY | grep -qiF "$NO_CHANGELOG" 34 | - name: Reject pull request if no CHANGELOG update 35 | if: ${{ always() && steps.changelog_updated.outcome == 'failure' }} 36 | uses: Ana06/automatic-pull-request-review@76aaf9b15b116a54e1da7a28a46f91fe089600bf # v0.2.0 37 | with: 38 | repo-token: ${{ secrets.GITHUB_TOKEN }} 39 | event: REQUEST_CHANGES 40 | body: "Please add bug fixes, new features, breaking changes and anything else you think is worthwhile mentioning to the `master (unreleased)` section of CHANGELOG.md. If no CHANGELOG update is needed add the following to the PR description: `${{ env.NO_CHANGELOG }}`" 41 | allow_duplicate: false 42 | - name: Dismiss previous review if CHANGELOG update 43 | uses: Ana06/automatic-pull-request-review@76aaf9b15b116a54e1da7a28a46f91fe089600bf # v0.2.0 44 | with: 45 | repo-token: ${{ secrets.GITHUB_TOKEN }} 46 | event: DISMISS 47 | body: "CHANGELOG updated or no update needed, thanks! :smile:" 48 | -------------------------------------------------------------------------------- /.github/workflows/pip-audit.yml: -------------------------------------------------------------------------------- 1 | name: PIP audit 2 | 3 | on: 4 | schedule: 5 | - cron: '0 8 * * 1' 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 20 11 | strategy: 12 | matrix: 13 | python-version: ["3.11"] 14 | 15 | steps: 16 | - name: Check out repository code 17 | uses: actions/checkout@v4 18 | 19 | - uses: pypa/gh-action-pip-audit@v1.0.8 20 | with: 21 | inputs: . 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # use PyPI trusted publishing, as described here: 2 | # https://blog.trailofbits.com/2023/05/23/trusted-publishing-a-new-benchmark-for-packaging-security/ 3 | name: publish to pypi 4 | 5 | on: 6 | release: 7 | types: [published] 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | pypi-publish: 14 | runs-on: ubuntu-latest 15 | environment: 16 | name: release 17 | permissions: 18 | id-token: write 19 | steps: 20 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 21 | - name: Set up Python 22 | uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 23 | with: 24 | python-version: '3.10' 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install -r requirements.txt 29 | pip install -e .[build] 30 | - name: build package 31 | run: | 32 | python -m build 33 | - name: upload package artifacts 34 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 35 | with: 36 | path: dist/* 37 | - name: publish package 38 | uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 # release/v1 39 | with: 40 | skip-existing: true 41 | verbose: true 42 | print-hash: true 43 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '43 4 * * 3' 14 | push: 15 | branches: [ "master" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard. 69 | - name: "Upload to code-scanning" 70 | uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 71 | with: 72 | sarif_file: results.sarif 73 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: tag 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: read-all 8 | 9 | jobs: 10 | tag: 11 | name: Tag capa rules 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout capa-rules 15 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 16 | with: 17 | repository: mandiant/capa-rules 18 | token: ${{ secrets.CAPA_TOKEN }} 19 | - name: Tag capa-rules 20 | run: | 21 | # user information is needed to create annotated tags (with a message) 22 | git config user.email 'capa-dev@mandiant.com' 23 | git config user.name 'Capa Bot' 24 | name=${{ github.event.release.tag_name }} 25 | git tag $name -m "https://github.com/mandiant/capa/releases/$name" 26 | # TODO update branch name-major=${name%%.*} 27 | - name: Push tag to capa-rules 28 | uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # v0.8.0 29 | with: 30 | repository: mandiant/capa-rules 31 | github_token: ${{ secrets.CAPA_TOKEN }} 32 | tags: true 33 | -------------------------------------------------------------------------------- /.github/workflows/web-tests.yml: -------------------------------------------------------------------------------- 1 | name: capa Explorer Web tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | paths: 7 | - 'web/explorer/**' 8 | workflow_call: # this allows the workflow to be called by other workflows 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | submodules: 'recursive' 19 | fetch-depth: 1 20 | show-progress: true 21 | 22 | - name: Set up Node 23 | uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 24 | with: 25 | node-version: 20 26 | cache: 'npm' 27 | cache-dependency-path: 'web/explorer/package-lock.json' 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | working-directory: web/explorer 32 | 33 | - name: Lint 34 | run: npm run lint 35 | working-directory: web/explorer 36 | 37 | - name: Format 38 | run: npm run format:check 39 | working-directory: web/explorer 40 | 41 | - name: Run unit tests 42 | run: npm run test 43 | working-directory: web/explorer 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .idea/* 107 | *.prof 108 | *.viv 109 | *.idb 110 | *.i64 111 | .vscode 112 | 113 | !rules/lib 114 | 115 | scripts/perf/*.txt 116 | scripts/perf/*.svg 117 | scripts/perf/*.zip 118 | 119 | .direnv 120 | .envrc 121 | .DS_Store 122 | */.DS_Store 123 | Pipfile 124 | Pipfile.lock 125 | /cache/ 126 | .github/binja/binaryninja 127 | .github/binja/download_headless.py 128 | .github/binja/BinaryNinja-headless.zip 129 | justfile 130 | data/ 131 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rules"] 2 | path = rules 3 | url = ../../mandiant/capa-rules.git 4 | [submodule "tests/data"] 5 | path = tests/data 6 | url = ../../mandiant/capa-testfiles.git 7 | -------------------------------------------------------------------------------- /.justfile: -------------------------------------------------------------------------------- 1 | @isort: 2 | pre-commit run isort --show-diff-on-failure --all-files 3 | 4 | @black: 5 | pre-commit run black --show-diff-on-failure --all-files 6 | 7 | @ruff: 8 | pre-commit run ruff --all-files 9 | 10 | @flake8: 11 | pre-commit run flake8 --hook-stage manual --all-files 12 | 13 | @mypy: 14 | pre-commit run mypy --hook-stage manual --all-files 15 | 16 | @deptry: 17 | pre-commit run deptry --hook-stage manual --all-files 18 | 19 | @lint: 20 | -just isort 21 | -just black 22 | -just ruff 23 | -just flake8 24 | -just mypy 25 | -just deptry 26 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - name: "The FLARE Team" 5 | title: "capa, a tool to identify capabilities in programs and sandbox traces." 6 | date-released: 2020-07-16 7 | url: "https://github.com/mandiant/capa" 8 | 9 | -------------------------------------------------------------------------------- /capa/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/__init__.py -------------------------------------------------------------------------------- /capa/capabilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/capabilities/__init__.py -------------------------------------------------------------------------------- /capa/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class UnsupportedRuntimeError(RuntimeError): 17 | pass 18 | 19 | 20 | class UnsupportedFormatError(ValueError): 21 | pass 22 | 23 | 24 | class UnsupportedArchError(ValueError): 25 | pass 26 | 27 | 28 | class UnsupportedOSError(ValueError): 29 | pass 30 | 31 | 32 | class EmptyReportError(ValueError): 33 | pass 34 | 35 | 36 | class InvalidArgument(ValueError): 37 | pass 38 | 39 | 40 | class NonExistantFunctionError(ValueError): 41 | pass 42 | 43 | 44 | class NonExistantProcessError(ValueError): 45 | pass 46 | -------------------------------------------------------------------------------- /capa/features/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/__init__.py -------------------------------------------------------------------------------- /capa/features/basicblock.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from capa.features.common import Feature 17 | 18 | 19 | class BasicBlock(Feature): 20 | def __init__(self, description=None): 21 | super().__init__(0, description=description) 22 | 23 | def __str__(self): 24 | return "basic block" 25 | 26 | def get_value_str(self): 27 | return "" 28 | -------------------------------------------------------------------------------- /capa/features/com/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from enum import Enum 16 | 17 | from capa.helpers import assert_never 18 | 19 | 20 | class ComType(Enum): 21 | CLASS = "class" 22 | INTERFACE = "interface" 23 | 24 | 25 | COM_PREFIXES = { 26 | ComType.CLASS: "CLSID_", 27 | ComType.INTERFACE: "IID_", 28 | } 29 | 30 | 31 | def load_com_database(com_type: ComType) -> dict[str, list[str]]: 32 | # lazy load these python files since they are so large. 33 | # that is, don't load them unless a COM feature is being handled. 34 | import capa.features.com.classes 35 | import capa.features.com.interfaces 36 | 37 | if com_type == ComType.CLASS: 38 | return capa.features.com.classes.COM_CLASSES 39 | elif com_type == ComType.INTERFACE: 40 | return capa.features.com.interfaces.COM_INTERFACES 41 | else: 42 | assert_never(com_type) 43 | -------------------------------------------------------------------------------- /capa/features/extractors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/binexport2/arch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/binexport2/arch/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/binexport2/arch/arm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/binexport2/arch/arm/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/binexport2/arch/arm/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from capa.features.extractors.binexport2.binexport2_pb2 import BinExport2 17 | 18 | 19 | def is_stack_register_expression(be2: BinExport2, expression: BinExport2.Expression) -> bool: 20 | return bool( 21 | expression and expression.type == BinExport2.Expression.REGISTER and expression.symbol.lower().endswith("sp") 22 | ) 23 | -------------------------------------------------------------------------------- /capa/features/extractors/binexport2/arch/intel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/binexport2/arch/intel/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/binexport2/basicblock.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from typing import Iterator 17 | 18 | from capa.features.common import Feature, Characteristic 19 | from capa.features.address import Address, AbsoluteVirtualAddress 20 | from capa.features.basicblock import BasicBlock 21 | from capa.features.extractors.binexport2 import FunctionContext, BasicBlockContext 22 | from capa.features.extractors.base_extractor import BBHandle, FunctionHandle 23 | from capa.features.extractors.binexport2.binexport2_pb2 import BinExport2 24 | 25 | 26 | def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[tuple[Feature, Address]]: 27 | fhi: FunctionContext = fh.inner 28 | bbi: BasicBlockContext = bbh.inner 29 | 30 | idx = fhi.ctx.idx 31 | 32 | basic_block_index: int = bbi.basic_block_index 33 | target_edges: list[BinExport2.FlowGraph.Edge] = idx.target_edges_by_basic_block_index[basic_block_index] 34 | if basic_block_index in (e.source_basic_block_index for e in target_edges): 35 | basic_block_address: int = idx.get_basic_block_address(basic_block_index) 36 | yield Characteristic("tight loop"), AbsoluteVirtualAddress(basic_block_address) 37 | 38 | 39 | def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[tuple[Feature, Address]]: 40 | """extract basic block features""" 41 | for bb_handler in BASIC_BLOCK_HANDLERS: 42 | for feature, addr in bb_handler(fh, bbh): 43 | yield feature, addr 44 | yield BasicBlock(), bbh.address 45 | 46 | 47 | BASIC_BLOCK_HANDLERS = (extract_bb_tight_loop,) 48 | -------------------------------------------------------------------------------- /capa/features/extractors/binexport2/function.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Iterator 16 | 17 | from capa.features.file import FunctionName 18 | from capa.features.common import Feature, Characteristic 19 | from capa.features.address import Address, AbsoluteVirtualAddress 20 | from capa.features.extractors import loops 21 | from capa.features.extractors.binexport2 import BinExport2Index, FunctionContext 22 | from capa.features.extractors.base_extractor import FunctionHandle 23 | from capa.features.extractors.binexport2.binexport2_pb2 import BinExport2 24 | 25 | 26 | def extract_function_calls_to(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: 27 | fhi: FunctionContext = fh.inner 28 | 29 | be2: BinExport2 = fhi.ctx.be2 30 | idx: BinExport2Index = fhi.ctx.idx 31 | 32 | flow_graph_index: int = fhi.flow_graph_index 33 | flow_graph_address: int = idx.flow_graph_address_by_index[flow_graph_index] 34 | vertex_index: int = idx.vertex_index_by_address[flow_graph_address] 35 | 36 | for caller_index in idx.callers_by_vertex_index[vertex_index]: 37 | caller: BinExport2.CallGraph.Vertex = be2.call_graph.vertex[caller_index] 38 | caller_address: int = caller.address 39 | yield Characteristic("calls to"), AbsoluteVirtualAddress(caller_address) 40 | 41 | 42 | def extract_function_loop(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: 43 | fhi: FunctionContext = fh.inner 44 | 45 | be2: BinExport2 = fhi.ctx.be2 46 | 47 | flow_graph_index: int = fhi.flow_graph_index 48 | flow_graph: BinExport2.FlowGraph = be2.flow_graph[flow_graph_index] 49 | 50 | edges: list[tuple[int, int]] = [] 51 | for edge in flow_graph.edge: 52 | edges.append((edge.source_basic_block_index, edge.target_basic_block_index)) 53 | 54 | if loops.has_loop(edges): 55 | yield Characteristic("loop"), fh.address 56 | 57 | 58 | def extract_function_name(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: 59 | fhi: FunctionContext = fh.inner 60 | 61 | be2: BinExport2 = fhi.ctx.be2 62 | idx: BinExport2Index = fhi.ctx.idx 63 | flow_graph_index: int = fhi.flow_graph_index 64 | 65 | flow_graph_address: int = idx.flow_graph_address_by_index[flow_graph_index] 66 | vertex_index: int = idx.vertex_index_by_address[flow_graph_address] 67 | vertex: BinExport2.CallGraph.Vertex = be2.call_graph.vertex[vertex_index] 68 | 69 | if vertex.HasField("mangled_name"): 70 | yield FunctionName(vertex.mangled_name), fh.address 71 | 72 | 73 | def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: 74 | for func_handler in FUNCTION_HANDLERS: 75 | for feature, addr in func_handler(fh): 76 | yield feature, addr 77 | 78 | 79 | FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_function_name) 80 | -------------------------------------------------------------------------------- /capa/features/extractors/binja/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/binja/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/binja/basicblock.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Iterator 16 | 17 | from binaryninja import BasicBlock as BinjaBasicBlock 18 | 19 | from capa.features.common import Feature, Characteristic 20 | from capa.features.address import Address 21 | from capa.features.basicblock import BasicBlock 22 | from capa.features.extractors.base_extractor import BBHandle, FunctionHandle 23 | 24 | 25 | def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[tuple[Feature, Address]]: 26 | """extract tight loop indicators from a basic block""" 27 | bb: BinjaBasicBlock = bbh.inner 28 | for edge in bb.outgoing_edges: 29 | if edge.target.start == bb.start: 30 | yield Characteristic("tight loop"), bbh.address 31 | 32 | 33 | def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[tuple[Feature, Address]]: 34 | """extract basic block features""" 35 | for bb_handler in BASIC_BLOCK_HANDLERS: 36 | for feature, addr in bb_handler(fh, bbh): 37 | yield feature, addr 38 | yield BasicBlock(), bbh.address 39 | 40 | 41 | BASIC_BLOCK_HANDLERS = (extract_bb_tight_loop,) 42 | -------------------------------------------------------------------------------- /capa/features/extractors/binja/global_.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | from typing import Iterator 17 | 18 | from binaryninja import BinaryView 19 | 20 | from capa.features.common import OS, OS_MACOS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature 21 | from capa.features.address import NO_ADDRESS, Address 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def extract_os(bv: BinaryView) -> Iterator[tuple[Feature, Address]]: 27 | name = bv.platform.name 28 | if "-" in name: 29 | name = name.split("-")[0] 30 | 31 | if name == "windows": 32 | yield OS(OS_WINDOWS), NO_ADDRESS 33 | 34 | elif name == "macos": 35 | yield OS(OS_MACOS), NO_ADDRESS 36 | 37 | elif name in ["linux", "freebsd", "decree"]: 38 | yield OS(name), NO_ADDRESS 39 | 40 | else: 41 | # we likely end up here: 42 | # 1. handling shellcode, or 43 | # 2. handling a new file format (e.g. macho) 44 | # 45 | # for (1) we can't do much - its shellcode and all bets are off. 46 | # we could maybe accept a further CLI argument to specify the OS, 47 | # but i think this would be rarely used. 48 | # rules that rely on OS conditions will fail to match on shellcode. 49 | # 50 | # for (2), this logic will need to be updated as the format is implemented. 51 | logger.debug("unsupported file format: %s, will not guess OS", name) 52 | return 53 | 54 | 55 | def extract_arch(bv: BinaryView) -> Iterator[tuple[Feature, Address]]: 56 | arch = bv.arch.name 57 | if arch == "x86_64": 58 | yield Arch(ARCH_AMD64), NO_ADDRESS 59 | elif arch == "x86": 60 | yield Arch(ARCH_I386), NO_ADDRESS 61 | else: 62 | # we likely end up here: 63 | # 1. handling a new architecture (e.g. aarch64) 64 | # 65 | # for (1), this logic will need to be updated as the format is implemented. 66 | logger.debug("unsupported architecture: %s", arch) 67 | return 68 | -------------------------------------------------------------------------------- /capa/features/extractors/binja/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import re 16 | from typing import Callable, Optional 17 | from dataclasses import dataclass 18 | 19 | from binaryninja import BinaryView, LowLevelILFunction, LowLevelILInstruction 20 | from binaryninja.architecture import InstructionTextToken 21 | 22 | 23 | @dataclass 24 | class DisassemblyInstruction: 25 | address: int 26 | length: int 27 | text: list[InstructionTextToken] 28 | 29 | 30 | LLIL_VISITOR = Callable[[LowLevelILInstruction, LowLevelILInstruction, int], bool] 31 | 32 | 33 | def visit_llil_exprs(il: LowLevelILInstruction, func: LLIL_VISITOR): 34 | # BN does not really support operand index at the disassembly level, so use the LLIL operand index as a substitute. 35 | # Note, this is NOT always guaranteed to be the same as disassembly operand. 36 | for i, op in enumerate(il.operands): 37 | if isinstance(op, LowLevelILInstruction) and func(op, il, i): 38 | visit_llil_exprs(op, func) 39 | 40 | 41 | def unmangle_c_name(name: str) -> str: 42 | # https://learn.microsoft.com/en-us/cpp/build/reference/decorated-names?view=msvc-170#FormatC 43 | # Possible variations for BaseThreadInitThunk: 44 | # @BaseThreadInitThunk@12 45 | # _BaseThreadInitThunk 46 | # _BaseThreadInitThunk@12 47 | # It is also possible for a function to have a `Stub` appended to its name: 48 | # _lstrlenWStub@4 49 | 50 | # A small optimization to avoid running the regex too many times 51 | # this still increases the unit test execution time from 170s to 200s, should be able to accelerate it 52 | # 53 | # TODO(xusheng): performance optimizations to improve test execution time 54 | # https://github.com/mandiant/capa/issues/1610 55 | if name[0] in ["@", "_"]: 56 | match = re.match(r"^[@|_](.*?)(Stub)?(@\d+)?$", name) 57 | if match: 58 | return match.group(1) 59 | 60 | return name 61 | 62 | 63 | def read_c_string(bv: BinaryView, offset: int, max_len: int) -> str: 64 | s: list[str] = [] 65 | while len(s) < max_len: 66 | try: 67 | c = bv.read(offset + len(s), 1)[0] 68 | except Exception: 69 | break 70 | 71 | if c == 0: 72 | break 73 | 74 | s.append(chr(c)) 75 | 76 | return "".join(s) 77 | 78 | 79 | def get_llil_instr_at_addr(bv: BinaryView, addr: int) -> Optional[LowLevelILInstruction]: 80 | arch = bv.arch 81 | buffer = bv.read(addr, arch.max_instr_length) 82 | llil = LowLevelILFunction(arch=arch) 83 | llil.current_address = addr 84 | if arch.get_instruction_low_level_il(buffer, addr, llil) == 0: 85 | return None 86 | return llil[0] 87 | -------------------------------------------------------------------------------- /capa/features/extractors/cape/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/cape/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/cape/call.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import logging 17 | from typing import Iterator 18 | 19 | import capa.features.extractors.helpers 20 | from capa.helpers import assert_never 21 | from capa.features.insn import API, Number 22 | from capa.features.common import String, Feature 23 | from capa.features.address import Address 24 | from capa.features.extractors.cape.models import Call 25 | from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | def extract_call_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[tuple[Feature, Address]]: 31 | """ 32 | this method extracts the given call's features (such as API name and arguments), 33 | and returns them as API, Number, and String features. 34 | 35 | args: 36 | ph: process handle (for defining the extraction scope) 37 | th: thread handle (for defining the extraction scope) 38 | ch: call handle (for defining the extraction scope) 39 | 40 | yields: 41 | Feature, address; where Feature is either: API, Number, or String. 42 | """ 43 | call: Call = ch.inner 44 | 45 | # list similar to disassembly: arguments right-to-left, call 46 | for arg in reversed(call.arguments): 47 | value = arg.value 48 | if isinstance(value, list) and len(value) == 0: 49 | # unsure why CAPE captures arguments as empty lists? 50 | continue 51 | 52 | elif isinstance(value, str): 53 | yield String(value), ch.address 54 | 55 | elif isinstance(value, int): 56 | yield Number(value), ch.address 57 | 58 | else: 59 | assert_never(value) 60 | 61 | for name in capa.features.extractors.helpers.generate_symbols("", call.api): 62 | yield API(name), ch.address 63 | 64 | 65 | def extract_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[tuple[Feature, Address]]: 66 | for handler in CALL_HANDLERS: 67 | for feature, addr in handler(ph, th, ch): 68 | yield feature, addr 69 | 70 | 71 | CALL_HANDLERS = (extract_call_features,) 72 | -------------------------------------------------------------------------------- /capa/features/extractors/cape/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from typing import Any 17 | 18 | from capa.features.extractors.base_extractor import ProcessHandle 19 | 20 | 21 | def find_process(processes: list[dict[str, Any]], ph: ProcessHandle) -> dict[str, Any]: 22 | """ 23 | find a specific process identified by a process handler. 24 | 25 | args: 26 | processes: a list of processes extracted by CAPE 27 | ph: handle of the sought process 28 | 29 | return: 30 | a CAPE-defined dictionary for the sought process' information 31 | """ 32 | 33 | for process in processes: 34 | if ph.address.ppid == process["parent_id"] and ph.address.pid == process["process_id"]: 35 | return process 36 | return {} 37 | -------------------------------------------------------------------------------- /capa/features/extractors/cape/process.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import logging 17 | from typing import Iterator 18 | 19 | from capa.features.common import String, Feature 20 | from capa.features.address import Address, ThreadAddress 21 | from capa.features.extractors.cape.models import Process 22 | from capa.features.extractors.base_extractor import ThreadHandle, ProcessHandle 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def get_threads(ph: ProcessHandle) -> Iterator[ThreadHandle]: 28 | """ 29 | get the threads associated with a given process 30 | """ 31 | process: Process = ph.inner 32 | threads: list[int] = process.threads 33 | 34 | for thread in threads: 35 | address: ThreadAddress = ThreadAddress(process=ph.address, tid=thread) 36 | yield ThreadHandle(address=address, inner={}) 37 | 38 | 39 | def extract_environ_strings(ph: ProcessHandle) -> Iterator[tuple[Feature, Address]]: 40 | """ 41 | extract strings from a process' provided environment variables. 42 | """ 43 | process: Process = ph.inner 44 | 45 | for value in (value for value in process.environ.values() if value): 46 | yield String(value), ph.address 47 | 48 | 49 | def extract_features(ph: ProcessHandle) -> Iterator[tuple[Feature, Address]]: 50 | for handler in PROCESS_HANDLERS: 51 | for feature, addr in handler(ph): 52 | yield feature, addr 53 | 54 | 55 | PROCESS_HANDLERS = (extract_environ_strings,) 56 | -------------------------------------------------------------------------------- /capa/features/extractors/cape/thread.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import logging 17 | from typing import Iterator 18 | 19 | from capa.features.address import DynamicCallAddress 20 | from capa.features.extractors.helpers import generate_symbols 21 | from capa.features.extractors.cape.models import Process 22 | from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def get_calls(ph: ProcessHandle, th: ThreadHandle) -> Iterator[CallHandle]: 28 | process: Process = ph.inner 29 | 30 | tid = th.address.tid 31 | for call_index, call in enumerate(process.calls): 32 | if call.thread_id != tid: 33 | continue 34 | 35 | for symbol in generate_symbols("", call.api): 36 | call.api = symbol 37 | 38 | addr = DynamicCallAddress(thread=th.address, id=call_index) 39 | yield CallHandle(address=addr, inner=call) 40 | -------------------------------------------------------------------------------- /capa/features/extractors/dnfile/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/dnfile/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/dnfile/file.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from __future__ import annotations 17 | 18 | from typing import Iterator 19 | 20 | import dnfile 21 | 22 | import capa.features.extractors.dotnetfile 23 | from capa.features.file import Import, FunctionName 24 | from capa.features.common import Class, Format, String, Feature, Namespace, Characteristic 25 | from capa.features.address import Address 26 | 27 | 28 | def extract_file_import_names(pe: dnfile.dnPE) -> Iterator[tuple[Import, Address]]: 29 | yield from capa.features.extractors.dotnetfile.extract_file_import_names(pe=pe) 30 | 31 | 32 | def extract_file_format(pe: dnfile.dnPE) -> Iterator[tuple[Format, Address]]: 33 | yield from capa.features.extractors.dotnetfile.extract_file_format(pe=pe) 34 | 35 | 36 | def extract_file_function_names(pe: dnfile.dnPE) -> Iterator[tuple[FunctionName, Address]]: 37 | yield from capa.features.extractors.dotnetfile.extract_file_function_names(pe=pe) 38 | 39 | 40 | def extract_file_strings(pe: dnfile.dnPE) -> Iterator[tuple[String, Address]]: 41 | yield from capa.features.extractors.dotnetfile.extract_file_strings(pe=pe) 42 | 43 | 44 | def extract_file_mixed_mode_characteristic_features(pe: dnfile.dnPE) -> Iterator[tuple[Characteristic, Address]]: 45 | yield from capa.features.extractors.dotnetfile.extract_file_mixed_mode_characteristic_features(pe=pe) 46 | 47 | 48 | def extract_file_namespace_features(pe: dnfile.dnPE) -> Iterator[tuple[Namespace, Address]]: 49 | yield from capa.features.extractors.dotnetfile.extract_file_namespace_features(pe=pe) 50 | 51 | 52 | def extract_file_class_features(pe: dnfile.dnPE) -> Iterator[tuple[Class, Address]]: 53 | yield from capa.features.extractors.dotnetfile.extract_file_class_features(pe=pe) 54 | 55 | 56 | def extract_features(pe: dnfile.dnPE) -> Iterator[tuple[Feature, Address]]: 57 | for file_handler in FILE_HANDLERS: 58 | for feature, address in file_handler(pe): 59 | yield feature, address 60 | 61 | 62 | FILE_HANDLERS = ( 63 | extract_file_import_names, 64 | extract_file_function_names, 65 | extract_file_strings, 66 | extract_file_format, 67 | extract_file_mixed_mode_characteristic_features, 68 | extract_file_namespace_features, 69 | extract_file_class_features, 70 | ) 71 | -------------------------------------------------------------------------------- /capa/features/extractors/dnfile/function.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from __future__ import annotations 17 | 18 | import logging 19 | from typing import Iterator 20 | 21 | from capa.features.common import Feature, Characteristic 22 | from capa.features.address import Address 23 | from capa.features.extractors.base_extractor import FunctionHandle 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | def extract_function_calls_to(fh: FunctionHandle) -> Iterator[tuple[Characteristic, Address]]: 29 | """extract callers to a function""" 30 | for dest in fh.ctx["calls_to"]: 31 | yield Characteristic("calls to"), dest 32 | 33 | 34 | def extract_function_calls_from(fh: FunctionHandle) -> Iterator[tuple[Characteristic, Address]]: 35 | """extract callers from a function""" 36 | for src in fh.ctx["calls_from"]: 37 | yield Characteristic("calls from"), src 38 | 39 | 40 | def extract_recursive_call(fh: FunctionHandle) -> Iterator[tuple[Characteristic, Address]]: 41 | """extract recursive function call""" 42 | if fh.address in fh.ctx["calls_to"]: 43 | yield Characteristic("recursive call"), fh.address 44 | 45 | 46 | def extract_function_loop(fh: FunctionHandle) -> Iterator[tuple[Characteristic, Address]]: 47 | """extract loop indicators from a function""" 48 | raise NotImplementedError() 49 | 50 | 51 | def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: 52 | for func_handler in FUNCTION_HANDLERS: 53 | for feature, addr in func_handler(fh): 54 | yield feature, addr 55 | 56 | 57 | FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_calls_from, extract_recursive_call) 58 | -------------------------------------------------------------------------------- /capa/features/extractors/dnfile/types.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from typing import Optional 17 | 18 | 19 | class DnType: 20 | def __init__( 21 | self, token: int, class_: tuple[str, ...], namespace: str = "", member: str = "", access: Optional[str] = None 22 | ): 23 | self.token: int = token 24 | self.access: Optional[str] = access 25 | self.namespace: str = namespace 26 | self.class_: tuple[str, ...] = class_ 27 | 28 | if member == ".ctor": 29 | member = "ctor" 30 | if member == ".cctor": 31 | member = "cctor" 32 | 33 | self.member: str = member 34 | 35 | def __hash__(self): 36 | return hash((self.token, self.access, self.namespace, self.class_, self.member)) 37 | 38 | def __eq__(self, other): 39 | return ( 40 | self.token == other.token 41 | and self.access == other.access 42 | and self.namespace == other.namespace 43 | and self.class_ == other.class_ 44 | and self.member == other.member 45 | ) 46 | 47 | def __str__(self): 48 | return DnType.format_name(self.class_, namespace=self.namespace, member=self.member) 49 | 50 | def __repr__(self): 51 | return str(self) 52 | 53 | @staticmethod 54 | def format_name(class_: tuple[str, ...], namespace: str = "", member: str = ""): 55 | if len(class_) > 1: 56 | class_str = "/".join(class_) # Concat items in tuple, separated by a "/" 57 | else: 58 | class_str = "".join(class_) # Convert tuple to str 59 | # like File::OpenRead 60 | name: str = f"{class_str}::{member}" if member else class_str 61 | if namespace: 62 | # like System.IO.File::OpenRead 63 | name = f"{namespace}.{name}" 64 | return name 65 | 66 | 67 | class DnUnmanagedMethod: 68 | def __init__(self, token: int, module: str, method: str): 69 | self.token: int = token 70 | self.module: str = module 71 | self.method: str = method 72 | 73 | def __hash__(self): 74 | return hash((self.token, self.module, self.method)) 75 | 76 | def __eq__(self, other): 77 | return self.token == other.token and self.module == other.module and self.method == other.method 78 | 79 | def __str__(self): 80 | return DnUnmanagedMethod.format_name(self.module, self.method) 81 | 82 | def __repr__(self): 83 | return str(self) 84 | 85 | @staticmethod 86 | def format_name(module, method): 87 | return f"{module}.{method}" 88 | -------------------------------------------------------------------------------- /capa/features/extractors/drakvuf/call.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import logging 17 | from typing import Iterator 18 | 19 | import capa.features.extractors.helpers 20 | from capa.features.insn import API, Number 21 | from capa.features.common import String, Feature 22 | from capa.features.address import Address 23 | from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle 24 | from capa.features.extractors.drakvuf.models import Call 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | def extract_call_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[tuple[Feature, Address]]: 30 | """ 31 | This method extracts the given call's features (such as API name and arguments), 32 | and returns them as API, Number, and String features. 33 | 34 | args: 35 | ph: process handle (for defining the extraction scope) 36 | th: thread handle (for defining the extraction scope) 37 | ch: call handle (for defining the extraction scope) 38 | 39 | yields: 40 | Feature, address; where Feature is either: API, Number, or String. 41 | """ 42 | call: Call = ch.inner 43 | 44 | # list similar to disassembly: arguments right-to-left, call 45 | for arg_value in reversed(call.arguments.values()): 46 | try: 47 | yield Number(int(arg_value, 0)), ch.address 48 | except ValueError: 49 | # DRAKVUF automatically resolves the contents of memory addresses, (e.g. Arg1="0xc6f217efe0:\"ntdll.dll\""). 50 | # For those cases we yield the entire string as it, since yielding the address only would 51 | # likely not provide any matches, and yielding just the memory contentswould probably be misleading, 52 | # but yielding the entire string would be helpful for an analyst looking at the verbose output 53 | yield String(arg_value), ch.address 54 | 55 | for name in capa.features.extractors.helpers.generate_symbols("", call.name): 56 | yield API(name), ch.address 57 | 58 | 59 | def extract_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[tuple[Feature, Address]]: 60 | for handler in CALL_HANDLERS: 61 | for feature, addr in handler(ph, th, ch): 62 | yield feature, addr 63 | 64 | 65 | CALL_HANDLERS = (extract_call_features,) 66 | -------------------------------------------------------------------------------- /capa/features/extractors/drakvuf/file.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import logging 17 | from typing import Iterator 18 | 19 | from capa.features.file import Import 20 | from capa.features.common import Feature 21 | from capa.features.address import Address, ThreadAddress, ProcessAddress, AbsoluteVirtualAddress 22 | from capa.features.extractors.helpers import generate_symbols 23 | from capa.features.extractors.base_extractor import ProcessHandle 24 | from capa.features.extractors.drakvuf.models import Call, DrakvufReport 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | def get_processes(calls: dict[ProcessAddress, dict[ThreadAddress, list[Call]]]) -> Iterator[ProcessHandle]: 30 | """ 31 | Get all the created processes for a sample. 32 | """ 33 | for proc_addr, calls_per_thread in calls.items(): 34 | sample_call = next(iter(calls_per_thread.values()))[0] # get process name 35 | yield ProcessHandle(proc_addr, inner={"process_name": sample_call.process_name}) 36 | 37 | 38 | def extract_import_names(report: DrakvufReport) -> Iterator[tuple[Feature, Address]]: 39 | """ 40 | Extract imported function names. 41 | """ 42 | if report.loaded_dlls is None: 43 | return 44 | dlls = report.loaded_dlls 45 | 46 | for dll in dlls: 47 | dll_base_name = dll.name.split("\\")[-1] 48 | for function_name, function_address in dll.imports.items(): 49 | for name in generate_symbols(dll_base_name, function_name, include_dll=True): 50 | yield Import(name), AbsoluteVirtualAddress(function_address) 51 | 52 | 53 | def extract_features(report: DrakvufReport) -> Iterator[tuple[Feature, Address]]: 54 | for handler in FILE_HANDLERS: 55 | for feature, addr in handler(report): 56 | yield feature, addr 57 | 58 | 59 | FILE_HANDLERS = ( 60 | # TODO(yelhamer): extract more file features from other DRAKVUF plugins 61 | # https://github.com/mandiant/capa/issues/2169 62 | extract_import_names, 63 | ) 64 | -------------------------------------------------------------------------------- /capa/features/extractors/drakvuf/global_.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import logging 17 | from typing import Iterator 18 | 19 | from capa.features.common import OS, FORMAT_PE, ARCH_AMD64, OS_WINDOWS, Arch, Format, Feature 20 | from capa.features.address import NO_ADDRESS, Address 21 | from capa.features.extractors.drakvuf.models import DrakvufReport 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def extract_format(report: DrakvufReport) -> Iterator[tuple[Feature, Address]]: 27 | # DRAKVUF sandbox currently supports only Windows as the guest: https://drakvuf-sandbox.readthedocs.io/en/latest/usage/getting_started.html 28 | yield Format(FORMAT_PE), NO_ADDRESS 29 | 30 | 31 | def extract_os(report: DrakvufReport) -> Iterator[tuple[Feature, Address]]: 32 | # DRAKVUF sandbox currently supports only PE files: https://drakvuf-sandbox.readthedocs.io/en/latest/usage/getting_started.html 33 | yield OS(OS_WINDOWS), NO_ADDRESS 34 | 35 | 36 | def extract_arch(report: DrakvufReport) -> Iterator[tuple[Feature, Address]]: 37 | # DRAKVUF sandbox currently supports only x64 Windows as the guest: https://drakvuf-sandbox.readthedocs.io/en/latest/usage/getting_started.html 38 | yield Arch(ARCH_AMD64), NO_ADDRESS 39 | 40 | 41 | def extract_features(report: DrakvufReport) -> Iterator[tuple[Feature, Address]]: 42 | for global_handler in GLOBAL_HANDLER: 43 | for feature, addr in global_handler(report): 44 | yield feature, addr 45 | 46 | 47 | GLOBAL_HANDLER = ( 48 | extract_format, 49 | extract_os, 50 | extract_arch, 51 | ) 52 | -------------------------------------------------------------------------------- /capa/features/extractors/drakvuf/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import itertools 17 | 18 | from capa.features.address import ThreadAddress, ProcessAddress 19 | from capa.features.extractors.drakvuf.models import Call, DrakvufReport 20 | 21 | 22 | def index_calls(report: DrakvufReport) -> dict[ProcessAddress, dict[ThreadAddress, list[Call]]]: 23 | # this method organizes calls into processes and threads, and then sorts them based on 24 | # timestamp so that we can address individual calls per index (CallAddress requires call index) 25 | result: dict[ProcessAddress, dict[ThreadAddress, list[Call]]] = {} 26 | for call in itertools.chain(report.syscalls, report.apicalls): 27 | if call.pid == 0: 28 | # DRAKVUF captures api/native calls from all processes running on the system. 29 | # we ignore the pid 0 since it's a system process and it's unlikely for it to 30 | # be hijacked or so on, in addition to capa addresses not supporting null pids 31 | continue 32 | proc_addr = ProcessAddress(pid=call.pid, ppid=call.ppid) 33 | thread_addr = ThreadAddress(process=proc_addr, tid=call.tid) 34 | if proc_addr not in result: 35 | result[proc_addr] = {} 36 | if thread_addr not in result[proc_addr]: 37 | result[proc_addr][thread_addr] = [] 38 | 39 | result[proc_addr][thread_addr].append(call) 40 | 41 | for proc, threads in result.items(): 42 | for thread in threads: 43 | result[proc][thread].sort(key=lambda call: call.timestamp) 44 | 45 | return result 46 | -------------------------------------------------------------------------------- /capa/features/extractors/drakvuf/process.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import logging 17 | from typing import Iterator 18 | 19 | from capa.features.common import String, Feature 20 | from capa.features.address import Address, ThreadAddress, ProcessAddress 21 | from capa.features.extractors.base_extractor import ThreadHandle, ProcessHandle 22 | from capa.features.extractors.drakvuf.models import Call 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def get_threads( 28 | calls: dict[ProcessAddress, dict[ThreadAddress, list[Call]]], ph: ProcessHandle 29 | ) -> Iterator[ThreadHandle]: 30 | """ 31 | Get the threads associated with a given process. 32 | """ 33 | for thread_addr in calls[ph.address]: 34 | yield ThreadHandle(address=thread_addr, inner={}) 35 | 36 | 37 | def extract_process_name(ph: ProcessHandle) -> Iterator[tuple[Feature, Address]]: 38 | yield String(ph.inner["process_name"]), ph.address 39 | 40 | 41 | def extract_features(ph: ProcessHandle) -> Iterator[tuple[Feature, Address]]: 42 | for handler in PROCESS_HANDLERS: 43 | for feature, addr in handler(ph): 44 | yield feature, addr 45 | 46 | 47 | PROCESS_HANDLERS = (extract_process_name,) 48 | -------------------------------------------------------------------------------- /capa/features/extractors/drakvuf/thread.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import logging 17 | from typing import Iterator 18 | 19 | from capa.features.address import ThreadAddress, ProcessAddress, DynamicCallAddress 20 | from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle 21 | from capa.features.extractors.drakvuf.models import Call 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def get_calls( 27 | sorted_calls: dict[ProcessAddress, dict[ThreadAddress, list[Call]]], ph: ProcessHandle, th: ThreadHandle 28 | ) -> Iterator[CallHandle]: 29 | for i, call in enumerate(sorted_calls[ph.address][th.address]): 30 | call_addr = DynamicCallAddress(thread=th.address, id=i) 31 | yield CallHandle(address=call_addr, inner=call) 32 | -------------------------------------------------------------------------------- /capa/features/extractors/ghidra/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/ghidra/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/ghidra/function.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Iterator 16 | 17 | import ghidra 18 | from ghidra.program.model.block import BasicBlockModel, SimpleBlockIterator 19 | 20 | import capa.features.extractors.ghidra.helpers 21 | from capa.features.common import Feature, Characteristic 22 | from capa.features.address import Address, AbsoluteVirtualAddress 23 | from capa.features.extractors import loops 24 | from capa.features.extractors.base_extractor import FunctionHandle 25 | 26 | 27 | def extract_function_calls_to(fh: FunctionHandle): 28 | """extract callers to a function""" 29 | f: ghidra.program.database.function.FunctionDB = fh.inner 30 | for ref in f.getSymbol().getReferences(): 31 | if ref.getReferenceType().isCall(): 32 | yield Characteristic("calls to"), AbsoluteVirtualAddress(ref.getFromAddress().getOffset()) 33 | 34 | 35 | def extract_function_loop(fh: FunctionHandle): 36 | f: ghidra.program.database.function.FunctionDB = fh.inner 37 | 38 | edges = [] 39 | for block in SimpleBlockIterator(BasicBlockModel(currentProgram()), f.getBody(), monitor()): # type: ignore [name-defined] # noqa: F821 40 | dests = block.getDestinations(monitor()) # type: ignore [name-defined] # noqa: F821 41 | s_addrs = block.getStartAddresses() 42 | 43 | while dests.hasNext(): # For loop throws Python TypeError 44 | for addr in s_addrs: 45 | edges.append((addr.getOffset(), dests.next().getDestinationAddress().getOffset())) 46 | 47 | if loops.has_loop(edges): 48 | yield Characteristic("loop"), AbsoluteVirtualAddress(f.getEntryPoint().getOffset()) 49 | 50 | 51 | def extract_recursive_call(fh: FunctionHandle): 52 | f: ghidra.program.database.function.FunctionDB = fh.inner 53 | 54 | for func in f.getCalledFunctions(monitor()): # type: ignore [name-defined] # noqa: F821 55 | if func.getEntryPoint().getOffset() == f.getEntryPoint().getOffset(): 56 | yield Characteristic("recursive call"), AbsoluteVirtualAddress(f.getEntryPoint().getOffset()) 57 | 58 | 59 | def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: 60 | for func_handler in FUNCTION_HANDLERS: 61 | for feature, addr in func_handler(fh): 62 | yield feature, addr 63 | 64 | 65 | FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call) 66 | 67 | 68 | def main(): 69 | """ """ 70 | features = [] 71 | for fhandle in capa.features.extractors.ghidra.helpers.get_function_symbols(): 72 | features.extend(list(extract_features(fhandle))) 73 | 74 | import pprint 75 | 76 | pprint.pprint(features) # noqa: T203 77 | 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /capa/features/extractors/ghidra/global_.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import contextlib 17 | from typing import Iterator 18 | 19 | import capa.ghidra.helpers 20 | import capa.features.extractors.elf 21 | import capa.features.extractors.ghidra.helpers 22 | from capa.features.common import OS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature 23 | from capa.features.address import NO_ADDRESS, Address 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | def extract_os() -> Iterator[tuple[Feature, Address]]: 29 | format_name: str = currentProgram().getExecutableFormat() # type: ignore [name-defined] # noqa: F821 30 | 31 | if "PE" in format_name: 32 | yield OS(OS_WINDOWS), NO_ADDRESS 33 | 34 | elif "ELF" in format_name: 35 | with contextlib.closing(capa.ghidra.helpers.GHIDRAIO()) as f: 36 | os = capa.features.extractors.elf.detect_elf_os(f) 37 | 38 | yield OS(os), NO_ADDRESS 39 | 40 | else: 41 | # we likely end up here: 42 | # 1. handling shellcode, or 43 | # 2. handling a new file format (e.g. macho) 44 | # 45 | # for (1) we can't do much - its shellcode and all bets are off. 46 | # we could maybe accept a further CLI argument to specify the OS, 47 | # but i think this would be rarely used. 48 | # rules that rely on OS conditions will fail to match on shellcode. 49 | # 50 | # for (2), this logic will need to be updated as the format is implemented. 51 | logger.debug("unsupported file format: %s, will not guess OS", format_name) 52 | return 53 | 54 | 55 | def extract_arch() -> Iterator[tuple[Feature, Address]]: 56 | lang_id = currentProgram().getMetadata().get("Language ID") # type: ignore [name-defined] # noqa: F821 57 | 58 | if "x86" in lang_id and "64" in lang_id: 59 | yield Arch(ARCH_AMD64), NO_ADDRESS 60 | 61 | elif "x86" in lang_id and "32" in lang_id: 62 | yield Arch(ARCH_I386), NO_ADDRESS 63 | 64 | elif "x86" not in lang_id: 65 | logger.debug("unsupported architecture: non-32-bit nor non-64-bit intel") 66 | return 67 | 68 | else: 69 | # we likely end up here: 70 | # 1. handling a new architecture (e.g. aarch64) 71 | # 72 | # for (1), this logic will need to be updated as the format is implemented. 73 | logger.debug("unsupported architecture: %s", lang_id) 74 | return 75 | -------------------------------------------------------------------------------- /capa/features/extractors/ida/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/ida/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/ida/function.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Iterator 16 | 17 | import idaapi 18 | import idautils 19 | 20 | import capa.features.extractors.ida.helpers 21 | from capa.features.common import Feature, Characteristic 22 | from capa.features.address import Address, AbsoluteVirtualAddress 23 | from capa.features.extractors import loops 24 | from capa.features.extractors.base_extractor import FunctionHandle 25 | 26 | 27 | def extract_function_calls_to(fh: FunctionHandle): 28 | """extract callers to a function""" 29 | for ea in idautils.CodeRefsTo(fh.inner.start_ea, True): 30 | yield Characteristic("calls to"), AbsoluteVirtualAddress(ea) 31 | 32 | 33 | def extract_function_loop(fh: FunctionHandle): 34 | """extract loop indicators from a function""" 35 | f: idaapi.func_t = fh.inner 36 | edges = [] 37 | 38 | # construct control flow graph 39 | for bb in idaapi.FlowChart(f): 40 | for succ in bb.succs(): 41 | edges.append((bb.start_ea, succ.start_ea)) 42 | 43 | if loops.has_loop(edges): 44 | yield Characteristic("loop"), fh.address 45 | 46 | 47 | def extract_recursive_call(fh: FunctionHandle): 48 | """extract recursive function call""" 49 | if capa.features.extractors.ida.helpers.is_function_recursive(fh.inner): 50 | yield Characteristic("recursive call"), fh.address 51 | 52 | 53 | def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: 54 | for func_handler in FUNCTION_HANDLERS: 55 | for feature, addr in func_handler(fh): 56 | yield feature, addr 57 | 58 | 59 | FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call) 60 | -------------------------------------------------------------------------------- /capa/features/extractors/ida/global_.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import contextlib 17 | from typing import Iterator 18 | 19 | import ida_loader 20 | 21 | import capa.ida.helpers 22 | import capa.features.extractors.elf 23 | from capa.features.common import OS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature 24 | from capa.features.address import NO_ADDRESS, Address 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | def extract_os() -> Iterator[tuple[Feature, Address]]: 30 | format_name: str = ida_loader.get_file_type_name() 31 | 32 | if "PE" in format_name: 33 | yield OS(OS_WINDOWS), NO_ADDRESS 34 | 35 | elif "ELF" in format_name: 36 | with contextlib.closing(capa.ida.helpers.IDAIO()) as f: 37 | os = capa.features.extractors.elf.detect_elf_os(f) 38 | 39 | yield OS(os), NO_ADDRESS 40 | 41 | else: 42 | # we likely end up here: 43 | # 1. handling shellcode, or 44 | # 2. handling a new file format (e.g. macho) 45 | # 46 | # for (1) we can't do much - its shellcode and all bets are off. 47 | # we could maybe accept a further CLI argument to specify the OS, 48 | # but i think this would be rarely used. 49 | # rules that rely on OS conditions will fail to match on shellcode. 50 | # 51 | # for (2), this logic will need to be updated as the format is implemented. 52 | logger.debug("unsupported file format: %s, will not guess OS", format_name) 53 | return 54 | 55 | 56 | def extract_arch() -> Iterator[tuple[Feature, Address]]: 57 | procname = capa.ida.helpers.get_processor_name() 58 | if procname == "metapc" and capa.ida.helpers.is_64bit(): 59 | yield Arch(ARCH_AMD64), NO_ADDRESS 60 | elif procname == "metapc" and capa.ida.helpers.is_32bit(): 61 | yield Arch(ARCH_I386), NO_ADDRESS 62 | elif procname == "metapc": 63 | logger.debug("unsupported architecture: non-32-bit nor non-64-bit intel") 64 | return 65 | else: 66 | # we likely end up here: 67 | # 1. handling a new architecture (e.g. aarch64) 68 | # 69 | # for (1), this logic will need to be updated as the format is implemented. 70 | logger.debug("unsupported architecture: %s", procname) 71 | return 72 | -------------------------------------------------------------------------------- /capa/features/extractors/loops.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import networkx 17 | from networkx.algorithms.components import strongly_connected_components 18 | 19 | 20 | def has_loop(edges, threshold=2): 21 | """check if a list of edges representing a directed graph contains a loop 22 | 23 | args: 24 | edges: list of edge sets representing a directed graph i.e. [(1, 2), (2, 1)] 25 | threshold: min number of nodes contained in loop 26 | 27 | returns: 28 | bool 29 | """ 30 | g = networkx.DiGraph() 31 | g.add_edges_from(edges) 32 | return any(len(comp) >= threshold for comp in strongly_connected_components(g)) 33 | -------------------------------------------------------------------------------- /capa/features/extractors/viv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/features/extractors/viv/__init__.py -------------------------------------------------------------------------------- /capa/features/extractors/viv/global_.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | from typing import Iterator 17 | 18 | from capa.features.common import ARCH_I386, ARCH_AMD64, Arch, Feature 19 | from capa.features.address import NO_ADDRESS, Address 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | def extract_arch(vw) -> Iterator[tuple[Feature, Address]]: 25 | arch = vw.getMeta("Architecture") 26 | if arch == "amd64": 27 | yield Arch(ARCH_AMD64), NO_ADDRESS 28 | 29 | elif arch == "i386": 30 | yield Arch(ARCH_I386), NO_ADDRESS 31 | 32 | else: 33 | # we likely end up here: 34 | # 1. handling a new architecture (e.g. aarch64) 35 | # 36 | # for (1), this logic will need to be updated as the format is implemented. 37 | logger.debug("unsupported architecture: %s", vw.arch.__class__.__name__) 38 | return 39 | -------------------------------------------------------------------------------- /capa/features/extractors/viv/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Optional 16 | 17 | from vivisect import VivWorkspace 18 | from vivisect.const import XR_TO, REF_CODE 19 | 20 | 21 | def get_coderef_from(vw: VivWorkspace, va: int) -> Optional[int]: 22 | """ 23 | return first code `tova` whose origin is the specified va 24 | return None if no code reference is found 25 | """ 26 | xrefs = vw.getXrefsFrom(va, REF_CODE) 27 | if len(xrefs) > 0: 28 | return xrefs[0][XR_TO] 29 | else: 30 | return None 31 | -------------------------------------------------------------------------------- /capa/features/extractors/vmray/call.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | from typing import Iterator 17 | 18 | import capa.features.extractors.helpers 19 | from capa.features.insn import API, Number 20 | from capa.features.common import String, Feature 21 | from capa.features.address import Address 22 | from capa.features.extractors.strings import is_printable_str 23 | from capa.features.extractors.vmray.models import PARAM_TYPE_INT, PARAM_TYPE_STR, Param, FunctionCall, hexint 24 | from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | def get_call_param_features(param: Param, ch: CallHandle) -> Iterator[tuple[Feature, Address]]: 30 | if param.deref is not None: 31 | # pointer types contain a special "deref" member that stores the deref'd value 32 | # so we check for this first and ignore Param.value as this always contains the 33 | # deref'd pointer value 34 | if param.deref.value is not None: 35 | if param.deref.type_ in PARAM_TYPE_INT: 36 | yield Number(hexint(param.deref.value)), ch.address 37 | elif param.deref.type_ in PARAM_TYPE_STR: 38 | if is_printable_str(param.deref.value): 39 | # parsing the data up to here results in double-escaped backslashes, remove those here 40 | yield String(param.deref.value.replace("\\\\", "\\")), ch.address 41 | else: 42 | logger.debug("skipping deref param type %s", param.deref.type_) 43 | elif param.value is not None: 44 | if param.type_ in PARAM_TYPE_INT: 45 | yield Number(hexint(param.value)), ch.address 46 | 47 | 48 | def extract_call_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[tuple[Feature, Address]]: 49 | call: FunctionCall = ch.inner 50 | 51 | if call.params_in: 52 | for param in call.params_in.params: 53 | yield from get_call_param_features(param, ch) 54 | 55 | for name in capa.features.extractors.helpers.generate_symbols("", call.name): 56 | yield API(name), ch.address 57 | 58 | 59 | def extract_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[tuple[Feature, Address]]: 60 | for handler in CALL_HANDLERS: 61 | for feature, addr in handler(ph, th, ch): 62 | yield feature, addr 63 | 64 | 65 | CALL_HANDLERS = (extract_call_features,) 66 | -------------------------------------------------------------------------------- /capa/features/extractors/vmray/global_.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import logging 17 | from typing import Iterator 18 | 19 | from capa.features.common import ( 20 | OS, 21 | OS_ANY, 22 | ARCH_ANY, 23 | OS_LINUX, 24 | ARCH_I386, 25 | FORMAT_PE, 26 | ARCH_AMD64, 27 | FORMAT_ELF, 28 | OS_WINDOWS, 29 | Arch, 30 | Format, 31 | Feature, 32 | ) 33 | from capa.features.address import NO_ADDRESS, Address 34 | from capa.features.extractors.vmray import VMRayAnalysis 35 | 36 | logger = logging.getLogger(__name__) 37 | 38 | 39 | def extract_arch(analysis: VMRayAnalysis) -> Iterator[tuple[Feature, Address]]: 40 | if "x86-32" in analysis.submission_type: 41 | yield Arch(ARCH_I386), NO_ADDRESS 42 | elif "x86-64" in analysis.submission_type: 43 | yield Arch(ARCH_AMD64), NO_ADDRESS 44 | else: 45 | yield Arch(ARCH_ANY), NO_ADDRESS 46 | 47 | logger.debug( 48 | "unrecognized arch for submission (filename: %s, file_type: %s)", 49 | analysis.submission_name, 50 | analysis.submission_type, 51 | ) 52 | 53 | 54 | def extract_format(analysis: VMRayAnalysis) -> Iterator[tuple[Feature, Address]]: 55 | if analysis.submission_static is not None: 56 | if analysis.submission_static.pe: 57 | yield Format(FORMAT_PE), NO_ADDRESS 58 | elif analysis.submission_static.elf: 59 | yield Format(FORMAT_ELF), NO_ADDRESS 60 | else: 61 | # there is no "FORMAT_ANY" to yield here, but few rules rely on the "format" feature 62 | # so this should be fine for now 63 | 64 | logger.debug( 65 | "unrecognized format for submission (filename: %s, file_type: %s)", 66 | analysis.submission_name, 67 | analysis.submission_type, 68 | ) 69 | 70 | 71 | def extract_os(analysis: VMRayAnalysis) -> Iterator[tuple[Feature, Address]]: 72 | if "windows" in analysis.submission_type.lower(): 73 | yield OS(OS_WINDOWS), NO_ADDRESS 74 | elif "linux" in analysis.submission_type.lower(): 75 | yield OS(OS_LINUX), NO_ADDRESS 76 | else: 77 | yield OS(OS_ANY), NO_ADDRESS 78 | 79 | logger.debug( 80 | "unrecognized os for submission (filename: %s, file_type: %s)", 81 | analysis.submission_name, 82 | analysis.submission_type, 83 | ) 84 | 85 | 86 | def extract_features(analysis: VMRayAnalysis) -> Iterator[tuple[Feature, Address]]: 87 | for global_handler in GLOBAL_HANDLER: 88 | for feature, addr in global_handler(analysis): 89 | yield feature, addr 90 | 91 | 92 | GLOBAL_HANDLER = ( 93 | extract_format, 94 | extract_os, 95 | extract_arch, 96 | ) 97 | -------------------------------------------------------------------------------- /capa/features/file.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from capa.features.common import Feature 17 | 18 | 19 | class Export(Feature): 20 | def __init__(self, value: str, description=None): 21 | # value is export name 22 | super().__init__(value, description=description) 23 | 24 | 25 | class Import(Feature): 26 | def __init__(self, value: str, description=None): 27 | # value is import name 28 | super().__init__(value, description=description) 29 | 30 | 31 | class Section(Feature): 32 | def __init__(self, value: str, description=None): 33 | # value is section name 34 | super().__init__(value, description=description) 35 | 36 | 37 | class FunctionName(Feature): 38 | """recognized name for statically linked function""" 39 | 40 | def __init__(self, name: str, description=None): 41 | # value is function name 42 | super().__init__(name, description=description) 43 | # override the name property set by `capa.features.Feature` 44 | # that would be `functionname` (note missing dash) 45 | self.name = "function-name" 46 | -------------------------------------------------------------------------------- /capa/ghidra/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/ghidra/__init__.py -------------------------------------------------------------------------------- /capa/ida/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/ida/__init__.py -------------------------------------------------------------------------------- /capa/ida/plugin/capa_explorer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from capa.ida.plugin import CapaExplorerPlugin 17 | 18 | 19 | def PLUGIN_ENTRY(): 20 | """mandatory entry point for IDAPython plugins 21 | 22 | copy this script to your IDA plugins directory and start the plugin by navigating to Edit > Plugins in IDA Pro 23 | """ 24 | return CapaExplorerPlugin() 25 | -------------------------------------------------------------------------------- /capa/ida/plugin/error.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class UserCancelledError(Exception): 17 | """throw exception when user cancels action""" 18 | 19 | pass 20 | -------------------------------------------------------------------------------- /capa/ida/plugin/extractor.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import ida_kernwin 17 | from PyQt5 import QtCore 18 | 19 | from capa.ida.plugin.error import UserCancelledError 20 | from capa.features.extractors.ida.extractor import IdaFeatureExtractor 21 | from capa.features.extractors.base_extractor import FunctionHandle 22 | 23 | 24 | class CapaExplorerProgressIndicator(QtCore.QObject): 25 | """implement progress signal, used during feature extraction""" 26 | 27 | progress = QtCore.pyqtSignal(str) 28 | 29 | def update(self, text): 30 | """emit progress update 31 | 32 | check if user cancelled action, raise exception for parent function to catch 33 | """ 34 | if ida_kernwin.user_cancelled(): 35 | raise UserCancelledError("user cancelled") 36 | self.progress.emit(f"extracting features from {text}") 37 | 38 | 39 | class CapaExplorerFeatureExtractor(IdaFeatureExtractor): 40 | """subclass the IdaFeatureExtractor 41 | 42 | track progress during feature extraction, also allow user to cancel feature extraction 43 | """ 44 | 45 | def __init__(self): 46 | super().__init__() 47 | self.indicator = CapaExplorerProgressIndicator() 48 | 49 | def extract_function_features(self, fh: FunctionHandle): 50 | self.indicator.update(f"function at {hex(fh.inner.start_ea)}") 51 | return super().extract_function_features(fh) 52 | -------------------------------------------------------------------------------- /capa/ida/plugin/hooks.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import idaapi 17 | 18 | 19 | class CapaExplorerIdaHooks(idaapi.UI_Hooks): 20 | def __init__(self, screen_ea_changed_hook, action_hooks): 21 | """facilitate IDA UI hooks 22 | 23 | @param screen_ea_changed_hook: function hook for IDA screen ea changed 24 | @param action_hooks: dict of IDA action handles 25 | """ 26 | super().__init__() 27 | 28 | self.screen_ea_changed_hook = screen_ea_changed_hook 29 | self.process_action_hooks = action_hooks 30 | self.process_action_handle = None 31 | self.process_action_meta = {} 32 | 33 | def preprocess_action(self, name): 34 | """called prior to action completed 35 | 36 | @param name: name of action defined by idagui.cfg 37 | 38 | @retval must be 0 39 | """ 40 | self.process_action_handle = self.process_action_hooks.get(name) 41 | 42 | if self.process_action_handle: 43 | self.process_action_handle(self.process_action_meta) 44 | 45 | # must return 0 for IDA 46 | return 0 47 | 48 | def postprocess_action(self): 49 | """called after action completed""" 50 | if not self.process_action_handle: 51 | return 52 | 53 | self.process_action_handle(self.process_action_meta, post=True) 54 | self.reset() 55 | 56 | def screen_ea_changed(self, curr_ea, prev_ea): 57 | """called after screen location is changed 58 | 59 | @param curr_ea: current location 60 | @param prev_ea: prev location 61 | """ 62 | self.screen_ea_changed_hook(idaapi.get_current_widget(), curr_ea, prev_ea) 63 | 64 | def reset(self): 65 | """reset internal state""" 66 | self.process_action_handle = None 67 | self.process_action_meta.clear() 68 | -------------------------------------------------------------------------------- /capa/optimizer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | 17 | import capa.engine as ceng 18 | import capa.features.common 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | def get_node_cost(node): 24 | if isinstance(node, (capa.features.common.OS, capa.features.common.Arch, capa.features.common.Format)): 25 | # we assume these are the most restrictive features: 26 | # authors commonly use them at the start of rules to restrict the category of samples to inspect 27 | return 0 28 | 29 | # elif "everything else": 30 | # return 1 31 | # 32 | # this should be all hash-lookup features. 33 | # see below. 34 | 35 | elif isinstance(node, (capa.features.common.Substring, capa.features.common.Regex, capa.features.common.Bytes)): 36 | # substring and regex features require a full scan of each string 37 | # which we anticipate is more expensive then a hash lookup feature (e.g. mnemonic or count). 38 | # 39 | # fun research: compute the average cost of these feature relative to hash feature 40 | # and adjust the factor accordingly. 41 | return 2 42 | 43 | elif isinstance(node, (ceng.Not, ceng.Range)): 44 | # the cost of these nodes are defined by the complexity of their single child. 45 | return 1 + get_node_cost(node.child) 46 | 47 | elif isinstance(node, (ceng.And, ceng.Or, ceng.Some)): 48 | # the cost of these nodes is the full cost of their children 49 | # as this is the worst-case scenario. 50 | return 1 + sum(map(get_node_cost, node.children)) 51 | 52 | else: 53 | # this should be all hash-lookup features. 54 | # we give this a arbitrary weight of 1. 55 | # the only thing more "important" than this is checking OS/Arch/Format. 56 | return 1 57 | 58 | 59 | def optimize_statement(statement): 60 | # this routine operates in-place 61 | 62 | if isinstance(statement, (ceng.And, ceng.Or, ceng.Some)): 63 | # has .children 64 | statement.children = sorted(statement.children, key=get_node_cost) 65 | return 66 | elif isinstance(statement, (ceng.Not, ceng.Range)): 67 | # has .child 68 | optimize_statement(statement.child) 69 | return 70 | else: 71 | # appears to be "simple" 72 | return 73 | 74 | 75 | def optimize_rule(rule): 76 | # this routine operates in-place 77 | optimize_statement(rule.statement) 78 | 79 | 80 | def optimize_rules(rules): 81 | logger.debug("optimizing %d rules", len(rules)) 82 | for rule in rules: 83 | optimize_rule(rule) 84 | return rules 85 | -------------------------------------------------------------------------------- /capa/perf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import collections 16 | 17 | # this structure is unstable and may change before the next major release. 18 | counters: collections.Counter[str] = collections.Counter() 19 | 20 | 21 | def reset(): 22 | global counters 23 | counters = collections.Counter() 24 | -------------------------------------------------------------------------------- /capa/render/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/capa/render/__init__.py -------------------------------------------------------------------------------- /capa/render/json.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import capa.render.result_document as rd 16 | from capa.rules import RuleSet 17 | from capa.engine import MatchResults 18 | 19 | 20 | def render(meta, rules: RuleSet, capabilities: MatchResults) -> str: 21 | return rd.ResultDocument.from_capa(meta, rules, capabilities).model_dump_json(exclude_none=True) 22 | -------------------------------------------------------------------------------- /capa/version.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = "9.1.0" 16 | 17 | 18 | def get_major_version(): 19 | return int(__version__.partition(".")[0]) 20 | -------------------------------------------------------------------------------- /doc/capa_quickstart.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/capa_quickstart.pdf -------------------------------------------------------------------------------- /doc/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | ## Why does capa trigger my Antivirus? Is the tool safe to use? 3 | The purpose of `capa` is to analyse the capabilities of a potentially malicious application or file. To achieve this, it needs to include portions of the data it is designed to detect as a basis for comparison. 4 | The release version of capa comes with embedded rules designed to detect common malware functionality. These rules possess similar features to malware and may trigger alerts. 5 | Additionally, Antivirus and Endpoint Detection and Response (EDR) products may alert on the way capa is packaged using PyInstaller. 6 | 7 | ## How can I ensure that capa is a benign program? 8 | We recommend downloading releases only from this repository's Release page. Alternatively, you can build capa yourself or use other Python installation methods. This project is open-source, ensuring transparency for everyone involved. 9 | For additional peace of mind, you can utilize VirusTotal to analyze unknown files against numerous antivirus products, sandboxes, and other analysis tools. It's worth noting that capa itself operates within VirusTotal. 10 | 11 | ### Understanding VirusTotal output 12 | VirusTotal tests files against a large number of Antivirus engines and sandboxes. There's often little insight into Antivirus detections, but you can further inspect dynamic analysis results produced by sandboxes. 13 | These details can be used to double-check alerts and understand detections. 14 | -------------------------------------------------------------------------------- /doc/img/approve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/approve.png -------------------------------------------------------------------------------- /doc/img/capa_web_explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/capa_web_explorer.png -------------------------------------------------------------------------------- /doc/img/changelog/flirt-ignore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/changelog/flirt-ignore.png -------------------------------------------------------------------------------- /doc/img/changelog/tab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/changelog/tab.gif -------------------------------------------------------------------------------- /doc/img/explorer_condensed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/explorer_condensed.png -------------------------------------------------------------------------------- /doc/img/explorer_expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/explorer_expanded.png -------------------------------------------------------------------------------- /doc/img/ghidra_backend_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/ghidra_backend_logo.png -------------------------------------------------------------------------------- /doc/img/ghidra_headless_analyzer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/ghidra_headless_analyzer.png -------------------------------------------------------------------------------- /doc/img/ghidra_script_mngr_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/ghidra_script_mngr_output.png -------------------------------------------------------------------------------- /doc/img/ghidra_script_mngr_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/ghidra_script_mngr_rules.png -------------------------------------------------------------------------------- /doc/img/ghidra_script_mngr_verbosity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/ghidra_script_mngr_verbosity.png -------------------------------------------------------------------------------- /doc/img/rulegen_expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/doc/img/rulegen_expanded.png -------------------------------------------------------------------------------- /doc/limitations.md: -------------------------------------------------------------------------------- 1 | # Packers 2 | Packed programs have often been obfuscated to hide their logic. Since capa cannot handle obfuscation well, results may be misleading or incomplete. If possible, users should unpack input files before analyzing them with capa. 3 | 4 | If capa detects that a program may be packed using its rules it warns the user. 5 | 6 | 7 | # Installers, run-time programs, etc. 8 | capa cannot handle installers, run-time programs, or other packaged applications like AutoIt well. This means that the results may be misleading or incomplete. 9 | 10 | If capa detects an installer, run-time program, etc. it warns the user. 11 | 12 | 13 | # Wrapper functions and matches in child functions 14 | Currently capa does not handle wrapper functions or other matches in child functions. 15 | 16 | Consider this example call tree where `f1` calls a wrapper function `f2` and the `CreateProcess` API. `f2` writes to a file. 17 | 18 | ``` 19 | f1 20 | f2 (WriteFile wrapper) 21 | CreateFile 22 | WriteFile 23 | CreateProcess 24 | ``` 25 | 26 | Here capa does not match a rule that hits on file creation and execution on function `f1`. 27 | 28 | Software often contains such nested calls because programmers wrap API calls in helper functions or because specific compilers or languages, such as Go, layer calls. 29 | 30 | While a feature to capture nested functionality is desirable it introduces various issues and complications. These include: 31 | 32 | - how to assign matches from child to parent functions? 33 | - a potential significant increase in analysis requirements and rule matching complexity 34 | 35 | Moreover, we require more real-world samples to see how prevalent this really is and how much it would improve capa's results. 36 | 37 | 38 | # Loop scope 39 | Encryption, encoding, or processing functions often contain loops and it could be beneficial to capture functionality within loops. 40 | 41 | However, tracking all basic blocks part of a loop especially with nested loop constructs is not trivial. 42 | 43 | As a compromise, capa provides the `characteristic(loop)` feature to filter on functions that contain a loop. 44 | 45 | We need more practical use cases and test samples to justify the additional workload to implement a full loop scope feature. 46 | 47 | 48 | # ATT&CK, MAEC, MBC, and other capability tagging 49 | capa uses namespaces to group capabilities (see https://github.com/mandiant/capa-rules/tree/master#namespace-organization). 50 | 51 | The `rule.meta` field also supports `att&ck`, `mbc`, and `maec` fields to associate rules with the respective taxonomy (see https://github.com/mandiant/capa-rules/blob/master/doc/format.md#meta-block). 52 | -------------------------------------------------------------------------------- /doc/release.md: -------------------------------------------------------------------------------- 1 | # Release checklist 2 | 3 | - [ ] Ensure all [milestoned issues/PRs](https://github.com/mandiant/capa/milestones) are addressed, or reassign to a new milestone. 4 | - [ ] Add the `don't merge` label to all PRs that are close to be ready to merge (or merge them if they are ready) in [capa](https://github.com/mandiant/capa/pulls) and [capa-rules](https://github.com/mandiant/capa-rules/pulls). 5 | - [ ] Ensure the [CI workflow succeeds in master](https://github.com/mandiant/capa/actions/workflows/tests.yml?query=branch%3Amaster). 6 | - [ ] Ensure that `python scripts/lint.py rules/ --thorough` succeeds (only `missing examples` offenses are allowed in the nursery). You can [manually trigger a thorough lint](https://github.com/mandiant/capa-rules/actions/workflows/tests.yml) in CI via the "Run workflow" option. 7 | - [ ] Review changes 8 | - capa https://github.com/mandiant/capa/compare/\...master 9 | - capa-rules https://github.com/mandiant/capa-rules/compare/\\...master 10 | - [ ] Update [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md) 11 | - Do not forget to add a nice introduction thanking contributors 12 | - Remember that we need a major release if we introduce breaking changes 13 | - Sections: see template below 14 | - Update `Raw diffs` links 15 | - Create placeholder for `master (unreleased)` section 16 | ``` 17 | ## master (unreleased) 18 | 19 | ### New Features 20 | 21 | ### Breaking Changes 22 | 23 | ### New Rules (0) 24 | 25 | - 26 | 27 | ### Bug Fixes 28 | 29 | ### capa Explorer Web 30 | 31 | ### capa Explorer IDA Pro plugin 32 | 33 | ### Development 34 | 35 | ### Raw diffs 36 | - [capa ...master](https://github.com/mandiant/capa/compare/...master) 37 | - [capa-rules ...master](https://github.com/mandiant/capa-rules/compare/...master) 38 | ``` 39 | - [ ] Update [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py) 40 | - [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py). Copy this checklist in the PR description. 41 | - [ ] Update the [homepage](https://github.com/mandiant/capa/blob/master/web/public/index.html) (i.e. What's New section) 42 | - [ ] After PR review, merge the PR and [create the release in GH](https://github.com/mandiant/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md). 43 | - Verify GH actions 44 | - [ ] [upload artifacts](https://github.com/mandiant/capa/releases) 45 | - [ ] [publish to PyPI](https://pypi.org/project/flare-capa) 46 | - [ ] [create tag in capa rules](https://github.com/mandiant/capa-rules/tags) 47 | - [ ] [create release in capa rules](https://github.com/mandiant/capa-rules/releases) 48 | - [ ] [Spread the word](https://twitter.com) 49 | - [ ] Update internal service 50 | -------------------------------------------------------------------------------- /doc/rules.md: -------------------------------------------------------------------------------- 1 | ### rules 2 | 3 | capa uses a collection of rules to identify capabilities within a program. 4 | The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard library rules that are distributed with capa. 5 | 6 | When you download a standalone version of capa, this standard library is embedded within the executable and capa will use these rules by default: 7 | 8 | ```console 9 | $ capa suspicious.exe 10 | ``` 11 | 12 | However, you may want to modify the rules for a variety of reasons: 13 | 14 | - develop new rules to find behaviors, 15 | - tweak existing rules to reduce false positives, 16 | - collect a private selection of rules not shared publicly. 17 | 18 | Or, you may want to use capa as a Python library within another application. 19 | 20 | In these scenarios, you must provide the rule set to capa as a directory on your file system. Do this using the `-r`/`--rules` parameter: 21 | 22 | ```console 23 | $ capa --rules /local/path/to/rules suspicious.exe 24 | ``` 25 | 26 | You can download the standard set of rules as ZIP or TGZ archives from the [capa-rules release page](https://github.com/mandiant/capa-rules/releases). 27 | 28 | Note that you must use match the rules major version with the capa major version, i.e., use `v1` rules with `v1` of capa. 29 | This is so that new versions of capa can update rule syntax, such as by adding new fields and logic. 30 | 31 | Otherwise, using rules with a mismatched version of capa may lead to errors like: 32 | 33 | ``` 34 | $ capa --rules /path/to/mismatched/rules suspicious.exe 35 | ERROR:lint:invalid rule: injection.yml: invalid rule: unexpected statement: instruction 36 | ``` 37 | 38 | You can check the version of capa you're currently using like this: 39 | 40 | ```console 41 | $ capa --version 42 | capa 3.0.3 43 | ``` 44 | -------------------------------------------------------------------------------- /doc/usage.md: -------------------------------------------------------------------------------- 1 | # capa usage 2 | 3 | See `capa -h` for all supported arguments and usage examples. 4 | 5 | ## tips and tricks 6 | 7 | ### only run selected rules 8 | Use the `-t` option to run rules with the given metadata value (see the rule fields `rule.meta.*`). 9 | For example, `capa -t william.ballenthin@mandiant.com` runs rules that reference Willi's email address (probably as the author), or 10 | `capa -t communication` runs rules with the namespace `communication`. 11 | 12 | ### only analyze selected functions 13 | Use the `--restrict-to-functions` option to extract capabilities from only a selected set of functions. This is useful for analyzing 14 | large functions and figuring out their capabilities and their address of occurance; for example: PEB access, RC4 encryption, etc. 15 | 16 | To use this, you can copy the virtual addresses from your favorite disassembler and pass them to capa as follows: 17 | `capa sample.exe --restrict-to-functions 0x4019C0,0x401CD0`. If you add the `-v` option then capa will extract the interesting parts of a function for you. 18 | 19 | ### only analyze selected processes 20 | Use the `--restrict-to-processes` option to extract capabilities from only a selected set of processes. This is useful for filtering the noise 21 | generated from analyzing non-malicious processes that can be reported by some sandboxes, as well as reduce the execution time 22 | by not analyzing such processes in the first place. 23 | 24 | To use this, you can pick the PIDs of the processes you are interested in from the sandbox-generated process tree (or from the sandbox-reported malware PID) 25 | and pass that to capa as follows: `capa report.log --restrict-to-processes 3888,3214,4299`. If you add the `-v` option then capa will tell you 26 | which threads perform what actions (encrypt/decrypt data, initiate a connection, etc.). 27 | 28 | ### IDA Pro plugin: capa explorer 29 | Please check out the [capa explorer documentation](/capa/ida/plugin/README.md). 30 | 31 | ### save time by reusing .viv files 32 | Set the environment variable `CAPA_SAVE_WORKSPACE` to instruct the underlying analysis engine to 33 | cache its intermediate results to the file system. For example, vivisect will create `.viv` files. 34 | Subsequently, capa may run faster when reprocessing the same input file. 35 | This is particularly useful during rule development as you repeatedly test a rule against a known sample. 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Dependencies with specific version constraints 2 | # used during development and building the standalone executables. 3 | # For these environments, use `pip install -r requirements.txt` 4 | # before installing capa from source/pypi. This will ensure 5 | # the following specific versions are used. 6 | # 7 | # Initially generated via: pip freeze | grep -v -- "-e" 8 | # Kept up to date by dependabot. 9 | annotated-types==0.7.0 10 | colorama==0.4.6 11 | cxxfilt==0.3.0 12 | dncil==1.0.2 13 | dnfile==0.15.0 14 | funcy==2.0 15 | humanize==4.12.0 16 | ida-netnode==3.0 17 | ida-settings==2.1.0 18 | intervaltree==3.1.0 19 | markdown-it-py==3.0.0 20 | mdurl==0.1.2 21 | msgpack==1.0.8 22 | networkx==3.4.2 23 | pefile==2024.8.26 24 | pip==25.1.1 25 | protobuf==6.30.1 26 | pyasn1==0.5.1 27 | pyasn1-modules==0.3.0 28 | pycparser==2.22 29 | pydantic==2.11.4 30 | # pydantic pins pydantic-core, 31 | # but dependabot updates these separately (which is broken) and is annoying, 32 | # so we rely on pydantic to pull in the right version of pydantic-core. 33 | # pydantic-core==2.23.4 34 | xmltodict==0.14.2 35 | pyelftools==0.32 36 | pygments==2.19.1 37 | python-flirt==0.9.2 38 | pyyaml==6.0.2 39 | rich==14.0.0 40 | ruamel-yaml==0.18.6 41 | ruamel-yaml-clib==0.2.8 42 | setuptools==80.9.0 43 | six==1.17.0 44 | sortedcontainers==2.4.0 45 | viv-utils==0.8.0 46 | vivisect==1.2.1 47 | msgspec==0.19.0 48 | -------------------------------------------------------------------------------- /scripts/cache-ruleset.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Create a cache of the given rules. 17 | This is only really intended to be used by CI to pre-cache rulesets 18 | that will be distributed within PyInstaller binaries. 19 | 20 | Usage: 21 | 22 | $ python scripts/cache-ruleset.py rules/ /path/to/cache/directory 23 | """ 24 | 25 | import sys 26 | import logging 27 | import argparse 28 | from pathlib import Path 29 | 30 | import capa.main 31 | import capa.rules 32 | import capa.engine 33 | import capa.helpers 34 | import capa.rules.cache 35 | import capa.features.insn 36 | 37 | logger = logging.getLogger("cache-ruleset") 38 | 39 | 40 | def main(argv=None): 41 | if argv is None: 42 | argv = sys.argv[1:] 43 | 44 | parser = argparse.ArgumentParser(description="Cache ruleset.") 45 | capa.main.install_common_args(parser) 46 | parser.add_argument("rules", type=str, help="Path to rules directory") 47 | parser.add_argument("cache", type=str, help="Path to cache directory") 48 | args = parser.parse_args(args=argv) 49 | 50 | # don't use capa.main.handle_common_args 51 | # because it expects a different format for the --rules argument 52 | 53 | if args.quiet: 54 | logging.basicConfig(level=logging.WARNING) 55 | logging.getLogger().setLevel(logging.WARNING) 56 | elif args.debug: 57 | logging.basicConfig(level=logging.DEBUG) 58 | logging.getLogger().setLevel(logging.DEBUG) 59 | else: 60 | logging.basicConfig(level=logging.INFO) 61 | logging.getLogger().setLevel(logging.INFO) 62 | 63 | try: 64 | cache_dir = Path(args.cache) 65 | cache_dir.mkdir(parents=True, exist_ok=True) 66 | rules = capa.rules.get_rules([Path(args.rules)], cache_dir) 67 | logger.info("successfully loaded %s rules", len(rules)) 68 | except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e: 69 | logger.error("%s", str(e)) 70 | return -1 71 | 72 | content = capa.rules.cache.get_ruleset_content(rules) 73 | id = capa.rules.cache.compute_cache_identifier(content) 74 | path = capa.rules.cache.get_cache_path(cache_dir, id) 75 | 76 | assert path.exists() 77 | logger.info("cached to: %s", path) 78 | 79 | 80 | if __name__ == "__main__": 81 | sys.exit(main()) 82 | -------------------------------------------------------------------------------- /scripts/capafmt.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Reformat the given capa rule into a consistent style. 17 | Use the -i flag to update the rule in-place. 18 | 19 | Usage: 20 | 21 | $ python capafmt.py -i foo.yml 22 | """ 23 | 24 | import sys 25 | import logging 26 | import argparse 27 | from pathlib import Path 28 | 29 | import capa.main 30 | import capa.rules 31 | 32 | logger = logging.getLogger("capafmt") 33 | 34 | 35 | def main(argv=None): 36 | if argv is None: 37 | argv = sys.argv[1:] 38 | 39 | parser = argparse.ArgumentParser(description="Capa rule formatter.") 40 | capa.main.install_common_args(parser) 41 | parser.add_argument("path", type=str, help="Path to rule to format") 42 | parser.add_argument( 43 | "-i", 44 | "--in-place", 45 | action="store_true", 46 | dest="in_place", 47 | help="Format the rule in place, otherwise, write formatted rule to STDOUT", 48 | ) 49 | parser.add_argument( 50 | "-c", 51 | "--check", 52 | action="store_true", 53 | help="Don't output (reformatted) rule, only return status. 0 = no changes, 1 = would reformat", 54 | ) 55 | args = parser.parse_args(args=argv) 56 | 57 | try: 58 | capa.main.handle_common_args(args) 59 | except capa.main.ShouldExitError as e: 60 | return e.status_code 61 | 62 | rule = capa.rules.Rule.from_yaml_file(args.path, use_ruamel=True) 63 | reformatted_rule = rule.to_yaml() 64 | 65 | if args.check: 66 | if rule.definition == reformatted_rule: 67 | logger.info("rule is formatted correctly, nice! (%s)", rule.name) 68 | return 0 69 | else: 70 | logger.info("rule requires reformatting (%s)", rule.name) 71 | if "\r\n" in rule.definition: 72 | logger.info("please make sure that the file uses LF (\\n) line endings only") 73 | return 1 74 | 75 | if args.in_place: 76 | Path(args.path).write_bytes(reformatted_rule.encode("utf-8")) 77 | else: 78 | print(reformatted_rule) 79 | 80 | return 0 81 | 82 | 83 | if __name__ == "__main__": 84 | sys.exit(main()) 85 | -------------------------------------------------------------------------------- /scripts/detect-backends.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import sys 17 | import logging 18 | import argparse 19 | import importlib.util 20 | 21 | import rich 22 | import rich.table 23 | 24 | import capa.main 25 | from capa.features.extractors.ida.idalib import find_idalib, load_idalib, is_idalib_installed 26 | from capa.features.extractors.binja.find_binja_api import find_binaryninja, load_binaryninja, is_binaryninja_installed 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | def is_vivisect_installed() -> bool: 32 | try: 33 | return importlib.util.find_spec("vivisect") is not None 34 | except ModuleNotFoundError: 35 | return False 36 | 37 | 38 | def load_vivisect() -> bool: 39 | try: 40 | import vivisect # noqa: F401 unused import 41 | 42 | return True 43 | except ImportError: 44 | return False 45 | 46 | 47 | def main(argv=None): 48 | if argv is None: 49 | argv = sys.argv[1:] 50 | 51 | parser = argparse.ArgumentParser(description="Detect analysis backends.") 52 | capa.main.install_common_args(parser, wanted=set()) 53 | args = parser.parse_args(args=argv) 54 | 55 | try: 56 | capa.main.handle_common_args(args) 57 | except capa.main.ShouldExitError as e: 58 | return e.status_code 59 | 60 | if args.debug: 61 | logging.getLogger("capa").setLevel(logging.DEBUG) 62 | logging.getLogger("viv_utils").setLevel(logging.DEBUG) 63 | else: 64 | logging.getLogger("capa").setLevel(logging.ERROR) 65 | logging.getLogger("viv_utils").setLevel(logging.ERROR) 66 | 67 | table = rich.table.Table() 68 | table.add_column("backend") 69 | table.add_column("already installed?") 70 | table.add_column("found?") 71 | table.add_column("loads?") 72 | 73 | if True: 74 | row = ["vivisect"] 75 | if is_vivisect_installed(): 76 | row.append("True") 77 | row.append("-") 78 | else: 79 | row.append("False") 80 | row.append("False") 81 | 82 | row.append(str(load_vivisect())) 83 | table.add_row(*row) 84 | 85 | if True: 86 | row = ["Binary Ninja"] 87 | if is_binaryninja_installed(): 88 | row.append("True") 89 | row.append("-") 90 | else: 91 | row.append("False") 92 | row.append(str(find_binaryninja() is not None)) 93 | 94 | row.append(str(load_binaryninja())) 95 | table.add_row(*row) 96 | 97 | if True: 98 | row = ["IDA idalib"] 99 | if is_idalib_installed(): 100 | row.append("True") 101 | row.append("-") 102 | else: 103 | row.append("False") 104 | row.append(str(find_idalib() is not None)) 105 | 106 | row.append(str(load_idalib())) 107 | table.add_row(*row) 108 | 109 | rich.print(table) 110 | 111 | 112 | if __name__ == "__main__": 113 | sys.exit(main()) 114 | -------------------------------------------------------------------------------- /scripts/detect-elf-os.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # Copyright 2021 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | detect-elf-os 18 | 19 | Attempt to detect the underlying OS that the given ELF file targets. 20 | """ 21 | import sys 22 | import logging 23 | import argparse 24 | import contextlib 25 | from typing import BinaryIO 26 | 27 | import capa.main 28 | import capa.helpers 29 | import capa.features.extractors.elf 30 | 31 | logger = logging.getLogger("capa.detect-elf-os") 32 | 33 | 34 | def main(argv=None): 35 | if capa.helpers.is_runtime_ida(): 36 | from capa.ida.helpers import IDAIO 37 | 38 | f: BinaryIO = IDAIO() # type: ignore 39 | 40 | else: 41 | if argv is None: 42 | argv = sys.argv[1:] 43 | 44 | parser = argparse.ArgumentParser(description="Detect the underlying OS for the given ELF file") 45 | capa.main.install_common_args(parser, wanted={"input_file"}) 46 | args = parser.parse_args(args=argv) 47 | 48 | try: 49 | capa.main.handle_common_args(args) 50 | capa.main.ensure_input_exists_from_cli(args) 51 | except capa.main.ShouldExitError as e: 52 | return e.status_code 53 | 54 | f = args.input_file.open("rb") 55 | 56 | with contextlib.closing(f): 57 | try: 58 | print(capa.features.extractors.elf.detect_elf_os(f)) 59 | return 0 60 | except capa.features.extractors.elf.CorruptElfFile as e: 61 | logger.error("corrupt ELF file: %s", str(e.args[0])) 62 | return -1 63 | 64 | 65 | if __name__ == "__main__": 66 | if capa.helpers.is_runtime_ida(): 67 | main() 68 | else: 69 | sys.exit(main()) 70 | -------------------------------------------------------------------------------- /scripts/minimize_vmray_results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2024 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | Extract files relevant to capa analysis from VMRay Analysis Archive and create a new ZIP file. 18 | """ 19 | import sys 20 | import logging 21 | import zipfile 22 | import argparse 23 | from pathlib import Path 24 | 25 | from capa.features.extractors.vmray import DEFAULT_ARCHIVE_PASSWORD, VMRayAnalysis 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | def main(argv=None): 31 | if argv is None: 32 | argv = sys.argv[1:] 33 | 34 | parser = argparse.ArgumentParser( 35 | description="Minimize VMRay Analysis Archive to ZIP file only containing relevant files" 36 | ) 37 | parser.add_argument( 38 | "analysis_archive", 39 | type=Path, 40 | help="path to VMRay Analysis Archive downloaded from Dynamic Analysis Report page", 41 | ) 42 | parser.add_argument( 43 | "-p", "--password", type=str, default="infected", help="password used to unzip and zip protected archives" 44 | ) 45 | args = parser.parse_args(args=argv) 46 | 47 | analysis_archive = args.analysis_archive 48 | 49 | vmra = VMRayAnalysis(analysis_archive) 50 | sv2_json = vmra.zipfile.read("logs/summary_v2.json", pwd=DEFAULT_ARCHIVE_PASSWORD) 51 | flog_xml = vmra.zipfile.read("logs/flog.xml", pwd=DEFAULT_ARCHIVE_PASSWORD) 52 | sample_file_buf = vmra.submission_bytes 53 | assert vmra.submission_meta is not None 54 | sample_sha256: str = vmra.submission_meta.hash_values.sha256.lower() 55 | 56 | new_zip_name = f"{analysis_archive.parent / analysis_archive.stem}_min.zip" 57 | with zipfile.ZipFile(new_zip_name, "w") as new_zip: 58 | new_zip.writestr("logs/summary_v2.json", sv2_json) 59 | new_zip.writestr("logs/flog.xml", flog_xml) 60 | new_zip.writestr(f"internal/static_analyses/{sample_sha256}/objects/files/{sample_sha256}", sample_file_buf) 61 | new_zip.setpassword(args.password.encode("ascii")) 62 | 63 | # ensure capa loads the minimized archive 64 | assert isinstance(VMRayAnalysis(Path(new_zip_name)), VMRayAnalysis) 65 | 66 | print(f"Created minimized VMRay archive '{new_zip_name}' with password '{args.password}'.") 67 | 68 | 69 | if __name__ == "__main__": 70 | sys.exit(main()) 71 | -------------------------------------------------------------------------------- /scripts/profile-memory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import gc 16 | import linecache 17 | import tracemalloc 18 | 19 | tracemalloc.start() 20 | 21 | 22 | def display_top(snapshot, key_type="lineno", limit=10): 23 | # via: https://docs.python.org/3/library/tracemalloc.html#pretty-top 24 | snapshot = snapshot.filter_traces( 25 | ( 26 | tracemalloc.Filter(False, ""), 27 | tracemalloc.Filter(False, ""), 28 | tracemalloc.Filter(False, ""), 29 | ) 30 | ) 31 | top_stats = snapshot.statistics(key_type) 32 | 33 | print(f"Top {limit} lines") 34 | for index, stat in enumerate(top_stats[:limit], 1): 35 | frame = stat.traceback[0] 36 | print(f"#{index}: {frame.filename}:{frame.lineno}: {(stat.size/1024):.1f} KiB") 37 | line = linecache.getline(frame.filename, frame.lineno).strip() 38 | if line: 39 | print(f" {line}") 40 | 41 | other = top_stats[limit:] 42 | if other: 43 | size = sum(stat.size for stat in other) 44 | print(f"{len(other)} other: {(size/1024):.1f} KiB") 45 | total = sum(stat.size for stat in top_stats) 46 | print(f"Total allocated size: {(total/1024):.1f} KiB") 47 | 48 | 49 | def main(): 50 | # import within main to keep isort happy 51 | # while also invoking tracemalloc.start() immediately upon start. 52 | import io 53 | import os 54 | import time 55 | import contextlib 56 | 57 | import psutil 58 | 59 | import capa.main 60 | 61 | count = int(os.environ.get("CAPA_PROFILE_COUNT", 1)) 62 | print(f"total iterations planned: {count} (set via env var CAPA_PROFILE_COUNT).") 63 | print() 64 | 65 | for i in range(count): 66 | print(f"iteration {i+1}/{count}...") 67 | with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): 68 | t0 = time.time() 69 | capa.main.main() 70 | t1 = time.time() 71 | 72 | gc.collect() 73 | 74 | process = psutil.Process(os.getpid()) 75 | print(f" duration: {(t1-t0):.2f}") 76 | print(f" rss: {(process.memory_info().rss / 1024 / 1024):.1f} MiB") 77 | print(f" vms: {(process.memory_info().vms / 1024 / 1024):.1f} MiB") 78 | 79 | print("done.") 80 | gc.collect() 81 | 82 | snapshot0 = tracemalloc.take_snapshot() 83 | display_top(snapshot0) 84 | 85 | 86 | main() 87 | -------------------------------------------------------------------------------- /scripts/proto-from-results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2023 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | proto-from-results-json.py 18 | 19 | Convert a JSON result document into the protobuf format. 20 | 21 | Example: 22 | 23 | $ capa --json foo.exe > foo.json 24 | $ python proto-from-results.py foo.json | hexyl | head 25 | ┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐ 26 | │00000000│ 0a d4 05 0a 1a 32 30 32 ┊ 33 2d 30 32 2d 31 30 20 │_.•_•202┊3-02-10 │ 27 | │00000010│ 31 31 3a 34 39 3a 35 32 ┊ 2e 36 39 33 34 30 30 12 │11:49:52┊.693400•│ 28 | │00000020│ 05 35 2e 30 2e 30 1a 34 ┊ 74 65 73 74 73 2f 64 61 │•5.0.0•4┊tests/da│ 29 | │00000030│ 74 61 2f 50 72 61 63 74 ┊ 69 63 61 6c 20 4d 61 6c │ta/Pract┊ical Mal│ 30 | │00000040│ 77 61 72 65 20 41 6e 61 ┊ 6c 79 73 69 73 20 4c 61 │ware Ana┊lysis La│ 31 | │00000050│ 62 20 30 31 2d 30 31 2e ┊ 64 6c 6c 5f 1a 02 2d 6a │b 01-01.┊dll_••-j│ 32 | │00000060│ 22 c4 01 0a 20 32 39 30 ┊ 39 33 34 63 36 31 64 65 │".•_ 290┊934c61de│ 33 | │00000070│ 39 31 37 36 61 64 36 38 ┊ 32 66 66 64 64 36 35 66 │9176ad68┊2ffdd65f│ 34 | │00000080│ 30 61 36 36 39 12 28 61 ┊ 34 62 33 35 64 65 37 31 │0a669•(a┊4b35de71│ 35 | 36 | """ 37 | import sys 38 | import logging 39 | import argparse 40 | from pathlib import Path 41 | 42 | import capa.main 43 | import capa.render.proto 44 | import capa.render.result_document 45 | 46 | logger = logging.getLogger("capa.proto-from-results-json") 47 | 48 | 49 | def main(argv=None): 50 | if argv is None: 51 | argv = sys.argv[1:] 52 | 53 | parser = argparse.ArgumentParser(description="Convert a capa JSON result document into the protobuf format") 54 | capa.main.install_common_args(parser) 55 | parser.add_argument("json", type=str, help="path to JSON result document file, produced by `capa --json`") 56 | args = parser.parse_args(args=argv) 57 | 58 | try: 59 | capa.main.handle_common_args(args) 60 | except capa.main.ShouldExitError as e: 61 | return e.status_code 62 | 63 | rd = capa.render.result_document.ResultDocument.from_file(Path(args.json)) 64 | pb = capa.render.proto.doc_to_pb2(rd) 65 | 66 | sys.stdout.buffer.write(pb.SerializeToString(deterministic=True)) 67 | sys.stdout.flush() 68 | 69 | 70 | if __name__ == "__main__": 71 | sys.exit(main()) 72 | -------------------------------------------------------------------------------- /scripts/proto-to-results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2023 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | proto-to-results-json.py 18 | 19 | Convert a protobuf result document into the JSON format. 20 | 21 | Example: 22 | 23 | $ capa --json foo.exe > foo.json 24 | $ python proto-from-results.py foo.json > foo.pb 25 | $ python proto-to-results.py foo.pb | jq . | head 26 | ────┼──────────────────────────────────────────────────── 27 | 1 │ { 28 | 2 │ "meta": { 29 | 3 │ "analysis": { 30 | 4 │ "arch": "i386", 31 | 5 │ "base_address": { 32 | 6 │ "type": "absolute", 33 | 7 │ "value": 268435456 34 | 8 │ }, 35 | 9 │ "extractor": "VivisectFeatureExtractor", 36 | 10 │ "feature_counts": { 37 | ────┴──────────────────────────────────────────────────── 38 | 39 | """ 40 | import sys 41 | import logging 42 | import argparse 43 | from pathlib import Path 44 | 45 | import capa.main 46 | import capa.render.json 47 | import capa.render.proto 48 | import capa.render.proto.capa_pb2 49 | import capa.render.result_document 50 | 51 | logger = logging.getLogger("capa.proto-to-results-json") 52 | 53 | 54 | def main(argv=None): 55 | if argv is None: 56 | argv = sys.argv[1:] 57 | 58 | parser = argparse.ArgumentParser(description="Convert a capa protobuf result document into the JSON format") 59 | capa.main.install_common_args(parser) 60 | parser.add_argument( 61 | "pb", type=str, help="path to protobuf result document file, produced by `proto-from-results.py`" 62 | ) 63 | args = parser.parse_args(args=argv) 64 | 65 | try: 66 | capa.main.handle_common_args(args) 67 | except capa.main.ShouldExitError as e: 68 | return e.status_code 69 | 70 | pb = Path(args.pb).read_bytes() 71 | 72 | rdpb = capa.render.proto.capa_pb2.ResultDocument() 73 | rdpb.ParseFromString(pb) 74 | 75 | rd = capa.render.proto.doc_from_pb2(rdpb) 76 | print(rd.model_dump_json(exclude_none=True, indent=2)) 77 | 78 | 79 | if __name__ == "__main__": 80 | sys.exit(main()) 81 | -------------------------------------------------------------------------------- /sigs/1_flare_msvc_rtf_32_64.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/sigs/1_flare_msvc_rtf_32_64.sig -------------------------------------------------------------------------------- /sigs/2_flare_msvc_atlmfc_32_64.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/sigs/2_flare_msvc_atlmfc_32_64.sig -------------------------------------------------------------------------------- /sigs/3_flare_common_libs.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/sigs/3_flare_common_libs.sig -------------------------------------------------------------------------------- /sigs/README.md: -------------------------------------------------------------------------------- 1 | # capa/sigs 2 | 3 | This directory contains FLIRT signatures that capa uses to identify library functions. 4 | Typically, capa will ignore library functions, which reduces false positives and improves runtime. 5 | 6 | These FLIRT signatures were generated by Mandiant using the Hex-Rays FLAIR tools such as `pcf` and `sigmake`. 7 | Mandiant generated the signatures from source data that they collected; these signatures are not derived from the FLIRT signatures distributed with IDA Pro. 8 | 9 | The signatures in this directory have the same license as capa: Apache 2.0. 10 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | # import all the symbols from our fixtures 17 | # and make available to test cases, implicitly. 18 | # this is thanks to pytest magic. 19 | # 20 | # see the following for a discussion: 21 | # https://www.revsys.com/tidbits/pytest-fixtures-are-magic/ 22 | # https://lobste.rs/s/j8xgym/pytest_fixtures_are_magic 23 | from fixtures import * # noqa: F403 [unable to detect undefined names] 24 | from fixtures import _692f_dotnetfile_extractor # noqa: F401 [imported but unused] 25 | from fixtures import _1c444_dotnetfile_extractor # noqa: F401 [imported but unused] 26 | from fixtures import _039a6_dotnetfile_extractor # noqa: F401 [imported but unused] 27 | from fixtures import _0953c_dotnetfile_extractor # noqa: F401 [imported but unused] 28 | -------------------------------------------------------------------------------- /tests/test_binja_features.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | from pathlib import Path 17 | 18 | import pytest 19 | import fixtures 20 | 21 | import capa.main 22 | import capa.features.file 23 | import capa.features.common 24 | 25 | logger = logging.getLogger(__file__) 26 | 27 | 28 | # We need to skip the binja test if we cannot import binaryninja, e.g., in GitHub CI. 29 | binja_present: bool = False 30 | try: 31 | import binaryninja 32 | 33 | try: 34 | binaryninja.load(source=b"\x90") 35 | except RuntimeError: 36 | logger.warning("Binary Ninja license is not valid, provide via $BN_LICENSE or license.dat") 37 | else: 38 | binja_present = True 39 | except ImportError: 40 | pass 41 | 42 | 43 | @pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed") 44 | @fixtures.parametrize( 45 | "sample,scope,feature,expected", 46 | fixtures.FEATURE_PRESENCE_TESTS + fixtures.FEATURE_SYMTAB_FUNC_TESTS + fixtures.FEATURE_BINJA_DATABASE_TESTS, 47 | indirect=["sample", "scope"], 48 | ) 49 | def test_binja_features(sample, scope, feature, expected): 50 | fixtures.do_test_feature_presence(fixtures.get_binja_extractor, sample, scope, feature, expected) 51 | 52 | 53 | @pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed") 54 | @fixtures.parametrize( 55 | "sample,scope,feature,expected", 56 | fixtures.FEATURE_COUNT_TESTS, 57 | indirect=["sample", "scope"], 58 | ) 59 | def test_binja_feature_counts(sample, scope, feature, expected): 60 | fixtures.do_test_feature_count(fixtures.get_binja_extractor, sample, scope, feature, expected) 61 | 62 | 63 | @pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed") 64 | def test_standalone_binja_backend(): 65 | CD = Path(__file__).resolve().parent 66 | test_path = CD / ".." / "tests" / "data" / "Practical Malware Analysis Lab 01-01.exe_" 67 | assert capa.main.main([str(test_path), "-b", capa.main.BACKEND_BINJA]) == 0 68 | 69 | 70 | @pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed") 71 | def test_binja_version(): 72 | version = binaryninja.core_version_info() 73 | assert version.major == 5 and version.minor == 0 74 | -------------------------------------------------------------------------------- /tests/test_dnfile_features.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import fixtures 16 | 17 | 18 | @fixtures.parametrize( 19 | "sample,scope,feature,expected", 20 | fixtures.FEATURE_PRESENCE_TESTS_DOTNET, 21 | indirect=["sample", "scope"], 22 | ) 23 | def test_dnfile_features(sample, scope, feature, expected): 24 | fixtures.do_test_feature_presence(fixtures.get_dnfile_extractor, sample, scope, feature, expected) 25 | 26 | 27 | @fixtures.parametrize( 28 | "sample,scope,feature,expected", 29 | fixtures.FEATURE_COUNT_TESTS_DOTNET, 30 | indirect=["sample", "scope"], 31 | ) 32 | def test_dnfile_feature_counts(sample, scope, feature, expected): 33 | fixtures.do_test_feature_count(fixtures.get_dnfile_extractor, sample, scope, feature, expected) 34 | -------------------------------------------------------------------------------- /tests/test_dotnetfile_features.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | import fixtures 17 | 18 | import capa.features.file 19 | 20 | 21 | @fixtures.parametrize( 22 | "sample,scope,feature,expected", 23 | fixtures.FEATURE_PRESENCE_TESTS_DOTNET, 24 | indirect=["sample", "scope"], 25 | ) 26 | def test_dotnetfile_features(sample, scope, feature, expected): 27 | if scope.__name__ != "file": 28 | pytest.xfail("dotnetfile only extracts file scope features") 29 | 30 | if isinstance(feature, capa.features.file.FunctionName): 31 | pytest.xfail("dotnetfile doesn't extract function names") 32 | 33 | fixtures.do_test_feature_presence(fixtures.get_dotnetfile_extractor, sample, scope, feature, expected) 34 | 35 | 36 | @fixtures.parametrize( 37 | "extractor,function,expected", 38 | [ 39 | ("b9f5b_dotnetfile_extractor", "is_dotnet_file", True), 40 | ("b9f5b_dotnetfile_extractor", "is_mixed_mode", False), 41 | ("mixed_mode_64_dotnetfile_extractor", "is_mixed_mode", True), 42 | ("b9f5b_dotnetfile_extractor", "get_entry_point", 0x6000007), 43 | ("b9f5b_dotnetfile_extractor", "get_runtime_version", (2, 5)), 44 | ("b9f5b_dotnetfile_extractor", "get_meta_version_string", "v2.0.50727"), 45 | ], 46 | ) 47 | def test_dotnetfile_extractor(request, extractor, function, expected): 48 | extractor_function = getattr(request.getfixturevalue(extractor), function) 49 | assert extractor_function() == expected 50 | -------------------------------------------------------------------------------- /tests/test_drakvuf_models.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | 17 | from capa.features.extractors.drakvuf.models import SystemCall 18 | 19 | 20 | def test_syscall_argument_construction(): 21 | call_dictionary = json.loads( 22 | r""" 23 | { 24 | "Plugin": "syscall", 25 | "TimeStamp": "1716999134.581449", 26 | "PID": 3888, 27 | "PPID": 2852, 28 | "TID": 368, 29 | "UserName": "SessionID", 30 | "UserId": 2, 31 | "ProcessName": "\\Device\\HarddiskVolume2\\Windows\\explorer.exe", 32 | "Method": "NtRemoveIoCompletionEx", 33 | "EventUID": "0x1f", 34 | "Module": "nt", 35 | "vCPU": 0, 36 | "CR3": "0x119b1002", 37 | "Syscall": 369, 38 | "NArgs": 6, 39 | "IoCompletionHandle": "0xffffffff80001ac0", 40 | "IoCompletionInformation": "0xfffff506a0284898", 41 | "Count": "0x1", 42 | "NumEntriesRemoved": "0xfffff506a02846bc", 43 | "Timeout": "0xfffff506a02846d8", 44 | "Alertable": "0x0" 45 | } 46 | """ 47 | ) 48 | call = SystemCall(**call_dictionary) 49 | assert len(call.arguments) == call.nargs 50 | assert call.arguments["IoCompletionHandle"] == "0xffffffff80001ac0" 51 | assert call.arguments["IoCompletionInformation"] == "0xfffff506a0284898" 52 | assert call.arguments["Count"] == "0x1" 53 | assert call.arguments["NumEntriesRemoved"] == "0xfffff506a02846bc" 54 | assert call.arguments["Timeout"] == "0xfffff506a02846d8" 55 | assert call.arguments["Alertable"] == "0x0" 56 | -------------------------------------------------------------------------------- /tests/test_function_id.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import fixtures 16 | 17 | import capa.features.insn 18 | 19 | 20 | def test_function_id_simple_match(pma16_01_extractor): 21 | assert pma16_01_extractor.is_library_function(0x407490) is True 22 | assert pma16_01_extractor.get_function_name(0x407490) == "__aulldiv" 23 | 24 | 25 | def test_function_id_gz_pat(pma16_01_extractor): 26 | # aullrem is stored in `test_aullrem.pat.gz` 27 | assert pma16_01_extractor.is_library_function(0x407500) is True 28 | assert pma16_01_extractor.get_function_name(0x407500) == "__aullrem" 29 | 30 | 31 | def test_function_id_complex_match(pma16_01_extractor): 32 | # 0x405714 is __spawnlp which requires recursive match of __spawnvp at 0x407FAB 33 | # (and __spawnvpe at 0x409DE8) 34 | assert pma16_01_extractor.is_library_function(0x405714) is True 35 | assert pma16_01_extractor.get_function_name(0x405714) == "__spawnlp" 36 | 37 | 38 | def test_function_id_api_feature(pma16_01_extractor): 39 | f = fixtures.get_function(pma16_01_extractor, 0x404548) 40 | features = fixtures.extract_function_features(pma16_01_extractor, f) 41 | assert capa.features.insn.API("__aulldiv") in features 42 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import codecs 17 | 18 | import capa.helpers 19 | from capa.features.extractors import helpers 20 | 21 | 22 | def test_all_zeros(): 23 | a = b"\x00\x00\x00\x00" 24 | b = codecs.decode(b"00000000", "hex") 25 | c = b"\x01\x00\x00\x00" 26 | d = codecs.decode(b"01000000", "hex") 27 | assert helpers.all_zeros(a) is True 28 | assert helpers.all_zeros(b) is True 29 | assert helpers.all_zeros(c) is False 30 | assert helpers.all_zeros(d) is False 31 | 32 | 33 | def test_generate_symbols(): 34 | assert list(helpers.generate_symbols("name.dll", "api", include_dll=True)) == list( 35 | helpers.generate_symbols("name", "api", include_dll=True) 36 | ) 37 | assert list(helpers.generate_symbols("name.dll", "api", include_dll=False)) == list( 38 | helpers.generate_symbols("name", "api", include_dll=False) 39 | ) 40 | 41 | # A/W import 42 | symbols = list(helpers.generate_symbols("kernel32", "CreateFileA", include_dll=True)) 43 | assert len(symbols) == 4 44 | assert "kernel32.CreateFileA" in symbols 45 | assert "kernel32.CreateFile" in symbols 46 | assert "CreateFileA" in symbols 47 | assert "CreateFile" in symbols 48 | 49 | # import 50 | symbols = list(helpers.generate_symbols("kernel32", "WriteFile", include_dll=True)) 51 | assert len(symbols) == 2 52 | assert "kernel32.WriteFile" in symbols 53 | assert "WriteFile" in symbols 54 | 55 | # ordinal import 56 | symbols = list(helpers.generate_symbols("ws2_32", "#1", include_dll=True)) 57 | assert len(symbols) == 1 58 | assert "ws2_32.#1" in symbols 59 | 60 | # A/W api 61 | symbols = list(helpers.generate_symbols("kernel32", "CreateFileA", include_dll=False)) 62 | assert len(symbols) == 2 63 | assert "CreateFileA" in symbols 64 | assert "CreateFile" in symbols 65 | 66 | # api 67 | symbols = list(helpers.generate_symbols("kernel32", "WriteFile", include_dll=False)) 68 | assert len(symbols) == 1 69 | assert "WriteFile" in symbols 70 | 71 | # ordinal api 72 | symbols = list(helpers.generate_symbols("ws2_32", "#1", include_dll=False)) 73 | assert len(symbols) == 1 74 | assert "ws2_32.#1" in symbols 75 | 76 | 77 | def test_is_dev_environment(): 78 | # testing environment should be a dev environment 79 | assert capa.helpers.is_dev_environment() is True 80 | -------------------------------------------------------------------------------- /tests/test_optimizer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import textwrap 17 | 18 | import capa.rules 19 | import capa.engine 20 | import capa.optimizer 21 | import capa.features.common 22 | from capa.engine import Or, And 23 | from capa.features.insn import Mnemonic 24 | from capa.features.common import Arch, Substring 25 | 26 | 27 | def test_optimizer_order(): 28 | rule = textwrap.dedent( 29 | """ 30 | rule: 31 | meta: 32 | name: test rule 33 | scopes: 34 | static: function 35 | dynamic: process 36 | features: 37 | - and: 38 | - substring: "foo" 39 | - arch: amd64 40 | - mnemonic: cmp 41 | - and: 42 | - bytes: 3 43 | - offset: 2 44 | - or: 45 | - number: 1 46 | - offset: 4 47 | """ 48 | ) 49 | r = capa.rules.Rule.from_yaml(rule) 50 | 51 | # before optimization 52 | children = list(r.statement.get_children()) 53 | assert isinstance(children[0], Substring) 54 | assert isinstance(children[1], Arch) 55 | assert isinstance(children[2], Mnemonic) 56 | assert isinstance(children[3], And) 57 | assert isinstance(children[4], Or) 58 | 59 | # after optimization 60 | capa.optimizer.optimize_rules([r]) 61 | children = list(r.statement.get_children()) 62 | 63 | # cost: 0 64 | assert isinstance(children[0], Arch) 65 | # cost: 1 66 | assert isinstance(children[1], Mnemonic) 67 | # cost: 2 68 | assert isinstance(children[2], Substring) 69 | # cost: 3 70 | assert isinstance(children[3], Or) 71 | # cost: 4 72 | assert isinstance(children[4], And) 73 | -------------------------------------------------------------------------------- /tests/test_pefile_features.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | import fixtures 17 | 18 | import capa.features.file 19 | 20 | 21 | @fixtures.parametrize( 22 | "sample,scope,feature,expected", 23 | fixtures.FEATURE_PRESENCE_TESTS, 24 | indirect=["sample", "scope"], 25 | ) 26 | def test_pefile_features(sample, scope, feature, expected): 27 | if scope.__name__ != "file": 28 | pytest.xfail("pefile only extracts file scope features") 29 | 30 | if isinstance(feature, capa.features.file.FunctionName): 31 | pytest.xfail("pefile doesn't extract function names") 32 | 33 | if ".elf" in sample.name: 34 | pytest.xfail("pefile doesn't handle ELF files") 35 | fixtures.do_test_feature_presence(fixtures.get_pefile_extractor, sample, scope, feature, expected) 36 | -------------------------------------------------------------------------------- /tests/test_viv_features.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import fixtures 16 | 17 | 18 | @fixtures.parametrize( 19 | "sample,scope,feature,expected", 20 | fixtures.FEATURE_PRESENCE_TESTS + fixtures.FEATURE_SYMTAB_FUNC_TESTS, 21 | indirect=["sample", "scope"], 22 | ) 23 | def test_viv_features(sample, scope, feature, expected): 24 | fixtures.do_test_feature_presence(fixtures.get_viv_extractor, sample, scope, feature, expected) 25 | 26 | 27 | @fixtures.parametrize( 28 | "sample,scope,feature,expected", 29 | fixtures.FEATURE_COUNT_TESTS, 30 | indirect=["sample", "scope"], 31 | ) 32 | def test_viv_feature_counts(sample, scope, feature, expected): 33 | fixtures.do_test_feature_count(fixtures.get_viv_extractor, sample, scope, feature, expected) 34 | -------------------------------------------------------------------------------- /web/explorer/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* eslint-env node */ 18 | require("@rushstack/eslint-patch/modern-module-resolution"); 19 | 20 | module.exports = { 21 | root: true, 22 | extends: ["plugin:vue/vue3-essential", "eslint:recommended", "@vue/eslint-config-prettier/skip-formatting"], 23 | parserOptions: { 24 | ecmaVersion: "latest" 25 | }, 26 | rules: { 27 | "vue/multi-word-component-names": "off" 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /web/explorer/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | # capa Explorer Web Releases 11 | releases/ 12 | 13 | # Dependencies, build results, and other generated files 14 | node_modules 15 | .DS_Store 16 | dist 17 | dist-ssr 18 | coverage 19 | *.local 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .vscode 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | # TypeScript incremental build info 33 | *.tsbuildinfo 34 | -------------------------------------------------------------------------------- /web/explorer/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": true, 4 | "tabWidth": 4, 5 | "singleQuote": false, 6 | "printWidth": 120, 7 | "trailingComma": "none", 8 | "htmlWhitespaceSensitivity": "ignore" 9 | } 10 | -------------------------------------------------------------------------------- /web/explorer/README.md: -------------------------------------------------------------------------------- 1 | # capa Explorer Web 2 | 3 | capa Explorer Web is a browser-based user interface for exploring program capabilities identified by capa. It provides an intuitive and interactive way to analyze and visualize the results of capa analysis. 4 | 5 | ## Features 6 | 7 | - **Import capa Results**: Easily upload or import capa JSON result files. 8 | - **Interactive Tree View**: Explore and filter rule matches in a hierarchical structure. 9 | - **Function Capabilities**: Group and filter capabilities by function for static analysis. 10 | - **Process Capabilities**: Group capabilities by process for dynamic analysis. 11 | 12 | ## Getting Started 13 | 14 | 1. **Access the application**: Open capa Explorer Web in your web browser. 15 | You can start using capa Explorer Web by accessing [https://mandiant.github.io/capa](https://mandiant.github.io/capa/explorer) or running it locally by downloading the offline release from the top right-hand corner and opening it in your web browser. 16 | 17 | 2. **Import capa results**: 18 | 19 | - Click on "Upload from local" to select a capa analysis document file from your computer (with a version higher than 7.0.0). 20 | - You can generate the analysis document by running `capa.exe -j results.json sample.exe_` 21 | - Or, paste a URL to a capa JSON file and click the arrow button to load it. 22 | - Like for the other import mechanisms, loading of both plain (`.json`) and GZIP compressed JSON (`.json.gz`) files is supported). 23 | - Alternatively, use the "Preview Static" or "Preview Dynamic" for sample data. 24 | 25 | 3. **Explore the results**: 26 | 27 | - Use the tree view to navigate through the identified capabilities. 28 | - Toggle between different views using the checkboxes in the settings panel: 29 | - "Show capabilities by function/process" for grouped analysis. 30 | - "Show distinct library rule matches" to include or exclude library rules. 31 | - "Show columns filters" to show per-column search filters. 32 | 33 | 4. **Interact with the results**: 34 | - Expand/collapse nodes in the table to see more details by clicking rows or clicking arrow icons. 35 | - Use the search and filter options to find specific features, functions or capabilities (rules). 36 | - Right click on rule names (and `match` nodes) to view their source code or additional information. 37 | 38 | ## Feedback and Contributions 39 | 40 | We welcome your feedback and contributions to improve the web-based capa explorer. Please report any issues or suggest enhancements through the `capa` GitHub repository. 41 | 42 | --- 43 | 44 | For developers interested in building or contributing to capa Explorer Web, please refer to our [Development Guide](DEVELOPMENT.md). 45 | -------------------------------------------------------------------------------- /web/explorer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | capa Explorer Web 24 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /web/explorer/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | }, 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /web/explorer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "capa-webui", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "build:bundle": "vite build --mode bundle --outDir=capa-explorer-web", 10 | "preview": "vite preview", 11 | "test": "vitest", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 13 | "format": "prettier --write src/", 14 | "format:check": "prettier --check src/" 15 | }, 16 | "dependencies": { 17 | "@highlightjs/vue-plugin": "^2.1.0", 18 | "@primevue/themes": "^4.0.0-rc.2", 19 | "pako": "^2.1.0", 20 | "plotly.js-dist": "^2.34.0", 21 | "primeflex": "^3.3.1", 22 | "primeicons": "^7.0.0", 23 | "primevue": "^4.0.0-rc.2", 24 | "vue": "^3.4.29", 25 | "vue-router": "^4.3.3" 26 | }, 27 | "devDependencies": { 28 | "@rushstack/eslint-patch": "^1.8.0", 29 | "@vitejs/plugin-vue": "^5.2.3", 30 | "@vue/eslint-config-prettier": "^9.0.0", 31 | "@vue/test-utils": "^2.4.6", 32 | "eslint": "^8.57.0", 33 | "eslint-plugin-vue": "^9.23.0", 34 | "jsdom": "^24.1.0", 35 | "prettier": "^3.2.5", 36 | "vite": "^6.3.4", 37 | "vite-plugin-singlefile": "^2.2.0", 38 | "vitest": "^3.0.9" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web/explorer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/explorer/public/favicon.ico -------------------------------------------------------------------------------- /web/explorer/releases/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## capa-explorer-web-v1.0.0-6a2330c 2 | - Release Date: 2024-11-27 13:03:17 UTC 3 | - SHA256: 3a7cf6927b0e8595f08b685669b215ef779eade622efd5e8d33efefadd849025 4 | 5 | -------------------------------------------------------------------------------- /web/explorer/releases/capa-explorer-web-v1.0.0-6a2330c.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/explorer/releases/capa-explorer-web-v1.0.0-6a2330c.zip -------------------------------------------------------------------------------- /web/explorer/src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /web/explorer/src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/explorer/src/assets/images/icon.png -------------------------------------------------------------------------------- /web/explorer/src/assets/images/logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/explorer/src/assets/images/logo-full.png -------------------------------------------------------------------------------- /web/explorer/src/assets/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | body { 18 | margin: 0 auto; 19 | font-weight: normal; 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | a { 24 | text-decoration: none; 25 | color: inherit; 26 | transition: color 0.15s ease-in-out; 27 | } 28 | 29 | a:hover { 30 | color: var(--primary-color); 31 | } 32 | 33 | .font-monospace { 34 | font-family: monospace; 35 | } 36 | 37 | .cursor-default { 38 | cursor: default; 39 | } 40 | 41 | /* mute and minimize match count text labels, e.g. "(2 matches)" */ 42 | .match-count { 43 | font-size: 0.85em; 44 | color: #6c757d; 45 | margin-left: 0.2rem; 46 | } 47 | 48 | /* remove the border from rows other than rule names */ 49 | .p-treetable-tbody > tr:not(:is([aria-level="1"])) > td { 50 | border: none !important; 51 | } 52 | -------------------------------------------------------------------------------- /web/explorer/src/components/BannerHeader.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 44 | 45 | 62 | -------------------------------------------------------------------------------- /web/explorer/src/components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | 54 | -------------------------------------------------------------------------------- /web/explorer/src/components/misc/LibraryTag.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /web/explorer/src/components/misc/VTIcon.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /web/explorer/src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import "primeicons/primeicons.css"; 18 | import "./assets/main.css"; 19 | 20 | import "highlight.js/styles/default.css"; 21 | import "primeflex/primeflex.css"; 22 | import "primeflex/themes/primeone-light.css"; 23 | 24 | import "highlight.js/lib/common"; 25 | import hljsVuePlugin from "@highlightjs/vue-plugin"; 26 | 27 | import { createApp } from "vue"; 28 | import PrimeVue from "primevue/config"; 29 | import Ripple from "primevue/ripple"; 30 | import Aura from "@primevue/themes/aura"; 31 | import App from "./App.vue"; 32 | import MenuBar from "primevue/menubar"; 33 | import Card from "primevue/card"; 34 | import Panel from "primevue/panel"; 35 | import Column from "primevue/column"; 36 | import Checkbox from "primevue/checkbox"; 37 | import FloatLabel from "primevue/floatlabel"; 38 | import Tooltip from "primevue/tooltip"; 39 | import Divider from "primevue/divider"; 40 | import ContextMenu from "primevue/contextmenu"; 41 | import ToastService from "primevue/toastservice"; 42 | import Toast from "primevue/toast"; 43 | import router from "./router"; 44 | 45 | import { definePreset } from "@primevue/themes"; 46 | 47 | const Noir = definePreset(Aura, { 48 | semantic: { 49 | colorScheme: { 50 | light: { 51 | primary: { 52 | color: "{slate.800}", 53 | inverseColor: "#ffffff", 54 | hoverColor: "{sky.800}", 55 | activeColor: "{sky.800}" 56 | } 57 | } 58 | } 59 | } 60 | }); 61 | 62 | const app = createApp(App); 63 | 64 | app.use(router); 65 | app.use(hljsVuePlugin); 66 | 67 | app.use(PrimeVue, { 68 | theme: { 69 | preset: Noir, 70 | options: { 71 | darkModeSelector: "light" 72 | } 73 | }, 74 | ripple: true 75 | }); 76 | app.use(ToastService); 77 | 78 | app.directive("tooltip", Tooltip); 79 | app.directive("ripple", Ripple); 80 | 81 | app.component("Card", Card); 82 | app.component("Divider", Divider); 83 | app.component("Toast", Toast); 84 | app.component("Panel", Panel); 85 | app.component("MenuBar", MenuBar); 86 | app.component("Checkbox", Checkbox); 87 | app.component("FloatLabel", FloatLabel); 88 | app.component("Column", Column); 89 | app.component("ContextMenu", ContextMenu); 90 | 91 | app.mount("#app"); 92 | -------------------------------------------------------------------------------- /web/explorer/src/router/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { createRouter, createWebHashHistory } from "vue-router"; 18 | import ImportView from "@/views/ImportView.vue"; 19 | import NotFoundView from "@/views/NotFoundView.vue"; 20 | import AnalysisView from "@/views/AnalysisView.vue"; 21 | 22 | import { rdocStore } from "@/store/rdocStore"; 23 | 24 | const router = createRouter({ 25 | history: createWebHashHistory(import.meta.env.BASE_URL), 26 | routes: [ 27 | { 28 | path: "/", 29 | name: "home", 30 | component: ImportView 31 | }, 32 | { 33 | path: "/analysis", 34 | name: "analysis", 35 | component: AnalysisView, 36 | beforeEnter: (to, from, next) => { 37 | // check if rdoc is loaded 38 | if (rdocStore.data.value !== null) { 39 | // rdocStore.data already contains the rdoc json - continue 40 | next(); 41 | } else { 42 | // rdoc is not loaded, check if the rdoc query param is set in the URL 43 | const rdocUrl = to.query.rdoc; 44 | if (rdocUrl) { 45 | // query param is set - try to load the rdoc from the homepage 46 | next({ name: "home", query: { rdoc: rdocUrl } }); 47 | } else { 48 | // no query param is set - go back home 49 | next({ name: "home" }); 50 | } 51 | } 52 | } 53 | }, 54 | // 404 Route - This should be the last route 55 | { 56 | path: "/:pathMatch(.*)*", 57 | name: "NotFound", 58 | component: NotFoundView 59 | } 60 | ] 61 | }); 62 | 63 | export default router; 64 | -------------------------------------------------------------------------------- /web/explorer/src/store/rdocStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ref } from "vue"; 18 | 19 | export const rdocStore = { 20 | data: ref(null), 21 | setData(newData) { 22 | this.data.value = newData; 23 | }, 24 | clearData() { 25 | this.data.value = null; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /web/explorer/src/utils/fileUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import pako from "pako"; 18 | 19 | /** 20 | * Checks if the given file is gzipped 21 | * @param {File} file - The file to check 22 | * @returns {Promise} - True if the file is gzipped, false otherwise 23 | */ 24 | export const isGzipped = async (file) => { 25 | const arrayBuffer = await file.arrayBuffer(); 26 | const uint8Array = new Uint8Array(arrayBuffer); 27 | return uint8Array[0] === 0x1f && uint8Array[1] === 0x8b; 28 | }; 29 | 30 | /** 31 | * Decompresses a gzipped file 32 | * @param {File} file - The gzipped file to decompress 33 | * @returns {Promise} - The decompressed file content as a string 34 | */ 35 | export const decompressGzip = async (file) => { 36 | const arrayBuffer = await file.arrayBuffer(); 37 | const uint8Array = new Uint8Array(arrayBuffer); 38 | const decompressed = pako.inflate(uint8Array, { to: "string" }); 39 | return decompressed; 40 | }; 41 | 42 | /** 43 | * Reads a file as text 44 | * @param {File} file - The file to read 45 | * @returns {Promise} - The file content as a string 46 | */ 47 | export const readFileAsText = (file) => { 48 | return new Promise((resolve, reject) => { 49 | const reader = new FileReader(); 50 | reader.onload = (event) => resolve(event.target.result); 51 | reader.onerror = (error) => reject(error); 52 | reader.readAsText(file); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /web/explorer/src/views/ImportView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | 70 | -------------------------------------------------------------------------------- /web/explorer/src/views/NotFoundView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 36 | -------------------------------------------------------------------------------- /web/explorer/vite.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { defineConfig } from "vite"; 18 | import vue from "@vitejs/plugin-vue"; 19 | import { viteSingleFile } from "vite-plugin-singlefile"; 20 | import { fileURLToPath, URL } from "node:url"; 21 | 22 | export default defineConfig(({ mode }) => { 23 | const isBundle = mode === "bundle"; 24 | 25 | return { 26 | base: "./", 27 | plugins: isBundle ? [vue(), viteSingleFile()] : [vue()], 28 | resolve: { 29 | alias: { 30 | "@": fileURLToPath(new URL("src", import.meta.url)), 31 | "@testfiles": fileURLToPath(new URL("../../tests/data", import.meta.url)) 32 | } 33 | }, 34 | assetsInclude: ["**/*.gz"] 35 | }; 36 | }); 37 | -------------------------------------------------------------------------------- /web/explorer/vitest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { defineConfig } from "vitest/config"; 18 | import vue from "@vitejs/plugin-vue"; 19 | 20 | export default defineConfig({ 21 | plugins: [vue()], 22 | test: { 23 | globals: true, 24 | environment: "jsdom", 25 | exclude: ["node_modules", "dist", ".idea", ".git", ".cache"], 26 | include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"] 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /web/public/.gitignore: -------------------------------------------------------------------------------- 1 | rules/ 2 | -------------------------------------------------------------------------------- /web/public/img/capa-default-pma0101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/public/img/capa-default-pma0101.png -------------------------------------------------------------------------------- /web/public/img/capa-rule-create-socket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/public/img/capa-rule-create-socket.png -------------------------------------------------------------------------------- /web/public/img/capa-vv-pma0101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/public/img/capa-vv-pma0101.png -------------------------------------------------------------------------------- /web/public/img/capa.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/public/img/capa.gif -------------------------------------------------------------------------------- /web/public/img/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/public/img/icon.ico -------------------------------------------------------------------------------- /web/public/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/public/img/icon.png -------------------------------------------------------------------------------- /web/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/public/img/logo.png -------------------------------------------------------------------------------- /web/rules/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | .direnv/ 7 | .env/ 8 | .envrc 9 | file_modification_dates.txt 10 | public/*.html 11 | public/pagefind/ 12 | public/index.html 13 | public/ 14 | -------------------------------------------------------------------------------- /web/rules/README.md: -------------------------------------------------------------------------------- 1 | # capa rules documentation website 2 | 3 | ## requirements 4 | 5 | - [just](https://github.com/casey/just) 6 | - [pagefind](https://pagefind.app/) 7 | - `pip install -r requirements` 8 | 9 | ## building 10 | 11 | ``` 12 | just clean 13 | just build 14 | ```` 15 | 16 | then `just serve` and visit http://127.0.0.1:8000/ or (upload `./public` somewhere). 17 | -------------------------------------------------------------------------------- /web/rules/justfile: -------------------------------------------------------------------------------- 1 | 2 | modified-dates: 3 | python scripts/modified-dates.py ../../rules/ ./file_modification_dates.txt 4 | 5 | 6 | build-rules: 7 | mkdir -p ./public/rules/ 8 | python scripts/build_rules.py ../../rules/ ./file_modification_dates.txt ./public/ 9 | 10 | 11 | build-root: 12 | python scripts/build_root.py ../../rules/ ./file_modification_dates.txt ./public/ 13 | 14 | 15 | index-website: build-rules build-root 16 | pagefind --site "public" 17 | 18 | 19 | build: modified-dates build-rules build-root index-website 20 | 21 | 22 | clean: 23 | rm -f file_modification_dates.txt 24 | rm -f public/index.html 25 | rm -rf public/*.html 26 | rm -rf public/pagefind 27 | 28 | 29 | serve: 30 | python -m http.server --b localhost --directory ./public 31 | -------------------------------------------------------------------------------- /web/rules/public/css/poppins.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* latin-ext */ 18 | @font-face { 19 | font-family: 'Poppins'; 20 | font-style: normal; 21 | font-weight: 400; 22 | font-display: swap; 23 | src: url(https://fonts.gstatic.com/s/poppins/v21/pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2'); 24 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 25 | } 26 | /* latin */ 27 | @font-face { 28 | font-family: 'Poppins'; 29 | font-style: normal; 30 | font-weight: 400; 31 | font-display: swap; 32 | src: url(https://fonts.gstatic.com/s/poppins/v21/pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2'); 33 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 34 | } 35 | /* latin-ext */ 36 | @font-face { 37 | font-family: 'Poppins'; 38 | font-style: normal; 39 | font-weight: 700; 40 | font-display: swap; 41 | src: url(https://fonts.gstatic.com/s/poppins/v21/pxiByp8kv8JHgFVrLCz7Z1JlFc-K.woff2) format('woff2'); 42 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 43 | } 44 | /* latin */ 45 | @font-face { 46 | font-family: 'Poppins'; 47 | font-style: normal; 48 | font-weight: 700; 49 | font-display: swap; 50 | src: url(https://fonts.gstatic.com/s/poppins/v21/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format('woff2'); 51 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 52 | } 53 | -------------------------------------------------------------------------------- /web/rules/public/css/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .pagefind-ui__result-thumb { 18 | display: none; 19 | } 20 | 21 | :root { 22 | /* from the icon */ 23 | --capa-blue: #2593d7; 24 | --capa-blue-darker: #1d74aa; 25 | 26 | --bs-primary: var(--capa-blue); 27 | --bs-primary-rgb: var(--capa-blue); 28 | } 29 | 30 | a:not(.btn) { 31 | color: var(--capa-blue); 32 | text-decoration: none; 33 | } 34 | 35 | a:not(.btn):hover { 36 | text-decoration: underline; 37 | text-decoration-color: var(--capa-blue) !important; 38 | } 39 | -------------------------------------------------------------------------------- /web/rules/public/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/rules/public/img/favicon.ico -------------------------------------------------------------------------------- /web/rules/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/rules/public/img/favicon.png -------------------------------------------------------------------------------- /web/rules/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/capa/eabb2cc8091826b598bcd495f9f631e0f3795a0a/web/rules/public/img/logo.png -------------------------------------------------------------------------------- /web/rules/requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml==6.0.2 2 | pygments==2.18.0 3 | -e ../.. # capa 4 | --------------------------------------------------------------------------------