mirror of
https://github.com/mandiant/capa.git
synced 2025-12-08 22:00:36 -08:00
Compare commits
362 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b7582606 | ||
|
|
791f5e2359 | ||
|
|
c409b2b7ed | ||
|
|
4501955728 | ||
|
|
6b4591de14 | ||
|
|
00cce585d6 | ||
|
|
19e2097f79 | ||
|
|
b67bd4d084 | ||
|
|
854759cb43 | ||
|
|
348e0b3203 | ||
|
|
03e2195582 | ||
|
|
076bb13e2d | ||
|
|
76bd1460ba | ||
|
|
14a7bab890 | ||
|
|
8ca88d94d5 | ||
|
|
9d3f732b33 | ||
|
|
d3e3c966d6 | ||
|
|
e402aab41d | ||
|
|
c73abb8855 | ||
|
|
04071606cd | ||
|
|
19698b1ba1 | ||
|
|
25e9e18097 | ||
|
|
3a21648e78 | ||
|
|
8dcb7a473e | ||
|
|
cf91503dc3 | ||
|
|
d8691edd15 | ||
|
|
56a6f9c83e | ||
|
|
e25e68e169 | ||
|
|
728742a1ad | ||
|
|
da273824d1 | ||
|
|
7a6f63cf2b | ||
|
|
d62734ecc2 | ||
|
|
5ccb642929 | ||
|
|
8d5fcdf287 | ||
|
|
be8499238c | ||
|
|
40c7714c48 | ||
|
|
460590cec0 | ||
|
|
25d2ef30e7 | ||
|
|
71ae51ef69 | ||
|
|
216bfb968d | ||
|
|
32cb0365f8 | ||
|
|
b299e4bc1f | ||
|
|
bc2802fd72 | ||
|
|
81a14838bd | ||
|
|
1c9a86ca20 | ||
|
|
32fefa60cc | ||
|
|
09bbe80dfb | ||
|
|
239ad4a17e | ||
|
|
ab3b074c6a | ||
|
|
e863ce5ff3 | ||
|
|
8e4c0e3040 | ||
|
|
401a0ee0ff | ||
|
|
f69fabc2b0 | ||
|
|
87f691677c | ||
|
|
ea9853e667 | ||
|
|
312dd0d40f | ||
|
|
44cbe664e4 | ||
|
|
6b8e2b3e81 | ||
|
|
ba9ab7c876 | ||
|
|
1af97f6681 | ||
|
|
05575e1e92 | ||
|
|
9d137a207f | ||
|
|
850ae5a916 | ||
|
|
e8054c277d | ||
|
|
e8ea461456 | ||
|
|
bb8991af8e | ||
|
|
368f635387 | ||
|
|
287e4282a9 | ||
|
|
1f6ce48e40 | ||
|
|
7cb31cf23c | ||
|
|
01e6619182 | ||
|
|
20d7bf1402 | ||
|
|
6b8983c0c4 | ||
|
|
97bd4992b1 | ||
|
|
843fd34737 | ||
|
|
dfc19d8cb2 | ||
|
|
1564f24330 | ||
|
|
0d87bb0504 | ||
|
|
db423d9b0a | ||
|
|
ebfba543e6 | ||
|
|
46c464282e | ||
|
|
aa225dac5c | ||
|
|
c2376eaf7b | ||
|
|
6451fa433b | ||
|
|
765c7cb792 | ||
|
|
b675c9a77c | ||
|
|
ac081336ba | ||
|
|
a15eb835f4 | ||
|
|
fcdaabf34e | ||
|
|
283aa27152 | ||
|
|
f856ea7454 | ||
|
|
ebb778ae0d | ||
|
|
e9e5d2bb12 | ||
|
|
bb1ef6ca56 | ||
|
|
7e64306f1c | ||
|
|
6b19e7b372 | ||
|
|
bb60099ab6 | ||
|
|
d609203fcd | ||
|
|
fcf200f13f | ||
|
|
7cb93c8ebd | ||
|
|
eb69b383a4 | ||
|
|
04d127f69f | ||
|
|
9dd39926d7 | ||
|
|
13d14f6cb6 | ||
|
|
260da8ed2c | ||
|
|
a6884db1d3 | ||
|
|
67d3916c41 | ||
|
|
b0ffc86399 | ||
|
|
07b4e1f8a2 | ||
|
|
4137923c2e | ||
|
|
33be4d1f8e | ||
|
|
8e9eadf98a | ||
|
|
9107819cf1 | ||
|
|
b74738adcf | ||
|
|
b229048b51 | ||
|
|
4fe7f784e9 | ||
|
|
b7b8792f70 | ||
|
|
e637e5a09e | ||
|
|
0ea6f1e270 | ||
|
|
f6bc42540c | ||
|
|
62701a2837 | ||
|
|
f60e3fc531 | ||
|
|
b6f0ee539b | ||
|
|
e70e1b0641 | ||
|
|
b9c4cc681b | ||
|
|
13261d0c41 | ||
|
|
8476aeee35 | ||
|
|
38cf1f1041 | ||
|
|
d81b123e97 | ||
|
|
029259b8ed | ||
|
|
e3f695b947 | ||
|
|
d25c86c08b | ||
|
|
4aad53c5b3 | ||
|
|
0028da5270 | ||
|
|
cf3494d427 | ||
|
|
3f33b82ace | ||
|
|
12f1851ba5 | ||
|
|
6da0e5d985 | ||
|
|
e2e84f7f50 | ||
|
|
106c31735e | ||
|
|
277e9d1551 | ||
|
|
9db01e340c | ||
|
|
626ea51c20 | ||
|
|
fd686ac591 | ||
|
|
17aab2c4fc | ||
|
|
216ac8dd96 | ||
|
|
d68e057439 | ||
|
|
3c2749734c | ||
|
|
5c60efa81f | ||
|
|
09d86245e5 | ||
|
|
2862cb35c2 | ||
|
|
c3aa306d6c | ||
|
|
6bec5d40bd | ||
|
|
da6c6cfb48 | ||
|
|
9353e46615 | ||
|
|
76913af20b | ||
|
|
bb86d1485c | ||
|
|
cd3086cfa4 | ||
|
|
120f34e8ef | ||
|
|
5495a8555c | ||
|
|
1a447013bd | ||
|
|
fccb533841 | ||
|
|
3b165c3d8e | ||
|
|
cd5199f873 | ||
|
|
202b5ddae7 | ||
|
|
0b70abca93 | ||
|
|
6de22a0264 | ||
|
|
fd811d1387 | ||
|
|
b617179525 | ||
|
|
28fc671ad5 | ||
|
|
e1b750f1e9 | ||
|
|
1ec680856d | ||
|
|
d79ea074f2 | ||
|
|
e68bcddfe0 | ||
|
|
4929d5936e | ||
|
|
1975b6455c | ||
|
|
1360e08389 | ||
|
|
40061b3c42 | ||
|
|
45fca7adea | ||
|
|
482686ab81 | ||
|
|
67f8c4d28c | ||
|
|
3f151a342b | ||
|
|
e87e8484b6 | ||
|
|
8726de0d65 | ||
|
|
7d1512a3de | ||
|
|
73d76d7aba | ||
|
|
1febb224d1 | ||
|
|
e3ea60d354 | ||
|
|
93cd1dcedd | ||
|
|
7b0270980d | ||
|
|
cce7774705 | ||
|
|
9ec9a6f439 | ||
|
|
97a3fba2c9 | ||
|
|
893352756f | ||
|
|
0cc06aa83d | ||
|
|
1888d0e7e3 | ||
|
|
52e24e560b | ||
|
|
c97d2d7244 | ||
|
|
833ec47170 | ||
|
|
07ae30875c | ||
|
|
76a4a5899f | ||
|
|
4d81b7ab98 | ||
|
|
b068890fa6 | ||
|
|
d10d2820b2 | ||
|
|
5239e40beb | ||
|
|
bce8f7b5e5 | ||
|
|
0cf9365816 | ||
|
|
30d23c4d97 | ||
|
|
b3ed42f5f9 | ||
|
|
508a09ef25 | ||
|
|
e517d7dd77 | ||
|
|
142b84f9c5 | ||
|
|
72607c6ae5 | ||
|
|
2fd01835dc | ||
|
|
80600f59c7 | ||
|
|
1ec1185850 | ||
|
|
22e12928a6 | ||
|
|
8ad74ddbb6 | ||
|
|
2c1d5592ca | ||
|
|
267f5e99b7 | ||
|
|
6b77c50ae8 | ||
|
|
8a0a24f269 | ||
|
|
4f2494dc59 | ||
|
|
2e5da3e2bd | ||
|
|
0ac21f036c | ||
|
|
4ecf3a1793 | ||
|
|
b14db68819 | ||
|
|
54106d60ae | ||
|
|
0622f45208 | ||
|
|
adb9de8d4b | ||
|
|
48dd64beba | ||
|
|
abaabae164 | ||
|
|
8316a74ca2 | ||
|
|
1dd2af7048 | ||
|
|
bbc4e5cd97 | ||
|
|
7da3ef89ca | ||
|
|
44e319a604 | ||
|
|
21c346d0c2 | ||
|
|
f9953d1e99 | ||
|
|
9bce98b0ae | ||
|
|
7f39a5b1d6 | ||
|
|
e9cc193dd4 | ||
|
|
5482021c75 | ||
|
|
5507991575 | ||
|
|
65114ec2d7 | ||
|
|
e4ae052f48 | ||
|
|
3ae8183a4a | ||
|
|
b59df659c9 | ||
|
|
519cfb842e | ||
|
|
ee98548bf9 | ||
|
|
8298347c19 | ||
|
|
54d749e845 | ||
|
|
25b9c88198 | ||
|
|
11ae44541b | ||
|
|
f26a109b4d | ||
|
|
d26897afca | ||
|
|
6869ef6520 | ||
|
|
4fbd2ba2b8 | ||
|
|
283ce41a5e | ||
|
|
4b1a5003df | ||
|
|
1cd0f44115 | ||
|
|
824e852184 | ||
|
|
4be0c40fe6 | ||
|
|
4f4adc04c8 | ||
|
|
60d400cf08 | ||
|
|
2f4d8e1d90 | ||
|
|
fdfa838a15 | ||
|
|
baef70d588 | ||
|
|
e24773436e | ||
|
|
a4a4016463 | ||
|
|
30535cb623 | ||
|
|
2355603340 | ||
|
|
9a23e6837d | ||
|
|
0488c86bc7 | ||
|
|
b4092980e3 | ||
|
|
18bdf23f03 | ||
|
|
ac6e9f8aae | ||
|
|
abb6d01c1d | ||
|
|
984c1b2d39 | ||
|
|
e3dcbbb386 | ||
|
|
a8f382ebe8 | ||
|
|
4fb10780ec | ||
|
|
efc7540aa6 | ||
|
|
f1c4ff8e17 | ||
|
|
f44b4ebebd | ||
|
|
19000409df | ||
|
|
42849573b3 | ||
|
|
c02440f4b7 | ||
|
|
676f98acc8 | ||
|
|
e3a9c75316 | ||
|
|
2a54689cc6 | ||
|
|
cd11787bd8 | ||
|
|
9171dc2dad | ||
|
|
c695b37b0e | ||
|
|
e1d0ba22c7 | ||
|
|
7debc54dbd | ||
|
|
7b50065fea | ||
|
|
37306af37a | ||
|
|
c03405c29f | ||
|
|
8fe8981570 | ||
|
|
463f2f1d62 | ||
|
|
9a5f4562b8 | ||
|
|
7bc298de1a | ||
|
|
cbadab8521 | ||
|
|
0eaf055a46 | ||
|
|
0eb4291b25 | ||
|
|
9d1f110d24 | ||
|
|
0f0a23946b | ||
|
|
5b2122a3c6 | ||
|
|
49231366f1 | ||
|
|
10a4381ad5 | ||
|
|
7707984237 | ||
|
|
f6b0673b0f | ||
|
|
1c1e5c02b0 | ||
|
|
fe13f9ce76 | ||
|
|
04e3f268f3 | ||
|
|
12234c3572 | ||
|
|
92cfc0caa7 | ||
|
|
58e4a30156 | ||
|
|
bf4695c6bf | ||
|
|
d63c6f1f9e | ||
|
|
08b3ae60d7 | ||
|
|
f5893d7bd3 | ||
|
|
3a90247e5b | ||
|
|
bb0dff0610 | ||
|
|
610a86e5e2 | ||
|
|
cabb9c0975 | ||
|
|
c28f4fc890 | ||
|
|
9a449b6bd9 | ||
|
|
65b5c46029 | ||
|
|
8857511e55 | ||
|
|
ffcabf1e0b | ||
|
|
c6b43d7492 | ||
|
|
8af3a19d61 | ||
|
|
2252e69eed | ||
|
|
5e85fc9ede | ||
|
|
4e529d5c1f | ||
|
|
0f9dd9095b | ||
|
|
b163f82a71 | ||
|
|
bd3cc18a25 | ||
|
|
4e2f175b9f | ||
|
|
fdd097a141 | ||
|
|
1b4e5258f8 | ||
|
|
1d78900862 | ||
|
|
8807d6844d | ||
|
|
318a3d1610 | ||
|
|
b86b66a29c | ||
|
|
c263670a21 | ||
|
|
fc840d8e7d | ||
|
|
b751a7bba3 | ||
|
|
c8765a4116 | ||
|
|
4955a23c52 | ||
|
|
16814c376f | ||
|
|
05fb1a5c00 | ||
|
|
df8056f415 | ||
|
|
fde1de3250 | ||
|
|
7ab8dbbd4e | ||
|
|
2ddb6b0773 | ||
|
|
5fd532845c | ||
|
|
2a59284621 | ||
|
|
9adb669921 | ||
|
|
034894330b |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -57,7 +57,7 @@ When we make a significant decision in how we maintain the project and what we c
|
||||
we will document it in the [capa issues tracker](https://github.com/mandiant/capa/issues).
|
||||
This is the best place review our discussions about what/how/why we do things in the project.
|
||||
If you have a question, check to see if it is documented there.
|
||||
If it is *not* documented there, or you can't find an answer, please open a issue.
|
||||
If it is *not* documented there, or you can't find an answer, please open an issue.
|
||||
We'll link to existing issues when appropriate to keep discussions in one place.
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
2
.github/flake8.ini
vendored
2
.github/flake8.ini
vendored
@@ -40,4 +40,4 @@ per-file-ignores =
|
||||
|
||||
copyright-check = True
|
||||
copyright-min-file-size = 1
|
||||
copyright-regexp = Copyright \(C\) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
copyright-regexp = Copyright \(C\) \d{4} Mandiant, Inc. All Rights Reserved.
|
||||
3
.github/mypy/mypy.ini
vendored
3
.github/mypy/mypy.ini
vendored
@@ -1,8 +1,5 @@
|
||||
[mypy]
|
||||
|
||||
[mypy-halo.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-tqdm.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
||||
2
.github/pyinstaller/hooks/hook-vivisect.py
vendored
2
.github/pyinstaller/hooks/hook-vivisect.py
vendored
@@ -24,7 +24,7 @@ excludedimports = [
|
||||
"pyqtwebengine",
|
||||
# the above are imported by these viv modules.
|
||||
# so really, we'd want to exclude these submodules of viv.
|
||||
# but i dont think this works.
|
||||
# but i don't think this works.
|
||||
"vqt",
|
||||
"vdb.qt",
|
||||
"envi.qt",
|
||||
|
||||
15
.github/pyinstaller/pyinstaller.spec
vendored
15
.github/pyinstaller/pyinstaller.spec
vendored
@@ -1,10 +1,19 @@
|
||||
# -*- mode: python -*-
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
import os.path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import wcwidth
|
||||
import capa.rules.cache
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# SPECPATH is a global variable which points to .spec file path
|
||||
capa_dir = Path(SPECPATH).parent.parent
|
||||
rules_dir = capa_dir / 'rules'
|
||||
cache_dir = capa_dir / 'cache'
|
||||
|
||||
if not capa.rules.cache.generate_rule_cache(rules_dir, cache_dir):
|
||||
sys.exit(-1)
|
||||
|
||||
a = Analysis(
|
||||
# when invoking pyinstaller from the project root,
|
||||
@@ -26,7 +35,7 @@ a = Analysis(
|
||||
# so we manually embed the wcwidth resources here.
|
||||
#
|
||||
# ref: https://stackoverflow.com/a/62278462/87207
|
||||
(os.path.dirname(wcwidth.__file__), "wcwidth"),
|
||||
(Path(wcwidth.__file__).parent, "wcwidth"),
|
||||
],
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets run from the project root.
|
||||
|
||||
8
.github/ruff.toml
vendored
8
.github/ruff.toml
vendored
@@ -1,16 +1,16 @@
|
||||
# Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default.
|
||||
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
select = ["E", "F"]
|
||||
lint.select = ["E", "F"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
lint.fixable = ["ALL"]
|
||||
lint.unfixable = []
|
||||
|
||||
# E402 module level import not at top of file
|
||||
# E722 do not use bare 'except'
|
||||
# E501 line too long
|
||||
ignore = ["E402", "E722", "E501"]
|
||||
lint.ignore = ["E402", "E722", "E501"]
|
||||
|
||||
line-length = 120
|
||||
|
||||
|
||||
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@@ -3,6 +3,10 @@ name: build
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'web/**'
|
||||
- 'doc/**'
|
||||
- '**.md'
|
||||
release:
|
||||
types: [edited, published]
|
||||
|
||||
@@ -32,18 +36,18 @@ jobs:
|
||||
artifact_name: capa.exe
|
||||
asset_name: windows
|
||||
python_version: 3.8
|
||||
- os: macos-11
|
||||
- os: macos-12
|
||||
# use older macOS for assumed better portability
|
||||
artifact_name: capa
|
||||
asset_name: macos
|
||||
python_version: 3.8
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up Python ${{ matrix.python_version }}
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
- if: matrix.os == 'ubuntu-20.04'
|
||||
@@ -51,9 +55,9 @@ jobs:
|
||||
- name: Upgrade pip, setuptools
|
||||
run: python -m pip install --upgrade pip setuptools
|
||||
- name: Install capa with build requirements
|
||||
run: pip install -e .[build]
|
||||
- name: Cache the rule set
|
||||
run: python ./scripts/cache-ruleset.py ./rules/ ./cache/
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install -e .[build]
|
||||
- name: Build standalone executable
|
||||
run: pyinstaller --log-level DEBUG .github/pyinstaller/pyinstaller.spec
|
||||
- name: Does it run (PE)?
|
||||
@@ -66,7 +70,7 @@ jobs:
|
||||
run: |
|
||||
7z e "tests/data/dynamic/cape/v2.2/d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json.gz"
|
||||
dist/capa -d "d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json"
|
||||
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
path: dist/${{ matrix.artifact_name }}
|
||||
@@ -90,7 +94,7 @@ jobs:
|
||||
asset_name: windows
|
||||
steps:
|
||||
- name: Download ${{ matrix.asset_name }}
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
- name: Set executable flag
|
||||
@@ -118,7 +122,7 @@ jobs:
|
||||
artifact_name: capa
|
||||
steps:
|
||||
- name: Download ${{ matrix.asset_name }}
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
- name: Set executable flag
|
||||
|
||||
9
.github/workflows/changelog.yml
vendored
9
.github/workflows/changelog.yml
vendored
@@ -7,7 +7,8 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check_changelog:
|
||||
@@ -19,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- name: Get changed files
|
||||
id: files
|
||||
uses: Ana06/get-changed-files@e0c398b7065a8d84700c471b6afc4116d1ba4e96 # v2.2.0
|
||||
uses: Ana06/get-changed-files@25f79e676e7ea1868813e21465014798211fad8c # v2.3.0
|
||||
- name: check changelog updated
|
||||
id: changelog_updated
|
||||
env:
|
||||
@@ -29,14 +30,14 @@ jobs:
|
||||
echo $FILES | grep -qF 'CHANGELOG.md' || echo $PR_BODY | grep -qiF "$NO_CHANGELOG"
|
||||
- name: Reject pull request if no CHANGELOG update
|
||||
if: ${{ always() && steps.changelog_updated.outcome == 'failure' }}
|
||||
uses: Ana06/automatic-pull-request-review@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.0
|
||||
uses: Ana06/automatic-pull-request-review@76aaf9b15b116a54e1da7a28a46f91fe089600bf # v0.2.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
event: REQUEST_CHANGES
|
||||
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 }}`"
|
||||
allow_duplicate: false
|
||||
- name: Dismiss previous review if CHANGELOG update
|
||||
uses: Ana06/automatic-pull-request-review@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.0
|
||||
uses: Ana06/automatic-pull-request-review@76aaf9b15b116a54e1da7a28a46f91fe089600bf # v0.2.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
event: DISMISS
|
||||
|
||||
7
.github/workflows/publish.yml
vendored
7
.github/workflows/publish.yml
vendored
@@ -17,20 +17,21 @@ jobs:
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -e .[build]
|
||||
- name: build package
|
||||
run: |
|
||||
python -m build
|
||||
- name: upload package artifacts
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
path: dist/*
|
||||
- name: publish package
|
||||
|
||||
8
.github/workflows/scorecard.yml
vendored
8
.github/workflows/scorecard.yml
vendored
@@ -32,12 +32,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6
|
||||
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -67,6 +67,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # v2.1.27
|
||||
uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
4
.github/workflows/tag.yml
vendored
4
.github/workflows/tag.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa-rules
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
repository: mandiant/capa-rules
|
||||
token: ${{ secrets.CAPA_TOKEN }}
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
git tag $name -m "https://github.com/mandiant/capa/releases/$name"
|
||||
# TODO update branch name-major=${name%%.*}
|
||||
- name: Push tag to capa-rules
|
||||
uses: ad-m/github-push-action@0fafdd62b84042d49ec0cb92d9cac7f7ce4ec79e # master
|
||||
uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # v0.8.0
|
||||
with:
|
||||
repository: mandiant/capa-rules
|
||||
github_token: ${{ secrets.CAPA_TOKEN }}
|
||||
|
||||
91
.github/workflows/tests.yml
vendored
91
.github/workflows/tests.yml
vendored
@@ -1,10 +1,22 @@
|
||||
name: CI
|
||||
|
||||
# tests.yml workflow will run for all changes except:
|
||||
# any file or directory under web/ or doc/
|
||||
# any Markdown (.md) file anywhere in the repository
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'web/**'
|
||||
- 'doc/**'
|
||||
- '**.md'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'web/**'
|
||||
- 'doc/**'
|
||||
- '**.md'
|
||||
|
||||
permissions: read-all
|
||||
|
||||
@@ -17,7 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
# The sync GH action in capa-rules relies on a single '- *$' in the CHANGELOG file
|
||||
- name: Ensure CHANGELOG has '- *$'
|
||||
run: |
|
||||
@@ -28,14 +40,16 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
# use latest available python to take advantage of best performance
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Install dependencies
|
||||
run: pip install -e .[dev]
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install -e .[dev,scripts]
|
||||
- name: Lint with ruff
|
||||
run: pre-commit run ruff
|
||||
- name: Lint with isort
|
||||
@@ -45,21 +59,25 @@ jobs:
|
||||
- name: Lint with flake8
|
||||
run: pre-commit run flake8 --hook-stage manual
|
||||
- name: Check types with mypy
|
||||
run: pre-commit run mypy --hook-stage manual
|
||||
run: pre-commit run mypy --hook-stage manual
|
||||
- name: Check imports against dependencies
|
||||
run: pre-commit run deptry --hook-stage manual
|
||||
|
||||
rule_linter:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Install capa
|
||||
run: pip install -e .[dev]
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install -e .[dev,scripts]
|
||||
- name: Run rule linter
|
||||
run: python scripts/lint.py rules/
|
||||
|
||||
@@ -70,7 +88,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-2019, macos-11]
|
||||
os: [ubuntu-20.04, windows-2019, macos-12]
|
||||
# across all operating systems
|
||||
python-version: ["3.8", "3.11"]
|
||||
include:
|
||||
@@ -83,18 +101,20 @@ jobs:
|
||||
python-version: "3.10"
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install pyyaml
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Install capa
|
||||
run: pip install -e .[dev]
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install -e .[dev,scripts]
|
||||
- name: Run tests (fast)
|
||||
# this set of tests runs about 80% of the cases in 20% of the time,
|
||||
# and should catch most errors quickly.
|
||||
@@ -106,22 +126,22 @@ jobs:
|
||||
name: Binary Ninja tests for ${{ matrix.python-version }}
|
||||
env:
|
||||
BN_SERIAL: ${{ secrets.BN_SERIAL }}
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [tests]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.11"]
|
||||
python-version: ["3.9", "3.11"]
|
||||
steps:
|
||||
- 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
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install pyyaml
|
||||
@@ -129,7 +149,9 @@ jobs:
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Install capa
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
run: pip install -e .[dev]
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install -e .[dev,scripts]
|
||||
- name: install Binary Ninja
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
run: |
|
||||
@@ -153,31 +175,23 @@ jobs:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.11"]
|
||||
java-version: ["17"]
|
||||
gradle-version: ["7.3"]
|
||||
ghidra-version: ["10.3"]
|
||||
public-version: ["PUBLIC_20230510"] # for ghidra releases
|
||||
jep-version: ["4.1.1"]
|
||||
ghidrathon-version: ["3.0.0"]
|
||||
ghidra-version: ["11.0.1"]
|
||||
public-version: ["PUBLIC_20240130"] # for ghidra releases
|
||||
ghidrathon-version: ["4.0.0"]
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Set up Java ${{ matrix.java-version }}
|
||||
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: ${{ matrix.java-version }}
|
||||
- name: Set up Gradle ${{ matrix.gradle-version }}
|
||||
uses: gradle/gradle-build-action@40b6781dcdec2762ad36556682ac74e31030cfe2 # v2.5.1
|
||||
with:
|
||||
gradle-version: ${{ matrix.gradle-version }}
|
||||
- name: Install Jep ${{ matrix.jep-version }}
|
||||
run : pip install jep==${{ matrix.jep-version }}
|
||||
- name: Install Ghidra ${{ matrix.ghidra-version }}
|
||||
run: |
|
||||
mkdir ./.github/ghidra
|
||||
@@ -186,14 +200,17 @@ jobs:
|
||||
- name: Install Ghidrathon
|
||||
run : |
|
||||
mkdir ./.github/ghidrathon
|
||||
curl -o ./.github/ghidrathon/ghidrathon-${{ matrix.ghidrathon-version }}.zip "https://codeload.github.com/mandiant/Ghidrathon/zip/refs/tags/v${{ matrix.ghidrathon-version }}"
|
||||
unzip .github/ghidrathon/ghidrathon-${{ matrix.ghidrathon-version }}.zip -d .github/ghidrathon/
|
||||
gradle -p ./.github/ghidrathon/Ghidrathon-${{ matrix.ghidrathon-version }}/ -PGHIDRA_INSTALL_DIR=$(pwd)/.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC
|
||||
unzip .github/ghidrathon/Ghidrathon-${{ matrix.ghidrathon-version }}/dist/*.zip -d .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC/Ghidra/Extensions
|
||||
wget "https://github.com/mandiant/Ghidrathon/releases/download/v${{ matrix.ghidrathon-version }}/Ghidrathon-v${{ matrix.ghidrathon-version}}.zip" -O ./.github/ghidrathon/ghidrathon-v${{ matrix.ghidrathon-version }}.zip
|
||||
unzip .github/ghidrathon/ghidrathon-v${{ matrix.ghidrathon-version }}.zip -d .github/ghidrathon/
|
||||
python -m pip install -r .github/ghidrathon/requirements.txt
|
||||
python .github/ghidrathon/ghidrathon_configure.py $(pwd)/.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC
|
||||
unzip .github/ghidrathon/Ghidrathon-v${{ matrix.ghidrathon-version }}.zip -d .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC/Ghidra/Extensions
|
||||
- name: Install pyyaml
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Install capa
|
||||
run: pip install -e .[dev]
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install -e .[dev,scripts]
|
||||
- name: Run tests
|
||||
run: |
|
||||
mkdir ./.github/ghidra/project
|
||||
@@ -201,4 +218,4 @@ jobs:
|
||||
cat ../output.log
|
||||
exit_code=$(cat ../output.log | grep exit | awk '{print $NF}')
|
||||
exit $exit_code
|
||||
|
||||
|
||||
|
||||
89
.github/workflows/web-deploy.yml
vendored
Normal file
89
.github/workflows/web-deploy.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
name: deploy web to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, "wb/webui-actions-1" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
|
||||
# Allows to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-landing-page:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: landing-page
|
||||
path: './web/public'
|
||||
|
||||
build-explorer:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 1
|
||||
show-progress: true
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/explorer/package-lock.json'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
working-directory: ./web/explorer
|
||||
- name: Generate release bundle
|
||||
run: npm run build:bundle
|
||||
working-directory: ./web/explorer
|
||||
- name: Zip release bundle
|
||||
run: zip -r public/capa-explorer-web.zip capa-explorer-web
|
||||
working-directory: ./web/explorer
|
||||
- name: Build
|
||||
run: npm run build
|
||||
working-directory: ./web/explorer
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: explorer
|
||||
path: './web/explorer/dist'
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-landing-page, build-explorer]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: landing-page
|
||||
path: './public/'
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: explorer
|
||||
path: './public/explorer'
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './public'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
42
.github/workflows/web-tests.yml
vendored
Normal file
42
.github/workflows/web-tests.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Capa Explorer Web tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'web/explorer/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 1
|
||||
show-progress: true
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/explorer/package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
working-directory: ./web/explorer
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
working-directory: ./web/explorer
|
||||
|
||||
- name: Format
|
||||
run: npm run format:check
|
||||
working-directory: ./web/explorer
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test
|
||||
working-directory: ./web/explorer
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,8 +1,6 @@
|
||||
[submodule "rules"]
|
||||
path = rules
|
||||
url = ../capa-rules.git
|
||||
branch = dynamic-syntax
|
||||
url = ../../mandiant/capa-rules.git
|
||||
[submodule "tests/data"]
|
||||
path = tests/data
|
||||
url = ../capa-testfiles.git
|
||||
branch = dynamic-feature-extractor
|
||||
url = ../../mandiant/capa-testfiles.git
|
||||
|
||||
@@ -110,6 +110,16 @@ repos:
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: deptry
|
||||
name: deptry
|
||||
stages: [push, manual]
|
||||
language: system
|
||||
entry: deptry .
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pytest-fast
|
||||
@@ -127,3 +137,4 @@ repos:
|
||||
- "--ignore=tests/test_scripts.py"
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
|
||||
149
CHANGELOG.md
149
CHANGELOG.md
@@ -4,10 +4,8 @@
|
||||
|
||||
### New Features
|
||||
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
||||
### New Rules (0)
|
||||
|
||||
-
|
||||
@@ -19,8 +17,147 @@
|
||||
### Development
|
||||
|
||||
### Raw diffs
|
||||
- [capa v7.0.0...master](https://github.com/mandiant/capa/compare/v7.0.0...master)
|
||||
- [capa-rules v7.0.0...master](https://github.com/mandiant/capa-rules/compare/v7.0.0...master)
|
||||
- [capa v7.2.0...master](https://github.com/mandiant/capa/compare/v7.2.0...master)
|
||||
- [capa-rules v7.2.0...master](https://github.com/mandiant/capa-rules/compare/v7.2.0...master)
|
||||
|
||||
### v7.2.0
|
||||
capa v7.2.0 introduces a first version of capa explorer web: a web-based user interface to inspect capa results using your browser. Users can inspect capa result JSON documents in an online web instance or a standalone HTML page for offline usage. capa explorer supports interactive exploring of capa results to make it easier to understand them. Users can filter, sort, and see the details of all identified capabilities. capa explorer web was worked on by @s-ff as part of a [GSoC project](https://summerofcode.withgoogle.com/programs/2024/projects/cR3hjbsq), and it is available at https://mandiant.github.io/capa/explorer/#/.
|
||||
|
||||
This release also adds a feature extractor for output from the DRAKVUF sandbox. Now, analysts can pass the resulting `drakmon.log` file to capa and extract capabilities from the artifacts captured by the sandbox. This feature extractor will also be added to the DRAKVUF sandbox as a post-processing script, and it was worked on by @yelhamer as part of a [GSoC project](https://summerofcode.withgoogle.com/programs/2024/projects/fCnBGuEC).
|
||||
|
||||
Additionally, we fixed several bugs handling ELF files, and added the ability to filter capa analysis by functions or processes. We also added support to the IDA Pro extractor to leverage analyst recovered API names.
|
||||
|
||||
Special thanks to our repeat and new contributors:
|
||||
* @lakshayletsgo for their first contribution in https://github.com/mandiant/capa/pull/2248
|
||||
* @msm-cert for their first contribution in https://github.com/mandiant/capa/pull/2143
|
||||
* @VascoSch92 for their first contribution in https://github.com/mandiant/capa/pull/2143
|
||||
|
||||
### New Features
|
||||
|
||||
- webui: explore capa analysis results in a web-based UI online and offline #2224 @s-ff
|
||||
- support analyzing DRAKVUF traces #2143 @yelhamer
|
||||
- IDA extractor: extract names from dynamically resolved APIs stored in renamed global variables #2201 @Ana06
|
||||
- cli: add the ability to select which specific functions or processes to analyze @yelhamer
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### New Rules (5)
|
||||
|
||||
- nursery/upload-file-to-onedrive jaredswilson@google.com ervinocampo@google.com
|
||||
- data-manipulation/encoding/base64/decode-data-using-base64-via-vbmi-lookup-table still@teamt5.org
|
||||
- communication/socket/attach-bpf-to-socket-on-linux jakub.jozwiak@mandiant.com
|
||||
- anti-analysis/anti-av/overwrite-dll-text-section-to-remove-hooks jakub.jozwiak@mandiant.com
|
||||
- nursery/delete-file-on-linux mehunhoff@google.com
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- elf: extract import / export symbols from stripped binaries #2096 @ygasparis
|
||||
- elf: fix handling of symbols in corrupt ELF files #2226 @williballenthin
|
||||
|
||||
### capa explorer IDA Pro plugin
|
||||
|
||||
### Development
|
||||
- CI: use macos-12 since macos-11 is deprecated and will be removed on June 28th, 2024 #2173 @mr-tz
|
||||
- CI: update Binary Ninja version to 4.1 and use Python 3.9 to test it #2211 @xusheng6
|
||||
- CI: update tests.yml workflow to exclude web and documentation files #2263 @s-ff
|
||||
- CI: update build.yml workflow to exclude web and documentation files #2270 @s-ff
|
||||
|
||||
### Raw diffs
|
||||
- [capa v7.1.0...7.2.0](https://github.com/mandiant/capa/compare/v7.1.0...7.2.0)
|
||||
- [capa-rules v7.1.0...7.2.0](https://github.com/mandiant/capa-rules/compare/v7.1.0...7.2.0)
|
||||
|
||||
## v7.1.0
|
||||
The v7.1.0 release brings large performance improvements to capa's rule matching engine.
|
||||
Additionally, we've fixed various bugs and added new features for people using and developing capa.
|
||||
|
||||
Special thanks to our repeat and new contributors:
|
||||
* @sjha2048 made their first contribution in https://github.com/mandiant/capa/pull/2000
|
||||
* @Rohit1123 made their first contribution in https://github.com/mandiant/capa/pull/1990
|
||||
* @psahithireddy made their first contribution in https://github.com/mandiant/capa/pull/2020
|
||||
* @Atlas-64 made their first contribution in https://github.com/mandiant/capa/pull/2018
|
||||
* @s-ff made their first contribution in https://github.com/mandiant/capa/pull/2011
|
||||
* @samadpls made their first contribution in https://github.com/mandiant/capa/pull/2024
|
||||
* @acelynnzhang made their first contribution in https://github.com/mandiant/capa/pull/2044
|
||||
* @RainRat made their first contribution in https://github.com/mandiant/capa/pull/2058
|
||||
* @ReversingWithMe made their first contribution in https://github.com/mandiant/capa/pull/2093
|
||||
* @malwarefrank made their first contribution in https://github.com/mandiant/capa/pull/2037
|
||||
|
||||
### New Features
|
||||
- Emit "dotnet" as format to ResultDocument when processing .NET files #2024 @samadpls
|
||||
- ELF: detect OS from statically-linked Go binaries #1978 @williballenthin
|
||||
- add function in capa/helpers to load plain and compressed JSON reports #1883 @Rohit1123
|
||||
- document Antivirus warnings and VirusTotal false positive detections #2028 @RionEV @mr-tz
|
||||
- Add json to sarif conversion script @reversingwithme
|
||||
- render maec/* fields #843 @s-ff
|
||||
- replace Halo spinner with Rich #2086 @s-ff
|
||||
- optimize rule matching #2080 @williballenthin
|
||||
- add aarch64 as a valid architecture #2144 mehunhoff@google.com @williballenthin
|
||||
- relax dependency version requirements for the capa library #2053 @williballenthin
|
||||
- add scripts dependency group and update documentation #2145 @mr-tz
|
||||
|
||||
### New Rules (25)
|
||||
|
||||
- impact/wipe-disk/delete-drive-layout-via-ioctl william.ballenthin@mandiant.com
|
||||
- host-interaction/driver/interact-with-driver-via-ioctl moritz.raabe@mandiant.com
|
||||
- host-interaction/driver/unload-driver moritz.raabe@mandiant.com
|
||||
- nursery/get-disk-information-via-ioctl william.ballenthin@mandiant.com
|
||||
- nursery/get-volume-information-via-ioctl william.ballenthin@mandiant.com
|
||||
- nursery/unmount-volume-via-ioctl william.ballenthin@mandiant.com
|
||||
- data-manipulation/encryption/rc4/encrypt-data-using-rc4-via-systemfunction033 daniel.stepanic@elastic.co
|
||||
- anti-analysis/anti-forensic/self-deletion/self-delete-using-alternate-data-streams daniel.stepanic@elastic.co
|
||||
- nursery/change-memory-permission-on-linux mehunhoff@google.com
|
||||
- nursery/check-file-permission-on-linux mehunhoff@google.com
|
||||
- nursery/check-if-process-is-running-under-android-emulator-on-android mehunhoff@google.com
|
||||
- nursery/map-or-unmap-memory-on-linux mehunhoff@google.com
|
||||
- persistence/act-as-share-provider-dll jakub.jozwiak@mandiant.com
|
||||
- persistence/act-as-windbg-extension jakub.jozwiak@mandiant.com
|
||||
- persistence/act-as-time-provider-dll jakub.jozwiak@mandiant.com
|
||||
- host-interaction/gui/window/hide/hide-graphical-window-from-taskbar jakub.jozwiak@mandiant.com
|
||||
- compiler/dart/compiled-with-dart jakub.jozwiak@mandiant.com
|
||||
- nursery/bypass-hidden-api-restrictions-via-jni-on-android mehunhoff@google.com
|
||||
- nursery/get-current-process-filesystem-mounts-on-linux mehunhoff@google.com
|
||||
- nursery/get-current-process-memory-mapping-on-linux mehunhoff@google.com
|
||||
- nursery/get-system-property-on-android mehunhoff@google.com
|
||||
- nursery/hook-routines-via-lsplant mehunhoff@google.com
|
||||
- nursery/load-packed-dex-via-jiagu-on-android mehunhoff@google.com
|
||||
- nursery/modify-api-blacklist-or-denylist-via-jni-on-android mehunhoff@google.com
|
||||
- nursery/truncate-file-on-linux mehunhoff@google.com
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- do some imports closer to where they are used #1810 @williballenthin
|
||||
- binja: fix and simplify stack string detection code after binja 4.0 @xusheng6
|
||||
- binja: add support for forwarded export #1646 @xusheng6
|
||||
- cape: support more report formats #2035 @mr-tz
|
||||
|
||||
### capa explorer IDA Pro plugin
|
||||
- replace deprecated IDA API find_binary with bin_search #1606 @s-ff
|
||||
|
||||
### Development
|
||||
|
||||
- ci: Fix PR review in the changelog check GH action #2004 @Ana06
|
||||
- ci: use rules number badge stored in our bot gist and generated using `schneegans/dynamic-badges-action` #2001 capa-rules#882 @Ana06
|
||||
- ci: update github workflows to use latest version of actions that were using a deprecated version of node #1967 #2003 capa-rules#883 @sjha2048 @Ana06
|
||||
- ci: update binja version to stable 4.0 #2016 @xusheng6
|
||||
- ci: update github workflows to reflect the latest ghidrathon installation and bumped up jep, ghidra versions #2020 @psahithireddy
|
||||
- ci: include rule caching in PyInstaller build process #2097 @s-ff
|
||||
- add deptry support #1497 @s-ff
|
||||
|
||||
### Raw diffs
|
||||
- [capa v7.0.1...v7.1.0](https://github.com/mandiant/capa/compare/v7.0.1...v7.1.0)
|
||||
- [capa-rules v7.0.1...v7.1.0](https://github.com/mandiant/capa-rules/compare/v7.0.1...v7.1.0)
|
||||
|
||||
## v7.0.1
|
||||
|
||||
This release fixes a circular import error when using capa as a library.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix potentially circular import errors #1969 @williballenthin
|
||||
|
||||
### Raw diffs
|
||||
- [capa v7.0.0...v7.0.1](https://github.com/mandiant/capa/compare/v7.0.0...v7.0.1)
|
||||
- [capa-rules v7.0.0...v7.0.1](https://github.com/mandiant/capa-rules/compare/v7.0.0...v7.0.1)
|
||||
|
||||
## v7.0.0
|
||||
This is the v7.0.0 release of capa which was mainly worked on during the Google Summer of Code (GSoC) 2023. A huge
|
||||
@@ -258,7 +395,7 @@ For those that use capa as a library, we've introduced some limited breaking cha
|
||||
- [capa-rules v5.1.0...v6.0.0](https://github.com/mandiant/capa-rules/compare/v5.1.0...v6.0.0)
|
||||
|
||||
## v5.1.0
|
||||
capa version 5.1.0 adds a Protocol Buffers (protobuf) format for result documents. Additionally, the [Vector35](https://vector35.com/) team contributed a new feature extractor using Binary Ninja. Other new features are a new CLI flag to override the detected operating system, functionality to read and render existing result documents, and a output color format that's easier to read.
|
||||
capa version 5.1.0 adds a Protocol Buffers (protobuf) format for result documents. Additionally, the [Vector35](https://vector35.com/) team contributed a new feature extractor using Binary Ninja. Other new features are a new CLI flag to override the detected operating system, functionality to read and render existing result documents, and an output color format that's easier to read.
|
||||
|
||||
Over 25 capa rules have been added and improved.
|
||||
|
||||
@@ -1457,7 +1594,7 @@ The IDA Pro integration is now distributed as a real plugin, instead of a script
|
||||
- updates distributed PyPI/`pip install --upgrade` without touching your `%IDADIR%`
|
||||
- generally doing thing the "right way"
|
||||
|
||||
How to get this new version? Its easy: download [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory and update your capa installation (incidentally, this is a good opportunity to migrate to `pip install flare-capa` instead of git checkouts). Now you should see the plugin listed in the `Edit > Plugins > FLARE capa explorer` menu in IDA.
|
||||
How to get this new version? It's easy: download [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory and update your capa installation (incidentally, this is a good opportunity to migrate to `pip install flare-capa` instead of git checkouts). Now you should see the plugin listed in the `Edit > Plugins > FLARE capa explorer` menu in IDA.
|
||||
|
||||
Please refer to the plugin [readme](https://github.com/mandiant/capa/blob/master/capa/ida/plugin/README.md) for additional information on installing and using the IDA Pro plugin.
|
||||
|
||||
|
||||
8
CITATION.cff
Normal file
8
CITATION.cff
Normal file
@@ -0,0 +1,8 @@
|
||||
cff-version: 1.2.0
|
||||
message: "If you use this software, please cite it as below."
|
||||
authors:
|
||||
- name: "The FLARE Team"
|
||||
title: "capa, a tool to identify capabilities in programs and sandbox traces."
|
||||
date-released: 2020-07-16
|
||||
url: "https://github.com/mandiant/capa"
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2023 Mandiant, Inc.
|
||||
Copyright (C) 2020 Mandiant, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
50
README.md
50
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://pypi.org/project/flare-capa)
|
||||
[](https://github.com/mandiant/capa/releases)
|
||||
[](https://github.com/mandiant/capa-rules)
|
||||
[](https://github.com/mandiant/capa-rules)
|
||||
[](https://github.com/mandiant/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
|
||||
[](https://github.com/mandiant/capa/releases)
|
||||
[](LICENSE.txt)
|
||||
@@ -11,12 +11,13 @@ capa detects capabilities in executable files.
|
||||
You run it against a PE, ELF, .NET module, shellcode file, or a sandbox report and it tells you what it thinks the program can do.
|
||||
For example, it might suggest that the file is a backdoor, is capable of installing services, or relies on HTTP to communicate.
|
||||
|
||||
Check out:
|
||||
- the overview in our first [capa blog post](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities)
|
||||
- the major version 2.0 updates described in our [second blog post](https://www.mandiant.com/resources/capa-2-better-stronger-faster)
|
||||
- the major version 3.0 (ELF support) described in the [third blog post](https://www.mandiant.com/resources/elfant-in-the-room-capa-v3)
|
||||
- the major version 4.0 (.NET support) described in the [fourth blog post](https://www.mandiant.com/resources/blog/capa-v4-casting-wider-net)
|
||||
To interactively inspect capa results in your browser use the [capa web explorer](https://mandiant.github.io/capa/explorer/).
|
||||
|
||||
If you want to inspect or write capa rules, head on over to the [capa-rules repository](https://github.com/mandiant/capa-rules). Otherwise, keep reading.
|
||||
|
||||
Below you find a list of [our capa blog posts with more details.](#blog-posts)
|
||||
|
||||
# example capa output
|
||||
```
|
||||
$ capa.exe suspicious.exe
|
||||
|
||||
@@ -71,16 +72,23 @@ Download stable releases of the standalone capa binaries [here](https://github.c
|
||||
|
||||
To use capa as a library or integrate with another tool, see [doc/installation.md](https://github.com/mandiant/capa/blob/master/doc/installation.md) for further setup instructions.
|
||||
|
||||
For more information about how to use capa, see [doc/usage.md](https://github.com/mandiant/capa/blob/master/doc/usage.md).
|
||||
# web explorer
|
||||
The [capa web explorer](https://mandiant.github.io/capa/explorer/) enables you to interactively explore capa results in your web browser. Besides the online version you can download a standalone HTML file for local offline usage.
|
||||
|
||||

|
||||
|
||||
More details on the web UI is available in the [capa web explorer README](https://github.com/mandiant/capa/blob/master/web/explorer/README.md).
|
||||
|
||||
# example
|
||||
|
||||
In the above sample output, we ran capa against an unknown binary (`suspicious.exe`),
|
||||
and the tool reported that the program can send HTTP requests, decode data via XOR and Base64,
|
||||
In the above sample output, we run capa against an unknown binary (`suspicious.exe`),
|
||||
and the tool reports that the program can send HTTP requests, decode data via XOR and Base64,
|
||||
install services, and spawn new processes.
|
||||
Taken together, this makes us think that `suspicious.exe` could be a persistent backdoor.
|
||||
Therefore, our next analysis step might be to run `suspicious.exe` in a sandbox and try to recover the command and control server.
|
||||
|
||||
## detailed results
|
||||
|
||||
By passing the `-vv` flag (for very verbose), capa reports exactly where it found evidence of these capabilities.
|
||||
This is useful for at least two reasons:
|
||||
|
||||
@@ -125,8 +133,11 @@ function @ 0x4011C0
|
||||
...
|
||||
```
|
||||
|
||||
Additionally, capa also supports analyzing [CAPE](https://github.com/kevoreilly/CAPEv2) sandbox reports for dynamic capabilty extraction.
|
||||
In order to use this, you first submit your sample to CAPE for analysis, and then run capa against the generated report (JSON).
|
||||
## analyzing sandbox reports
|
||||
Additionally, capa also supports analyzing sandbox reports for dynamic capability extraction.
|
||||
In order to use this, you first submit your sample to one of supported sandboxes for analysis, and then run capa against the generated report file.
|
||||
|
||||
Currently, capa supports the [CAPE sandbox](https://github.com/kevoreilly/CAPEv2) and the [DRAKVUF sandbox](https://github.com/CERT-Polska/drakvuf-sandbox/). In order to use either, simply run capa against the generated file (JSON for CAPE or LOG for DRAKVUF sandbox) and it will automatically detect the sandbox and extract capabilities from it.
|
||||
|
||||
Here's an example of running capa against a packed binary, and then running capa against the CAPE report of that binary:
|
||||
|
||||
@@ -215,6 +226,7 @@ $ capa 05be49819139a3fdcdbddbdefd298398779521f3d68daa25275cc77508e42310.json
|
||||
┕━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙
|
||||
```
|
||||
|
||||
# capa rules
|
||||
capa uses a collection of rules to identify capabilities within a program.
|
||||
These rules are easy to write, even for those new to reverse engineering.
|
||||
By authoring rules, you can extend the capabilities that capa recognizes.
|
||||
@@ -251,15 +263,27 @@ rule:
|
||||
- property/read: System.Net.Sockets.TcpClient::Client
|
||||
```
|
||||
|
||||
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
|
||||
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard rules that are distributed with capa.
|
||||
Please learn to write rules and contribute new entries as you find interesting techniques in malware.
|
||||
|
||||
# IDA Pro plugin: capa explorer
|
||||
If you use IDA Pro, then you can use the [capa explorer](https://github.com/mandiant/capa/tree/master/capa/ida/plugin) plugin.
|
||||
capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted directly from your IDA Pro database.
|
||||
It also uses your local changes to the .idb to extract better features, such as when you rename a global variable that contains a dynamically resolved API address.
|
||||
|
||||

|
||||
|
||||
If you use Ghidra, you can use the Python 3 [Ghidra feature extractor](/capa/ghidra/). This integration enables capa to extract features directly from your Ghidra database, which can help you identify capabilities in programs that you analyze using Ghidra.
|
||||
# Ghidra integration
|
||||
If you use Ghidra, then you can use the [capa + Ghidra integration](/capa/ghidra/) to run capa's analysis directly on your Ghidra database and render the results in Ghidra's user interface.
|
||||
|
||||
<img src="https://github.com/mandiant/capa/assets/66766340/eeae33f4-99d4-42dc-a5e8-4c1b8c661492" width=300>
|
||||
|
||||
# blog posts
|
||||
- [Dynamic capa: Exploring Executable Run-Time Behavior with the CAPE Sandbox](https://www.mandiant.com/resources/blog/dynamic-capa-executable-behavior-cape-sandbox)
|
||||
- [capa v4: casting a wider .NET](https://www.mandiant.com/resources/blog/capa-v4-casting-wider-net) (.NET support)
|
||||
- [ELFant in the Room – capa v3](https://www.mandiant.com/resources/elfant-in-the-room-capa-v3) (ELF support)
|
||||
- [capa 2.0: Better, Stronger, Faster](https://www.mandiant.com/resources/capa-2-better-stronger-faster)
|
||||
- [capa: Automatically Identify Malware Capabilities](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities)
|
||||
|
||||
# further information
|
||||
## capa
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# 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 sys
|
||||
import logging
|
||||
import itertools
|
||||
import collections
|
||||
@@ -65,7 +66,7 @@ def find_thread_capabilities(
|
||||
features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
# matches found at the call scope.
|
||||
# might be found at different calls, thats ok.
|
||||
# might be found at different calls, that's ok.
|
||||
call_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
for ch in extractor.get_calls(ph, th):
|
||||
@@ -103,11 +104,11 @@ def find_process_capabilities(
|
||||
process_features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
# matches found at the basic threads.
|
||||
# might be found at different threads, thats ok.
|
||||
# might be found at different threads, that's ok.
|
||||
thread_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
# matches found at the call scope.
|
||||
# might be found at different calls, thats ok.
|
||||
# might be found at different calls, that's ok.
|
||||
call_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
for th in extractor.get_threads(ph):
|
||||
@@ -147,6 +148,11 @@ def find_dynamic_capabilities(
|
||||
def pbar(s, *args, **kwargs):
|
||||
return s
|
||||
|
||||
elif not sys.stderr.isatty():
|
||||
# don't display progress bar when stderr is redirected to a file
|
||||
def pbar(s, *args, **kwargs):
|
||||
return s
|
||||
|
||||
processes = list(extractor.get_processes())
|
||||
|
||||
pb = pbar(processes, desc="matching", unit=" processes", leave=False)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# 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 sys
|
||||
import time
|
||||
import logging
|
||||
import itertools
|
||||
@@ -66,7 +67,7 @@ def find_basic_block_capabilities(
|
||||
features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
# matches found at the instruction scope.
|
||||
# might be found at different instructions, thats ok.
|
||||
# might be found at different instructions, that's ok.
|
||||
insn_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
for insn in extractor.get_instructions(f, bb):
|
||||
@@ -106,11 +107,11 @@ def find_code_capabilities(
|
||||
function_features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
# matches found at the basic block scope.
|
||||
# might be found at different basic blocks, thats ok.
|
||||
# might be found at different basic blocks, that's ok.
|
||||
bb_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
# matches found at the instruction scope.
|
||||
# might be found at different instructions, thats ok.
|
||||
# might be found at different instructions, that's ok.
|
||||
insn_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
for bb in extractor.get_basic_blocks(fh):
|
||||
@@ -156,6 +157,11 @@ def find_static_capabilities(
|
||||
def pbar(s, *args, **kwargs):
|
||||
return s
|
||||
|
||||
elif not sys.stderr.isatty():
|
||||
# don't display progress bar when stderr is redirected to a file
|
||||
def pbar(s, *args, **kwargs):
|
||||
return s
|
||||
|
||||
functions = list(extractor.get_functions())
|
||||
n_funcs = len(functions)
|
||||
|
||||
@@ -182,9 +188,16 @@ def find_static_capabilities(
|
||||
)
|
||||
t1 = time.time()
|
||||
|
||||
match_count = sum(len(res) for res in function_matches.values())
|
||||
match_count += sum(len(res) for res in bb_matches.values())
|
||||
match_count += sum(len(res) for res in insn_matches.values())
|
||||
match_count = 0
|
||||
for name, matches_ in itertools.chain(
|
||||
function_matches.items(), bb_matches.items(), insn_matches.items()
|
||||
):
|
||||
# in practice, most matches are derived rules,
|
||||
# like "check OS version/5bf4c7f39fd4492cbed0f6dc7d596d49"
|
||||
# but when we log to the human, they really care about "real" rules.
|
||||
if not ruleset.rules[name].is_subscope_rule():
|
||||
match_count += len(matches_)
|
||||
|
||||
logger.debug(
|
||||
"analyzed function 0x%x and extracted %d features, %d matches in %0.02fs",
|
||||
f.address,
|
||||
@@ -213,7 +226,7 @@ def find_static_capabilities(
|
||||
all_file_matches, feature_count = find_file_capabilities(ruleset, extractor, function_and_lower_features)
|
||||
feature_counts.file = feature_count
|
||||
|
||||
matches = dict(
|
||||
matches: MatchResults = dict(
|
||||
itertools.chain(
|
||||
# each rule exists in exactly one scope,
|
||||
# so there won't be any overlap among these following MatchResults,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -102,14 +102,14 @@ class And(Statement):
|
||||
super().__init__(description=description)
|
||||
self.children = children
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
def evaluate(self, features: FeatureSet, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.and"] += 1
|
||||
|
||||
if short_circuit:
|
||||
results = []
|
||||
for child in self.children:
|
||||
result = child.evaluate(ctx, short_circuit=short_circuit)
|
||||
result = child.evaluate(features, short_circuit=short_circuit)
|
||||
results.append(result)
|
||||
if not result:
|
||||
# short circuit
|
||||
@@ -117,7 +117,7 @@ class And(Statement):
|
||||
|
||||
return Result(True, self, results)
|
||||
else:
|
||||
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
|
||||
results = [child.evaluate(features, short_circuit=short_circuit) for child in self.children]
|
||||
success = all(results)
|
||||
return Result(success, self, results)
|
||||
|
||||
@@ -135,14 +135,14 @@ class Or(Statement):
|
||||
super().__init__(description=description)
|
||||
self.children = children
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
def evaluate(self, features: FeatureSet, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.or"] += 1
|
||||
|
||||
if short_circuit:
|
||||
results = []
|
||||
for child in self.children:
|
||||
result = child.evaluate(ctx, short_circuit=short_circuit)
|
||||
result = child.evaluate(features, short_circuit=short_circuit)
|
||||
results.append(result)
|
||||
if result:
|
||||
# short circuit as soon as we hit one match
|
||||
@@ -150,7 +150,7 @@ class Or(Statement):
|
||||
|
||||
return Result(False, self, results)
|
||||
else:
|
||||
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
|
||||
results = [child.evaluate(features, short_circuit=short_circuit) for child in self.children]
|
||||
success = any(results)
|
||||
return Result(success, self, results)
|
||||
|
||||
@@ -162,11 +162,11 @@ class Not(Statement):
|
||||
super().__init__(description=description)
|
||||
self.child = child
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
def evaluate(self, features: FeatureSet, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.not"] += 1
|
||||
|
||||
results = [self.child.evaluate(ctx, short_circuit=short_circuit)]
|
||||
results = [self.child.evaluate(features, short_circuit=short_circuit)]
|
||||
success = not results[0]
|
||||
return Result(success, self, results)
|
||||
|
||||
@@ -185,7 +185,7 @@ class Some(Statement):
|
||||
self.count = count
|
||||
self.children = children
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
def evaluate(self, features: FeatureSet, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.some"] += 1
|
||||
|
||||
@@ -193,7 +193,7 @@ class Some(Statement):
|
||||
results = []
|
||||
satisfied_children_count = 0
|
||||
for child in self.children:
|
||||
result = child.evaluate(ctx, short_circuit=short_circuit)
|
||||
result = child.evaluate(features, short_circuit=short_circuit)
|
||||
results.append(result)
|
||||
if result:
|
||||
satisfied_children_count += 1
|
||||
@@ -204,7 +204,7 @@ class Some(Statement):
|
||||
|
||||
return Result(False, self, results)
|
||||
else:
|
||||
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
|
||||
results = [child.evaluate(features, short_circuit=short_circuit) for child in self.children]
|
||||
# note that here we cast the child result as a bool
|
||||
# because we've overridden `__bool__` above.
|
||||
#
|
||||
@@ -214,7 +214,7 @@ class Some(Statement):
|
||||
|
||||
|
||||
class Range(Statement):
|
||||
"""match if the child is contained in the ctx set with a count in the given range."""
|
||||
"""match if the child is contained in the feature set with a count in the given range."""
|
||||
|
||||
def __init__(self, child, min=None, max=None, description=None):
|
||||
super().__init__(description=description)
|
||||
@@ -222,15 +222,15 @@ class Range(Statement):
|
||||
self.min = min if min is not None else 0
|
||||
self.max = max if max is not None else (1 << 64 - 1)
|
||||
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
def evaluate(self, features: FeatureSet, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.range"] += 1
|
||||
|
||||
count = len(ctx.get(self.child, []))
|
||||
count = len(features.get(self.child, []))
|
||||
if self.min == 0 and count == 0:
|
||||
return Result(True, self, [])
|
||||
|
||||
return Result(self.min <= count <= self.max, self, [], locations=ctx.get(self.child))
|
||||
return Result(self.min <= count <= self.max, self, [], locations=features.get(self.child))
|
||||
|
||||
def __str__(self):
|
||||
if self.max == (1 << 64 - 1):
|
||||
@@ -250,7 +250,7 @@ class Subscope(Statement):
|
||||
self.scope = scope
|
||||
self.child = child
|
||||
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
def evaluate(self, features: FeatureSet, short_circuit=True):
|
||||
raise ValueError("cannot evaluate a subscope directly!")
|
||||
|
||||
|
||||
@@ -270,6 +270,14 @@ class Subscope(Statement):
|
||||
MatchResults = Mapping[str, List[Tuple[Address, Result]]]
|
||||
|
||||
|
||||
def get_rule_namespaces(rule: "capa.rules.Rule") -> Iterator[str]:
|
||||
namespace = rule.meta.get("namespace")
|
||||
if namespace:
|
||||
while namespace:
|
||||
yield namespace
|
||||
namespace, _, _ = namespace.rpartition("/")
|
||||
|
||||
|
||||
def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations: Iterable[Address]):
|
||||
"""
|
||||
record into the given featureset that the given rule matched at the given locations.
|
||||
@@ -280,11 +288,8 @@ def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations:
|
||||
updates `features` in-place. doesn't modify the remaining arguments.
|
||||
"""
|
||||
features[capa.features.common.MatchedRule(rule.name)].update(locations)
|
||||
namespace = rule.meta.get("namespace")
|
||||
if namespace:
|
||||
while namespace:
|
||||
features[capa.features.common.MatchedRule(namespace)].update(locations)
|
||||
namespace, _, _ = namespace.rpartition("/")
|
||||
for namespace in get_rule_namespaces(rule):
|
||||
features[capa.features.common.MatchedRule(namespace)].update(locations)
|
||||
|
||||
|
||||
def match(rules: List["capa.rules.Rule"], features: FeatureSet, addr: Address) -> Tuple[FeatureSet, MatchResults]:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -23,3 +23,15 @@ class UnsupportedOSError(ValueError):
|
||||
|
||||
class EmptyReportError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidArgument(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class NonExistantFunctionError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class NonExistantProcessError(ValueError):
|
||||
pass
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -93,7 +93,7 @@ class ThreadAddress(Address):
|
||||
|
||||
|
||||
class DynamicCallAddress(Address):
|
||||
"""addesses a call in a dynamic execution trace"""
|
||||
"""addresses a call in a dynamic execution trace"""
|
||||
|
||||
def __init__(self, thread: ThreadAddress, id: int):
|
||||
assert id >= 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -128,7 +128,7 @@ class Feature(abc.ABC): # noqa: B024
|
||||
|
||||
def __lt__(self, other):
|
||||
# implementing sorting by serializing to JSON is a huge hack.
|
||||
# its slow, inelegant, and probably doesn't work intuitively;
|
||||
# it's slow, inelegant, and probably doesn't work intuitively;
|
||||
# however, we only use it for deterministic output, so it's good enough for now.
|
||||
|
||||
# circular import
|
||||
@@ -166,10 +166,10 @@ class Feature(abc.ABC): # noqa: B024
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def evaluate(self, ctx: Dict["Feature", Set[Address]], **kwargs) -> Result:
|
||||
def evaluate(self, features: "capa.engine.FeatureSet", short_circuit=True) -> Result:
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature." + self.name] += 1
|
||||
return Result(self in ctx, self, [], locations=ctx.get(self, set()))
|
||||
return Result(self in features, self, [], locations=features.get(self, set()))
|
||||
|
||||
|
||||
class MatchedRule(Feature):
|
||||
@@ -207,7 +207,7 @@ class Substring(String):
|
||||
super().__init__(value, description=description)
|
||||
self.value = value
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
def evaluate(self, features: "capa.engine.FeatureSet", short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.substring"] += 1
|
||||
|
||||
@@ -216,7 +216,7 @@ class Substring(String):
|
||||
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
|
||||
|
||||
assert isinstance(self.value, str)
|
||||
for feature, locations in ctx.items():
|
||||
for feature, locations in features.items():
|
||||
if not isinstance(feature, (String,)):
|
||||
continue
|
||||
|
||||
@@ -227,7 +227,7 @@ class Substring(String):
|
||||
if self.value in feature.value:
|
||||
matches[feature.value].update(locations)
|
||||
if short_circuit:
|
||||
# we found one matching string, thats sufficient to match.
|
||||
# we found one matching string, that's sufficient to match.
|
||||
# don't collect other matching strings in this mode.
|
||||
break
|
||||
|
||||
@@ -299,7 +299,7 @@ class Regex(String):
|
||||
f"invalid regular expression: {value} it should use Python syntax, try it at https://pythex.org"
|
||||
) from exc
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
def evaluate(self, features: "capa.engine.FeatureSet", short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.regex"] += 1
|
||||
|
||||
@@ -307,7 +307,7 @@ class Regex(String):
|
||||
# will unique the locations later on.
|
||||
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
|
||||
|
||||
for feature, locations in ctx.items():
|
||||
for feature, locations in features.items():
|
||||
if not isinstance(feature, (String,)):
|
||||
continue
|
||||
|
||||
@@ -322,7 +322,7 @@ class Regex(String):
|
||||
if self.re.search(feature.value):
|
||||
matches[feature.value].update(locations)
|
||||
if short_circuit:
|
||||
# we found one matching string, thats sufficient to match.
|
||||
# we found one matching string, that's sufficient to match.
|
||||
# don't collect other matching strings in this mode.
|
||||
break
|
||||
|
||||
@@ -384,12 +384,14 @@ class Bytes(Feature):
|
||||
super().__init__(value, description=description)
|
||||
self.value = value
|
||||
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
def evaluate(self, features: "capa.engine.FeatureSet", short_circuit=True):
|
||||
assert isinstance(self.value, bytes)
|
||||
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.bytes"] += 1
|
||||
capa.perf.counters["evaluate.feature.bytes." + str(len(self.value))] += 1
|
||||
|
||||
assert isinstance(self.value, bytes)
|
||||
for feature, locations in ctx.items():
|
||||
for feature, locations in features.items():
|
||||
if not isinstance(feature, (Bytes,)):
|
||||
continue
|
||||
|
||||
@@ -407,9 +409,10 @@ class Bytes(Feature):
|
||||
# other candidates here: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
|
||||
ARCH_I386 = "i386"
|
||||
ARCH_AMD64 = "amd64"
|
||||
ARCH_AARCH64 = "aarch64"
|
||||
# dotnet
|
||||
ARCH_ANY = "any"
|
||||
VALID_ARCH = (ARCH_I386, ARCH_AMD64, ARCH_ANY)
|
||||
VALID_ARCH = (ARCH_I386, ARCH_AMD64, ARCH_AARCH64, ARCH_ANY)
|
||||
|
||||
|
||||
class Arch(Feature):
|
||||
@@ -434,11 +437,11 @@ class OS(Feature):
|
||||
super().__init__(value, description=description)
|
||||
self.name = "os"
|
||||
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
def evaluate(self, features: "capa.engine.FeatureSet", short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature." + self.name] += 1
|
||||
|
||||
for feature, locations in ctx.items():
|
||||
for feature, locations in features.items():
|
||||
if not isinstance(feature, (OS,)):
|
||||
continue
|
||||
|
||||
@@ -458,6 +461,7 @@ FORMAT_AUTO = "auto"
|
||||
FORMAT_SC32 = "sc32"
|
||||
FORMAT_SC64 = "sc64"
|
||||
FORMAT_CAPE = "cape"
|
||||
FORMAT_DRAKVUF = "drakvuf"
|
||||
FORMAT_FREEZE = "freeze"
|
||||
FORMAT_RESULT = "result"
|
||||
STATIC_FORMATS = {
|
||||
@@ -471,6 +475,7 @@ STATIC_FORMATS = {
|
||||
}
|
||||
DYNAMIC_FORMATS = {
|
||||
FORMAT_CAPE,
|
||||
FORMAT_DRAKVUF,
|
||||
FORMAT_FREEZE,
|
||||
FORMAT_RESULT,
|
||||
}
|
||||
@@ -486,6 +491,6 @@ class Format(Feature):
|
||||
def is_global_feature(feature):
|
||||
"""
|
||||
is this a feature that is extracted at every scope?
|
||||
today, these are OS and arch features.
|
||||
today, these are OS, arch, and format features.
|
||||
"""
|
||||
return isinstance(feature, (OS, Arch))
|
||||
return isinstance(feature, (OS, Arch, Format))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -9,7 +9,9 @@
|
||||
import abc
|
||||
import hashlib
|
||||
import dataclasses
|
||||
from typing import Any, Dict, Tuple, Union, Iterator
|
||||
from copy import copy
|
||||
from types import MethodType
|
||||
from typing import Any, Set, Dict, Tuple, Union, Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
# TODO(williballenthin): use typing.TypeAlias directly when Python 3.9 is deprecated
|
||||
@@ -75,7 +77,7 @@ class BBHandle:
|
||||
|
||||
@dataclass
|
||||
class InsnHandle:
|
||||
"""reference to a instruction recognized by a feature extractor.
|
||||
"""reference to an instruction recognized by a feature extractor.
|
||||
|
||||
Attributes:
|
||||
address: the address of the instruction address.
|
||||
@@ -296,6 +298,22 @@ class StaticFeatureExtractor:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def FunctionFilter(extractor: StaticFeatureExtractor, functions: Set) -> StaticFeatureExtractor:
|
||||
original_get_functions = extractor.get_functions
|
||||
|
||||
def filtered_get_functions(self):
|
||||
yield from (f for f in original_get_functions() if f.address in functions)
|
||||
|
||||
# we make a copy of the original extractor object and then update its get_functions() method with the decorated filter one.
|
||||
# this is in order to preserve the original extractor object's get_functions() method, in case it is used elsewhere in the code.
|
||||
# an example where this is important is in our testfiles where we may use the same extractor object with different tests,
|
||||
# with some of these tests needing to install a functions filter on the extractor object.
|
||||
new_extractor = copy(extractor)
|
||||
new_extractor.get_functions = MethodType(filtered_get_functions, extractor) # type: ignore
|
||||
|
||||
return new_extractor
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessHandle:
|
||||
"""
|
||||
@@ -467,4 +485,20 @@ class DynamicFeatureExtractor:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def ProcessFilter(extractor: DynamicFeatureExtractor, processes: Set) -> DynamicFeatureExtractor:
|
||||
original_get_processes = extractor.get_processes
|
||||
|
||||
def filtered_get_processes(self):
|
||||
yield from (f for f in original_get_processes() if f.address.pid in processes)
|
||||
|
||||
# we make a copy of the original extractor object and then update its get_processes() method with the decorated filter one.
|
||||
# this is in order to preserve the original extractor object's get_processes() method, in case it is used elsewhere in the code.
|
||||
# an example where this is important is in our testfiles where we may use the same extractor object with different tests,
|
||||
# with some of these tests needing to install a processes filter on the extractor object.
|
||||
new_extractor = copy(extractor)
|
||||
new_extractor.get_processes = MethodType(filtered_get_processes, extractor) # type: ignore
|
||||
|
||||
return new_extractor
|
||||
|
||||
|
||||
FeatureExtractor: TypeAlias = Union[StaticFeatureExtractor, DynamicFeatureExtractor]
|
||||
|
||||
@@ -7,17 +7,15 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from binaryninja import Function, Settings
|
||||
from binaryninja import Function
|
||||
from binaryninja import BasicBlock as BinjaBasicBlock
|
||||
from binaryninja import (
|
||||
BinaryView,
|
||||
SymbolType,
|
||||
RegisterValueType,
|
||||
VariableSourceType,
|
||||
MediumLevelILSetVar,
|
||||
MediumLevelILOperation,
|
||||
MediumLevelILBasicBlock,
|
||||
MediumLevelILInstruction,
|
||||
@@ -29,11 +27,6 @@ from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
|
||||
|
||||
use_const_outline: bool = False
|
||||
settings: Settings = Settings()
|
||||
if settings.contains("analysis.outlining.builtins") and settings.get_bool("analysis.outlining.builtins"):
|
||||
use_const_outline = True
|
||||
|
||||
|
||||
def get_printable_len_ascii(s: bytes) -> int:
|
||||
"""Return string length if all operand bytes are ascii or utf16-le printable"""
|
||||
@@ -65,7 +58,7 @@ def get_stack_string_len(f: Function, il: MediumLevelILInstruction) -> int:
|
||||
|
||||
addr = target.value.value
|
||||
sym = bv.get_symbol_at(addr)
|
||||
if not sym or sym.type != SymbolType.LibraryFunctionSymbol:
|
||||
if not sym or sym.type not in [SymbolType.LibraryFunctionSymbol, SymbolType.SymbolicFunctionSymbol]:
|
||||
return 0
|
||||
|
||||
if sym.name not in ["__builtin_strncpy", "__builtin_strcpy", "__builtin_wcscpy"]:
|
||||
@@ -91,52 +84,6 @@ def get_stack_string_len(f: Function, il: MediumLevelILInstruction) -> int:
|
||||
return max(get_printable_len_ascii(bytes(s)), get_printable_len_wide(bytes(s)))
|
||||
|
||||
|
||||
def get_printable_len(il: MediumLevelILSetVar) -> int:
|
||||
"""Return string length if all operand bytes are ascii or utf16-le printable"""
|
||||
width = il.dest.type.width
|
||||
value = il.src.value.value
|
||||
|
||||
if width == 1:
|
||||
chars = struct.pack("<B", value & 0xFF)
|
||||
elif width == 2:
|
||||
chars = struct.pack("<H", value & 0xFFFF)
|
||||
elif width == 4:
|
||||
chars = struct.pack("<I", value & 0xFFFFFFFF)
|
||||
elif width == 8:
|
||||
chars = struct.pack("<Q", value & 0xFFFFFFFFFFFFFFFF)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def is_printable_ascii(chars_: bytes):
|
||||
return all(c < 127 and chr(c) in string.printable for c in chars_)
|
||||
|
||||
def is_printable_utf16le(chars_: bytes):
|
||||
if all(c == 0x00 for c in chars_[1::2]):
|
||||
return is_printable_ascii(chars_[::2])
|
||||
|
||||
if is_printable_ascii(chars):
|
||||
return width
|
||||
|
||||
if is_printable_utf16le(chars):
|
||||
return width // 2
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def is_mov_imm_to_stack(il: MediumLevelILInstruction) -> bool:
|
||||
"""verify instruction moves immediate onto stack"""
|
||||
if il.operation != MediumLevelILOperation.MLIL_SET_VAR:
|
||||
return False
|
||||
|
||||
if il.src.operation != MediumLevelILOperation.MLIL_CONST:
|
||||
return False
|
||||
|
||||
if il.dest.source_type != VariableSourceType.StackVariableSourceType:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def bb_contains_stackstring(f: Function, bb: MediumLevelILBasicBlock) -> bool:
|
||||
"""check basic block for stackstring indicators
|
||||
|
||||
@@ -144,14 +91,10 @@ def bb_contains_stackstring(f: Function, bb: MediumLevelILBasicBlock) -> bool:
|
||||
"""
|
||||
count = 0
|
||||
for il in bb:
|
||||
if use_const_outline:
|
||||
count += get_stack_string_len(f, il)
|
||||
else:
|
||||
if is_mov_imm_to_stack(il):
|
||||
count += get_printable_len(il)
|
||||
count += get_stack_string_len(f, il)
|
||||
if count > MIN_STACKSTRING_LEN:
|
||||
return True
|
||||
|
||||
if count > MIN_STACKSTRING_LEN:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ from capa.features.extractors.base_extractor import (
|
||||
|
||||
class BinjaFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self, bv: binja.BinaryView):
|
||||
super().__init__(hashes=SampleHashes.from_bytes(bv.file.raw.read(0, len(bv.file.raw))))
|
||||
super().__init__(hashes=SampleHashes.from_bytes(bv.file.raw.read(0, bv.file.raw.length)))
|
||||
self.bv = bv
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.binja.file.extract_file_format(self.bv))
|
||||
|
||||
@@ -74,13 +74,18 @@ def extract_file_embedded_pe(bv: BinaryView) -> Iterator[Tuple[Feature, Address]
|
||||
|
||||
def extract_file_export_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function exports"""
|
||||
for sym in bv.get_symbols_of_type(SymbolType.FunctionSymbol):
|
||||
for sym in bv.get_symbols_of_type(SymbolType.FunctionSymbol) + bv.get_symbols_of_type(SymbolType.DataSymbol):
|
||||
if sym.binding in [SymbolBinding.GlobalBinding, SymbolBinding.WeakBinding]:
|
||||
name = sym.short_name
|
||||
yield Export(name), AbsoluteVirtualAddress(sym.address)
|
||||
unmangled_name = unmangle_c_name(name)
|
||||
if name != unmangled_name:
|
||||
yield Export(unmangled_name), AbsoluteVirtualAddress(sym.address)
|
||||
if name.startswith("__forwarder_name(") and name.endswith(")"):
|
||||
yield Export(name[17:-1]), AbsoluteVirtualAddress(sym.address)
|
||||
yield Characteristic("forwarded export"), AbsoluteVirtualAddress(sym.address)
|
||||
else:
|
||||
yield Export(name), AbsoluteVirtualAddress(sym.address)
|
||||
|
||||
unmangled_name = unmangle_c_name(name)
|
||||
if name != unmangled_name:
|
||||
yield Export(unmangled_name), AbsoluteVirtualAddress(sym.address)
|
||||
|
||||
for sym in bv.get_symbols_of_type(SymbolType.DataSymbol):
|
||||
if sym.binding not in [SymbolBinding.GlobalBinding]:
|
||||
|
||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
# When the script gets executed as a standalone executable (via PyInstaller), `import binaryninja` does not work because
|
||||
# we have excluded the binaryninja module in `pyinstaller.spec`. The trick here is to call the system Python and try
|
||||
# to find out the path of the binaryninja module that has been installed.
|
||||
# Note, including the binaryninja module in the `pyintaller.spec` would not work, since the binaryninja module tries to
|
||||
# Note, including the binaryninja module in the `pyinstaller.spec` would not work, since the binaryninja module tries to
|
||||
# find the binaryninja core e.g., `libbinaryninjacore.dylib`, using a relative path. And this does not work when the
|
||||
# binaryninja module is extracted by the PyInstaller.
|
||||
code = r"""
|
||||
|
||||
@@ -48,7 +48,7 @@ def extract_format(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
else:
|
||||
logger.warning("unknown file format, file command output: %s", report.target.file.type)
|
||||
raise ValueError(
|
||||
"unrecognized file format from the CAPE report; output of file command: {report.target.file.type}"
|
||||
f"unrecognized file format from the CAPE report; output of file command: {report.target.file.type}"
|
||||
)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ def extract_os(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
else:
|
||||
# if the operating system information is missing from the cape report, it's likely a bug
|
||||
logger.warning("unrecognized OS: %s", file_output)
|
||||
raise ValueError("unrecognized OS from the CAPE report; output of file command: {file_output}")
|
||||
raise ValueError(f"unrecognized OS from the CAPE report; output of file command: {file_output}")
|
||||
else:
|
||||
# the sample is shellcode
|
||||
logger.debug("unsupported file format, file command output: %s", file_output)
|
||||
|
||||
@@ -46,7 +46,7 @@ class FlexibleModel(BaseModel):
|
||||
|
||||
|
||||
# use this type to indicate that we won't model this data.
|
||||
# because its not relevant to our use in capa.
|
||||
# because it's not relevant to our use in capa.
|
||||
#
|
||||
# while its nice to have full coverage of the data shape,
|
||||
# it can easily change and break our parsing.
|
||||
@@ -230,7 +230,7 @@ class File(FlexibleModel):
|
||||
sha1: str
|
||||
sha256: str
|
||||
sha512: str
|
||||
sha3_384: str
|
||||
sha3_384: Optional[str] = None
|
||||
ssdeep: str
|
||||
# unsure why this would ever be "False"
|
||||
tlsh: Optional[Union[str, bool]] = None
|
||||
@@ -356,8 +356,8 @@ class Behavior(ExactModel):
|
||||
anomaly: List[str]
|
||||
encryptedbuffers: List[EncryptedBuffer]
|
||||
# these are small objects that describe atomic events,
|
||||
# like file move, registery access.
|
||||
# we'll detect the same with our API call analyis.
|
||||
# like file move, registry access.
|
||||
# we'll detect the same with our API call analysis.
|
||||
enhanced: Skip = None
|
||||
|
||||
|
||||
@@ -398,7 +398,7 @@ class CapeReport(FlexibleModel):
|
||||
behavior: Behavior
|
||||
|
||||
# post-processed results: payloads and extracted configs
|
||||
CAPE: Optional[Cape] = None
|
||||
CAPE: Optional[Union[Cape, List]] = None
|
||||
dropped: Optional[List[File]] = None
|
||||
procdump: Optional[List[ProcessFile]] = None
|
||||
procmemory: ListTODO
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -83,7 +83,7 @@ def read_dotnet_user_string(pe: dnfile.dnPE, token: StringToken) -> Optional[str
|
||||
return None
|
||||
|
||||
try:
|
||||
user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get_us(token.rid)
|
||||
user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get(token.rid)
|
||||
except UnicodeDecodeError as e:
|
||||
logger.debug("failed to decode #US stream index 0x%08x (%s)", token.rid, e)
|
||||
return None
|
||||
@@ -119,14 +119,14 @@ def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
access: Optional[str]
|
||||
|
||||
# assume .NET imports starting with get_/set_ are used to access a property
|
||||
if member_ref.Name.startswith("get_"):
|
||||
member_ref_name: str = str(member_ref.Name)
|
||||
if member_ref_name.startswith("get_"):
|
||||
access = FeatureAccess.READ
|
||||
elif member_ref.Name.startswith("set_"):
|
||||
elif member_ref_name.startswith("set_"):
|
||||
access = FeatureAccess.WRITE
|
||||
else:
|
||||
access = None
|
||||
|
||||
member_ref_name: str = member_ref.Name
|
||||
if member_ref_name.startswith(("get_", "set_")):
|
||||
# remove get_/set_ from MemberRef name
|
||||
member_ref_name = member_ref_name[4:]
|
||||
@@ -212,7 +212,7 @@ def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
token: int = calculate_dotnet_token_value(method.table.number, method.row_index)
|
||||
access: Optional[str] = accessor_map.get(token)
|
||||
|
||||
method_name: str = method.row.Name
|
||||
method_name: str = str(method.row.Name)
|
||||
if method_name.startswith(("get_", "set_")):
|
||||
# remove get_/set_
|
||||
method_name = method_name[4:]
|
||||
@@ -289,8 +289,8 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod]
|
||||
logger.debug("ImplMap[0x%X] ImportScope row is None", rid)
|
||||
module = ""
|
||||
else:
|
||||
module = impl_map.ImportScope.row.Name
|
||||
method: str = impl_map.ImportName
|
||||
module = str(impl_map.ImportScope.row.Name)
|
||||
method: str = str(impl_map.ImportName)
|
||||
|
||||
member_forward_table: int
|
||||
if impl_map.MemberForwarded.table is None:
|
||||
@@ -320,8 +320,11 @@ def get_dotnet_table_row(pe: dnfile.dnPE, table_index: int, row_index: int) -> O
|
||||
if row_index - 1 <= 0:
|
||||
return None
|
||||
|
||||
table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(table_index)
|
||||
if table is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
table = pe.net.mdtables.tables.get(table_index, [])
|
||||
return table[row_index - 1]
|
||||
except IndexError:
|
||||
return None
|
||||
@@ -334,7 +337,7 @@ def resolve_nested_typedef_name(
|
||||
|
||||
if index in nested_class_table:
|
||||
typedef_name = []
|
||||
name = typedef.TypeName
|
||||
name = str(typedef.TypeName)
|
||||
|
||||
# Append the current typedef name
|
||||
typedef_name.append(name)
|
||||
@@ -343,24 +346,24 @@ def resolve_nested_typedef_name(
|
||||
# Iterate through the typedef table to resolve the nested name
|
||||
table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, nested_class_table[index])
|
||||
if table_row is None:
|
||||
return typedef.TypeNamespace, tuple(typedef_name[::-1])
|
||||
return str(typedef.TypeNamespace), tuple(typedef_name[::-1])
|
||||
|
||||
name = table_row.TypeName
|
||||
name = str(table_row.TypeName)
|
||||
typedef_name.append(name)
|
||||
index = nested_class_table[index]
|
||||
|
||||
# Document the root enclosing details
|
||||
table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, nested_class_table[index])
|
||||
if table_row is None:
|
||||
return typedef.TypeNamespace, tuple(typedef_name[::-1])
|
||||
return str(typedef.TypeNamespace), tuple(typedef_name[::-1])
|
||||
|
||||
enclosing_name = table_row.TypeName
|
||||
enclosing_name = str(table_row.TypeName)
|
||||
typedef_name.append(enclosing_name)
|
||||
|
||||
return table_row.TypeNamespace, tuple(typedef_name[::-1])
|
||||
return str(table_row.TypeNamespace), tuple(typedef_name[::-1])
|
||||
|
||||
else:
|
||||
return typedef.TypeNamespace, (typedef.TypeName,)
|
||||
return str(typedef.TypeNamespace), (str(typedef.TypeName),)
|
||||
|
||||
|
||||
def resolve_nested_typeref_name(
|
||||
@@ -370,29 +373,29 @@ def resolve_nested_typeref_name(
|
||||
# If the ResolutionScope decodes to a typeRef type then it is nested
|
||||
if isinstance(typeref.ResolutionScope.table, dnfile.mdtable.TypeRef):
|
||||
typeref_name = []
|
||||
name = typeref.TypeName
|
||||
name = str(typeref.TypeName)
|
||||
# Not appending the current typeref name to avoid potential duplicate
|
||||
|
||||
# Validate index
|
||||
table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeRef.number, index)
|
||||
if table_row is None:
|
||||
return typeref.TypeNamespace, (typeref.TypeName,)
|
||||
return str(typeref.TypeNamespace), (str(typeref.TypeName),)
|
||||
|
||||
while isinstance(table_row.ResolutionScope.table, dnfile.mdtable.TypeRef):
|
||||
# Iterate through the typeref table to resolve the nested name
|
||||
typeref_name.append(name)
|
||||
name = table_row.TypeName
|
||||
name = str(table_row.TypeName)
|
||||
table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeRef.number, table_row.ResolutionScope.row_index)
|
||||
if table_row is None:
|
||||
return typeref.TypeNamespace, tuple(typeref_name[::-1])
|
||||
return str(typeref.TypeNamespace), tuple(typeref_name[::-1])
|
||||
|
||||
# Document the root enclosing details
|
||||
typeref_name.append(table_row.TypeName)
|
||||
typeref_name.append(str(table_row.TypeName))
|
||||
|
||||
return table_row.TypeNamespace, tuple(typeref_name[::-1])
|
||||
return str(table_row.TypeNamespace), tuple(typeref_name[::-1])
|
||||
|
||||
else:
|
||||
return typeref.TypeNamespace, (typeref.TypeName,)
|
||||
return str(typeref.TypeNamespace), (str(typeref.TypeName),)
|
||||
|
||||
|
||||
def get_dotnet_nested_class_table_index(pe: dnfile.dnPE) -> Dict[int, int]:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -49,8 +49,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_file_format(**kwargs) -> Iterator[Tuple[Format, Address]]:
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
yield Format(FORMAT_DOTNET), NO_ADDRESS
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Import, Address]]:
|
||||
@@ -78,12 +78,12 @@ def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple
|
||||
for _, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
# emit internal .NET namespaces
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
namespaces.add(typedef.TypeNamespace)
|
||||
namespaces.add(str(typedef.TypeNamespace))
|
||||
|
||||
for _, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
|
||||
# emit external .NET namespaces
|
||||
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
|
||||
namespaces.add(typeref.TypeNamespace)
|
||||
namespaces.add(str(typeref.TypeNamespace))
|
||||
|
||||
# namespaces may be empty, discard
|
||||
namespaces.discard("")
|
||||
|
||||
56
capa/features/extractors/drakvuf/call.py
Normal file
56
capa/features/extractors/drakvuf/call.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
# 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
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.insn import API, Number
|
||||
from capa.features.common import String, Feature
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle
|
||||
from capa.features.extractors.drakvuf.models import Call
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_call_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
This method extracts the given call's features (such as API name and arguments),
|
||||
and returns them as API, Number, and String features.
|
||||
|
||||
args:
|
||||
ph: process handle (for defining the extraction scope)
|
||||
th: thread handle (for defining the extraction scope)
|
||||
ch: call handle (for defining the extraction scope)
|
||||
|
||||
yields:
|
||||
Feature, address; where Feature is either: API, Number, or String.
|
||||
"""
|
||||
call: Call = ch.inner
|
||||
|
||||
# list similar to disassembly: arguments right-to-left, call
|
||||
for arg_value in reversed(call.arguments.values()):
|
||||
try:
|
||||
yield Number(int(arg_value, 0)), ch.address
|
||||
except ValueError:
|
||||
# DRAKVUF automatically resolves the contents of memory addresses, (e.g. Arg1="0xc6f217efe0:\"ntdll.dll\"").
|
||||
# For those cases we yield the entire string as it, since yielding the address only would
|
||||
# likely not provide any matches, and yielding just the memory contentswould probably be misleading,
|
||||
# but yielding the entire string would be helpful for an analyst looking at the verbose output
|
||||
yield String(arg_value), ch.address
|
||||
|
||||
yield API(call.name), ch.address
|
||||
|
||||
|
||||
def extract_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for handler in CALL_HANDLERS:
|
||||
for feature, addr in handler(ph, th, ch):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
CALL_HANDLERS = (extract_call_features,)
|
||||
96
capa/features/extractors/drakvuf/extractor.py
Normal file
96
capa/features/extractors/drakvuf/extractor.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
# 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
|
||||
from typing import Dict, List, Tuple, Union, Iterator
|
||||
|
||||
import capa.features.extractors.drakvuf.call
|
||||
import capa.features.extractors.drakvuf.file
|
||||
import capa.features.extractors.drakvuf.thread
|
||||
import capa.features.extractors.drakvuf.global_
|
||||
import capa.features.extractors.drakvuf.process
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import NO_ADDRESS, Address, ThreadAddress, ProcessAddress, AbsoluteVirtualAddress, _NoAddress
|
||||
from capa.features.extractors.base_extractor import (
|
||||
CallHandle,
|
||||
SampleHashes,
|
||||
ThreadHandle,
|
||||
ProcessHandle,
|
||||
DynamicFeatureExtractor,
|
||||
)
|
||||
from capa.features.extractors.drakvuf.models import Call, DrakvufReport
|
||||
from capa.features.extractors.drakvuf.helpers import index_calls
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DrakvufExtractor(DynamicFeatureExtractor):
|
||||
def __init__(self, report: DrakvufReport):
|
||||
super().__init__(
|
||||
# DRAKVUF currently does not yield hash information about the sample in its output
|
||||
hashes=SampleHashes(md5="", sha1="", sha256="")
|
||||
)
|
||||
|
||||
self.report: DrakvufReport = report
|
||||
|
||||
# sort the api calls to prevent going through the entire list each time
|
||||
self.sorted_calls: Dict[ProcessAddress, Dict[ThreadAddress, List[Call]]] = index_calls(report)
|
||||
|
||||
# pre-compute these because we'll yield them at *every* scope.
|
||||
self.global_features = list(capa.features.extractors.drakvuf.global_.extract_features(self.report))
|
||||
|
||||
def get_base_address(self) -> Union[AbsoluteVirtualAddress, _NoAddress, None]:
|
||||
# DRAKVUF currently does not yield information about the PE's address
|
||||
return NO_ADDRESS
|
||||
|
||||
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from self.global_features
|
||||
|
||||
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.drakvuf.file.extract_features(self.report)
|
||||
|
||||
def get_processes(self) -> Iterator[ProcessHandle]:
|
||||
yield from capa.features.extractors.drakvuf.file.get_processes(self.sorted_calls)
|
||||
|
||||
def extract_process_features(self, ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.drakvuf.process.extract_features(ph)
|
||||
|
||||
def get_process_name(self, ph: ProcessHandle) -> str:
|
||||
return ph.inner["process_name"]
|
||||
|
||||
def get_threads(self, ph: ProcessHandle) -> Iterator[ThreadHandle]:
|
||||
yield from capa.features.extractors.drakvuf.process.get_threads(self.sorted_calls, ph)
|
||||
|
||||
def extract_thread_features(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
if False:
|
||||
# force this routine to be a generator,
|
||||
# but we don't actually have any elements to generate.
|
||||
yield Characteristic("never"), NO_ADDRESS
|
||||
return
|
||||
|
||||
def get_calls(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[CallHandle]:
|
||||
yield from capa.features.extractors.drakvuf.thread.get_calls(self.sorted_calls, ph, th)
|
||||
|
||||
def get_call_name(self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> str:
|
||||
call: Call = ch.inner
|
||||
call_name = "{}({}){}".format(
|
||||
call.name,
|
||||
", ".join(f"{arg_name}={arg_value}" for arg_name, arg_value in call.arguments.items()),
|
||||
(f" -> {getattr(call, 'return_value', '')}"), # SysCalls don't have a return value, while WinApi calls do
|
||||
)
|
||||
return call_name
|
||||
|
||||
def extract_call_features(
|
||||
self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.drakvuf.call.extract_features(ph, th, ch)
|
||||
|
||||
@classmethod
|
||||
def from_report(cls, report: Iterator[Dict]) -> "DrakvufExtractor":
|
||||
dr = DrakvufReport.from_raw_report(report)
|
||||
return DrakvufExtractor(report=dr)
|
||||
56
capa/features/extractors/drakvuf/file.py
Normal file
56
capa/features/extractors/drakvuf/file.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
# 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
|
||||
from typing import Dict, List, Tuple, Iterator
|
||||
|
||||
from capa.features.file import Import
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, ThreadAddress, ProcessAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.helpers import generate_symbols
|
||||
from capa.features.extractors.base_extractor import ProcessHandle
|
||||
from capa.features.extractors.drakvuf.models import Call, DrakvufReport
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_processes(calls: Dict[ProcessAddress, Dict[ThreadAddress, List[Call]]]) -> Iterator[ProcessHandle]:
|
||||
"""
|
||||
Get all the created processes for a sample.
|
||||
"""
|
||||
for proc_addr, calls_per_thread in calls.items():
|
||||
sample_call = next(iter(calls_per_thread.values()))[0] # get process name
|
||||
yield ProcessHandle(proc_addr, inner={"process_name": sample_call.process_name})
|
||||
|
||||
|
||||
def extract_import_names(report: DrakvufReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
Extract imported function names.
|
||||
"""
|
||||
if report.loaded_dlls is None:
|
||||
return
|
||||
dlls = report.loaded_dlls
|
||||
|
||||
for dll in dlls:
|
||||
dll_base_name = dll.name.split("\\")[-1]
|
||||
for function_name, function_address in dll.imports.items():
|
||||
for name in generate_symbols(dll_base_name, function_name, include_dll=True):
|
||||
yield Import(name), AbsoluteVirtualAddress(function_address)
|
||||
|
||||
|
||||
def extract_features(report: DrakvufReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for handler in FILE_HANDLERS:
|
||||
for feature, addr in handler(report):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
# TODO(yelhamer): extract more file features from other DRAKVUF plugins
|
||||
# https://github.com/mandiant/capa/issues/2169
|
||||
extract_import_names,
|
||||
)
|
||||
44
capa/features/extractors/drakvuf/global_.py
Normal file
44
capa/features/extractors/drakvuf/global_.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
# 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
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.common import OS, FORMAT_PE, ARCH_AMD64, OS_WINDOWS, Arch, Format, Feature
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
from capa.features.extractors.drakvuf.models import DrakvufReport
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_format(report: DrakvufReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
# DRAKVUF sandbox currently supports only Windows as the guest: https://drakvuf-sandbox.readthedocs.io/en/latest/usage/getting_started.html
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_os(report: DrakvufReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
# DRAKVUF sandbox currently supports only PE files: https://drakvuf-sandbox.readthedocs.io/en/latest/usage/getting_started.html
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_arch(report: DrakvufReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
# DRAKVUF sandbox currently supports only x64 Windows as the guest: https://drakvuf-sandbox.readthedocs.io/en/latest/usage/getting_started.html
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_features(report: DrakvufReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for global_handler in GLOBAL_HANDLER:
|
||||
for feature, addr in global_handler(report):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
GLOBAL_HANDLER = (
|
||||
extract_format,
|
||||
extract_os,
|
||||
extract_arch,
|
||||
)
|
||||
39
capa/features/extractors/drakvuf/helpers.py
Normal file
39
capa/features/extractors/drakvuf/helpers.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
# 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 itertools
|
||||
from typing import Dict, List
|
||||
|
||||
from capa.features.address import ThreadAddress, ProcessAddress
|
||||
from capa.features.extractors.drakvuf.models import Call, DrakvufReport
|
||||
|
||||
|
||||
def index_calls(report: DrakvufReport) -> Dict[ProcessAddress, Dict[ThreadAddress, List[Call]]]:
|
||||
# this method organizes calls into processes and threads, and then sorts them based on
|
||||
# timestamp so that we can address individual calls per index (CallAddress requires call index)
|
||||
result: Dict[ProcessAddress, Dict[ThreadAddress, List[Call]]] = {}
|
||||
for call in itertools.chain(report.syscalls, report.apicalls):
|
||||
if call.pid == 0:
|
||||
# DRAKVUF captures api/native calls from all processes running on the system.
|
||||
# we ignore the pid 0 since it's a system process and it's unlikely for it to
|
||||
# be hijacked or so on, in addition to capa addresses not supporting null pids
|
||||
continue
|
||||
proc_addr = ProcessAddress(pid=call.pid, ppid=call.ppid)
|
||||
thread_addr = ThreadAddress(process=proc_addr, tid=call.tid)
|
||||
if proc_addr not in result:
|
||||
result[proc_addr] = {}
|
||||
if thread_addr not in result[proc_addr]:
|
||||
result[proc_addr][thread_addr] = []
|
||||
|
||||
result[proc_addr][thread_addr].append(call)
|
||||
|
||||
for proc, threads in result.items():
|
||||
for thread in threads:
|
||||
result[proc][thread].sort(key=lambda call: call.timestamp)
|
||||
|
||||
return result
|
||||
137
capa/features/extractors/drakvuf/models.py
Normal file
137
capa/features/extractors/drakvuf/models.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
# 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
|
||||
from typing import Any, Dict, List, Iterator
|
||||
|
||||
from pydantic import Field, BaseModel, ConfigDict, model_validator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIRED_SYSCALL_FIELD_NAMES = {
|
||||
"Plugin",
|
||||
"TimeStamp",
|
||||
"PID",
|
||||
"PPID",
|
||||
"TID",
|
||||
"UserName",
|
||||
"UserId",
|
||||
"ProcessName",
|
||||
"Method",
|
||||
"EventUID",
|
||||
"Module",
|
||||
"vCPU",
|
||||
"CR3",
|
||||
"Syscall",
|
||||
"NArgs",
|
||||
}
|
||||
|
||||
|
||||
class ConciseModel(BaseModel):
|
||||
ConfigDict(extra="ignore")
|
||||
|
||||
|
||||
class DiscoveredDLL(ConciseModel):
|
||||
plugin_name: str = Field(alias="Plugin")
|
||||
event: str = Field(alias="Event")
|
||||
name: str = Field(alias="DllName")
|
||||
pid: int = Field(alias="PID")
|
||||
|
||||
|
||||
class LoadedDLL(ConciseModel):
|
||||
plugin_name: str = Field(alias="Plugin")
|
||||
event: str = Field(alias="Event")
|
||||
name: str = Field(alias="DllName")
|
||||
imports: Dict[str, int] = Field(alias="Rva")
|
||||
|
||||
|
||||
class Call(ConciseModel):
|
||||
plugin_name: str = Field(alias="Plugin")
|
||||
timestamp: str = Field(alias="TimeStamp")
|
||||
process_name: str = Field(alias="ProcessName")
|
||||
ppid: int = Field(alias="PPID")
|
||||
pid: int = Field(alias="PID")
|
||||
tid: int = Field(alias="TID")
|
||||
name: str = Field(alias="Method")
|
||||
arguments: Dict[str, str]
|
||||
|
||||
|
||||
class WinApiCall(Call):
|
||||
# This class models Windows API calls captured by DRAKVUF (DLLs, etc.).
|
||||
arguments: Dict[str, str] = Field(alias="Arguments")
|
||||
event: str = Field(alias="Event")
|
||||
return_value: str = Field(alias="ReturnValue")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def build_arguments(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
args = values["Arguments"]
|
||||
values["Arguments"] = dict(arg.split("=", 1) for arg in args)
|
||||
return values
|
||||
|
||||
|
||||
class SystemCall(Call):
|
||||
# This class models native Windows API calls captured by DRAKVUF.
|
||||
# Schema: {
|
||||
# "Plugin": "syscall",
|
||||
# "TimeStamp": "1716999134.582553",
|
||||
# "PID": 3888, "PPID": 2852, "TID": 368, "UserName": "SessionID", "UserId": 2,
|
||||
# "ProcessName": "\\Device\\HarddiskVolume2\\Windows\\explorer.exe",
|
||||
# "Method": "NtSetIoCompletionEx",
|
||||
# "EventUID": "0x27",
|
||||
# "Module": "nt",
|
||||
# "vCPU": 0,
|
||||
# "CR3": "0x119b1002",
|
||||
# "Syscall": 419,
|
||||
# "NArgs": 6,
|
||||
# "IoCompletionHandle": "0xffffffff80001ac0", "IoCompletionReserveHandle": "0xffffffff8000188c",
|
||||
# "KeyContext": "0x0", "ApcContext": "0x2", "IoStatus": "0x7ffb00000000", "IoStatusInformation": "0x0"
|
||||
# }
|
||||
# The keys up until "NArgs" are common to all the native calls that DRAKVUF reports, with
|
||||
# the remaining keys representing the call's specific arguments.
|
||||
syscall_number: int = Field(alias="Syscall")
|
||||
module: str = Field(alias="Module")
|
||||
nargs: int = Field(alias="NArgs")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def build_extra(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# DRAKVUF stores argument names and values as entries in the syscall's entry.
|
||||
# This model validator collects those arguments into a list in the model.
|
||||
values["arguments"] = {
|
||||
name: value for name, value in values.items() if name not in REQUIRED_SYSCALL_FIELD_NAMES
|
||||
}
|
||||
return values
|
||||
|
||||
|
||||
class DrakvufReport(ConciseModel):
|
||||
syscalls: List[SystemCall] = []
|
||||
apicalls: List[WinApiCall] = []
|
||||
discovered_dlls: List[DiscoveredDLL] = []
|
||||
loaded_dlls: List[LoadedDLL] = []
|
||||
|
||||
@classmethod
|
||||
def from_raw_report(cls, entries: Iterator[Dict]) -> "DrakvufReport":
|
||||
report = cls()
|
||||
|
||||
for entry in entries:
|
||||
plugin = entry.get("Plugin")
|
||||
# TODO(yelhamer): add support for more DRAKVUF plugins
|
||||
# https://github.com/mandiant/capa/issues/2181
|
||||
if plugin == "syscall":
|
||||
report.syscalls.append(SystemCall(**entry))
|
||||
elif plugin == "apimon":
|
||||
event = entry.get("Event")
|
||||
if event == "api_called":
|
||||
report.apicalls.append(WinApiCall(**entry))
|
||||
elif event == "dll_loaded":
|
||||
report.loaded_dlls.append(LoadedDLL(**entry))
|
||||
elif event == "dll_discovered":
|
||||
report.discovered_dlls.append(DiscoveredDLL(**entry))
|
||||
|
||||
return report
|
||||
40
capa/features/extractors/drakvuf/process.py
Normal file
40
capa/features/extractors/drakvuf/process.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
# 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
|
||||
from typing import Dict, List, Tuple, Iterator
|
||||
|
||||
from capa.features.common import String, Feature
|
||||
from capa.features.address import Address, ThreadAddress, ProcessAddress
|
||||
from capa.features.extractors.base_extractor import ThreadHandle, ProcessHandle
|
||||
from capa.features.extractors.drakvuf.models import Call
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_threads(
|
||||
calls: Dict[ProcessAddress, Dict[ThreadAddress, List[Call]]], ph: ProcessHandle
|
||||
) -> Iterator[ThreadHandle]:
|
||||
"""
|
||||
Get the threads associated with a given process.
|
||||
"""
|
||||
for thread_addr in calls[ph.address]:
|
||||
yield ThreadHandle(address=thread_addr, inner={})
|
||||
|
||||
|
||||
def extract_process_name(ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield String(ph.inner["process_name"]), ph.address
|
||||
|
||||
|
||||
def extract_features(ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for handler in PROCESS_HANDLERS:
|
||||
for feature, addr in handler(ph):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
PROCESS_HANDLERS = (extract_process_name,)
|
||||
24
capa/features/extractors/drakvuf/thread.py
Normal file
24
capa/features/extractors/drakvuf/thread.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
# 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
|
||||
from typing import Dict, List, Iterator
|
||||
|
||||
from capa.features.address import ThreadAddress, ProcessAddress, DynamicCallAddress
|
||||
from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle
|
||||
from capa.features.extractors.drakvuf.models import Call
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_calls(
|
||||
sorted_calls: Dict[ProcessAddress, Dict[ThreadAddress, List[Call]]], ph: ProcessHandle, th: ThreadHandle
|
||||
) -> Iterator[CallHandle]:
|
||||
for i, call in enumerate(sorted_calls[ph.address][th.address]):
|
||||
call_addr = DynamicCallAddress(thread=th.address, id=i)
|
||||
yield CallHandle(address=call_addr, inner=call)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -10,10 +10,11 @@ import logging
|
||||
import itertools
|
||||
import collections
|
||||
from enum import Enum
|
||||
from typing import Set, Dict, List, Tuple, BinaryIO, Iterator, Optional
|
||||
from typing import TYPE_CHECKING, Set, Dict, List, Tuple, BinaryIO, Iterator, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
import Elf # from vivisect
|
||||
if TYPE_CHECKING:
|
||||
import Elf # from vivisect
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,6 +58,10 @@ class OS(str, Enum):
|
||||
SYLLABLE = "syllable"
|
||||
NACL = "nacl"
|
||||
ANDROID = "android"
|
||||
DRAGONFLYBSD = "dragonfly BSD"
|
||||
ILLUMOS = "illumos"
|
||||
ZOS = "z/os"
|
||||
UNIX = "unix"
|
||||
|
||||
|
||||
# via readelf: https://github.com/bminor/binutils-gdb/blob/c0e94211e1ac05049a4ce7c192c9d14d1764eb3e/binutils/readelf.c#L19635-L19658
|
||||
@@ -80,6 +85,8 @@ class Phdr:
|
||||
paddr: int
|
||||
filesz: int
|
||||
buf: bytes
|
||||
flags: int
|
||||
memsz: int
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -205,7 +212,7 @@ class ELF:
|
||||
15: OS.AROS,
|
||||
16: OS.FENIXOS,
|
||||
17: OS.CLOUD,
|
||||
# 53: "SORTFIX", # i can't find any reference to this OS, i dont think it exists
|
||||
# 53: "SORTFIX", # i can't find any reference to this OS, i don't think it exists
|
||||
# 64: "ARM_AEABI", # not an OS
|
||||
# 97: "ARM", # not an OS
|
||||
# 255: "STANDALONE", # not an OS
|
||||
@@ -314,24 +321,23 @@ class ELF:
|
||||
phent_offset = i * self.e_phentsize
|
||||
phent = self.phbuf[phent_offset : phent_offset + self.e_phentsize]
|
||||
|
||||
(p_type,) = struct.unpack_from(self.endian + "I", phent, 0x0)
|
||||
logger.debug("ph:p_type: 0x%04x", p_type)
|
||||
|
||||
if self.bitness == 32:
|
||||
p_offset, p_vaddr, p_paddr, p_filesz = struct.unpack_from(self.endian + "IIII", phent, 0x4)
|
||||
p_type, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_flags = struct.unpack_from(
|
||||
self.endian + "IIIIIII", phent, 0x0
|
||||
)
|
||||
elif self.bitness == 64:
|
||||
p_offset, p_vaddr, p_paddr, p_filesz = struct.unpack_from(self.endian + "QQQQ", phent, 0x8)
|
||||
p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz = struct.unpack_from(
|
||||
self.endian + "IIQQQQQ", phent, 0x0
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
logger.debug("ph:p_offset: 0x%02x p_filesz: 0x%04x", p_offset, p_filesz)
|
||||
|
||||
self.f.seek(p_offset)
|
||||
buf = self.f.read(p_filesz)
|
||||
if len(buf) != p_filesz:
|
||||
raise ValueError("failed to read program header content")
|
||||
|
||||
return Phdr(p_type, p_offset, p_vaddr, p_paddr, p_filesz, buf)
|
||||
return Phdr(p_type, p_offset, p_vaddr, p_paddr, p_filesz, buf, p_flags, p_memsz)
|
||||
|
||||
@property
|
||||
def program_headers(self):
|
||||
@@ -356,8 +362,6 @@ class ELF:
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
logger.debug("sh:sh_offset: 0x%02x sh_size: 0x%04x", sh_offset, sh_size)
|
||||
|
||||
self.f.seek(sh_offset)
|
||||
buf = self.f.read(sh_size)
|
||||
if len(buf) != sh_size:
|
||||
@@ -724,7 +728,7 @@ class SymTab:
|
||||
yield from self.symbols
|
||||
|
||||
@classmethod
|
||||
def from_viv(cls, elf: Elf.Elf) -> Optional["SymTab"]:
|
||||
def from_viv(cls, elf: "Elf.Elf") -> Optional["SymTab"]:
|
||||
endian = "<" if elf.getEndian() == 0 else ">"
|
||||
bitness = elf.bits
|
||||
|
||||
@@ -866,6 +870,8 @@ def guess_os_from_ident_directive(elf: ELF) -> Optional[OS]:
|
||||
return OS.LINUX
|
||||
elif "Red Hat" in comment:
|
||||
return OS.LINUX
|
||||
elif "Alpine" in comment:
|
||||
return OS.LINUX
|
||||
elif "Android" in comment:
|
||||
return OS.ANDROID
|
||||
|
||||
@@ -951,11 +957,506 @@ def guess_os_from_symtab(elf: ELF) -> Optional[OS]:
|
||||
|
||||
for os, hints in keywords.items():
|
||||
if any(hint in sym_name for hint in hints):
|
||||
logger.debug("symtab: %s looks like %s", sym_name, os)
|
||||
return os
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_go_binary(elf: ELF) -> bool:
|
||||
for shdr in elf.section_headers:
|
||||
if shdr.get_name(elf) == ".note.go.buildid":
|
||||
logger.debug("go buildinfo: found section .note.go.buildid")
|
||||
return True
|
||||
|
||||
# The `go version` command enumerates sections for the name `.go.buildinfo`
|
||||
# (in addition to looking for the BUILDINFO_MAGIC) to check if an executable is go or not.
|
||||
# See references to the `errNotGoExe` error here:
|
||||
# https://github.com/golang/go/blob/master/src/debug/buildinfo/buildinfo.go#L41
|
||||
for shdr in elf.section_headers:
|
||||
if shdr.get_name(elf) == ".go.buildinfo":
|
||||
logger.debug("go buildinfo: found section .go.buildinfo")
|
||||
return True
|
||||
|
||||
# other strategy used by FLOSS: search for known runtime strings.
|
||||
# https://github.com/mandiant/flare-floss/blob/b2ca8adfc5edf278861dd6bff67d73da39683b46/floss/language/identify.py#L88
|
||||
return False
|
||||
|
||||
|
||||
def get_go_buildinfo_data(elf: ELF) -> Optional[bytes]:
|
||||
for shdr in elf.section_headers:
|
||||
if shdr.get_name(elf) == ".go.buildinfo":
|
||||
logger.debug("go buildinfo: found section .go.buildinfo")
|
||||
return shdr.buf
|
||||
|
||||
PT_LOAD = 0x1
|
||||
PF_X = 1
|
||||
PF_W = 2
|
||||
for phdr in elf.program_headers:
|
||||
if phdr.type != PT_LOAD:
|
||||
continue
|
||||
|
||||
if (phdr.flags & (PF_X | PF_W)) == PF_W:
|
||||
logger.debug("go buildinfo: found data segment")
|
||||
return phdr.buf
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def read_data(elf: ELF, rva: int, size: int) -> Optional[bytes]:
|
||||
# ELF segments are for runtime data,
|
||||
# ELF sections are for link-time data.
|
||||
# So we want to read Program Headers/Segments.
|
||||
for phdr in elf.program_headers:
|
||||
if phdr.vaddr <= rva < phdr.vaddr + phdr.memsz:
|
||||
segment_data = phdr.buf
|
||||
|
||||
# pad the section with NULLs
|
||||
# assume page alignment is already handled.
|
||||
# might need more hardening here.
|
||||
if len(segment_data) < phdr.memsz:
|
||||
segment_data += b"\x00" * (phdr.memsz - len(segment_data))
|
||||
|
||||
segment_offset = rva - phdr.vaddr
|
||||
return segment_data[segment_offset : segment_offset + size]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def read_go_slice(elf: ELF, rva: int) -> Optional[bytes]:
|
||||
if elf.bitness == 32:
|
||||
struct_size = 8
|
||||
struct_format = elf.endian + "II"
|
||||
elif elf.bitness == 64:
|
||||
struct_size = 16
|
||||
struct_format = elf.endian + "QQ"
|
||||
else:
|
||||
raise ValueError("invalid psize")
|
||||
|
||||
struct_buf = read_data(elf, rva, struct_size)
|
||||
if not struct_buf:
|
||||
return None
|
||||
|
||||
addr, length = struct.unpack_from(struct_format, struct_buf, 0)
|
||||
|
||||
return read_data(elf, addr, length)
|
||||
|
||||
|
||||
def guess_os_from_go_buildinfo(elf: ELF) -> Optional[OS]:
|
||||
"""
|
||||
In a binary compiled by Go, the buildinfo structure may contain
|
||||
metadata about the build environment, including the configured
|
||||
GOOS, which specifies the target operating system.
|
||||
|
||||
Search for and parse the buildinfo structure,
|
||||
which may be found in the .go.buildinfo section,
|
||||
and often contains this metadata inline. Otherwise,
|
||||
follow a few byte slices to the relevant information.
|
||||
|
||||
This strategy is derived from GoReSym.
|
||||
"""
|
||||
buf = get_go_buildinfo_data(elf)
|
||||
if not buf:
|
||||
logger.debug("go buildinfo: no buildinfo section")
|
||||
return None
|
||||
|
||||
assert isinstance(buf, bytes)
|
||||
|
||||
# The build info blob left by the linker is identified by
|
||||
# a 16-byte header, consisting of:
|
||||
# - buildInfoMagic (14 bytes),
|
||||
# - the binary's pointer size (1 byte), and
|
||||
# - whether the binary is big endian (1 byte).
|
||||
#
|
||||
# Then:
|
||||
# - virtual address to Go string: runtime.buildVersion
|
||||
# - virtual address to Go string: runtime.modinfo
|
||||
#
|
||||
# On 32-bit platforms, the last 8 bytes are unused.
|
||||
#
|
||||
# If the endianness has the 2 bit set, then the pointers are zero,
|
||||
# and the 32-byte header is followed by varint-prefixed string data
|
||||
# for the two string values we care about.
|
||||
# https://github.com/mandiant/GoReSym/blob/0860a1b1b4f3495e9fb7e71eb4386bf3e0a7c500/buildinfo/buildinfo.go#L185-L193
|
||||
BUILDINFO_MAGIC = b"\xFF Go buildinf:"
|
||||
|
||||
try:
|
||||
index = buf.index(BUILDINFO_MAGIC)
|
||||
except ValueError:
|
||||
logger.debug("go buildinfo: no buildinfo magic")
|
||||
return None
|
||||
|
||||
psize, flags = struct.unpack_from("<bb", buf, index + len(BUILDINFO_MAGIC))
|
||||
assert psize in (4, 8)
|
||||
is_big_endian = flags & 0b01
|
||||
has_inline_strings = flags & 0b10
|
||||
logger.debug("go buildinfo: psize: %d big endian: %s inline: %s", psize, is_big_endian, has_inline_strings)
|
||||
|
||||
GOOS_TO_OS = {
|
||||
b"aix": OS.AIX,
|
||||
b"android": OS.ANDROID,
|
||||
b"dragonfly": OS.DRAGONFLYBSD,
|
||||
b"freebsd": OS.FREEBSD,
|
||||
b"hurd": OS.HURD,
|
||||
b"illumos": OS.ILLUMOS,
|
||||
b"linux": OS.LINUX,
|
||||
b"netbsd": OS.NETBSD,
|
||||
b"openbsd": OS.OPENBSD,
|
||||
b"solaris": OS.SOLARIS,
|
||||
b"zos": OS.ZOS,
|
||||
b"windows": None, # PE format
|
||||
b"plan9": None, # a.out format
|
||||
b"ios": None, # Mach-O format
|
||||
b"darwin": None, # Mach-O format
|
||||
b"nacl": None, # dropped in GO 1.14
|
||||
b"js": None,
|
||||
}
|
||||
|
||||
if has_inline_strings:
|
||||
# This is the common case/path. Most samples will have an inline GOOS string.
|
||||
#
|
||||
# To find samples on VT, use these VTGrep searches:
|
||||
#
|
||||
# content: {ff 20 47 6f 20 62 75 69 6c 64 69 6e 66 3a 04 02}
|
||||
# content: {ff 20 47 6f 20 62 75 69 6c 64 69 6e 66 3a 08 02}
|
||||
|
||||
# If present, the GOOS key will be found within
|
||||
# the current buildinfo data region.
|
||||
#
|
||||
# Brute force the k-v pair, like `GOOS=linux`,
|
||||
# rather than try to parse the data, which would be fragile.
|
||||
for key, os in GOOS_TO_OS.items():
|
||||
if (b"GOOS=" + key) in buf:
|
||||
logger.debug("go buildinfo: found os: %s", os)
|
||||
return os
|
||||
else:
|
||||
# This is the uncommon path. Most samples will have an inline GOOS string.
|
||||
#
|
||||
# To find samples on VT, use the referenced VTGrep content searches.
|
||||
info_format = {
|
||||
# content: {ff 20 47 6f 20 62 75 69 6c 64 69 6e 66 3a 04 00}
|
||||
# like: 71e617e5cc7fda89bf67422ff60f437e9d54622382c5ed6ff31f75e601f9b22e
|
||||
# in which the modinfo doesn't have GOOS.
|
||||
(4, False): "<II",
|
||||
# content: {ff 20 47 6f 20 62 75 69 6c 64 69 6e 66 3a 08 00}
|
||||
# like: 93d3b3e2a904c6c909e20f2f76c3c2e8d0c81d535eb46e5493b5701f461816c3
|
||||
# in which the modinfo doesn't have GOOS.
|
||||
(8, False): "<QQ",
|
||||
# content: {ff 20 47 6f 20 62 75 69 6c 64 69 6e 66 3a 04 01}
|
||||
# (no matches on VT today)
|
||||
(4, True): ">II",
|
||||
# content: {ff 20 47 6f 20 62 75 69 6c 64 69 6e 66 3a 08 01}
|
||||
# like: d44ba497964050c0e3dd2a192c511e4c3c4f17717f0322a554d64b797ee4690a
|
||||
# in which the modinfo doesn't have GOOS.
|
||||
(8, True): ">QQ",
|
||||
}
|
||||
|
||||
build_version_address, modinfo_address = struct.unpack_from(
|
||||
info_format[(psize, is_big_endian)], buf, index + 0x10
|
||||
)
|
||||
logger.debug("go buildinfo: build version address: 0x%x", build_version_address)
|
||||
logger.debug("go buildinfo: modinfo address: 0x%x", modinfo_address)
|
||||
|
||||
build_version = read_go_slice(elf, build_version_address)
|
||||
if build_version:
|
||||
logger.debug("go buildinfo: build version: %s", build_version.decode("utf-8"))
|
||||
|
||||
modinfo = read_go_slice(elf, modinfo_address)
|
||||
if modinfo:
|
||||
if modinfo[-0x11] == ord("\n"):
|
||||
# Strip module framing: sentinel strings delimiting the module info.
|
||||
# These are cmd/go/internal/modload/build.infoStart and infoEnd.
|
||||
# Which should probably be:
|
||||
# infoStart, _ = hex.DecodeString("3077af0c9274080241e1c107e6d618e6")
|
||||
# infoEnd, _ = hex.DecodeString("f932433186182072008242104116d8f2")
|
||||
modinfo = modinfo[0x10:-0x10]
|
||||
logger.debug("go buildinfo: modinfo: %s", modinfo.decode("utf-8"))
|
||||
|
||||
if not modinfo:
|
||||
return None
|
||||
|
||||
for key, os in GOOS_TO_OS.items():
|
||||
# Brute force the k-v pair, like `GOOS=linux`,
|
||||
# rather than try to parse the data, which would be fragile.
|
||||
if (b"GOOS=" + key) in modinfo:
|
||||
logger.debug("go buildinfo: found os: %s", os)
|
||||
return os
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def guess_os_from_go_source(elf: ELF) -> Optional[OS]:
|
||||
"""
|
||||
In a binary compiled by Go, runtime metadata may contain
|
||||
references to the source filenames, including the
|
||||
src/runtime/os_* files, whose name indicates the
|
||||
target operating system.
|
||||
|
||||
Confirm the given ELF seems to be built by Go,
|
||||
and then look for strings that look like
|
||||
Go source filenames.
|
||||
|
||||
This strategy is derived from GoReSym.
|
||||
"""
|
||||
if not is_go_binary(elf):
|
||||
return None
|
||||
|
||||
for phdr in elf.program_headers:
|
||||
buf = phdr.buf
|
||||
NEEDLE_OS = b"/src/runtime/os_"
|
||||
try:
|
||||
index = buf.index(NEEDLE_OS)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
rest = buf[index + len(NEEDLE_OS) : index + len(NEEDLE_OS) + 32]
|
||||
filename = rest.partition(b".go")[0].decode("utf-8")
|
||||
logger.debug("go source: filename: /src/runtime/os_%s.go", filename)
|
||||
|
||||
# via: https://cs.opensource.google/go/go/+/master:src/runtime/;bpv=1;bpt=0
|
||||
# candidates today:
|
||||
# - aix
|
||||
# - android
|
||||
# - darwin
|
||||
# - darwin_arm64
|
||||
# - dragonfly
|
||||
# - freebsd
|
||||
# - freebsd2
|
||||
# - freebsd_amd64
|
||||
# - freebsd_arm
|
||||
# - freebsd_arm64
|
||||
# - freebsd_noauxv
|
||||
# - freebsd_riscv64
|
||||
# - illumos
|
||||
# - js
|
||||
# - linux
|
||||
# - linux_arm
|
||||
# - linux_arm64
|
||||
# - linux_be64
|
||||
# - linux_generic
|
||||
# - linux_loong64
|
||||
# - linux_mips64x
|
||||
# - linux_mipsx
|
||||
# - linux_noauxv
|
||||
# - linux_novdso
|
||||
# - linux_ppc64x
|
||||
# - linux_riscv64
|
||||
# - linux_s390x
|
||||
# - linux_x86
|
||||
# - netbsd
|
||||
# - netbsd_386
|
||||
# - netbsd_amd64
|
||||
# - netbsd_arm
|
||||
# - netbsd_arm64
|
||||
# - nonopenbsd
|
||||
# - only_solaris
|
||||
# - openbsd
|
||||
# - openbsd_arm
|
||||
# - openbsd_arm64
|
||||
# - openbsd_libc
|
||||
# - openbsd_mips64
|
||||
# - openbsd_syscall
|
||||
# - openbsd_syscall1
|
||||
# - openbsd_syscall2
|
||||
# - plan9
|
||||
# - plan9_arm
|
||||
# - solaris
|
||||
# - unix
|
||||
# - unix_nonlinux
|
||||
# - wasip1
|
||||
# - wasm
|
||||
# - windows
|
||||
# - windows_arm
|
||||
# - windows_arm64
|
||||
|
||||
OS_FILENAME_TO_OS = {
|
||||
"aix": OS.AIX,
|
||||
"android": OS.ANDROID,
|
||||
"dragonfly": OS.DRAGONFLYBSD,
|
||||
"freebsd": OS.FREEBSD,
|
||||
"freebsd2": OS.FREEBSD,
|
||||
"freebsd_": OS.FREEBSD,
|
||||
"illumos": OS.ILLUMOS,
|
||||
"linux": OS.LINUX,
|
||||
"netbsd": OS.NETBSD,
|
||||
"only_solaris": OS.SOLARIS,
|
||||
"openbsd": OS.OPENBSD,
|
||||
"solaris": OS.SOLARIS,
|
||||
"unix_nonlinux": OS.UNIX,
|
||||
}
|
||||
|
||||
for prefix, os in OS_FILENAME_TO_OS.items():
|
||||
if filename.startswith(prefix):
|
||||
return os
|
||||
|
||||
for phdr in elf.program_headers:
|
||||
buf = phdr.buf
|
||||
NEEDLE_RT0 = b"/src/runtime/rt0_"
|
||||
try:
|
||||
index = buf.index(NEEDLE_RT0)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
rest = buf[index + len(NEEDLE_RT0) : index + len(NEEDLE_RT0) + 32]
|
||||
filename = rest.partition(b".s")[0].decode("utf-8")
|
||||
logger.debug("go source: filename: /src/runtime/rt0_%s.s", filename)
|
||||
|
||||
# via: https://cs.opensource.google/go/go/+/master:src/runtime/;bpv=1;bpt=0
|
||||
# candidates today:
|
||||
# - aix_ppc64
|
||||
# - android_386
|
||||
# - android_amd64
|
||||
# - android_arm
|
||||
# - android_arm64
|
||||
# - darwin_amd64
|
||||
# - darwin_arm64
|
||||
# - dragonfly_amd64
|
||||
# - freebsd_386
|
||||
# - freebsd_amd64
|
||||
# - freebsd_arm
|
||||
# - freebsd_arm64
|
||||
# - freebsd_riscv64
|
||||
# - illumos_amd64
|
||||
# - ios_amd64
|
||||
# - ios_arm64
|
||||
# - js_wasm
|
||||
# - linux_386
|
||||
# - linux_amd64
|
||||
# - linux_arm
|
||||
# - linux_arm64
|
||||
# - linux_loong64
|
||||
# - linux_mips64x
|
||||
# - linux_mipsx
|
||||
# - linux_ppc64
|
||||
# - linux_ppc64le
|
||||
# - linux_riscv64
|
||||
# - linux_s390x
|
||||
# - netbsd_386
|
||||
# - netbsd_amd64
|
||||
# - netbsd_arm
|
||||
# - netbsd_arm64
|
||||
# - openbsd_386
|
||||
# - openbsd_amd64
|
||||
# - openbsd_arm
|
||||
# - openbsd_arm64
|
||||
# - openbsd_mips64
|
||||
# - openbsd_ppc64
|
||||
# - openbsd_riscv64
|
||||
# - plan9_386
|
||||
# - plan9_amd64
|
||||
# - plan9_arm
|
||||
# - solaris_amd64
|
||||
# - wasip1_wasm
|
||||
# - windows_386
|
||||
# - windows_amd64
|
||||
# - windows_arm
|
||||
# - windows_arm64
|
||||
|
||||
RT0_FILENAME_TO_OS = {
|
||||
"aix": OS.AIX,
|
||||
"android": OS.ANDROID,
|
||||
"dragonfly": OS.DRAGONFLYBSD,
|
||||
"freebsd": OS.FREEBSD,
|
||||
"illumos": OS.ILLUMOS,
|
||||
"linux": OS.LINUX,
|
||||
"netbsd": OS.NETBSD,
|
||||
"openbsd": OS.OPENBSD,
|
||||
"solaris": OS.SOLARIS,
|
||||
}
|
||||
|
||||
for prefix, os in RT0_FILENAME_TO_OS.items():
|
||||
if filename.startswith(prefix):
|
||||
return os
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def guess_os_from_vdso_strings(elf: ELF) -> Optional[OS]:
|
||||
"""
|
||||
The "vDSO" (virtual dynamic shared object) is a small shared
|
||||
library that the kernel automatically maps into the address space
|
||||
of all user-space applications.
|
||||
|
||||
Some statically linked executables include small dynamic linker
|
||||
routines that finds these vDSO symbols, using the ASCII
|
||||
symbol name and version. We can therefore recognize the pairs
|
||||
(symbol, version) to guess the binary targets Linux.
|
||||
"""
|
||||
for phdr in elf.program_headers:
|
||||
buf = phdr.buf
|
||||
|
||||
# We don't really use the arch, but its interesting for documentation
|
||||
# I suppose we could restrict the arch here to what's in the ELF header,
|
||||
# but that's even more work. Let's see if this is sufficient.
|
||||
for arch, symbol, version in (
|
||||
# via: https://man7.org/linux/man-pages/man7/vdso.7.html
|
||||
("arm", b"__vdso_gettimeofday", b"LINUX_2.6"),
|
||||
("arm", b"__vdso_clock_gettime", b"LINUX_2.6"),
|
||||
("aarch64", b"__kernel_rt_sigreturn", b"LINUX_2.6.39"),
|
||||
("aarch64", b"__kernel_gettimeofday", b"LINUX_2.6.39"),
|
||||
("aarch64", b"__kernel_clock_gettime", b"LINUX_2.6.39"),
|
||||
("aarch64", b"__kernel_clock_getres", b"LINUX_2.6.39"),
|
||||
("mips", b"__kernel_gettimeofday", b"LINUX_2.6"),
|
||||
("mips", b"__kernel_clock_gettime", b"LINUX_2.6"),
|
||||
("ia64", b"__kernel_sigtramp", b"LINUX_2.5"),
|
||||
("ia64", b"__kernel_syscall_via_break", b"LINUX_2.5"),
|
||||
("ia64", b"__kernel_syscall_via_epc", b"LINUX_2.5"),
|
||||
("ppc/32", b"__kernel_clock_getres", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_clock_gettime", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_clock_gettime64", b"LINUX_5.11"),
|
||||
("ppc/32", b"__kernel_datapage_offset", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_get_syscall_map", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_get_tbfreq", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_getcpu", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_gettimeofday", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_sigtramp_rt32", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_sigtramp32", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_sync_dicache", b"LINUX_2.6.15"),
|
||||
("ppc/32", b"__kernel_sync_dicache_p5", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_clock_getres", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_clock_gettime", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_datapage_offset", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_get_syscall_map", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_get_tbfreq", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_getcpu", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_gettimeofday", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_sigtramp_rt64", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_sync_dicache", b"LINUX_2.6.15"),
|
||||
("ppc/64", b"__kernel_sync_dicache_p5", b"LINUX_2.6.15"),
|
||||
("riscv", b"__vdso_rt_sigreturn", b"LINUX_4.15"),
|
||||
("riscv", b"__vdso_gettimeofday", b"LINUX_4.15"),
|
||||
("riscv", b"__vdso_clock_gettime", b"LINUX_4.15"),
|
||||
("riscv", b"__vdso_clock_getres", b"LINUX_4.15"),
|
||||
("riscv", b"__vdso_getcpu", b"LINUX_4.15"),
|
||||
("riscv", b"__vdso_flush_icache", b"LINUX_4.15"),
|
||||
("s390", b"__kernel_clock_getres", b"LINUX_2.6.29"),
|
||||
("s390", b"__kernel_clock_gettime", b"LINUX_2.6.29"),
|
||||
("s390", b"__kernel_gettimeofday", b"LINUX_2.6.29"),
|
||||
("superh", b"__kernel_rt_sigreturn", b"LINUX_2.6"),
|
||||
("superh", b"__kernel_sigreturn", b"LINUX_2.6"),
|
||||
("superh", b"__kernel_vsyscall", b"LINUX_2.6"),
|
||||
("i386", b"__kernel_sigreturn", b"LINUX_2.5"),
|
||||
("i386", b"__kernel_rt_sigreturn", b"LINUX_2.5"),
|
||||
("i386", b"__kernel_vsyscall", b"LINUX_2.5"),
|
||||
("i386", b"__vdso_clock_gettime", b"LINUX_2.6"),
|
||||
("i386", b"__vdso_gettimeofday", b"LINUX_2.6"),
|
||||
("i386", b"__vdso_time", b"LINUX_2.6"),
|
||||
("x86-64", b"__vdso_clock_gettime", b"LINUX_2.6"),
|
||||
("x86-64", b"__vdso_getcpu", b"LINUX_2.6"),
|
||||
("x86-64", b"__vdso_gettimeofday", b"LINUX_2.6"),
|
||||
("x86-64", b"__vdso_time", b"LINUX_2.6"),
|
||||
("x86/32", b"__vdso_clock_gettime", b"LINUX_2.6"),
|
||||
("x86/32", b"__vdso_getcpu", b"LINUX_2.6"),
|
||||
("x86/32", b"__vdso_gettimeofday", b"LINUX_2.6"),
|
||||
("x86/32", b"__vdso_time", b"LINUX_2.6"),
|
||||
):
|
||||
if symbol in buf and version in buf:
|
||||
logger.debug("vdso string: %s %s %s", arch, symbol.decode("ascii"), version.decode("ascii"))
|
||||
return OS.LINUX
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def detect_elf_os(f) -> str:
|
||||
"""
|
||||
f: type Union[BinaryIO, IDAIO, GHIDRAIO]
|
||||
@@ -1022,6 +1523,27 @@ def detect_elf_os(f) -> str:
|
||||
logger.warning("Error guessing OS from symbol table: %s", e)
|
||||
symtab_guess = None
|
||||
|
||||
try:
|
||||
goos_guess = guess_os_from_go_buildinfo(elf)
|
||||
logger.debug("guess: Go buildinfo: %s", goos_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from Go buildinfo: %s", e)
|
||||
goos_guess = None
|
||||
|
||||
try:
|
||||
gosrc_guess = guess_os_from_go_source(elf)
|
||||
logger.debug("guess: Go source: %s", gosrc_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from Go source path: %s", e)
|
||||
gosrc_guess = None
|
||||
|
||||
try:
|
||||
vdso_guess = guess_os_from_vdso_strings(elf)
|
||||
logger.debug("guess: vdso strings: %s", vdso_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from vdso strings: %s", e)
|
||||
symtab_guess = None
|
||||
|
||||
ret = None
|
||||
|
||||
if osabi_guess:
|
||||
@@ -1045,11 +1567,24 @@ def detect_elf_os(f) -> str:
|
||||
elif symtab_guess:
|
||||
ret = symtab_guess
|
||||
|
||||
elif goos_guess:
|
||||
ret = goos_guess
|
||||
|
||||
elif gosrc_guess:
|
||||
# prefer goos_guess to this method,
|
||||
# which is just string interpretation.
|
||||
ret = gosrc_guess
|
||||
|
||||
elif ident_guess:
|
||||
# at the bottom because we don't trust this too much
|
||||
# due to potential for bugs with cross-compilation.
|
||||
ret = ident_guess
|
||||
|
||||
elif vdso_guess:
|
||||
# at the bottom because this is just scanning strings,
|
||||
# which isn't very authoritative.
|
||||
ret = vdso_guess
|
||||
|
||||
return ret.value if ret is not None else "unknown"
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -10,8 +10,7 @@ import logging
|
||||
from typing import Tuple, Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from elftools.elf.elffile import ELFFile, SymbolTableSection
|
||||
from elftools.elf.relocation import RelocationSection
|
||||
from elftools.elf.elffile import ELFFile, DynamicSegment, SymbolTableSection
|
||||
|
||||
import capa.features.extractors.common
|
||||
from capa.features.file import Export, Import, Section
|
||||
@@ -47,17 +46,47 @@ def extract_file_export_names(elf: ELFFile, **kwargs):
|
||||
|
||||
yield Export(symbol.name), AbsoluteVirtualAddress(symbol.entry.st_value)
|
||||
|
||||
for segment in elf.iter_segments():
|
||||
if not isinstance(segment, DynamicSegment):
|
||||
continue
|
||||
|
||||
tab_ptr, tab_offset = segment.get_table_offset("DT_SYMTAB")
|
||||
if tab_ptr is None or tab_offset is None:
|
||||
logger.debug("Dynamic segment doesn't contain DT_SYMTAB")
|
||||
continue
|
||||
|
||||
logger.debug("Dynamic segment contains %s symbols: ", segment.num_symbols())
|
||||
|
||||
for symbol in segment.iter_symbols():
|
||||
# The following conditions are based on the following article
|
||||
# http://www.m4b.io/elf/export/binary/analysis/2015/05/25/what-is-an-elf-export.html
|
||||
if not symbol.name:
|
||||
continue
|
||||
if symbol.entry.st_info.type not in ["STT_FUNC", "STT_OBJECT", "STT_IFUNC"]:
|
||||
continue
|
||||
if symbol.entry.st_value == 0:
|
||||
continue
|
||||
if symbol.entry.st_shndx == "SHN_UNDEF":
|
||||
continue
|
||||
|
||||
yield Export(symbol.name), AbsoluteVirtualAddress(symbol.entry.st_value)
|
||||
|
||||
|
||||
def extract_file_import_names(elf: ELFFile, **kwargs):
|
||||
# Create a dictionary to store symbol names by their index
|
||||
symbol_names = {}
|
||||
|
||||
# Extract symbol names and store them in the dictionary
|
||||
for section in elf.iter_sections():
|
||||
if not isinstance(section, SymbolTableSection):
|
||||
for segment in elf.iter_segments():
|
||||
if not isinstance(segment, DynamicSegment):
|
||||
continue
|
||||
|
||||
for _, symbol in enumerate(section.iter_symbols()):
|
||||
tab_ptr, tab_offset = segment.get_table_offset("DT_SYMTAB")
|
||||
if tab_ptr is None or tab_offset is None:
|
||||
logger.debug("Dynamic segment doesn't contain DT_SYMTAB")
|
||||
continue
|
||||
|
||||
for _, symbol in enumerate(segment.iter_symbols()):
|
||||
# The following conditions are based on the following article
|
||||
# http://www.m4b.io/elf/export/binary/analysis/2015/05/25/what-is-an-elf-export.html
|
||||
if not symbol.name:
|
||||
@@ -73,21 +102,28 @@ def extract_file_import_names(elf: ELFFile, **kwargs):
|
||||
|
||||
symbol_names[_] = symbol.name
|
||||
|
||||
for section in elf.iter_sections():
|
||||
if not isinstance(section, RelocationSection):
|
||||
for segment in elf.iter_segments():
|
||||
if not isinstance(segment, DynamicSegment):
|
||||
continue
|
||||
|
||||
if section["sh_entsize"] == 0:
|
||||
logger.debug("Symbol table '%s' has a sh_entsize of zero!", section.name)
|
||||
continue
|
||||
relocation_tables = segment.get_relocation_tables()
|
||||
logger.debug("Dynamic Segment contains %s relocation tables:", len(relocation_tables))
|
||||
|
||||
logger.debug("Symbol table '%s' contains %s entries:", section.name, section.num_relocations())
|
||||
for relocation_table in relocation_tables.values():
|
||||
relocations = []
|
||||
for i in range(relocation_table.num_relocations()):
|
||||
try:
|
||||
relocations.append(relocation_table.get_relocation(i))
|
||||
except TypeError:
|
||||
# ELF is corrupt and the relocation table is invalid,
|
||||
# so stop processing it.
|
||||
break
|
||||
|
||||
for relocation in section.iter_relocations():
|
||||
# Extract the symbol name from the symbol table using the symbol index in the relocation
|
||||
if relocation["r_info_sym"] not in symbol_names:
|
||||
continue
|
||||
yield Import(symbol_names[relocation["r_info_sym"]]), FileOffsetAddress(relocation["r_offset"])
|
||||
for relocation in relocations:
|
||||
# Extract the symbol name from the symbol table using the symbol index in the relocation
|
||||
if relocation["r_info_sym"] not in symbol_names:
|
||||
continue
|
||||
yield Import(symbol_names[relocation["r_info_sym"]]), FileOffsetAddress(relocation["r_offset"])
|
||||
|
||||
|
||||
def extract_file_section_names(elf: ELFFile, **kwargs):
|
||||
|
||||
@@ -34,7 +34,7 @@ class GhidraFeatureExtractor(StaticFeatureExtractor):
|
||||
# https://ghidra.re/ghidra_docs/api/ghidra/program/model/listing/Program.html
|
||||
#
|
||||
# the hashes are stored in the database, not computed on the fly,
|
||||
# so its probably not trivial to add SHA1.
|
||||
# so it's probably not trivial to add SHA1.
|
||||
sha1="",
|
||||
sha256=capa.ghidra.helpers.get_file_sha256(),
|
||||
)
|
||||
|
||||
@@ -260,7 +260,7 @@ def dereference_ptr(insn: ghidra.program.database.code.InstructionDB):
|
||||
if thfunc and thfunc.isThunk():
|
||||
return handle_thunk(to_deref)
|
||||
else:
|
||||
# if it doesn't poin to a thunk, it's usually a jmp to a label
|
||||
# if it doesn't point to a thunk, it's usually a jmp to a label
|
||||
return to_deref
|
||||
if not dat:
|
||||
return to_deref
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -10,6 +10,7 @@ from typing import Any, Dict, Tuple, Iterator, Optional
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
import ida_nalt
|
||||
import idautils
|
||||
import ida_bytes
|
||||
import ida_segment
|
||||
@@ -17,6 +18,8 @@ import ida_segment
|
||||
from capa.features.address import AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
IDA_NALT_ENCODING = ida_nalt.get_default_encoding_idx(ida_nalt.BPU_1B) # use one byte-per-character encoding
|
||||
|
||||
|
||||
def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
|
||||
"""yield all ea of a given byte sequence
|
||||
@@ -26,11 +29,16 @@ def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
|
||||
end: max virtual address
|
||||
seq: bytes to search e.g. b"\x01\x03"
|
||||
"""
|
||||
patterns = ida_bytes.compiled_binpat_vec_t()
|
||||
|
||||
seqstr = " ".join([f"{b:02x}" for b in seq])
|
||||
err = ida_bytes.parse_binpat_str(patterns, 0, seqstr, 16, IDA_NALT_ENCODING)
|
||||
|
||||
if err:
|
||||
return
|
||||
|
||||
while True:
|
||||
# TODO(mike-hunhoff): find_binary is deprecated. Please use ida_bytes.bin_search() instead.
|
||||
# https://github.com/mandiant/capa/issues/1606
|
||||
ea = idaapi.find_binary(start, end, seqstr, 0, idaapi.SEARCH_DOWN)
|
||||
ea = ida_bytes.bin_search(start, end, patterns, ida_bytes.BIN_SEARCH_FORWARD)
|
||||
if ea == idaapi.BADADDR:
|
||||
break
|
||||
start = ea + 1
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
# 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.
|
||||
from typing import Any, Dict, Tuple, Iterator
|
||||
import re
|
||||
from typing import Any, Dict, Tuple, Iterator, Optional
|
||||
|
||||
import idc
|
||||
import ida_ua
|
||||
import idaapi
|
||||
import idautils
|
||||
|
||||
@@ -35,9 +37,9 @@ def get_externs(ctx: Dict[str, Any]) -> Dict[int, Any]:
|
||||
return ctx["externs_cache"]
|
||||
|
||||
|
||||
def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[Any]:
|
||||
def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Optional[Tuple[str, str]]:
|
||||
"""check instruction for API call"""
|
||||
info = ()
|
||||
info = None
|
||||
ref = insn.ea
|
||||
|
||||
# attempt to resolve API calls by following chained thunks to a reasonable depth
|
||||
@@ -52,7 +54,7 @@ def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[A
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
info = funcs.get(ref, ())
|
||||
info = funcs.get(ref)
|
||||
if info:
|
||||
break
|
||||
|
||||
@@ -60,8 +62,7 @@ def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[A
|
||||
if not f or not (f.flags & idaapi.FUNC_THUNK):
|
||||
break
|
||||
|
||||
if info:
|
||||
yield info
|
||||
return info
|
||||
|
||||
|
||||
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
@@ -76,16 +77,39 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
|
||||
if insn.get_canon_mnem() not in ("call", "jmp"):
|
||||
return
|
||||
|
||||
# check calls to imported functions
|
||||
for api in check_for_api_call(insn, get_imports(fh.ctx)):
|
||||
# check call to imported functions
|
||||
api = check_for_api_call(insn, get_imports(fh.ctx))
|
||||
if api:
|
||||
# tuple (<module>, <function>, <ordinal>)
|
||||
for name in capa.features.extractors.helpers.generate_symbols(api[0], api[1]):
|
||||
yield API(name), ih.address
|
||||
# a call instruction should only call one function, stop if a call to an import is extracted
|
||||
return
|
||||
|
||||
# check calls to extern functions
|
||||
for api in check_for_api_call(insn, get_externs(fh.ctx)):
|
||||
# check call to extern functions
|
||||
api = check_for_api_call(insn, get_externs(fh.ctx))
|
||||
if api:
|
||||
# tuple (<module>, <function>, <ordinal>)
|
||||
yield API(api[1]), ih.address
|
||||
# a call instruction should only call one function, stop if a call to an extern is extracted
|
||||
return
|
||||
|
||||
# extract dynamically resolved APIs stored in renamed globals (renamed for example using `renimp.idc`)
|
||||
# examples: `CreateProcessA`, `HttpSendRequestA`
|
||||
if insn.Op1.type == ida_ua.o_mem:
|
||||
op_addr = insn.Op1.addr
|
||||
op_name = idaapi.get_name(op_addr)
|
||||
# when renaming a global using an API name, IDA assigns it the function type
|
||||
# ensure we do not extract something wrong by checking that the address has a name and a type
|
||||
# we could check that the type is a function definition, but that complicates the code
|
||||
if (not op_name.startswith("off_")) and idc.get_type(op_addr):
|
||||
# Remove suffix used in repeated names, for example _0 in VirtualFree_0
|
||||
match = re.match(r"(.+)_\d+", op_name)
|
||||
if match:
|
||||
op_name = match.group(1)
|
||||
# the global name does not include the DLL name, so we can't extract it
|
||||
for name in capa.features.extractors.helpers.generate_symbols("", op_name):
|
||||
yield API(name), ih.address
|
||||
|
||||
# extract IDA/FLIRT recognized API functions
|
||||
targets = tuple(idautils.CodeRefsFrom(insn.ea, False))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# strings code from FLOSS, https://github.com/mandiant/flare-floss
|
||||
#
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -113,7 +113,7 @@ def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterato
|
||||
if f.vw.metadata["Format"] == "elf":
|
||||
if "symtab" not in fh.ctx["cache"]:
|
||||
# the symbol table gets stored as a function's attribute in order to avoid running
|
||||
# this code everytime the call is made, thus preventing the computational overhead.
|
||||
# this code every time the call is made, thus preventing the computational overhead.
|
||||
try:
|
||||
fh.ctx["cache"]["symtab"] = SymTab.from_viv(f.vw.parsedbin)
|
||||
except Exception:
|
||||
@@ -598,7 +598,7 @@ def extract_op_number_features(
|
||||
|
||||
if f.vw.probeMemory(v, 1, envi.memory.MM_READ):
|
||||
# this is a valid address
|
||||
# assume its not also a constant.
|
||||
# assume it's not also a constant.
|
||||
return
|
||||
|
||||
if insn.mnem == "add" and insn.opers[0].isReg() and insn.opers[0].reg == envi.archs.i386.regs.REG_ESP:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
capa freeze file format: `| capa0000 | + zlib(utf-8(json(...)))`
|
||||
|
||||
Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
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: [package root]/LICENSE.txt
|
||||
@@ -22,7 +22,6 @@ from pydantic import Field, BaseModel, ConfigDict
|
||||
# https://github.com/mandiant/capa/issues/1699
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
import capa.loader
|
||||
import capa.helpers
|
||||
import capa.version
|
||||
import capa.features.file
|
||||
@@ -383,7 +382,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str:
|
||||
address=Address.from_capa(addr),
|
||||
feature=feature_from_capa(feature),
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `basic_block` as a argument due to alias
|
||||
# Mypy is unable to recognise `basic_block` as an argument due to alias
|
||||
for feature, addr in extractor.extract_basic_block_features(f, bb)
|
||||
]
|
||||
|
||||
@@ -420,7 +419,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str:
|
||||
features=tuple(ffeatures),
|
||||
basic_blocks=basic_blocks,
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `basic_blocks` as a argument due to alias
|
||||
# Mypy is unable to recognise `basic_blocks` as an argument due to alias
|
||||
)
|
||||
|
||||
features = StaticFeatures(
|
||||
@@ -428,7 +427,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str:
|
||||
file=tuple(file_features),
|
||||
functions=tuple(function_features),
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `global_` as a argument due to alias
|
||||
# Mypy is unable to recognise `global_` as an argument due to alias
|
||||
|
||||
freeze = Freeze(
|
||||
version=CURRENT_VERSION,
|
||||
@@ -438,7 +437,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str:
|
||||
extractor=Extractor(name=extractor.__class__.__name__),
|
||||
features=features,
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `base_address` as a argument due to alias
|
||||
# Mypy is unable to recognise `base_address` as an argument due to alias
|
||||
|
||||
return freeze.model_dump_json()
|
||||
|
||||
@@ -486,7 +485,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str:
|
||||
address=Address.from_capa(addr),
|
||||
feature=feature_from_capa(feature),
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `basic_block` as a argument due to alias
|
||||
# Mypy is unable to recognise `basic_block` as an argument due to alias
|
||||
for feature, addr in extractor.extract_thread_features(p, t)
|
||||
]
|
||||
|
||||
@@ -533,7 +532,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str:
|
||||
file=tuple(file_features),
|
||||
processes=tuple(process_features),
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `global_` as a argument due to alias
|
||||
# Mypy is unable to recognise `global_` as an argument due to alias
|
||||
|
||||
# workaround around mypy issue: https://github.com/python/mypy/issues/1424
|
||||
get_base_addr = getattr(extractor, "get_base_addr", None)
|
||||
@@ -547,7 +546,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str:
|
||||
extractor=Extractor(name=extractor.__class__.__name__),
|
||||
features=features,
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `base_address` as a argument due to alias
|
||||
# Mypy is unable to recognise `base_address` as an argument due to alias
|
||||
|
||||
return freeze.model_dump_json()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -132,7 +132,7 @@ def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
|
||||
elif isinstance(f, capa.features.file.Import):
|
||||
assert isinstance(f.value, str)
|
||||
return ImportFeature(import_=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `import_` as a argument due to alias
|
||||
# Mypy is unable to recognise `import_` as an argument due to alias
|
||||
|
||||
elif isinstance(f, capa.features.file.Section):
|
||||
assert isinstance(f.value, str)
|
||||
@@ -141,7 +141,7 @@ def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
|
||||
elif isinstance(f, capa.features.file.FunctionName):
|
||||
assert isinstance(f.value, str)
|
||||
return FunctionNameFeature(function_name=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `function_name` as a argument due to alias
|
||||
# Mypy is unable to recognise `function_name` as an argument due to alias
|
||||
|
||||
# must come before check for String due to inheritance
|
||||
elif isinstance(f, capa.features.common.Substring):
|
||||
@@ -160,7 +160,7 @@ def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
|
||||
elif isinstance(f, capa.features.common.Class):
|
||||
assert isinstance(f.value, str)
|
||||
return ClassFeature(class_=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `class_` as a argument due to alias
|
||||
# Mypy is unable to recognise `class_` as an argument due to alias
|
||||
|
||||
elif isinstance(f, capa.features.common.Namespace):
|
||||
assert isinstance(f.value, str)
|
||||
@@ -197,12 +197,12 @@ def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
|
||||
elif isinstance(f, capa.features.insn.OperandNumber):
|
||||
assert isinstance(f.value, int)
|
||||
return OperandNumberFeature(index=f.index, operand_number=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `operand_number` as a argument due to alias
|
||||
# Mypy is unable to recognise `operand_number` as an argument due to alias
|
||||
|
||||
elif isinstance(f, capa.features.insn.OperandOffset):
|
||||
assert isinstance(f.value, int)
|
||||
return OperandOffsetFeature(index=f.index, operand_offset=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `operand_offset` as a argument due to alias
|
||||
# Mypy is unable to recognise `operand_offset` as an argument due to alias
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"feature_from_capa({type(f)}) not implemented")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,184 +1,107 @@
|
||||
<div align="center">
|
||||
<img src="/doc/img/ghidra_backend_logo.png" width=300 height=175>
|
||||
<img src="../../doc/img/ghidra_backend_logo.png" width=240 height=125>
|
||||
</div>
|
||||
|
||||
The Ghidra feature extractor is an application of the FLARE team's open-source project, Ghidrathon, to integrate capa with Ghidra using Python 3. capa is a framework that uses a well-defined collection of rules to identify capabilities in a program. You can run capa against a PE file, ELF file, or shellcode and it tells you what it thinks the program can do. For example, it might suggest that the program is a backdoor, can install services, or relies on HTTP to communicate. The Ghidra feature extractor can be used to run capa analysis on your Ghidra databases without needing access to the original binary file. As a part of this integration, we've developed two scripts, [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py), to display capa results directly in Ghidra.
|
||||
# capa + Ghidra
|
||||
|
||||
### Using `capa_explorer.py`
|
||||
[capa](https://github.com/mandiant/capa) is the FLARE team’s open-source tool that detects capabilities in executable files. [Ghidra](https://github.com/NationalSecurityAgency/ghidra) is an open-source software reverse engineering framework created and maintained by the National Security Agency Research Directorate. capa + Ghidra brings capa’s detection capabilities directly to Ghidra’s user interface helping speed up your reverse engineering tasks by identifying what parts of a program suggest interesting behavior, such as setting a registry value. You can execute the included Python 3 scripts [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) or [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) to run capa’s analysis and view the results in Ghidra. You may be asking yourself, “Python 3 scripts in Ghidra?”. You read that correctly. This integration is written entirely in Python 3 and relies on [Ghidrathon]( https://github.com/mandiant/ghidrathon), an open source Ghidra extension that adds Python 3 scripting to Ghidra.
|
||||
|
||||
`capa_explorer.py` integrates capa results directly into Ghidra's UI. In the Symbol Tree Window, under the Namespaces section, you can find the matched rules as well as the corresponding functions that contain the matched features:
|
||||
Check out our capa + Ghidra blog posts:
|
||||
* [Riding Dragons: capa Harnesses Ghidra](https://www.mandiant.com/resources/blog/capa-harnesses-ghidra)
|
||||
|
||||

|
||||
## UI Integration
|
||||
[capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) renders capa results in Ghidra's UI to help you quickly navigate them. This includes adding matched functions to Ghidra’s Symbol Tree and Bookmarks windows and adding comments to functions that indicate matched capabilities and features. You can execute this script using Ghidra’s Script Manager window.
|
||||
|
||||
Labeled functions may be clicked in the Symbol Tree Window to navigate Ghidra's Disassembly Listing and Decompilation windows to the function locations. A comment listing each matched capa rule is inserted at the beginning of the function and a comment for each matched capa feature is added at the matched address within the function. These comments can be viewed using Ghidra's Disassembly Listing and Decompilation windows:
|
||||
### Symbol Tree Window
|
||||
Matched functions are added to Ghidra's Symbol Tree window under a custom namespace that maps to the capabilities' [capa namespace](https://github.com/mandiant/capa-rules/blob/master/doc/format.md#rule-namespace).
|
||||
<div align="center">
|
||||
<img src="https://github.com/mandiant/capa/assets/66766340/eeae33f4-99d4-42dc-a5e8-4c1b8c661492" width=300>
|
||||
</div>
|
||||
|
||||

|
||||
### Comments
|
||||
|
||||
The script also adds bookmarks for capa matches that are categorized under MITRE ATT&CK and Malware Behavior Catalog. These may be found and navigated using Ghidra's Bookmarks Window:
|
||||
Comments are added at the beginning of matched functions indicating matched capabilities and inline comments are added to functions indicating matched features. You can view these comments in Ghidra’s Disassembly Listing and Decompile windows.
|
||||
<div align="center">
|
||||
<img src="https://github.com/mandiant/capa/assets/66766340/bb2b4170-7fd4-45fc-8c7b-ff8f2e2f101b" width=1000>
|
||||
</div>
|
||||
|
||||

|
||||
### Bookmarks
|
||||
|
||||
### Using `capa_ghidra.py`
|
||||
Bookmarks are added to functions that matched a capability that is mapped to a MITRE ATT&CK and/or Malware Behavior Catalog (MBC) technique. You can view these bookmarks in Ghidra's Bookmarks window.
|
||||
<div align="center">
|
||||
<img src="https://github.com/mandiant/capa/assets/66766340/7f9a66a9-7be7-4223-91c6-4b8fc4651336" width=825>
|
||||
</div>
|
||||
|
||||
`capa_ghidra.py` displays capa results in Ghidra's Console window and can be executed using Ghidra's Headless Analyzer. The following is an example of running `capa_ghidra.py` using the Ghidra Script Manager:
|
||||
## Text-based Integration
|
||||
|
||||
Selecting capa rules:
|
||||
<img src="/doc/img/ghidra_script_mngr_rules.png">
|
||||
[capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) outputs text-based capa results that mirror the output of capa’s standalone tool. You can execute this script using Ghidra’s Script Manager and view its output in Ghidra’s Console window.
|
||||
|
||||
Choosing output format:
|
||||
<img src="/doc/img/ghidra_script_mngr_verbosity.png">
|
||||
<div align="center">
|
||||
<img src="../../doc/img/ghidra_script_mngr_output.png" width=700>
|
||||
</div>
|
||||
|
||||
Viewing results in Ghidra Console Window:
|
||||
<img src="/doc/img/ghidra_script_mngr_output.png">
|
||||
You can also execute [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidra's Headless Analyzer to view its output in a terminal window.
|
||||
|
||||
## Installation
|
||||
<div align="center">
|
||||
<img src="../../doc/img/ghidra_headless_analyzer.png">
|
||||
</div>
|
||||
|
||||
### Requirements
|
||||
# Getting Started
|
||||
|
||||
## Requirements
|
||||
|
||||
| Tool | Version | Source |
|
||||
|------------|---------|--------|
|
||||
| capa | `>= 7.0.0` | https://github.com/mandiant/capa/releases |
|
||||
| Ghidrathon | `>= 3.0.0` | https://github.com/mandiant/Ghidrathon/releases |
|
||||
| Ghidra | `>= 10.3.2` | https://github.com/NationalSecurityAgency/ghidra/releases |
|
||||
| Python | `>= 3.8.0` | https://www.python.org/downloads |
|
||||
|
||||
You can run capa in Ghidra by completing the following steps using the Python 3 interpreter that you have configured for your Ghidrathon installation:
|
||||
## Installation
|
||||
|
||||
**Note**: capa + Ghidra relies on [Ghidrathon]( https://github.com/mandiant/ghidrathon) to execute Python 3 code in Ghidra. You must first install and configure Ghidrathon using the [steps outlined in its README]( https://github.com/mandiant/ghidrathon?tab=readme-ov-file#installing-ghidrathon). Then, you must use the Python 3 interpreter that you configured with Ghidrathon to complete the following steps:
|
||||
|
||||
1. Install capa and its dependencies from PyPI using the following command:
|
||||
```bash
|
||||
$ pip install flare-capa
|
||||
```
|
||||
|
||||
2. Download and extract the [official capa rules](https://github.com/mandiant/capa-rules/releases) that match the capa version you have installed. Use the following command to view the version of capa you have installed:
|
||||
2. Download and extract the [official capa rules](https://github.com/mandiant/capa-rules/releases) that match the capa version you have installed. You can use the following command to view the version of capa you have installed:
|
||||
```bash
|
||||
$ pip show flare-capa
|
||||
OR
|
||||
$ capa --version
|
||||
```
|
||||
|
||||
3. Copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) to your `$USER_HOME/ghidra_scripts` directory or manually add the absolute path of each script to the Ghidra Script Manager.
|
||||
3. Copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) to your `ghidra_scripts` directory or manually add the parent directory of each script using Ghidra’s Script Manager.
|
||||
|
||||
## Usage
|
||||
|
||||
After completing the installation steps you can execute [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using the Ghidra Script Manager. You can also execute [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidra's Headless Analyzer.
|
||||
You can execute [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidra’s Script Manager. [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) can also be executed using Ghidra's Headless Analyzer.
|
||||
|
||||
### Ghidra Script Manager
|
||||
### Execution using Ghidra’s Script Manager
|
||||
|
||||
Use the following steps to execute [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidra's Script Manager:
|
||||
1. Open the Ghidra Script Manager by navigating to `Window > Script Manager`
|
||||
2. Locate [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) by selecting the `Python 3 > capa` category or using the Ghidra Script Manager search functionality
|
||||
3. Double-click [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) or [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) to execute the script
|
||||
You can execute [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidra's Script Manager as follows:
|
||||
1. Navigate to `Window > Script Manager`
|
||||
2. Expand the `Python 3 > capa` category
|
||||
3. Double-click a script to execute it
|
||||
|
||||
If you don't see [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) make sure you have copied these scripts to your `$USER_HOME/ghidra_scripts` directory or manually added the absolute path of each script to the Ghidra Script Manager.
|
||||
Both scripts ask you to provide the path of your capa rules directory (see installation step 2). [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) also has you choose one of `default`, `verbose`, and `vverbose` output formats which mirror the output formats of capa’s standalone tool.
|
||||
|
||||
Both scripts ask you to provide the path of your capa rules directory. [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) also asks you to select `default`, `verbose`, and `vverbose` output formats used when writing output to the Ghidra Console Window.
|
||||
### Execution using Ghidra’s Headless Analyzer
|
||||
|
||||
### Ghidra Headless Analyzer
|
||||
You can execute [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidra’s Headless Analyzer by invoking the `analyzeHeadless` script included with Ghidra in its `support` directory. The following arguments must be provided:
|
||||
|
||||
To execute [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using the Ghidra Headless Analyzer, you can use the Ghidra `analyzeHeadless` script located in your `<ghidra_install_path>/support` directory. You will need to provide the following arguments to the Ghidra `analyzeHeadless` script:
|
||||
|
||||
1. `<ghidra_project_path>`: path to Ghidra project
|
||||
2. `<ghidra_project_name>`: name of Ghidra Project
|
||||
3. `-process <sample_name>`: name of sample `<sample_name>`
|
||||
4. `-ScriptPath <capa_ghidra_path>`: OPTIONAL argument specifying the absolute path of [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py)
|
||||
5. `-PostScript capa_ghidra.py`: execute [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) as post-analysis script
|
||||
6. `"<capa_args>"`: single, quoted string containing capa arguments that must specify capa rules directory and output format, e.g. `"<capa_rules_path> --verbose"`. [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) supports `default`, `verbose`, `vverbose` and `json` formats when executed using the Ghidra Headless Analyzer. [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) writes output to the console window used to execute the Ghidra `analyzeHeadless` script.
|
||||
| Argument | Description |
|
||||
|----|----|
|
||||
|`<project_path>`| Path to Ghidra project|
|
||||
| `<project_name>`| Name of Ghidra Project|
|
||||
| `-Process <sample_name>` OR `-Import <sample_path>`| Name of sample `<sample_name>` already imported into `<project_name>` OR absolute path of sample `<sample_path>` to import into `<project_name>`|
|
||||
| `-ScriptPath <script_path>`| OPTIONAL parent directory `<script_path>` of [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py)|
|
||||
| `-PostScript capa_ghidra.py`| Execute [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) after Ghidra analysis|
|
||||
| `"<script_args>"`| Quoted string `"<script_args>"` containing script arguments passed to [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) that must specify a capa rules path and optionally the output format (`--verbose`, `--vverbose`, `--json`) – you can specify `”help”` to view the script’s help message |
|
||||
|
||||
The following is an example of combining these arguments into a single `analyzeHeadless` script command:
|
||||
|
||||
```
|
||||
<ghidra_install_path>/support/analyzeHeadless <ghidra_project_path> <ghidra_project_name> -process <sample_name> -PostScript capa_ghidra.py "<capa_rules_path> --verbose"
|
||||
```
|
||||
|
||||
You may also want to run capa against a sample that you have not yet imported into your Ghidra project. The following is an example of importing a sample and running [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using a single `analyzeHeadless` script command:
|
||||
|
||||
```
|
||||
<ghidra_install_path>/support/analyzeHeadless <ghidra_project_path> <ghidra_project_name> -Import <sample_path> -PostScript capa_ghidra.py "<capa_rules_path> --verbose"
|
||||
```
|
||||
|
||||
You can also provide [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) the single argument `"help"` to view supported arguments when running the script using the Ghidra Headless Analyzer:
|
||||
```
|
||||
<ghidra_install_path>/support/analyzeHeadless <ghidra_project_path> <ghidra_project_name> -process <sample_name> -PostScript capa_ghidra.py "help"
|
||||
```
|
||||
|
||||
The following is an example of running [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) against a shellcode sample using the Ghidra `analyzeHeadless` script:
|
||||
```
|
||||
$ analyzeHeadless /home/wumbo/Desktop/ghidra_projects/ capa_test -process 499c2a85f6e8142c3f48d4251c9c7cd6.raw32 -processor x86:LE:32:default -PostScript capa_ghidra.py "/home/wumbo/capa/rules -vv"
|
||||
[...]
|
||||
|
||||
INFO REPORT: Analysis succeeded for file: /499c2a85f6e8142c3f48d4251c9c7cd6.raw32 (HeadlessAnalyzer)
|
||||
INFO SCRIPT: /home/wumbo/ghidra_scripts/capa_ghidra.py (HeadlessAnalyzer)
|
||||
md5 499c2a85f6e8142c3f48d4251c9c7cd6
|
||||
sha1
|
||||
sha256 e8e02191c1b38c808d27a899ac164b3675eb5cadd3a8907b0ffa863714000e72
|
||||
path /home/wumbo/capa/tests/data/499c2a85f6e8142c3f48d4251c9c7cd6.raw32
|
||||
timestamp 2023-08-29 17:57:00.946588
|
||||
capa version 6.1.0
|
||||
os unknown os
|
||||
format Raw Binary
|
||||
arch x86
|
||||
extractor ghidra
|
||||
base address global
|
||||
rules /home/wumbo/capa/rules
|
||||
function count 42
|
||||
library function count 0
|
||||
total feature count 1970
|
||||
|
||||
contain loop (24 matches, only showing first match of library rule)
|
||||
author moritz.raabe@mandiant.com
|
||||
scope function
|
||||
function @ 0x0
|
||||
or:
|
||||
characteristic: loop @ 0x0
|
||||
characteristic: tight loop @ 0x278
|
||||
|
||||
contain obfuscated stackstrings
|
||||
namespace anti-analysis/obfuscation/string/stackstring
|
||||
author moritz.raabe@mandiant.com
|
||||
scope basic block
|
||||
att&ck Defense Evasion::Obfuscated Files or Information::Indicator Removal from Tools [T1027.005]
|
||||
mbc Anti-Static Analysis::Executable Code Obfuscation::Argument Obfuscation [B0032.020], Anti-Static Analysis::Executable Code Obfuscation::Stack Strings [B0032.017]
|
||||
basic block @ 0x0 in function 0x0
|
||||
characteristic: stack string @ 0x0
|
||||
|
||||
encode data using XOR
|
||||
namespace data-manipulation/encoding/xor
|
||||
author moritz.raabe@mandiant.com
|
||||
scope basic block
|
||||
att&ck Defense Evasion::Obfuscated Files or Information [T1027]
|
||||
mbc Defense Evasion::Obfuscated Files or Information::Encoding-Standard Algorithm [E1027.m02], Data::Encode Data::XOR [C0026.002]
|
||||
basic block @ 0x8AF in function 0x8A1
|
||||
and:
|
||||
characteristic: tight loop @ 0x8AF
|
||||
characteristic: nzxor @ 0x8C0
|
||||
not: = filter for potential false positives
|
||||
or:
|
||||
or: = unsigned bitwise negation operation (~i)
|
||||
number: 0xFFFFFFFF = bitwise negation for unsigned 32 bits
|
||||
number: 0xFFFFFFFFFFFFFFFF = bitwise negation for unsigned 64 bits
|
||||
or: = signed bitwise negation operation (~i)
|
||||
number: 0xFFFFFFF = bitwise negation for signed 32 bits
|
||||
number: 0xFFFFFFFFFFFFFFF = bitwise negation for signed 64 bits
|
||||
or: = Magic constants used in the implementation of strings functions.
|
||||
number: 0x7EFEFEFF = optimized string constant for 32 bits
|
||||
number: 0x81010101 = -0x81010101 = 0x7EFEFEFF
|
||||
number: 0x81010100 = 0x81010100 = ~0x7EFEFEFF
|
||||
number: 0x7EFEFEFEFEFEFEFF = optimized string constant for 64 bits
|
||||
number: 0x8101010101010101 = -0x8101010101010101 = 0x7EFEFEFEFEFEFEFF
|
||||
number: 0x8101010101010100 = 0x8101010101010100 = ~0x7EFEFEFEFEFEFEFF
|
||||
|
||||
get OS information via KUSER_SHARED_DATA
|
||||
namespace host-interaction/os/version
|
||||
author @mr-tz
|
||||
scope function
|
||||
att&ck Discovery::System Information Discovery [T1082]
|
||||
references https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntexapi_x/kuser_shared_data/index.htm
|
||||
function @ 0x1CA6
|
||||
or:
|
||||
number: 0x7FFE026C = NtMajorVersion @ 0x1D18
|
||||
|
||||
|
||||
|
||||
Script /home/wumbo/ghidra_scripts/capa_ghidra.py called exit with code 0
|
||||
|
||||
[...]
|
||||
```bash
|
||||
$ analyzeHeadless /home/wumbo/demo demo -Import /home/wumbo/capa/tests/data/Practical\ Malware\ Analysis\ Lab\ 01-01.dll_ -PostScript capa_ghidra.py "/home/wumbo/capa/rules --verbose"
|
||||
```
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Integrate capa results with Ghidra UI
|
||||
# Run capa against loaded Ghidra database and render results in Ghidra UI
|
||||
# @author Colton Gabertan (gabertan.colton@gmail.com)
|
||||
# @category Python 3.capa
|
||||
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Run capa against loaded Ghidra database
|
||||
# Run capa against loaded Ghidra database and render results in Ghidra Console window
|
||||
# @author Mike Hunhoff (mehunhoff@google.com)
|
||||
# @category Python 3.capa
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -6,15 +6,16 @@
|
||||
# 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 sys
|
||||
import json
|
||||
import gzip
|
||||
import inspect
|
||||
import logging
|
||||
import contextlib
|
||||
import importlib.util
|
||||
from typing import NoReturn
|
||||
from typing import Dict, Union, BinaryIO, Iterator, NoReturn
|
||||
from pathlib import Path
|
||||
|
||||
import tqdm
|
||||
import msgspec.json
|
||||
|
||||
from capa.exceptions import UnsupportedFormatError
|
||||
from capa.features.common import (
|
||||
@@ -24,13 +25,16 @@ from capa.features.common import (
|
||||
FORMAT_SC64,
|
||||
FORMAT_DOTNET,
|
||||
FORMAT_FREEZE,
|
||||
FORMAT_DRAKVUF,
|
||||
FORMAT_UNKNOWN,
|
||||
Format,
|
||||
)
|
||||
|
||||
EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32")
|
||||
EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64")
|
||||
EXTENSIONS_DYNAMIC = ("json", "json_")
|
||||
# CAPE extensions: .json, .json_, .json.gz
|
||||
# DRAKVUF Sandbox extensions: .log, .log.gz
|
||||
EXTENSIONS_DYNAMIC = ("json", "json_", "json.gz", "log", ".log.gz")
|
||||
EXTENSIONS_ELF = "elf_"
|
||||
EXTENSIONS_FREEZE = "frz"
|
||||
|
||||
@@ -70,9 +74,58 @@ def assert_never(value) -> NoReturn:
|
||||
assert False, f"Unhandled value: {value} ({type(value).__name__})" # noqa: B011
|
||||
|
||||
|
||||
def get_format_from_report(sample: Path) -> str:
|
||||
report = json.load(sample.open(encoding="utf-8"))
|
||||
def load_json_from_path(json_path: Path):
|
||||
with gzip.open(json_path, "r") as compressed_report:
|
||||
try:
|
||||
report_json = compressed_report.read()
|
||||
except gzip.BadGzipFile:
|
||||
report = msgspec.json.decode(json_path.read_text(encoding="utf-8"))
|
||||
else:
|
||||
report = msgspec.json.decode(report_json)
|
||||
return report
|
||||
|
||||
|
||||
def decode_json_lines(fd: Union[BinaryIO, gzip.GzipFile]):
|
||||
for line in fd:
|
||||
try:
|
||||
line_s = line.strip().decode()
|
||||
obj = msgspec.json.decode(line_s)
|
||||
yield obj
|
||||
except (msgspec.DecodeError, UnicodeDecodeError):
|
||||
# sometimes DRAKVUF reports bad method names and/or malformed JSON
|
||||
logger.debug("bad DRAKVUF log line: %s", line)
|
||||
|
||||
|
||||
def load_jsonl_from_path(jsonl_path: Path) -> Iterator[Dict]:
|
||||
try:
|
||||
with gzip.open(jsonl_path, "rb") as fg:
|
||||
yield from decode_json_lines(fg)
|
||||
except gzip.BadGzipFile:
|
||||
with jsonl_path.open(mode="rb") as f:
|
||||
yield from decode_json_lines(f)
|
||||
|
||||
|
||||
def load_one_jsonl_from_path(jsonl_path: Path):
|
||||
# this loads one json line to avoid the overhead of loading the entire file
|
||||
try:
|
||||
with gzip.open(jsonl_path, "rb") as f:
|
||||
line = next(iter(f))
|
||||
except gzip.BadGzipFile:
|
||||
with jsonl_path.open(mode="rb") as f:
|
||||
line = next(iter(f))
|
||||
finally:
|
||||
line = msgspec.json.decode(line.decode(errors="ignore"))
|
||||
return line
|
||||
|
||||
|
||||
def get_format_from_report(sample: Path) -> str:
|
||||
if sample.name.endswith((".log", "log.gz")):
|
||||
line = load_one_jsonl_from_path(sample)
|
||||
if "Plugin" in line:
|
||||
return FORMAT_DRAKVUF
|
||||
return FORMAT_UNKNOWN
|
||||
|
||||
report = load_json_from_path(sample)
|
||||
if "CAPE" in report:
|
||||
return FORMAT_CAPE
|
||||
|
||||
@@ -178,9 +231,20 @@ def log_unsupported_cape_report_error(error: str):
|
||||
logger.error("-" * 80)
|
||||
|
||||
|
||||
def log_empty_cape_report_error(error: str):
|
||||
def log_unsupported_drakvuf_report_error(error: str):
|
||||
logger.error("-" * 80)
|
||||
logger.error(" CAPE report is empty or only contains little useful data: %s", error)
|
||||
logger.error(" Input file is not a valid DRAKVUF output file: %s", error)
|
||||
logger.error(" ")
|
||||
logger.error(" capa currently only supports analyzing standard DRAKVUF outputs in JSONL format.")
|
||||
logger.error(
|
||||
" Please make sure your report file is in the standard format and contains both the static and dynamic sections."
|
||||
)
|
||||
logger.error("-" * 80)
|
||||
|
||||
|
||||
def log_empty_sandbox_report_error(error: str, sandbox_name: str):
|
||||
logger.error("-" * 80)
|
||||
logger.error(" %s report is empty or only contains little useful data: %s", sandbox_name, error)
|
||||
logger.error(" ")
|
||||
logger.error(" Please make sure the sandbox run captures useful behaviour of your sample.")
|
||||
logger.error("-" * 80)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -81,6 +81,7 @@ can update using the `Settings` button.
|
||||
* Double-click the `Address` column to navigate your Disassembly view to the address of the associated feature
|
||||
* Double-click a result in the `Rule Information` column to expand its children
|
||||
* Select a checkbox in the `Rule Information` column to highlight the address of the associated feature in your Disassembly view
|
||||
* Reanalyze if you renamed global variables that store dynamically resolved APIs. capa will use these to improve its analysis.
|
||||
|
||||
#### Tips for Rule Generator
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -125,7 +125,7 @@ def install_icon():
|
||||
return False
|
||||
|
||||
# resource leak here. need to call `ida_kernwin.free_custom_icon`?
|
||||
# however, since we're not cycling this icon a lot, its probably ok.
|
||||
# however, since we're not cycling this icon a lot, it's probably ok.
|
||||
# expect to leak exactly one icon per application load.
|
||||
icon = ida_kernwin.load_custom_icon(data=ICON)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -196,7 +196,7 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
|
||||
f" - {author}",
|
||||
" scopes:",
|
||||
f" static: {scope}",
|
||||
" dynamic: unspecified",
|
||||
" dynamic: unsupported",
|
||||
" references:",
|
||||
" - <insert_references>",
|
||||
" examples:",
|
||||
@@ -764,7 +764,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
|
||||
node = self.make_child_node_from_feature(parent, parse_yaml_line(line.strip()))
|
||||
|
||||
# append our new node in case its a parent for another node
|
||||
# append our new node in case it's a parent for another node
|
||||
if node:
|
||||
stack.append(node)
|
||||
|
||||
|
||||
100
capa/loader.py
100
capa/loader.py
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
@@ -6,13 +6,13 @@
|
||||
# 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 sys
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
import contextlib
|
||||
from typing import Set, Dict, List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
import halo
|
||||
from rich.console import Console
|
||||
from typing_extensions import assert_never
|
||||
|
||||
import capa.perf
|
||||
@@ -31,9 +31,6 @@ import capa.features.extractors
|
||||
import capa.render.result_document
|
||||
import capa.render.result_document as rdoc
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.pefile
|
||||
import capa.features.extractors.elffile
|
||||
import capa.features.extractors.dotnetfile
|
||||
import capa.features.extractors.base_extractor
|
||||
import capa.features.extractors.cape.extractor
|
||||
from capa.rules import RuleSet
|
||||
@@ -48,6 +45,7 @@ from capa.features.common import (
|
||||
FORMAT_SC32,
|
||||
FORMAT_SC64,
|
||||
FORMAT_DOTNET,
|
||||
FORMAT_DRAKVUF,
|
||||
)
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.base_extractor import (
|
||||
@@ -64,9 +62,14 @@ BACKEND_DOTNET = "dotnet"
|
||||
BACKEND_BINJA = "binja"
|
||||
BACKEND_PEFILE = "pefile"
|
||||
BACKEND_CAPE = "cape"
|
||||
BACKEND_DRAKVUF = "drakvuf"
|
||||
BACKEND_FREEZE = "freeze"
|
||||
|
||||
|
||||
class CorruptFile(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def is_supported_format(sample: Path) -> bool:
|
||||
"""
|
||||
Return if this is a supported file based on magic header values
|
||||
@@ -140,24 +143,43 @@ def get_workspace(path: Path, input_format: str, sigpaths: List[Path]):
|
||||
import viv_utils.flirt
|
||||
|
||||
logger.debug("generating vivisect workspace for: %s", path)
|
||||
if input_format == FORMAT_AUTO:
|
||||
if not is_supported_format(path):
|
||||
raise UnsupportedFormatError()
|
||||
|
||||
# don't analyze, so that we can add our Flirt function analyzer first.
|
||||
vw = viv_utils.getWorkspace(str(path), analyze=False, should_save=False)
|
||||
elif input_format in {FORMAT_PE, FORMAT_ELF}:
|
||||
vw = viv_utils.getWorkspace(str(path), analyze=False, should_save=False)
|
||||
elif input_format == FORMAT_SC32:
|
||||
# these are not analyzed nor saved.
|
||||
vw = viv_utils.getShellcodeWorkspaceFromFile(str(path), arch="i386", analyze=False)
|
||||
elif input_format == FORMAT_SC64:
|
||||
vw = viv_utils.getShellcodeWorkspaceFromFile(str(path), arch="amd64", analyze=False)
|
||||
else:
|
||||
raise ValueError("unexpected format: " + input_format)
|
||||
try:
|
||||
if input_format == FORMAT_AUTO:
|
||||
if not is_supported_format(path):
|
||||
raise UnsupportedFormatError()
|
||||
|
||||
# don't analyze, so that we can add our Flirt function analyzer first.
|
||||
vw = viv_utils.getWorkspace(str(path), analyze=False, should_save=False)
|
||||
elif input_format in {FORMAT_PE, FORMAT_ELF}:
|
||||
vw = viv_utils.getWorkspace(str(path), analyze=False, should_save=False)
|
||||
elif input_format == FORMAT_SC32:
|
||||
# these are not analyzed nor saved.
|
||||
vw = viv_utils.getShellcodeWorkspaceFromFile(str(path), arch="i386", analyze=False)
|
||||
elif input_format == FORMAT_SC64:
|
||||
vw = viv_utils.getShellcodeWorkspaceFromFile(str(path), arch="amd64", analyze=False)
|
||||
else:
|
||||
raise ValueError("unexpected format: " + input_format)
|
||||
except Exception as e:
|
||||
# vivisect raises raw Exception instances, and we don't want
|
||||
# to do a subclass check via isinstance.
|
||||
if type(e) is Exception and "Couldn't convert rva" in e.args[0]:
|
||||
raise CorruptFile(e.args[0]) from e
|
||||
|
||||
viv_utils.flirt.register_flirt_signature_analyzers(vw, [str(s) for s in sigpaths])
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
# unfortuately viv raises a raw Exception (not any subclass).
|
||||
# This happens when the module isn't found, such as with a viv upgrade.
|
||||
#
|
||||
# Remove the symbolic switch case solver.
|
||||
# This is only enabled for ELF files, not PE files.
|
||||
# During the following performance investigation, this analysis module
|
||||
# had some terrible worst-case behavior.
|
||||
# We can put up with slightly worse CFG reconstruction in order to avoid this.
|
||||
# https://github.com/mandiant/capa/issues/1989#issuecomment-1948022767
|
||||
vw.delFuncAnalysisModule("vivisect.analysis.generic.symswitchcase")
|
||||
|
||||
vw.analyze()
|
||||
|
||||
logger.debug("%s", get_meta_str(vw))
|
||||
@@ -180,12 +202,22 @@ def get_extractor(
|
||||
UnsupportedArchError
|
||||
UnsupportedOSError
|
||||
"""
|
||||
|
||||
# stderr=True is used here to redirect the spinner banner to stderr, so that users can redirect capa's output.
|
||||
console = Console(stderr=True, quiet=disable_progress)
|
||||
|
||||
if backend == BACKEND_CAPE:
|
||||
import capa.features.extractors.cape.extractor
|
||||
|
||||
report = json.loads(input_path.read_text(encoding="utf-8"))
|
||||
report = capa.helpers.load_json_from_path(input_path)
|
||||
return capa.features.extractors.cape.extractor.CapeExtractor.from_report(report)
|
||||
|
||||
elif backend == BACKEND_DRAKVUF:
|
||||
import capa.features.extractors.drakvuf.extractor
|
||||
|
||||
report = capa.helpers.load_jsonl_from_path(input_path)
|
||||
return capa.features.extractors.drakvuf.extractor.DrakvufExtractor.from_report(report)
|
||||
|
||||
elif backend == BACKEND_DOTNET:
|
||||
import capa.features.extractors.dnfile.extractor
|
||||
|
||||
@@ -226,7 +258,7 @@ def get_extractor(
|
||||
if os_ == OS_AUTO and not is_supported_os(input_path):
|
||||
raise UnsupportedOSError()
|
||||
|
||||
with halo.Halo(text="analyzing program", spinner="simpleDots", stream=sys.stderr, enabled=not disable_progress):
|
||||
with console.status("analyzing program...", spinner="dots"):
|
||||
bv: BinaryView = binaryninja.load(str(input_path))
|
||||
if bv is None:
|
||||
raise RuntimeError(f"Binary Ninja cannot open file {input_path}")
|
||||
@@ -251,7 +283,7 @@ def get_extractor(
|
||||
if os_ == OS_AUTO and not is_supported_os(input_path):
|
||||
raise UnsupportedOSError()
|
||||
|
||||
with halo.Halo(text="analyzing program", spinner="simpleDots", stream=sys.stderr, enabled=not disable_progress):
|
||||
with console.status("analyzing program...", spinner="dots"):
|
||||
vw = get_workspace(input_path, input_format, sigpaths)
|
||||
|
||||
if should_save_workspace:
|
||||
@@ -276,20 +308,40 @@ def get_extractor(
|
||||
def get_file_extractors(input_file: Path, input_format: str) -> List[FeatureExtractor]:
|
||||
file_extractors: List[FeatureExtractor] = []
|
||||
|
||||
# we use lazy importing here to avoid eagerly loading dependencies
|
||||
# that some specialized environments may not have,
|
||||
# e.g., those that run capa without vivisect.
|
||||
|
||||
if input_format == FORMAT_PE:
|
||||
import capa.features.extractors.pefile
|
||||
|
||||
file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(input_file))
|
||||
|
||||
elif input_format == FORMAT_DOTNET:
|
||||
import capa.features.extractors.pefile
|
||||
import capa.features.extractors.dotnetfile
|
||||
|
||||
file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(input_file))
|
||||
file_extractors.append(capa.features.extractors.dotnetfile.DotnetFileFeatureExtractor(input_file))
|
||||
|
||||
elif input_format == FORMAT_ELF:
|
||||
import capa.features.extractors.elffile
|
||||
|
||||
file_extractors.append(capa.features.extractors.elffile.ElfFeatureExtractor(input_file))
|
||||
|
||||
elif input_format == FORMAT_CAPE:
|
||||
report = json.loads(input_file.read_text(encoding="utf-8"))
|
||||
import capa.features.extractors.cape.extractor
|
||||
|
||||
report = capa.helpers.load_json_from_path(input_file)
|
||||
file_extractors.append(capa.features.extractors.cape.extractor.CapeExtractor.from_report(report))
|
||||
|
||||
elif input_format == FORMAT_DRAKVUF:
|
||||
import capa.helpers
|
||||
import capa.features.extractors.drakvuf.extractor
|
||||
|
||||
report = capa.helpers.load_jsonl_from_path(input_file)
|
||||
file_extractors.append(capa.features.extractors.drakvuf.extractor.DrakvufExtractor.from_report(report))
|
||||
|
||||
return file_extractors
|
||||
|
||||
|
||||
|
||||
170
capa/main.py
170
capa/main.py
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
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: [package root]/LICENSE.txt
|
||||
@@ -17,7 +17,7 @@ import argparse
|
||||
import textwrap
|
||||
import contextlib
|
||||
from types import TracebackType
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Set, Dict, List, Optional, TypedDict
|
||||
from pathlib import Path
|
||||
|
||||
import colorama
|
||||
@@ -40,24 +40,29 @@ import capa.features.extractors
|
||||
import capa.render.result_document
|
||||
import capa.render.result_document as rdoc
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.pefile
|
||||
import capa.features.extractors.elffile
|
||||
import capa.features.extractors.dotnetfile
|
||||
import capa.features.extractors.base_extractor
|
||||
import capa.features.extractors.cape.extractor
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
from capa.loader import BACKEND_VIV, BACKEND_CAPE, BACKEND_BINJA, BACKEND_DOTNET, BACKEND_FREEZE, BACKEND_PEFILE
|
||||
from capa.loader import (
|
||||
BACKEND_VIV,
|
||||
BACKEND_CAPE,
|
||||
BACKEND_BINJA,
|
||||
BACKEND_DOTNET,
|
||||
BACKEND_FREEZE,
|
||||
BACKEND_PEFILE,
|
||||
BACKEND_DRAKVUF,
|
||||
)
|
||||
from capa.helpers import (
|
||||
get_file_taste,
|
||||
get_auto_format,
|
||||
log_unsupported_os_error,
|
||||
log_unsupported_arch_error,
|
||||
log_empty_cape_report_error,
|
||||
log_unsupported_format_error,
|
||||
log_empty_sandbox_report_error,
|
||||
log_unsupported_cape_report_error,
|
||||
log_unsupported_drakvuf_report_error,
|
||||
)
|
||||
from capa.exceptions import (
|
||||
InvalidArgument,
|
||||
EmptyReportError,
|
||||
UnsupportedOSError,
|
||||
UnsupportedArchError,
|
||||
@@ -78,9 +83,18 @@ from capa.features.common import (
|
||||
FORMAT_DOTNET,
|
||||
FORMAT_FREEZE,
|
||||
FORMAT_RESULT,
|
||||
FORMAT_DRAKVUF,
|
||||
STATIC_FORMATS,
|
||||
DYNAMIC_FORMATS,
|
||||
)
|
||||
from capa.capabilities.common import find_capabilities, has_file_limitation, find_file_capabilities
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor, StaticFeatureExtractor, DynamicFeatureExtractor
|
||||
from capa.features.extractors.base_extractor import (
|
||||
ProcessFilter,
|
||||
FunctionFilter,
|
||||
FeatureExtractor,
|
||||
StaticFeatureExtractor,
|
||||
DynamicFeatureExtractor,
|
||||
)
|
||||
|
||||
RULES_PATH_DEFAULT_STRING = "(embedded rules)"
|
||||
SIGNATURES_PATH_DEFAULT_STRING = "(embedded signatures)"
|
||||
@@ -101,10 +115,17 @@ E_MISSING_CAPE_STATIC_ANALYSIS = 21
|
||||
E_MISSING_CAPE_DYNAMIC_ANALYSIS = 22
|
||||
E_EMPTY_REPORT = 23
|
||||
E_UNSUPPORTED_GHIDRA_EXECUTION_MODE = 24
|
||||
E_INVALID_INPUT_FORMAT = 25
|
||||
E_INVALID_FEATURE_EXTRACTOR = 26
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
|
||||
class FilterConfig(TypedDict, total=False):
|
||||
processes: Set[int]
|
||||
functions: Set[int]
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def timing(msg: str):
|
||||
t0 = time.time()
|
||||
@@ -237,6 +258,7 @@ def install_common_args(parser, wanted=None):
|
||||
(FORMAT_SC32, "32-bit shellcode"),
|
||||
(FORMAT_SC64, "64-bit shellcode"),
|
||||
(FORMAT_CAPE, "CAPE sandbox report"),
|
||||
(FORMAT_DRAKVUF, "DRAKVUF sandbox report"),
|
||||
(FORMAT_FREEZE, "features previously frozen by capa"),
|
||||
]
|
||||
format_help = ", ".join([f"{f[0]}: {f[1]}" for f in formats])
|
||||
@@ -251,13 +273,14 @@ def install_common_args(parser, wanted=None):
|
||||
|
||||
if "backend" in wanted:
|
||||
backends = [
|
||||
(BACKEND_AUTO, "(default) detect apppropriate backend automatically"),
|
||||
(BACKEND_AUTO, "(default) detect appropriate backend automatically"),
|
||||
(BACKEND_VIV, "vivisect"),
|
||||
(BACKEND_PEFILE, "pefile (file features only)"),
|
||||
(BACKEND_BINJA, "Binary Ninja"),
|
||||
(BACKEND_DOTNET, ".NET"),
|
||||
(BACKEND_FREEZE, "capa freeze"),
|
||||
(BACKEND_CAPE, "CAPE"),
|
||||
(BACKEND_DRAKVUF, "DRAKVUF"),
|
||||
]
|
||||
backend_help = ", ".join([f"{f[0]}: {f[1]}" for f in backends])
|
||||
parser.add_argument(
|
||||
@@ -269,6 +292,22 @@ def install_common_args(parser, wanted=None):
|
||||
help=f"select backend, {backend_help}",
|
||||
)
|
||||
|
||||
if "restrict-to-functions" in wanted:
|
||||
parser.add_argument(
|
||||
"--restrict-to-functions",
|
||||
type=lambda s: s.replace(" ", "").split(","),
|
||||
default=[],
|
||||
help="provide a list of comma-separated function virtual addresses to analyze (static analysis).",
|
||||
)
|
||||
|
||||
if "restrict-to-processes" in wanted:
|
||||
parser.add_argument(
|
||||
"--restrict-to-processes",
|
||||
type=lambda s: s.replace(" ", "").split(","),
|
||||
default=[],
|
||||
help="provide a list of comma-separated process IDs to analyze (dynamic analysis).",
|
||||
)
|
||||
|
||||
if "os" in wanted:
|
||||
oses = [
|
||||
(OS_AUTO, "detect OS automatically - default"),
|
||||
@@ -322,7 +361,7 @@ def install_common_args(parser, wanted=None):
|
||||
# Library code should *not* call these functions.
|
||||
#
|
||||
# These main routines may raise `ShouldExitError` to indicate the program
|
||||
# ...should exit. Its a tiny step away from doing `sys.exit()` directly.
|
||||
# ...should exit. It's a tiny step away from doing `sys.exit()` directly.
|
||||
# I'm not sure if we should just do that. In the meantime, programs should
|
||||
# handle `ShouldExitError` and pass the status code to `sys.exit()`.
|
||||
#
|
||||
@@ -343,8 +382,9 @@ def handle_common_args(args):
|
||||
- rules: file system path to rule files.
|
||||
- signatures: file system path to signature files.
|
||||
|
||||
the following field may be added:
|
||||
the following fields may be added:
|
||||
- is_default_rules: if the default rules were used.
|
||||
- is_default_signatures: if the default signatures were used.
|
||||
|
||||
args:
|
||||
args: The parsed command line arguments from `install_common_args`.
|
||||
@@ -437,25 +477,11 @@ def handle_common_args(args):
|
||||
|
||||
if hasattr(args, "signatures"):
|
||||
if args.signatures == SIGNATURES_PATH_DEFAULT_STRING:
|
||||
logger.debug("-" * 80)
|
||||
logger.debug(" Using default embedded signatures.")
|
||||
logger.debug(
|
||||
" To provide your own signatures, use the form `capa.exe --signature ./path/to/signatures/ /path/to/mal.exe`."
|
||||
)
|
||||
logger.debug("-" * 80)
|
||||
|
||||
sigs_path = get_default_root() / "sigs"
|
||||
|
||||
if not sigs_path.exists():
|
||||
logger.error(
|
||||
"Using default signature path, but it doesn't exist. " # noqa: G003 [logging statement uses +]
|
||||
+ "Please install the signatures first: "
|
||||
+ "https://github.com/mandiant/capa/blob/master/doc/installation.md#method-2-using-capa-as-a-python-library."
|
||||
)
|
||||
raise IOError(f"signatures path {sigs_path} does not exist or cannot be accessed")
|
||||
args.is_default_signatures = True
|
||||
else:
|
||||
sigs_path = Path(args.signatures)
|
||||
logger.debug("using signatures path: %s", sigs_path)
|
||||
args.is_default_signatures = False
|
||||
|
||||
args.signatures = sigs_path
|
||||
|
||||
@@ -523,6 +549,9 @@ def get_backend_from_cli(args, input_format: str) -> str:
|
||||
if input_format == FORMAT_CAPE:
|
||||
return BACKEND_CAPE
|
||||
|
||||
if input_format == FORMAT_DRAKVUF:
|
||||
return BACKEND_DRAKVUF
|
||||
|
||||
elif input_format == FORMAT_DOTNET:
|
||||
return BACKEND_DOTNET
|
||||
|
||||
@@ -547,7 +576,7 @@ def get_sample_path_from_cli(args, backend: str) -> Optional[Path]:
|
||||
raises:
|
||||
ShouldExitError: if the program is invoked incorrectly and should exit.
|
||||
"""
|
||||
if backend == BACKEND_CAPE:
|
||||
if backend in (BACKEND_CAPE, BACKEND_DRAKVUF):
|
||||
return None
|
||||
else:
|
||||
return args.input_file
|
||||
@@ -650,12 +679,17 @@ def get_file_extractors_from_cli(args, input_format: str) -> List[FeatureExtract
|
||||
except UnsupportedFormatError as e:
|
||||
if input_format == FORMAT_CAPE:
|
||||
log_unsupported_cape_report_error(str(e))
|
||||
elif input_format == FORMAT_DRAKVUF:
|
||||
log_unsupported_drakvuf_report_error(str(e))
|
||||
else:
|
||||
log_unsupported_format_error()
|
||||
raise ShouldExitError(E_INVALID_FILE_TYPE) from e
|
||||
except EmptyReportError as e:
|
||||
if input_format == FORMAT_CAPE:
|
||||
log_empty_cape_report_error(str(e))
|
||||
log_empty_sandbox_report_error(str(e), sandbox_name="CAPE")
|
||||
raise ShouldExitError(E_EMPTY_REPORT) from e
|
||||
elif input_format == FORMAT_DRAKVUF:
|
||||
log_empty_sandbox_report_error(str(e), sandbox_name="DRAKVUF")
|
||||
raise ShouldExitError(E_EMPTY_REPORT) from e
|
||||
else:
|
||||
log_unsupported_format_error()
|
||||
@@ -706,6 +740,24 @@ def get_signatures_from_cli(args, input_format: str, backend: str) -> List[Path]
|
||||
logger.debug("skipping library code matching: signatures only supports PE files")
|
||||
return []
|
||||
|
||||
if args.is_default_signatures:
|
||||
logger.debug("-" * 80)
|
||||
logger.debug(" Using default embedded signatures.")
|
||||
logger.debug(
|
||||
" To provide your own signatures, use the form `capa.exe --signature ./path/to/signatures/ /path/to/mal.exe`."
|
||||
)
|
||||
logger.debug("-" * 80)
|
||||
|
||||
if not args.signatures.exists():
|
||||
logger.error(
|
||||
"Using default signature path, but it doesn't exist. " # noqa: G003 [logging statement uses +]
|
||||
+ "Please install the signatures first: "
|
||||
+ "https://github.com/mandiant/capa/blob/master/doc/installation.md#method-2-using-capa-as-a-python-library."
|
||||
)
|
||||
raise IOError(f"signatures path {args.signatures} does not exist or cannot be accessed")
|
||||
else:
|
||||
logger.debug("using signatures path: %s", args.signatures)
|
||||
|
||||
try:
|
||||
return capa.loader.get_signatures(args.signatures)
|
||||
except IOError as e:
|
||||
@@ -729,9 +781,10 @@ def get_extractor_from_cli(args, input_format: str, backend: str) -> FeatureExtr
|
||||
|
||||
os_ = get_os_from_cli(args, backend)
|
||||
sample_path = get_sample_path_from_cli(args, backend)
|
||||
extractor_filters = get_extractor_filters_from_cli(args, input_format)
|
||||
|
||||
try:
|
||||
return capa.loader.get_extractor(
|
||||
extractor = capa.loader.get_extractor(
|
||||
args.input_file,
|
||||
input_format,
|
||||
os_,
|
||||
@@ -741,9 +794,12 @@ def get_extractor_from_cli(args, input_format: str, backend: str) -> FeatureExtr
|
||||
disable_progress=args.quiet or args.debug,
|
||||
sample_path=sample_path,
|
||||
)
|
||||
return apply_extractor_filters(extractor, extractor_filters)
|
||||
except UnsupportedFormatError as e:
|
||||
if input_format == FORMAT_CAPE:
|
||||
log_unsupported_cape_report_error(str(e))
|
||||
elif input_format == FORMAT_DRAKVUF:
|
||||
log_unsupported_drakvuf_report_error(str(e))
|
||||
else:
|
||||
log_unsupported_format_error()
|
||||
raise ShouldExitError(E_INVALID_FILE_TYPE) from e
|
||||
@@ -753,6 +809,41 @@ def get_extractor_from_cli(args, input_format: str, backend: str) -> FeatureExtr
|
||||
except UnsupportedOSError as e:
|
||||
log_unsupported_os_error()
|
||||
raise ShouldExitError(E_INVALID_FILE_OS) from e
|
||||
except capa.loader.CorruptFile as e:
|
||||
logger.error("Input file '%s' is not a valid file: %s", args.input_file, str(e))
|
||||
raise ShouldExitError(E_CORRUPT_FILE) from e
|
||||
|
||||
|
||||
def get_extractor_filters_from_cli(args, input_format) -> FilterConfig:
|
||||
if not hasattr(args, "restrict_to_processes") and not hasattr(args, "restrict_to_functions"):
|
||||
# no processes or function filters were installed in the args
|
||||
return {}
|
||||
|
||||
if input_format in STATIC_FORMATS:
|
||||
if args.restrict_to_processes:
|
||||
raise InvalidArgument("Cannot filter processes with static analysis.")
|
||||
return {"functions": {int(addr, 0) for addr in args.restrict_to_functions}}
|
||||
elif input_format in DYNAMIC_FORMATS:
|
||||
if args.restrict_to_functions:
|
||||
raise InvalidArgument("Cannot filter functions with dynamic analysis.")
|
||||
return {"processes": {int(pid, 0) for pid in args.restrict_to_processes}}
|
||||
else:
|
||||
raise ShouldExitError(E_INVALID_INPUT_FORMAT)
|
||||
|
||||
|
||||
def apply_extractor_filters(extractor: FeatureExtractor, extractor_filters: FilterConfig):
|
||||
if not any(extractor_filters.values()):
|
||||
return extractor
|
||||
|
||||
# if the user specified extractor filters, then apply them here
|
||||
if isinstance(extractor, StaticFeatureExtractor):
|
||||
assert extractor_filters["functions"]
|
||||
return FunctionFilter(extractor, extractor_filters["functions"])
|
||||
elif isinstance(extractor, DynamicFeatureExtractor):
|
||||
assert extractor_filters["processes"]
|
||||
return ProcessFilter(extractor, extractor_filters["processes"])
|
||||
else:
|
||||
raise ShouldExitError(E_INVALID_FEATURE_EXTRACTOR)
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None):
|
||||
@@ -794,7 +885,20 @@ def main(argv: Optional[List[str]] = None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description=desc, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
install_common_args(parser, {"input_file", "format", "backend", "os", "signatures", "rules", "tag"})
|
||||
install_common_args(
|
||||
parser,
|
||||
{
|
||||
"input_file",
|
||||
"format",
|
||||
"backend",
|
||||
"os",
|
||||
"signatures",
|
||||
"rules",
|
||||
"tag",
|
||||
"restrict-to-functions",
|
||||
"restrict-to-processes",
|
||||
},
|
||||
)
|
||||
parser.add_argument("-j", "--json", action="store_true", help="emit JSON instead of text")
|
||||
args = parser.parse_args(args=argv)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
|
||||
# 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: [package root]/LICENSE.txt
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user