mirror of
https://github.com/mandiant/capa.git
synced 2025-12-18 02:09:17 -08:00
Compare commits
84 Commits
copilot/fi
...
idalib-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93fa034655 | ||
|
|
2a66ef8fba | ||
|
|
5a0d35e826 | ||
|
|
28d107c0f3 | ||
|
|
0d44fc5414 | ||
|
|
654338dcc0 | ||
|
|
34488b35fc | ||
|
|
dc08843e2d | ||
|
|
40b01f0998 | ||
|
|
b96a3b6b23 | ||
|
|
43e5e60901 | ||
|
|
0f9f72dbd5 | ||
|
|
fd9f584cc4 | ||
|
|
c3b785e217 | ||
|
|
6ae17f7ef4 | ||
|
|
13297ad324 | ||
|
|
9b42b45d21 | ||
|
|
d17264c928 | ||
|
|
f313852e70 | ||
|
|
c0ae1352c6 | ||
|
|
ccb3e6de74 | ||
|
|
26c6ffd62d | ||
|
|
18923601c7 | ||
|
|
1568ce4832 | ||
|
|
ffce77b13d | ||
|
|
074f7c742c | ||
|
|
895b2440c0 | ||
|
|
c901f809a2 | ||
|
|
308b3e5c1c | ||
|
|
7844ebb144 | ||
|
|
e393cff0e1 | ||
|
|
7780b9e8a8 | ||
|
|
8d39765e7b | ||
|
|
dec0bcfe79 | ||
|
|
99ccecba4e | ||
|
|
af27463c37 | ||
|
|
f4f47b4d55 | ||
|
|
adc2401136 | ||
|
|
0ff7855467 | ||
|
|
d5411cadad | ||
|
|
cbd6d2a189 | ||
|
|
29af6dc875 | ||
|
|
66a3aac815 | ||
|
|
7525de7bbd | ||
|
|
cbd1cb2b7d | ||
|
|
503c34b8f9 | ||
|
|
888295b37a | ||
|
|
5f9c908315 | ||
|
|
cb2e2323f9 | ||
|
|
cf463676b2 | ||
|
|
b5e5840a63 | ||
|
|
f252b6bbd0 | ||
|
|
eda53ab3c1 | ||
|
|
5ea63770ba | ||
|
|
6795813fbe | ||
|
|
ca708ca52e | ||
|
|
68cf74d60c | ||
|
|
5a0c47419f | ||
|
|
4dbdd9dcfa | ||
|
|
82cbfd33db | ||
|
|
5906bb3ecf | ||
|
|
08319f598f | ||
|
|
e6df6ad0cd | ||
|
|
add09df061 | ||
|
|
acb34e88d6 | ||
|
|
0099e75704 | ||
|
|
da0803b671 | ||
|
|
789747282d | ||
|
|
3bc2d9915c | ||
|
|
5974440ab7 | ||
|
|
b9d517a70b | ||
|
|
e5b8788620 | ||
|
|
ec411f1552 | ||
|
|
6871adc9dc | ||
|
|
07880c1418 | ||
|
|
5a6c8ca7c1 | ||
|
|
3bd8371d0c | ||
|
|
d0c87ef32c | ||
|
|
bd2731f87f | ||
|
|
4a167d7188 | ||
|
|
c01bc346fc | ||
|
|
826330f511 | ||
|
|
40e5095577 | ||
|
|
c7eede3c53 |
27
.bumpversion.toml
Normal file
27
.bumpversion.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[tool.bumpversion]
|
||||||
|
current_version = "9.3.1"
|
||||||
|
|
||||||
|
[[tool.bumpversion.files]]
|
||||||
|
filename = "capa/version.py"
|
||||||
|
search = '__version__ = "{current_version}"'
|
||||||
|
replace = '__version__ = "{new_version}"'
|
||||||
|
|
||||||
|
[[tool.bumpversion.files]]
|
||||||
|
filename = "capa/ida/plugin/ida-plugin.json"
|
||||||
|
search = '"version": "{current_version}"'
|
||||||
|
replace = '"version": "{new_version}"'
|
||||||
|
|
||||||
|
[[tool.bumpversion.files]]
|
||||||
|
filename = "capa/ida/plugin/ida-plugin.json"
|
||||||
|
search = '"flare-capa=={current_version}"'
|
||||||
|
replace = '"flare-capa=={new_version}"'
|
||||||
|
|
||||||
|
[[tool.bumpversion.files]]
|
||||||
|
filename = "CHANGELOG.md"
|
||||||
|
search = "v{current_version}...master"
|
||||||
|
replace = "v{current_version}...{new_version}"
|
||||||
|
|
||||||
|
[[tool.bumpversion.files]]
|
||||||
|
filename = "CHANGELOG.md"
|
||||||
|
search = "master (unreleased)"
|
||||||
|
replace = "v{new_version}"
|
||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -46,8 +46,8 @@ jobs:
|
|||||||
# artifact_name: capa.exe
|
# artifact_name: capa.exe
|
||||||
# asset_name: windows-arm64
|
# asset_name: windows-arm64
|
||||||
# python_version: '3.12'
|
# python_version: '3.12'
|
||||||
- os: macos-13
|
- os: macos-15-intel
|
||||||
# use older macOS for assumed better portability
|
# macos-15-intel is the lowest native intel build
|
||||||
artifact_name: capa
|
artifact_name: capa
|
||||||
asset_name: macos
|
asset_name: macos
|
||||||
python_version: '3.10'
|
python_version: '3.10'
|
||||||
|
|||||||
67
.github/workflows/tests.yml
vendored
67
.github/workflows/tests.yml
vendored
@@ -42,10 +42,10 @@ jobs:
|
|||||||
- name: Checkout capa
|
- name: Checkout capa
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
# use latest available python to take advantage of best performance
|
# use latest available python to take advantage of best performance
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.13
|
||||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.13"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
@@ -70,10 +70,10 @@ jobs:
|
|||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.13
|
||||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.13"
|
||||||
- name: Install capa
|
- name: Install capa
|
||||||
run: |
|
run: |
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
@@ -88,13 +88,11 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-22.04, windows-2022, macos-13]
|
os: [ubuntu-22.04, ubuntu-22.04-arm, windows-2022, macos-15-intel, macos-14]
|
||||||
# across all operating systems
|
# across all operating systems
|
||||||
python-version: ["3.10", "3.11"]
|
python-version: ["3.10", "3.13"]
|
||||||
include:
|
include:
|
||||||
# on Ubuntu run these as well
|
# on Ubuntu run these as well
|
||||||
- os: ubuntu-22.04
|
|
||||||
python-version: "3.10"
|
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
@@ -131,7 +129,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11"]
|
python-version: ["3.10", "3.13"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout capa with submodules
|
- name: Checkout capa with submodules
|
||||||
# do only run if BN_SERIAL is available, have to do this in every step, see https://github.com/orgs/community/discussions/26726#discussioncomment-3253118
|
# do only run if BN_SERIAL is available, have to do this in every step, see https://github.com/orgs/community/discussions/26726#discussioncomment-3253118
|
||||||
@@ -173,7 +171,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11"]
|
python-version: ["3.10", "3.13"]
|
||||||
java-version: ["17"]
|
java-version: ["17"]
|
||||||
ghidra-version: ["11.0.1"]
|
ghidra-version: ["11.0.1"]
|
||||||
public-version: ["PUBLIC_20240130"] # for ghidra releases
|
public-version: ["PUBLIC_20240130"] # for ghidra releases
|
||||||
@@ -219,3 +217,52 @@ jobs:
|
|||||||
exit_code=$(cat ../output.log | grep exit | awk '{print $NF}')
|
exit_code=$(cat ../output.log | grep exit | awk '{print $NF}')
|
||||||
exit $exit_code
|
exit $exit_code
|
||||||
|
|
||||||
|
idalib-tests:
|
||||||
|
name: IDA ${{ matrix.ida.version }} tests for ${{ matrix.python-version }}
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: [tests]
|
||||||
|
env:
|
||||||
|
IDA_LICENSE_ID: ${{ secrets.IDA_LICENSE_ID }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.13"]
|
||||||
|
ida:
|
||||||
|
- version: 9.0
|
||||||
|
slug: "release/9.0/ida-essential/ida-essential_90_x64linux.run"
|
||||||
|
- version: 9.2
|
||||||
|
slug: "release/9.2/ida-essential/ida-essential_92_x64linux.run"
|
||||||
|
steps:
|
||||||
|
- name: Checkout capa with submodules
|
||||||
|
# do only run if IDA_LICENSE_ID is available, have to do this in every step, see https://github.com/orgs/community/discussions/26726#discussioncomment-3253118
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Setup uv
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Install dependencies
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
run: sudo apt-get install -y libyaml-dev
|
||||||
|
- name: Install capa
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
run: |
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -e .[dev,scripts]
|
||||||
|
pip install idapro
|
||||||
|
- name: Install IDA ${{ matrix.ida.version }}
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
run: |
|
||||||
|
uv run hcli --disable-updates ida install --download-id ${{ matrix.ida.slug }} --license-id ${{ secrets.IDA_LICENSE_ID }} --set-default --yes
|
||||||
|
env:
|
||||||
|
HCLI_API_KEY: ${{ secrets.HCLI_API_KEY }}
|
||||||
|
IDA_LICENSE_ID: ${{ secrets.IDA_LICENSE_ID }}
|
||||||
|
- name: Run tests
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
run: pytest -v tests/test_idalib_features.py # explicitly refer to the idalib tests for performance. other tests run above.
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -122,6 +122,7 @@ scripts/perf/*.zip
|
|||||||
*/.DS_Store
|
*/.DS_Store
|
||||||
Pipfile
|
Pipfile
|
||||||
Pipfile.lock
|
Pipfile.lock
|
||||||
|
uv.lock
|
||||||
/cache/
|
/cache/
|
||||||
.github/binja/binaryninja
|
.github/binja/binaryninja
|
||||||
.github/binja/download_headless.py
|
.github/binja/download_headless.py
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ repos:
|
|||||||
- "--ignore=tests/test_ghidra_features.py"
|
- "--ignore=tests/test_ghidra_features.py"
|
||||||
- "--ignore=tests/test_ida_features.py"
|
- "--ignore=tests/test_ida_features.py"
|
||||||
- "--ignore=tests/test_viv_features.py"
|
- "--ignore=tests/test_viv_features.py"
|
||||||
|
- "--ignore=tests/test_idalib_features.py"
|
||||||
- "--ignore=tests/test_main.py"
|
- "--ignore=tests/test_main.py"
|
||||||
- "--ignore=tests/test_scripts.py"
|
- "--ignore=tests/test_scripts.py"
|
||||||
always_run: true
|
always_run: true
|
||||||
|
|||||||
77
CHANGELOG.md
77
CHANGELOG.md
@@ -3,11 +3,62 @@
|
|||||||
## master (unreleased)
|
## master (unreleased)
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
- ci: add support for arm64 binary releases
|
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
### New Rules (14)
|
### New Rules (4)
|
||||||
|
|
||||||
|
- nursery/run-as-nodejs-native-module mehunhoff@google.com
|
||||||
|
- nursery/inject-shellcode-using-thread-pool-work-insertion-with-tp_io still@teamt5.org
|
||||||
|
- nursery/inject-shellcode-using-thread-pool-work-insertion-with-tp_timer still@teamt5.org
|
||||||
|
- nursery/inject-shellcode-using-thread-pool-work-insertion-with-tp_work still@teamt5.org
|
||||||
|
-
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fixed insecure deserialization vulnerability in YAML loading @0x1622 (#2770)
|
||||||
|
|
||||||
|
### capa Explorer Web
|
||||||
|
|
||||||
|
### capa Explorer IDA Pro plugin
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
- ci: deprecate macos-13 runner and use Python v3.13 for testing @mike-hunhoff #2777
|
||||||
|
|
||||||
|
### Raw diffs
|
||||||
|
- [capa v9.3.1...master](https://github.com/mandiant/capa/compare/v9.3.1...master)
|
||||||
|
- [capa-rules v9.3.1...master](https://github.com/mandiant/capa-rules/compare/v9.3.1...master)
|
||||||
|
|
||||||
|
## v9.3.1
|
||||||
|
|
||||||
|
This patch release fixes a missing import for the capa explorer plugin for IDA Pro.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- add missing ida-netnode dependency to project.toml @mike-hunhoff #2765
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
- ci: bump binja min version @mike-hunhoff #2763
|
||||||
|
|
||||||
|
### Raw diffs
|
||||||
|
- [capa v9.3.0...master](https://github.com/mandiant/capa/compare/v9.3.0...master)
|
||||||
|
- [capa-rules v9.3.0...master](https://github.com/mandiant/capa-rules/compare/v9.3.0...master)
|
||||||
|
|
||||||
|
## v9.3.0
|
||||||
|
|
||||||
|
capa v9.3.0 comes with over 20 new and/or impoved rules.
|
||||||
|
For IDA users the capa explorer plugin is now available via the IDA Pro plugin repository and contains Qt compatibility layer for PyQt5 and PySide6 support.
|
||||||
|
Additionally a Binary Ninja bug has been fixed. Released binaries now include ARM64 binaries (Linux and macOS).
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- ci: add support for arm64 binary releases
|
||||||
|
- tests: run tests against IDA via idalib @williballenthin #2742
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
### New Rules (24)
|
||||||
|
|
||||||
- anti-analysis/anti-vm/vm-detection/detect-mouse-movement-via-activity-checks-on-windows tevajdr@gmail.com
|
- anti-analysis/anti-vm/vm-detection/detect-mouse-movement-via-activity-checks-on-windows tevajdr@gmail.com
|
||||||
- nursery/create-executable-heap moritz.raabe@mandiant.com
|
- nursery/create-executable-heap moritz.raabe@mandiant.com
|
||||||
@@ -22,21 +73,37 @@
|
|||||||
- linking/static/grpc/linked-against-grpc jakubjozwiak@google.com
|
- linking/static/grpc/linked-against-grpc jakubjozwiak@google.com
|
||||||
- linking/static/hp-socket/linked-against-hp-socket jakubjozwiak@google.com
|
- linking/static/hp-socket/linked-against-hp-socket jakubjozwiak@google.com
|
||||||
- load-code/execute-jscript-via-vsaengine-in-dotnet jakubjozwiak@google.com
|
- load-code/execute-jscript-via-vsaengine-in-dotnet jakubjozwiak@google.com
|
||||||
-
|
- linking/static/funchook/linked-against-funchook jakubjozwiak@google.com
|
||||||
|
- linking/static/plthook/linked-against-plthook jakubjozwiak@google.com
|
||||||
|
- host-interaction/network/enumerate-tcp-connections-via-wmi-com-api jakubjozwiak@google.com
|
||||||
|
- host-interaction/network/routing-table/create-routing-table-entry jakubjozwiak@google.com
|
||||||
|
- host-interaction/network/routing-table/get-routing-table michael.hunhoff@mandiant.com
|
||||||
|
- host-interaction/file-system/use-io_uring-io-interface-on-linux jakubjozwiak@google.com
|
||||||
|
- collection/keylog/log-keystrokes-via-direct-input zeze-zeze
|
||||||
|
- nursery/compiled-from-fsharp mehunhoff@google.com
|
||||||
|
- nursery/decrypt-data-using-aes-via-dotnet mehunhoff@google.com
|
||||||
|
- nursery/get-dotnet-assembly-entry-point mehunhoff@google.com
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
- binja: fix a crash during feature extraction when the MLIL is unavailable @xusheng6 #2714
|
||||||
|
|
||||||
### capa Explorer Web
|
### capa Explorer Web
|
||||||
|
|
||||||
### capa Explorer IDA Pro plugin
|
### capa Explorer IDA Pro plugin
|
||||||
|
|
||||||
|
- add `ida-plugin.json` for inclusion in the IDA Pro plugin repository @williballenthin
|
||||||
|
- ida plugin: add Qt compatibility layer for PyQt5 and PySide6 support @williballenthin #2707
|
||||||
|
- delay import to not load Qt* when running under idalib @mr-tz #2752
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
- ci: remove redundant "test_run" action from build workflow @mike-hunhoff #2692
|
- ci: remove redundant "test_run" action from build workflow @mike-hunhoff #2692
|
||||||
|
- dev: add bumpmyversion to bump and sync versions across the project @mr-tz
|
||||||
|
|
||||||
### Raw diffs
|
### Raw diffs
|
||||||
- [capa v9.2.1...master](https://github.com/mandiant/capa/compare/v9.2.1...master)
|
- [capa v9.2.1...9.3.0](https://github.com/mandiant/capa/compare/v9.2.1...9.3.0)
|
||||||
- [capa-rules v9.2.1...master](https://github.com/mandiant/capa-rules/compare/v9.2.1...master)
|
- [capa-rules v9.2.1...9.3.0](https://github.com/mandiant/capa-rules/compare/v9.2.1...9.3.0)
|
||||||
|
|
||||||
## v9.2.1
|
## v9.2.1
|
||||||
|
|
||||||
|
|||||||
@@ -315,3 +315,6 @@ If you use Ghidra, then you can use the [capa + Ghidra integration](/capa/ghidra
|
|||||||
|
|
||||||
## capa testfiles
|
## capa testfiles
|
||||||
The [capa-testfiles repository](https://github.com/mandiant/capa-testfiles) contains the data we use to test capa's code and rules
|
The [capa-testfiles repository](https://github.com/mandiant/capa-testfiles) contains the data we use to test capa's code and rules
|
||||||
|
|
||||||
|
## mailing list
|
||||||
|
Subscribe to the FLARE mailing list for community announcements! Email "subscribe" to [flare-external@google.com](mailto:flare-external@google.com?subject=subscribe).
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ from binaryninja import (
|
|||||||
Function,
|
Function,
|
||||||
BinaryView,
|
BinaryView,
|
||||||
SymbolType,
|
SymbolType,
|
||||||
ILException,
|
|
||||||
RegisterValueType,
|
RegisterValueType,
|
||||||
VariableSourceType,
|
VariableSourceType,
|
||||||
LowLevelILOperation,
|
LowLevelILOperation,
|
||||||
@@ -192,9 +191,8 @@ def extract_stackstring(fh: FunctionHandle):
|
|||||||
if bv is None:
|
if bv is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
mlil = func.mlil
|
||||||
mlil = func.mlil
|
if mlil is None:
|
||||||
except ILException:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for block in mlil.basic_blocks:
|
for block in mlil.basic_blocks:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import idaapi
|
|||||||
import idautils
|
import idautils
|
||||||
|
|
||||||
import capa.features.extractors.ida.helpers
|
import capa.features.extractors.ida.helpers
|
||||||
|
from capa.features.file import FunctionName
|
||||||
from capa.features.common import Feature, Characteristic
|
from capa.features.common import Feature, Characteristic
|
||||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||||
from capa.features.extractors import loops
|
from capa.features.extractors import loops
|
||||||
@@ -50,10 +51,35 @@ def extract_recursive_call(fh: FunctionHandle):
|
|||||||
yield Characteristic("recursive call"), fh.address
|
yield Characteristic("recursive call"), fh.address
|
||||||
|
|
||||||
|
|
||||||
|
def extract_function_name(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
|
||||||
|
ea = fh.inner.start_ea
|
||||||
|
name = idaapi.get_name(ea)
|
||||||
|
yield FunctionName(name), fh.address
|
||||||
|
if name.startswith("_"):
|
||||||
|
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||||
|
# extract features for both the mangled and un-mangled representations.
|
||||||
|
# e.g. `_fwrite` -> `fwrite`
|
||||||
|
# see: https://stackoverflow.com/a/2628384/87207
|
||||||
|
yield FunctionName(name[1:]), fh.address
|
||||||
|
|
||||||
|
|
||||||
|
def extract_function_alternative_names(fh: FunctionHandle):
|
||||||
|
"""Get all alternative names for an address."""
|
||||||
|
|
||||||
|
for aname in capa.features.extractors.ida.helpers.get_function_alternative_names(fh.inner.start_ea):
|
||||||
|
yield FunctionName(aname), fh.address
|
||||||
|
|
||||||
|
|
||||||
def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
|
def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
|
||||||
for func_handler in FUNCTION_HANDLERS:
|
for func_handler in FUNCTION_HANDLERS:
|
||||||
for feature, addr in func_handler(fh):
|
for feature, addr in func_handler(fh):
|
||||||
yield feature, addr
|
yield feature, addr
|
||||||
|
|
||||||
|
|
||||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call)
|
FUNCTION_HANDLERS = (
|
||||||
|
extract_function_calls_to,
|
||||||
|
extract_function_loop,
|
||||||
|
extract_recursive_call,
|
||||||
|
extract_function_name,
|
||||||
|
extract_function_alternative_names,
|
||||||
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import idaapi
|
|||||||
import ida_nalt
|
import ida_nalt
|
||||||
import idautils
|
import idautils
|
||||||
import ida_bytes
|
import ida_bytes
|
||||||
|
import ida_funcs
|
||||||
import ida_segment
|
import ida_segment
|
||||||
|
|
||||||
from capa.features.address import AbsoluteVirtualAddress
|
from capa.features.address import AbsoluteVirtualAddress
|
||||||
@@ -436,3 +437,23 @@ def is_basic_block_return(bb: idaapi.BasicBlock) -> bool:
|
|||||||
def has_sib(oper: idaapi.op_t) -> bool:
|
def has_sib(oper: idaapi.op_t) -> bool:
|
||||||
# via: https://reverseengineering.stackexchange.com/a/14300
|
# via: https://reverseengineering.stackexchange.com/a/14300
|
||||||
return oper.specflag1 == 1
|
return oper.specflag1 == 1
|
||||||
|
|
||||||
|
|
||||||
|
def get_function_alternative_names(fva: int):
|
||||||
|
"""Get all alternative names for an address."""
|
||||||
|
|
||||||
|
# Check indented comment
|
||||||
|
cmt = ida_bytes.get_cmt(fva, False) # False = non-repeatable
|
||||||
|
if cmt:
|
||||||
|
for line in cmt.split("\n"):
|
||||||
|
if line.startswith("Alternative name is '") and line.endswith("'"):
|
||||||
|
name = line[len("Alternative name is '") : -1] # Extract name between quotes
|
||||||
|
yield name
|
||||||
|
|
||||||
|
# Check function comment
|
||||||
|
func_cmt = ida_funcs.get_func_cmt(idaapi.get_func(fva), False)
|
||||||
|
if func_cmt:
|
||||||
|
for line in func_cmt.split("\n"):
|
||||||
|
if line.startswith("Alternative name is '") and line.endswith("'"):
|
||||||
|
name = line[len("Alternative name is '") : -1]
|
||||||
|
yield name
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import idautils
|
|||||||
|
|
||||||
import capa.features.extractors.helpers
|
import capa.features.extractors.helpers
|
||||||
import capa.features.extractors.ida.helpers
|
import capa.features.extractors.ida.helpers
|
||||||
|
from capa.features.file import FunctionName
|
||||||
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
||||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
|
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
|
||||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||||
@@ -129,8 +130,8 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
|
|||||||
# not a function (start)
|
# not a function (start)
|
||||||
return
|
return
|
||||||
|
|
||||||
if target_func.flags & idaapi.FUNC_LIB:
|
name = idaapi.get_name(target_func.start_ea)
|
||||||
name = idaapi.get_name(target_func.start_ea)
|
if target_func.flags & idaapi.FUNC_LIB or not name.startswith("sub_"):
|
||||||
yield API(name), ih.address
|
yield API(name), ih.address
|
||||||
if name.startswith("_"):
|
if name.startswith("_"):
|
||||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||||
@@ -139,6 +140,10 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
|
|||||||
# see: https://stackoverflow.com/a/2628384/87207
|
# see: https://stackoverflow.com/a/2628384/87207
|
||||||
yield API(name[1:]), ih.address
|
yield API(name[1:]), ih.address
|
||||||
|
|
||||||
|
for altname in capa.features.extractors.ida.helpers.get_function_alternative_names(target_func.start_ea):
|
||||||
|
yield FunctionName(altname), ih.address
|
||||||
|
yield API(altname), ih.address
|
||||||
|
|
||||||
|
|
||||||
def extract_insn_number_features(
|
def extract_insn_number_features(
|
||||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import logging
|
|||||||
import idaapi
|
import idaapi
|
||||||
import ida_kernwin
|
import ida_kernwin
|
||||||
|
|
||||||
from capa.ida.plugin.form import CapaExplorerForm
|
|
||||||
from capa.ida.plugin.icon import ICON
|
from capa.ida.plugin.icon import ICON
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -74,6 +73,9 @@ class CapaExplorerPlugin(idaapi.plugin_t):
|
|||||||
arg (int): bitflag. Setting LSB enables automatic analysis upon
|
arg (int): bitflag. Setting LSB enables automatic analysis upon
|
||||||
loading. The other bits are currently undefined. See `form.Options`.
|
loading. The other bits are currently undefined. See `form.Options`.
|
||||||
"""
|
"""
|
||||||
|
# delay import to not trigger load of Qt components when not running in idaq, i.e., in idalib
|
||||||
|
from capa.ida.plugin.form import CapaExplorerForm
|
||||||
|
|
||||||
if not self.form:
|
if not self.form:
|
||||||
self.form = CapaExplorerForm(self.PLUGIN_NAME, arg)
|
self.form = CapaExplorerForm(self.PLUGIN_NAME, arg)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
|
|
||||||
|
|
||||||
import ida_kernwin
|
import ida_kernwin
|
||||||
from PyQt5 import QtCore
|
|
||||||
|
|
||||||
from capa.ida.plugin.error import UserCancelledError
|
from capa.ida.plugin.error import UserCancelledError
|
||||||
|
from capa.ida.plugin.qt_compat import QtCore, Signal
|
||||||
from capa.features.extractors.ida.extractor import IdaFeatureExtractor
|
from capa.features.extractors.ida.extractor import IdaFeatureExtractor
|
||||||
from capa.features.extractors.base_extractor import FunctionHandle
|
from capa.features.extractors.base_extractor import FunctionHandle
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ from capa.features.extractors.base_extractor import FunctionHandle
|
|||||||
class CapaExplorerProgressIndicator(QtCore.QObject):
|
class CapaExplorerProgressIndicator(QtCore.QObject):
|
||||||
"""implement progress signal, used during feature extraction"""
|
"""implement progress signal, used during feature extraction"""
|
||||||
|
|
||||||
progress = QtCore.pyqtSignal(str)
|
progress = Signal(str)
|
||||||
|
|
||||||
def update(self, text):
|
def update(self, text):
|
||||||
"""emit progress update
|
"""emit progress update
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ from pathlib import Path
|
|||||||
import idaapi
|
import idaapi
|
||||||
import ida_kernwin
|
import ida_kernwin
|
||||||
import ida_settings
|
import ida_settings
|
||||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
|
||||||
|
|
||||||
import capa.main
|
import capa.main
|
||||||
import capa.rules
|
import capa.rules
|
||||||
@@ -51,6 +50,7 @@ from capa.ida.plugin.hooks import CapaExplorerIdaHooks
|
|||||||
from capa.ida.plugin.model import CapaExplorerDataModel
|
from capa.ida.plugin.model import CapaExplorerDataModel
|
||||||
from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearchProxyModel
|
from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearchProxyModel
|
||||||
from capa.ida.plugin.extractor import CapaExplorerFeatureExtractor
|
from capa.ida.plugin.extractor import CapaExplorerFeatureExtractor
|
||||||
|
from capa.ida.plugin.qt_compat import QtGui, QtCore, QtWidgets
|
||||||
from capa.features.extractors.base_extractor import FunctionHandle
|
from capa.features.extractors.base_extractor import FunctionHandle
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1358,7 +1358,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
|
|
||||||
@param state: checked state
|
@param state: checked state
|
||||||
"""
|
"""
|
||||||
if state == QtCore.Qt.Checked:
|
if state:
|
||||||
self.limit_results_to_function(idaapi.get_func(idaapi.get_screen_ea()))
|
self.limit_results_to_function(idaapi.get_func(idaapi.get_screen_ea()))
|
||||||
else:
|
else:
|
||||||
self.range_model_proxy.reset_address_range_filter()
|
self.range_model_proxy.reset_address_range_filter()
|
||||||
@@ -1367,7 +1367,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
|
|
||||||
def slot_checkbox_limit_features_by_ea(self, state):
|
def slot_checkbox_limit_features_by_ea(self, state):
|
||||||
""" """
|
""" """
|
||||||
if state == QtCore.Qt.Checked:
|
if state:
|
||||||
self.view_rulegen_features.filter_items_by_ea(idaapi.get_screen_ea())
|
self.view_rulegen_features.filter_items_by_ea(idaapi.get_screen_ea())
|
||||||
else:
|
else:
|
||||||
self.view_rulegen_features.show_all_items()
|
self.view_rulegen_features.show_all_items()
|
||||||
|
|||||||
38
capa/ida/plugin/ida-plugin.json
Normal file
38
capa/ida/plugin/ida-plugin.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"IDAMetadataDescriptorVersion": 1,
|
||||||
|
"plugin": {
|
||||||
|
"name": "capa",
|
||||||
|
"entryPoint": "capa_explorer.py",
|
||||||
|
"version": "9.3.1",
|
||||||
|
"idaVersions": ">=7.4",
|
||||||
|
"description": "Identify capabilities in executable files using FLARE's capa framework",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"categories": [
|
||||||
|
"malware-analysis",
|
||||||
|
"api-scripting-and-automation",
|
||||||
|
"ui-ux-and-visualization"
|
||||||
|
],
|
||||||
|
"pythonDependencies": ["flare-capa==9.3.1"],
|
||||||
|
"urls": {
|
||||||
|
"repository": "https://github.com/mandiant/capa"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{"name": "Willi Ballenthin", "email": "wballenthin@hex-rays.com"},
|
||||||
|
{"name": "Moritz Raabe", "email": "moritzraabe@google.com"},
|
||||||
|
{"name": "Mike Hunhoff", "email": "mike.hunhoff@gmail.com"},
|
||||||
|
{"name": "Yacine Elhamer", "email": "elhamer.yacine@gmail.com"}
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"capability-detection",
|
||||||
|
"malware-analysis",
|
||||||
|
"behavior-analysis",
|
||||||
|
"reverse-engineering",
|
||||||
|
"att&ck",
|
||||||
|
"rule-engine",
|
||||||
|
"feature-extraction",
|
||||||
|
"yara-like-rules",
|
||||||
|
"static-analysis",
|
||||||
|
"dynamic-analysis"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,10 +18,10 @@ from typing import Iterator, Optional
|
|||||||
|
|
||||||
import idc
|
import idc
|
||||||
import idaapi
|
import idaapi
|
||||||
from PyQt5 import QtCore
|
|
||||||
|
|
||||||
import capa.ida.helpers
|
import capa.ida.helpers
|
||||||
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
|
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||||
|
from capa.ida.plugin.qt_compat import QtCore, qt_get_item_flag_tristate
|
||||||
|
|
||||||
|
|
||||||
def info_to_name(display):
|
def info_to_name(display):
|
||||||
@@ -55,7 +55,7 @@ class CapaExplorerDataItem:
|
|||||||
self.flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
self.flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||||
|
|
||||||
if self._can_check:
|
if self._can_check:
|
||||||
self.flags = self.flags | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsTristate
|
self.flags = self.flags | QtCore.Qt.ItemIsUserCheckable | qt_get_item_flag_tristate()
|
||||||
|
|
||||||
if self.pred:
|
if self.pred:
|
||||||
self.pred.appendChild(self)
|
self.pred.appendChild(self)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from collections import deque
|
|||||||
|
|
||||||
import idc
|
import idc
|
||||||
import idaapi
|
import idaapi
|
||||||
from PyQt5 import QtGui, QtCore
|
|
||||||
|
|
||||||
import capa.rules
|
import capa.rules
|
||||||
import capa.ida.helpers
|
import capa.ida.helpers
|
||||||
@@ -42,6 +41,7 @@ from capa.ida.plugin.item import (
|
|||||||
CapaExplorerInstructionViewItem,
|
CapaExplorerInstructionViewItem,
|
||||||
)
|
)
|
||||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||||
|
from capa.ida.plugin.qt_compat import QtGui, QtCore
|
||||||
|
|
||||||
# default highlight color used in IDA window
|
# default highlight color used in IDA window
|
||||||
DEFAULT_HIGHLIGHT = 0xE6C700
|
DEFAULT_HIGHLIGHT = 0xE6C700
|
||||||
@@ -269,7 +269,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
|||||||
visited.add(child_index)
|
visited.add(child_index)
|
||||||
|
|
||||||
for idx in range(self.rowCount(child_index)):
|
for idx in range(self.rowCount(child_index)):
|
||||||
stack.append(child_index.child(idx, 0))
|
stack.append(self.index(idx, 0, child_index))
|
||||||
|
|
||||||
def reset_ida_highlighting(self, item, checked):
|
def reset_ida_highlighting(self, item, checked):
|
||||||
"""reset IDA highlight for item
|
"""reset IDA highlight for item
|
||||||
|
|||||||
@@ -12,10 +12,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from PyQt5 import QtCore
|
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
|
|
||||||
from capa.ida.plugin.model import CapaExplorerDataModel
|
from capa.ida.plugin.model import CapaExplorerDataModel
|
||||||
|
from capa.ida.plugin.qt_compat import Qt, QtCore
|
||||||
|
|
||||||
|
|
||||||
class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
|
class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
|
||||||
|
|||||||
79
capa/ida/plugin/qt_compat.py
Normal file
79
capa/ida/plugin/qt_compat.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Copyright 2020 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Qt compatibility layer for capa IDA Pro plugin.
|
||||||
|
|
||||||
|
Handles PyQt5 (IDA < 9.2) vs PySide6 (IDA >= 9.2) differences.
|
||||||
|
This module provides a unified import interface for Qt modules and handles
|
||||||
|
API changes between Qt5 and Qt6.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# IDA 9.2+ uses PySide6
|
||||||
|
from PySide6 import QtGui, QtCore, QtWidgets
|
||||||
|
from PySide6.QtGui import QAction
|
||||||
|
|
||||||
|
QT_LIBRARY = "PySide6"
|
||||||
|
Signal = QtCore.Signal
|
||||||
|
except ImportError:
|
||||||
|
# Older IDA versions use PyQt5
|
||||||
|
try:
|
||||||
|
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||||
|
from PyQt5.QtWidgets import QAction
|
||||||
|
|
||||||
|
QT_LIBRARY = "PyQt5"
|
||||||
|
Signal = QtCore.pyqtSignal
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("Neither PySide6 nor PyQt5 is available. Cannot initialize capa IDA plugin.")
|
||||||
|
|
||||||
|
Qt = QtCore.Qt
|
||||||
|
|
||||||
|
|
||||||
|
def qt_get_item_flag_tristate():
|
||||||
|
"""
|
||||||
|
Get the tristate item flag compatible with Qt5 and Qt6.
|
||||||
|
|
||||||
|
Qt5 (PyQt5): Uses Qt.ItemIsTristate
|
||||||
|
Qt6 (PySide6): Qt.ItemIsTristate was removed, uses Qt.ItemIsAutoTristate
|
||||||
|
|
||||||
|
ItemIsAutoTristate automatically manages tristate based on child checkboxes,
|
||||||
|
matching the original ItemIsTristate behavior where parent checkboxes reflect
|
||||||
|
the check state of their children.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The appropriate flag value for the Qt version
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: If the tristate flag cannot be found in the Qt library
|
||||||
|
"""
|
||||||
|
if QT_LIBRARY == "PySide6":
|
||||||
|
# Qt6: ItemIsTristate was removed, replaced with ItemIsAutoTristate
|
||||||
|
# Try different possible locations (API varies slightly across PySide6 versions)
|
||||||
|
if hasattr(Qt, "ItemIsAutoTristate"):
|
||||||
|
return Qt.ItemIsAutoTristate
|
||||||
|
elif hasattr(Qt, "ItemFlag") and hasattr(Qt.ItemFlag, "ItemIsAutoTristate"):
|
||||||
|
return Qt.ItemFlag.ItemIsAutoTristate
|
||||||
|
else:
|
||||||
|
raise AttributeError(
|
||||||
|
"Cannot find ItemIsAutoTristate in PySide6. "
|
||||||
|
+ "Your PySide6 version may be incompatible with capa. "
|
||||||
|
+ f"Available Qt attributes: {[attr for attr in dir(Qt) if 'Item' in attr]}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Qt5: Use the original ItemIsTristate flag
|
||||||
|
return Qt.ItemIsTristate
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["qt_get_item_flag_tristate", "Signal", "QAction", "QtGui", "QtCore", "QtWidgets"]
|
||||||
@@ -18,7 +18,6 @@ from collections import Counter
|
|||||||
|
|
||||||
import idc
|
import idc
|
||||||
import idaapi
|
import idaapi
|
||||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
|
||||||
|
|
||||||
import capa.rules
|
import capa.rules
|
||||||
import capa.engine
|
import capa.engine
|
||||||
@@ -28,6 +27,7 @@ import capa.features.basicblock
|
|||||||
from capa.ida.plugin.item import CapaExplorerFunctionItem
|
from capa.ida.plugin.item import CapaExplorerFunctionItem
|
||||||
from capa.features.address import AbsoluteVirtualAddress, _NoAddress
|
from capa.features.address import AbsoluteVirtualAddress, _NoAddress
|
||||||
from capa.ida.plugin.model import CapaExplorerDataModel
|
from capa.ida.plugin.model import CapaExplorerDataModel
|
||||||
|
from capa.ida.plugin.qt_compat import QtGui, QtCore, Signal, QAction, QtWidgets
|
||||||
|
|
||||||
MAX_SECTION_SIZE = 750
|
MAX_SECTION_SIZE = 750
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ def calc_item_depth(o):
|
|||||||
|
|
||||||
def build_action(o, display, data, slot):
|
def build_action(o, display, data, slot):
|
||||||
""" """
|
""" """
|
||||||
action = QtWidgets.QAction(display, o)
|
action = QAction(display, o)
|
||||||
|
|
||||||
action.setData(data)
|
action.setData(data)
|
||||||
action.triggered.connect(lambda checked: slot(action))
|
action.triggered.connect(lambda checked: slot(action))
|
||||||
@@ -312,7 +312,7 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
|
|||||||
|
|
||||||
|
|
||||||
class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||||
updated = QtCore.pyqtSignal()
|
updated = Signal()
|
||||||
|
|
||||||
def __init__(self, preview, parent=None):
|
def __init__(self, preview, parent=None):
|
||||||
""" """
|
""" """
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
import datetime
|
||||||
@@ -23,24 +22,13 @@ from pathlib import Path
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from typing_extensions import assert_never
|
from typing_extensions import assert_never
|
||||||
|
|
||||||
import capa.perf
|
|
||||||
import capa.rules
|
import capa.rules
|
||||||
import capa.engine
|
|
||||||
import capa.helpers
|
|
||||||
import capa.version
|
import capa.version
|
||||||
import capa.render.json
|
|
||||||
import capa.rules.cache
|
|
||||||
import capa.render.default
|
|
||||||
import capa.render.verbose
|
|
||||||
import capa.features.common
|
import capa.features.common
|
||||||
import capa.features.freeze as frz
|
import capa.features.freeze as frz
|
||||||
import capa.render.vverbose
|
|
||||||
import capa.features.extractors
|
import capa.features.extractors
|
||||||
import capa.render.result_document
|
|
||||||
import capa.render.result_document as rdoc
|
import capa.render.result_document as rdoc
|
||||||
import capa.features.extractors.common
|
import capa.features.extractors.common
|
||||||
import capa.features.extractors.base_extractor
|
|
||||||
import capa.features.extractors.cape.extractor
|
|
||||||
from capa.rules import RuleSet
|
from capa.rules import RuleSet
|
||||||
from capa.engine import MatchResults
|
from capa.engine import MatchResults
|
||||||
from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError
|
from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError
|
||||||
@@ -338,12 +326,23 @@ def get_extractor(
|
|||||||
import capa.features.extractors.ida.extractor
|
import capa.features.extractors.ida.extractor
|
||||||
|
|
||||||
logger.debug("idalib: opening database...")
|
logger.debug("idalib: opening database...")
|
||||||
# idalib writes to stdout (ugh), so we have to capture that
|
idapro.enable_console_messages(False)
|
||||||
# so as not to screw up structured output.
|
with console.status("analyzing program...", spinner="dots"):
|
||||||
with capa.helpers.stdout_redirector(io.BytesIO()):
|
# we set the primary and secondary Lumina servers to 0.0.0.0 to disable Lumina,
|
||||||
with console.status("analyzing program...", spinner="dots"):
|
# which sometimes provides bad names, including overwriting names from debug info.
|
||||||
if idapro.open_database(str(input_path), run_auto_analysis=True):
|
#
|
||||||
raise RuntimeError("failed to analyze input file")
|
# return values from open_database:
|
||||||
|
# 0 - Success (database not packed)
|
||||||
|
# 1 - Success (database was packed)
|
||||||
|
# 2 - User cancelled or 32-64 bit conversion failed
|
||||||
|
# 4 - Database initialization failed
|
||||||
|
# -1 - Generic errors (database already open, auto-analysis failed, etc.)
|
||||||
|
# -2 - User cancelled operation
|
||||||
|
ret = idapro.open_database(
|
||||||
|
str(input_path), run_auto_analysis=True, args="-Olumina:host=0.0.0.0 -Osecondary_lumina:host=0.0.0.0"
|
||||||
|
)
|
||||||
|
if ret not in (0, 1):
|
||||||
|
raise RuntimeError("failed to analyze input file")
|
||||||
|
|
||||||
logger.debug("idalib: waiting for analysis...")
|
logger.debug("idalib: waiting for analysis...")
|
||||||
ida_auto.auto_wait()
|
ida_auto.auto_wait()
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ class ShouldExitError(Exception):
|
|||||||
"""raised when a main-related routine indicates the program should exit."""
|
"""raised when a main-related routine indicates the program should exit."""
|
||||||
|
|
||||||
def __init__(self, status_code: int):
|
def __init__(self, status_code: int):
|
||||||
|
super().__init__(status_code)
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -274,12 +274,8 @@ SUPPORTED_FEATURES[Scope.FUNCTION].update(SUPPORTED_FEATURES[Scope.BASIC_BLOCK])
|
|||||||
|
|
||||||
|
|
||||||
class InvalidRule(ValueError):
|
class InvalidRule(ValueError):
|
||||||
def __init__(self, msg):
|
|
||||||
super().__init__()
|
|
||||||
self.msg = msg
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"invalid rule: {self.msg}"
|
return f"invalid rule: {super().__str__()}"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
@@ -289,20 +285,15 @@ class InvalidRuleWithPath(InvalidRule):
|
|||||||
def __init__(self, path, msg):
|
def __init__(self, path, msg):
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
self.path = path
|
self.path = path
|
||||||
self.msg = msg
|
|
||||||
self.__cause__ = None
|
self.__cause__ = None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"invalid rule: {self.path}: {self.msg}"
|
return f"invalid rule: {self.path}: {super(InvalidRule, self).__str__()}"
|
||||||
|
|
||||||
|
|
||||||
class InvalidRuleSet(ValueError):
|
class InvalidRuleSet(ValueError):
|
||||||
def __init__(self, msg):
|
|
||||||
super().__init__()
|
|
||||||
self.msg = msg
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"invalid rule set: {self.msg}"
|
return f"invalid rule set: {super().__str__()}"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
@@ -1102,15 +1093,15 @@ class Rule:
|
|||||||
@lru_cache()
|
@lru_cache()
|
||||||
def _get_yaml_loader():
|
def _get_yaml_loader():
|
||||||
try:
|
try:
|
||||||
# prefer to use CLoader to be fast, see #306
|
# prefer to use CLoader to be fast, see #306 / CSafeLoader is the same as CLoader but with safe loading
|
||||||
# on Linux, make sure you install libyaml-dev or similar
|
# on Linux, make sure you install libyaml-dev or similar
|
||||||
# on Windows, get WHLs from pyyaml.org/pypi
|
# on Windows, get WHLs from pyyaml.org/pypi
|
||||||
logger.debug("using libyaml CLoader.")
|
logger.debug("using libyaml CSafeLoader.")
|
||||||
return yaml.CLoader
|
return yaml.CSafeLoader
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("unable to import libyaml CLoader, falling back to Python yaml parser.")
|
logger.debug("unable to import libyaml CSafeLoader, falling back to Python yaml parser.")
|
||||||
logger.debug("this will be slower to load rules.")
|
logger.debug("this will be slower to load rules.")
|
||||||
return yaml.Loader
|
return yaml.SafeLoader
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_ruamel_yaml_parser():
|
def _get_ruamel_yaml_parser():
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
__version__ = "9.2.1"
|
__version__ = "9.3.1"
|
||||||
|
|
||||||
|
|
||||||
def get_major_version():
|
def get_major_version():
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
- [ ] Review changes
|
- [ ] Review changes
|
||||||
- capa https://github.com/mandiant/capa/compare/\<last-release\>...master
|
- capa https://github.com/mandiant/capa/compare/\<last-release\>...master
|
||||||
- capa-rules https://github.com/mandiant/capa-rules/compare/\<last-release>\...master
|
- capa-rules https://github.com/mandiant/capa-rules/compare/\<last-release>\...master
|
||||||
|
- [ ] Run `$ bump-my-version bump {patch/minor/major} [--allow-dirty]` to update [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py) and other version files
|
||||||
- [ ] Update [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md)
|
- [ ] Update [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md)
|
||||||
- Do not forget to add a nice introduction thanking contributors
|
- Do not forget to add a nice introduction thanking contributors
|
||||||
- Remember that we need a major release if we introduce breaking changes
|
- Remember that we need a major release if we introduce breaking changes
|
||||||
@@ -36,7 +37,6 @@
|
|||||||
- [capa <release>...master](https://github.com/mandiant/capa/compare/<release>...master)
|
- [capa <release>...master](https://github.com/mandiant/capa/compare/<release>...master)
|
||||||
- [capa-rules <release>...master](https://github.com/mandiant/capa-rules/compare/<release>...master)
|
- [capa-rules <release>...master](https://github.com/mandiant/capa-rules/compare/<release>...master)
|
||||||
```
|
```
|
||||||
- [ ] Update [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py)
|
|
||||||
- [ ] 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.
|
- [ ] 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.
|
||||||
- [ ] Update the [homepage](https://github.com/mandiant/capa/blob/master/web/public/index.html) (i.e. What's New section)
|
- [ ] Update the [homepage](https://github.com/mandiant/capa/blob/master/web/public/index.html) (i.e. What's New section)
|
||||||
- [ ] 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).
|
- [ ] 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).
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ dependencies = [
|
|||||||
# comments and context.
|
# comments and context.
|
||||||
"pyyaml>=6",
|
"pyyaml>=6",
|
||||||
"colorama>=0.4",
|
"colorama>=0.4",
|
||||||
"ida-settings>=2",
|
"ida-netnode>=3.0",
|
||||||
|
"ida-settings>=3.1.0",
|
||||||
"ruamel.yaml>=0.18",
|
"ruamel.yaml>=0.18",
|
||||||
"pefile>=2023.2.7",
|
"pefile>=2023.2.7",
|
||||||
"pyelftools>=0.31",
|
"pyelftools>=0.31",
|
||||||
@@ -104,7 +105,7 @@ dependencies = [
|
|||||||
|
|
||||||
"networkx>=3",
|
"networkx>=3",
|
||||||
|
|
||||||
"dnfile>=0.15.0",
|
"dnfile>=0.17.0",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
||||||
@@ -121,14 +122,14 @@ dev = [
|
|||||||
# we want all developer environments to be consistent.
|
# we want all developer environments to be consistent.
|
||||||
# These dependencies are not used in production environments
|
# These dependencies are not used in production environments
|
||||||
# and should not conflict with other libraries/tooling.
|
# and should not conflict with other libraries/tooling.
|
||||||
"pre-commit==4.2.0",
|
"pre-commit==4.5.0",
|
||||||
"pytest==8.0.0",
|
"pytest==8.0.0",
|
||||||
"pytest-sugar==1.0.0",
|
"pytest-sugar==1.1.1",
|
||||||
"pytest-instafail==0.5.0",
|
"pytest-instafail==0.5.0",
|
||||||
"flake8==7.3.0",
|
"flake8==7.3.0",
|
||||||
"flake8-bugbear==24.12.12",
|
"flake8-bugbear==25.11.29",
|
||||||
"flake8-encodings==0.5.1",
|
"flake8-encodings==0.5.1",
|
||||||
"flake8-comprehensions==3.16.0",
|
"flake8-comprehensions==3.17.0",
|
||||||
"flake8-logging-format==0.9.0",
|
"flake8-logging-format==0.9.0",
|
||||||
"flake8-no-implicit-concat==0.3.5",
|
"flake8-no-implicit-concat==0.3.5",
|
||||||
"flake8-print==5.0.0",
|
"flake8-print==5.0.0",
|
||||||
@@ -136,19 +137,20 @@ dev = [
|
|||||||
"flake8-simplify==0.22.0",
|
"flake8-simplify==0.22.0",
|
||||||
"flake8-use-pathlib==0.3.0",
|
"flake8-use-pathlib==0.3.0",
|
||||||
"flake8-copyright==0.2.4",
|
"flake8-copyright==0.2.4",
|
||||||
"ruff==0.12.0",
|
"ruff==0.14.7",
|
||||||
"black==25.1.0",
|
"black==25.12.0",
|
||||||
"isort==6.0.0",
|
"isort==6.0.0",
|
||||||
"mypy==1.17.1",
|
"mypy==1.17.1",
|
||||||
"mypy-protobuf==3.6.0",
|
"mypy-protobuf==3.6.0",
|
||||||
"PyGithub==2.6.0",
|
"PyGithub==2.6.0",
|
||||||
|
"bump-my-version==1.2.4",
|
||||||
# type stubs for mypy
|
# type stubs for mypy
|
||||||
"types-backports==0.1.3",
|
"types-backports==0.1.3",
|
||||||
"types-colorama==0.4.15.11",
|
"types-colorama==0.4.15.11",
|
||||||
"types-PyYAML==6.0.8",
|
"types-PyYAML==6.0.8",
|
||||||
"types-psutil==7.0.0.20250218",
|
"types-psutil==7.1.3.20251202",
|
||||||
"types_requests==2.32.0.20240712",
|
"types_requests==2.32.0.20240712",
|
||||||
"types-protobuf==6.30.2.20250516",
|
"types-protobuf==6.32.1.20250918",
|
||||||
"deptry==0.23.0"
|
"deptry==0.23.0"
|
||||||
]
|
]
|
||||||
build = [
|
build = [
|
||||||
@@ -156,16 +158,18 @@ build = [
|
|||||||
# we want all developer environments to be consistent.
|
# we want all developer environments to be consistent.
|
||||||
# These dependencies are not used in production environments
|
# These dependencies are not used in production environments
|
||||||
# and should not conflict with other libraries/tooling.
|
# and should not conflict with other libraries/tooling.
|
||||||
"pyinstaller==6.14.1",
|
"pyinstaller==6.16.0",
|
||||||
"setuptools==80.9.0",
|
"setuptools==80.9.0",
|
||||||
"build==1.2.2"
|
"build==1.3.0"
|
||||||
]
|
]
|
||||||
scripts = [
|
scripts = [
|
||||||
|
# can (optionally) be more lenient on dependencies here
|
||||||
|
# see comment on dependencies for more context
|
||||||
"jschema_to_python==1.2.3",
|
"jschema_to_python==1.2.3",
|
||||||
"psutil==7.0.0",
|
"psutil==7.1.2",
|
||||||
"stix2==3.0.1",
|
"stix2==3.0.1",
|
||||||
"sarif_om==1.0.4",
|
"sarif_om==1.0.4",
|
||||||
"requests==2.32.3",
|
"requests>=2.32.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.deptry]
|
[tool.deptry]
|
||||||
@@ -197,7 +201,8 @@ known_first_party = [
|
|||||||
"idc",
|
"idc",
|
||||||
"java",
|
"java",
|
||||||
"netnode",
|
"netnode",
|
||||||
"PyQt5"
|
"PyQt5",
|
||||||
|
"PySide6"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.deptry.per_rule_ignores]
|
[tool.deptry.per_rule_ignores]
|
||||||
@@ -205,6 +210,7 @@ known_first_party = [
|
|||||||
DEP002 = [
|
DEP002 = [
|
||||||
"black",
|
"black",
|
||||||
"build",
|
"build",
|
||||||
|
"bump-my-version",
|
||||||
"deptry",
|
"deptry",
|
||||||
"flake8",
|
"flake8",
|
||||||
"flake8-bugbear",
|
"flake8-bugbear",
|
||||||
|
|||||||
@@ -10,38 +10,39 @@ annotated-types==0.7.0
|
|||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
cxxfilt==0.3.0
|
cxxfilt==0.3.0
|
||||||
dncil==1.0.2
|
dncil==1.0.2
|
||||||
dnfile==0.16.4
|
dnfile==0.17.0
|
||||||
funcy==2.0
|
funcy==2.0
|
||||||
humanize==4.12.0
|
humanize==4.14.0
|
||||||
ida-netnode==3.0
|
ida-netnode==3.0
|
||||||
ida-settings==2.1.0
|
ida-settings==3.2.2
|
||||||
intervaltree==3.1.0
|
intervaltree==3.1.0
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==4.0.0
|
||||||
mdurl==0.1.2
|
mdurl==0.1.2
|
||||||
msgpack==1.0.8
|
msgpack==1.0.8
|
||||||
networkx==3.4.2
|
networkx==3.4.2
|
||||||
pefile==2024.8.26
|
pefile==2024.8.26
|
||||||
pip==25.1.1
|
pip==25.3
|
||||||
protobuf==6.31.1
|
protobuf==6.33.1
|
||||||
pyasn1==0.5.1
|
pyasn1==0.5.1
|
||||||
pyasn1-modules==0.3.0
|
pyasn1-modules==0.3.0
|
||||||
pycparser==2.22
|
pycparser==2.23
|
||||||
pydantic==2.11.4
|
pydantic==2.12.4
|
||||||
# pydantic pins pydantic-core,
|
# pydantic pins pydantic-core,
|
||||||
# but dependabot updates these separately (which is broken) and is annoying,
|
# but dependabot updates these separately (which is broken) and is annoying,
|
||||||
# so we rely on pydantic to pull in the right version of pydantic-core.
|
# so we rely on pydantic to pull in the right version of pydantic-core.
|
||||||
# pydantic-core==2.23.4
|
# pydantic-core==2.23.4
|
||||||
xmltodict==0.14.2
|
xmltodict==1.0.2
|
||||||
pyelftools==0.32
|
pyelftools==0.32
|
||||||
pygments==2.19.1
|
pygments==2.19.1
|
||||||
python-flirt==0.9.2
|
python-flirt==0.9.2
|
||||||
pyyaml==6.0.2
|
pyyaml==6.0.2
|
||||||
rich==14.0.0
|
rich==14.2.0
|
||||||
ruamel-yaml==0.18.6
|
ruamel-yaml==0.18.6
|
||||||
ruamel-yaml-clib==0.2.8
|
ruamel-yaml-clib==0.2.14
|
||||||
setuptools==80.9.0
|
setuptools==80.9.0
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
sortedcontainers==2.4.0
|
sortedcontainers==2.4.0
|
||||||
viv-utils==0.8.0
|
viv-utils==0.8.0
|
||||||
vivisect==1.2.1
|
vivisect==1.2.1
|
||||||
msgspec==0.19.0
|
msgspec==0.20.0
|
||||||
|
bump-my-version==1.2.4
|
||||||
|
|||||||
2
rules
2
rules
Submodule rules updated: 13e8622f8a...6120dfb6e0
Submodule tests/data updated: 1723908479...cfca4022ee
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
import contextlib
|
import contextlib
|
||||||
import collections
|
import collections
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -20,7 +20,6 @@ from functools import lru_cache
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import capa.main
|
|
||||||
import capa.features.file
|
import capa.features.file
|
||||||
import capa.features.insn
|
import capa.features.insn
|
||||||
import capa.features.common
|
import capa.features.common
|
||||||
@@ -53,6 +52,7 @@ from capa.features.extractors.base_extractor import (
|
|||||||
)
|
)
|
||||||
from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor
|
from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
CD = Path(__file__).resolve().parent
|
CD = Path(__file__).resolve().parent
|
||||||
DOTNET_DIR = CD / "data" / "dotnet"
|
DOTNET_DIR = CD / "data" / "dotnet"
|
||||||
DNFILE_TESTFILES = DOTNET_DIR / "dnfile-testfiles"
|
DNFILE_TESTFILES = DOTNET_DIR / "dnfile-testfiles"
|
||||||
@@ -200,6 +200,77 @@ def get_binja_extractor(path: Path):
|
|||||||
return extractor
|
return extractor
|
||||||
|
|
||||||
|
|
||||||
|
# we can't easily cache this because the extractor relies on global state (the opened database)
|
||||||
|
# which also has to be closed elsewhere. so, the idalib tests will just take a little bit to run.
|
||||||
|
def get_idalib_extractor(path: Path):
|
||||||
|
import capa.features.extractors.ida.idalib as idalib
|
||||||
|
|
||||||
|
if not idalib.has_idalib():
|
||||||
|
raise RuntimeError("cannot find IDA idalib module.")
|
||||||
|
|
||||||
|
if not idalib.load_idalib():
|
||||||
|
raise RuntimeError("failed to load IDA idalib module.")
|
||||||
|
|
||||||
|
import idapro
|
||||||
|
import ida_auto
|
||||||
|
|
||||||
|
import capa.features.extractors.ida.extractor
|
||||||
|
|
||||||
|
logger.debug("idalib: opening database...")
|
||||||
|
|
||||||
|
idapro.enable_console_messages(False)
|
||||||
|
|
||||||
|
# load resource if explicitly needed for test
|
||||||
|
load_resource = ""
|
||||||
|
if "Lab 12-04.exe_" in path.name:
|
||||||
|
load_resource = " -R"
|
||||||
|
|
||||||
|
# we set the primary and secondary Lumina servers to 0.0.0.0 to disable Lumina,
|
||||||
|
# which sometimes provides bad names, including overwriting names from debug info.
|
||||||
|
#
|
||||||
|
# return values from open_database:
|
||||||
|
# 0 - Success (database not packed)
|
||||||
|
# 1 - Success (database was packed)
|
||||||
|
# 2 - User cancelled or 32-64 bit conversion failed
|
||||||
|
# 4 - Database initialization failed
|
||||||
|
# -1 - Generic errors (database already open, auto-analysis failed, etc.)
|
||||||
|
# -2 - User cancelled operation
|
||||||
|
ret = idapro.open_database(
|
||||||
|
str(path), run_auto_analysis=True, args=f"-Olumina:host=0.0.0.0 -Osecondary_lumina:host=0.0.0.0{load_resource}"
|
||||||
|
)
|
||||||
|
if ret not in (0, 1):
|
||||||
|
raise RuntimeError("failed to analyze input file")
|
||||||
|
|
||||||
|
logger.debug("idalib: waiting for analysis...")
|
||||||
|
ida_auto.auto_wait()
|
||||||
|
logger.debug("idalib: opened database.")
|
||||||
|
|
||||||
|
extractor = capa.features.extractors.ida.extractor.IdaFeatureExtractor()
|
||||||
|
fixup_idalib(path, extractor)
|
||||||
|
return extractor
|
||||||
|
|
||||||
|
|
||||||
|
def fixup_idalib(path: Path, extractor):
|
||||||
|
"""
|
||||||
|
IDA fixups to overcome differences between backends
|
||||||
|
"""
|
||||||
|
import idaapi
|
||||||
|
import ida_funcs
|
||||||
|
|
||||||
|
def remove_library_id_flag(fva):
|
||||||
|
f = idaapi.get_func(fva)
|
||||||
|
f.flags &= ~ida_funcs.FUNC_LIB
|
||||||
|
ida_funcs.update_func(f)
|
||||||
|
|
||||||
|
if "kernel32-64" in path.name:
|
||||||
|
# remove (correct) library function id, so we can test x64 thunk
|
||||||
|
remove_library_id_flag(0x1800202B0)
|
||||||
|
|
||||||
|
if "al-khaser_x64" in path.name:
|
||||||
|
# remove (correct) library function id, so we can test x64 nested thunk
|
||||||
|
remove_library_id_flag(0x14004B4F0)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def get_cape_extractor(path):
|
def get_cape_extractor(path):
|
||||||
from capa.helpers import load_json_from_path
|
from capa.helpers import load_json_from_path
|
||||||
@@ -894,20 +965,8 @@ FEATURE_PRESENCE_TESTS = sorted(
|
|||||||
("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), False),
|
("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), False),
|
||||||
("mimikatz", "function=0x4556E5", capa.features.insn.API("LsaQueryInformationPolicy"), True),
|
("mimikatz", "function=0x4556E5", capa.features.insn.API("LsaQueryInformationPolicy"), True),
|
||||||
# insn/api: x64
|
# insn/api: x64
|
||||||
(
|
|
||||||
"kernel32-64",
|
|
||||||
"function=0x180001010",
|
|
||||||
capa.features.insn.API("RtlVirtualUnwind"),
|
|
||||||
True,
|
|
||||||
),
|
|
||||||
("kernel32-64", "function=0x180001010", capa.features.insn.API("RtlVirtualUnwind"), True),
|
("kernel32-64", "function=0x180001010", capa.features.insn.API("RtlVirtualUnwind"), True),
|
||||||
# insn/api: x64 thunk
|
# insn/api: x64 thunk
|
||||||
(
|
|
||||||
"kernel32-64",
|
|
||||||
"function=0x1800202B0",
|
|
||||||
capa.features.insn.API("RtlCaptureContext"),
|
|
||||||
True,
|
|
||||||
),
|
|
||||||
("kernel32-64", "function=0x1800202B0", capa.features.insn.API("RtlCaptureContext"), True),
|
("kernel32-64", "function=0x1800202B0", capa.features.insn.API("RtlCaptureContext"), True),
|
||||||
# insn/api: x64 nested thunk
|
# insn/api: x64 nested thunk
|
||||||
("al-khaser x64", "function=0x14004B4F0", capa.features.insn.API("__vcrt_GetModuleHandle"), True),
|
("al-khaser x64", "function=0x14004B4F0", capa.features.insn.API("__vcrt_GetModuleHandle"), True),
|
||||||
@@ -995,20 +1054,20 @@ FEATURE_PRESENCE_TESTS = sorted(
|
|||||||
("pma16-01", "file", OS(OS_WINDOWS), True),
|
("pma16-01", "file", OS(OS_WINDOWS), True),
|
||||||
("pma16-01", "file", OS(OS_LINUX), False),
|
("pma16-01", "file", OS(OS_LINUX), False),
|
||||||
("mimikatz", "file", OS(OS_WINDOWS), True),
|
("mimikatz", "file", OS(OS_WINDOWS), True),
|
||||||
("pma16-01", "function=0x404356", OS(OS_WINDOWS), True),
|
("pma16-01", "function=0x401100", OS(OS_WINDOWS), True),
|
||||||
("pma16-01", "function=0x404356,bb=0x4043B9", OS(OS_WINDOWS), True),
|
("pma16-01", "function=0x401100,bb=0x401130", OS(OS_WINDOWS), True),
|
||||||
("mimikatz", "function=0x40105D", OS(OS_WINDOWS), True),
|
("mimikatz", "function=0x40105D", OS(OS_WINDOWS), True),
|
||||||
("pma16-01", "file", Arch(ARCH_I386), True),
|
("pma16-01", "file", Arch(ARCH_I386), True),
|
||||||
("pma16-01", "file", Arch(ARCH_AMD64), False),
|
("pma16-01", "file", Arch(ARCH_AMD64), False),
|
||||||
("mimikatz", "file", Arch(ARCH_I386), True),
|
("mimikatz", "file", Arch(ARCH_I386), True),
|
||||||
("pma16-01", "function=0x404356", Arch(ARCH_I386), True),
|
("pma16-01", "function=0x401100", Arch(ARCH_I386), True),
|
||||||
("pma16-01", "function=0x404356,bb=0x4043B9", Arch(ARCH_I386), True),
|
("pma16-01", "function=0x401100,bb=0x401130", Arch(ARCH_I386), True),
|
||||||
("mimikatz", "function=0x40105D", Arch(ARCH_I386), True),
|
("mimikatz", "function=0x40105D", Arch(ARCH_I386), True),
|
||||||
("pma16-01", "file", Format(FORMAT_PE), True),
|
("pma16-01", "file", Format(FORMAT_PE), True),
|
||||||
("pma16-01", "file", Format(FORMAT_ELF), False),
|
("pma16-01", "file", Format(FORMAT_ELF), False),
|
||||||
("mimikatz", "file", Format(FORMAT_PE), True),
|
("mimikatz", "file", Format(FORMAT_PE), True),
|
||||||
# format is also a global feature
|
# format is also a global feature
|
||||||
("pma16-01", "function=0x404356", Format(FORMAT_PE), True),
|
("pma16-01", "function=0x401100", Format(FORMAT_PE), True),
|
||||||
("mimikatz", "function=0x456BB9", Format(FORMAT_PE), True),
|
("mimikatz", "function=0x456BB9", Format(FORMAT_PE), True),
|
||||||
# elf support
|
# elf support
|
||||||
("7351f.elf", "file", OS(OS_LINUX), True),
|
("7351f.elf", "file", OS(OS_LINUX), True),
|
||||||
|
|||||||
@@ -70,4 +70,4 @@ def test_standalone_binja_backend():
|
|||||||
@pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed")
|
@pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed")
|
||||||
def test_binja_version():
|
def test_binja_version():
|
||||||
version = binaryninja.core_version_info()
|
version = binaryninja.core_version_info()
|
||||||
assert version.major == 5 and version.minor == 1
|
assert version.major == 5 and version.minor == 2
|
||||||
|
|||||||
63
tests/test_idalib_features.py
Normal file
63
tests/test_idalib_features.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Copyright 2020 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import fixtures
|
||||||
|
|
||||||
|
import capa.features.extractors.ida.idalib
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
idalib_present = capa.features.extractors.ida.idalib.has_idalib()
|
||||||
|
if idalib_present:
|
||||||
|
try:
|
||||||
|
import idapro # noqa: F401 [imported but unused]
|
||||||
|
except ImportError:
|
||||||
|
idalib_present = False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(idalib_present is False, reason="Skip idalib tests if the idalib Python API is not installed")
|
||||||
|
@fixtures.parametrize(
|
||||||
|
"sample,scope,feature,expected",
|
||||||
|
fixtures.FEATURE_PRESENCE_TESTS + fixtures.FEATURE_SYMTAB_FUNC_TESTS,
|
||||||
|
indirect=["sample", "scope"],
|
||||||
|
)
|
||||||
|
def test_idalib_features(sample, scope, feature, expected):
|
||||||
|
try:
|
||||||
|
fixtures.do_test_feature_presence(fixtures.get_idalib_extractor, sample, scope, feature, expected)
|
||||||
|
finally:
|
||||||
|
logger.debug("closing database...")
|
||||||
|
import idapro
|
||||||
|
|
||||||
|
idapro.close_database(save=False)
|
||||||
|
logger.debug("opened database.")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(idalib_present is False, reason="Skip idalib tests if the idalib Python API is not installed")
|
||||||
|
@fixtures.parametrize(
|
||||||
|
"sample,scope,feature,expected",
|
||||||
|
fixtures.FEATURE_COUNT_TESTS,
|
||||||
|
indirect=["sample", "scope"],
|
||||||
|
)
|
||||||
|
def test_idalib_feature_counts(sample, scope, feature, expected):
|
||||||
|
try:
|
||||||
|
fixtures.do_test_feature_count(fixtures.get_idalib_extractor, sample, scope, feature, expected)
|
||||||
|
finally:
|
||||||
|
logger.debug("closing database...")
|
||||||
|
import idapro
|
||||||
|
|
||||||
|
idapro.close_database(save=False)
|
||||||
|
logger.debug("closed database.")
|
||||||
25
web/explorer/package-lock.json
generated
25
web/explorer/package-lock.json
generated
@@ -27,7 +27,7 @@
|
|||||||
"eslint-plugin-vue": "^9.23.0",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.1.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"vite": "^6.3.4",
|
"vite": "^6.4.1",
|
||||||
"vite-plugin-singlefile": "^2.2.0",
|
"vite-plugin-singlefile": "^2.2.0",
|
||||||
"vitest": "^3.0.9"
|
"vitest": "^3.0.9"
|
||||||
}
|
}
|
||||||
@@ -2272,10 +2272,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.4.2",
|
"version": "10.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||||
"integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==",
|
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"foreground-child": "^3.1.0",
|
"foreground-child": "^3.1.0",
|
||||||
"jackspeak": "^3.1.2",
|
"jackspeak": "^3.1.2",
|
||||||
@@ -2287,9 +2288,6 @@
|
|||||||
"bin": {
|
"bin": {
|
||||||
"glob": "dist/esm/bin.mjs"
|
"glob": "dist/esm/bin.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.18"
|
|
||||||
},
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
@@ -2641,10 +2639,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
},
|
},
|
||||||
@@ -3801,9 +3800,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.4",
|
"version": "6.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"eslint-plugin-vue": "^9.23.0",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.1.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"vite": "^6.3.4",
|
"vite": "^6.4.1",
|
||||||
"vite-plugin-singlefile": "^2.2.0",
|
"vite-plugin-singlefile": "^2.2.0",
|
||||||
"vitest": "^3.0.9"
|
"vitest": "^3.0.9"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,18 @@
|
|||||||
|
|
||||||
<h2 class="mt-3">Tool Updates</h2>
|
<h2 class="mt-3">Tool Updates</h2>
|
||||||
|
|
||||||
|
<h3 class="mt-2">v9.3.1 (<em>2025-11-19</em>)</h3>
|
||||||
|
<p class="mt-0">
|
||||||
|
This patch release fixes a missing import for the capa explorer plugin for IDA Pro.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="mt-2">v9.3.0 (<em>2025-11-12</em>)</h3>
|
||||||
|
<p class="mt-0">
|
||||||
|
capa v9.3.0 comes with over 20 new and/or impoved rules.
|
||||||
|
For IDA users the capa explorer plugin is now available via the IDA Pro plugin repository and contains Qt compatibility layer for PyQt5 and PySide6 support.
|
||||||
|
Additionally a Binary Ninja bug has been fixed. Released binaries now include ARM64 binaries (Linux and macOS).
|
||||||
|
</p>
|
||||||
|
|
||||||
<h3 class="mt-2">v9.2.1 (<em>2025-06-06</em>)</h3>
|
<h3 class="mt-2">v9.2.1 (<em>2025-06-06</em>)</h3>
|
||||||
<p class="mt-0">
|
<p class="mt-0">
|
||||||
This point release fixes bugs including removing an unnecessary PyInstaller warning message and enabling the standalone binary to execute on systems running older versions of glibc.
|
This point release fixes bugs including removing an unnecessary PyInstaller warning message and enabling the standalone binary to execute on systems running older versions of glibc.
|
||||||
|
|||||||
Reference in New Issue
Block a user