Compare commits

...

335 Commits

Author SHA1 Message Date
Mike Hunhoff
30272d5df6 Update capa/features/extractors/dnfile/extractor.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-02-28 15:21:31 -07:00
Mike Hunhoff
23d076e0dc use function address when emitting instructions 2023-02-27 12:01:59 -07:00
Mike Hunhoff
e99525a11e PR changes 2023-02-24 14:52:31 -07:00
Mike Hunhoff
c3778cf7b1 update CHANGELOG 2023-02-24 14:48:09 -07:00
Mike Hunhoff
969403ae51 dotnet: add support for basic blocks 2023-02-24 14:42:38 -07:00
Capa Bot
17f70bb87c Sync capa rules submodule 2023-02-23 08:47:24 +00:00
Capa Bot
7a1f2f4b3b Sync capa rules submodule 2023-02-22 19:24:48 +00:00
Capa Bot
599d3ac92c Sync capa rules submodule 2023-02-21 21:38:32 +00:00
Capa Bot
02f8e57e66 Sync capa rules submodule 2023-02-21 10:46:20 +00:00
Capa Bot
5e600d02a8 Sync capa rules submodule 2023-02-20 08:05:09 +00:00
Capa Bot
b9edb6dbc9 Sync capa-testfiles submodule 2023-02-16 10:31:51 +00:00
Capa Bot
6e5302e5ec Sync capa rules submodule 2023-02-15 16:46:14 +00:00
Capa Bot
4b472c8564 Sync capa rules submodule 2023-02-15 15:16:41 +00:00
Capa Bot
4ccf6f0e69 Sync capa rules submodule 2023-02-15 10:57:23 +00:00
Capa Bot
eac3d8336d Sync capa-testfiles submodule 2023-02-15 10:56:23 +00:00
Capa Bot
53475c9643 Sync capa rules submodule 2023-02-15 10:55:49 +00:00
Willi Ballenthin
3c0361fd5c Merge pull request #1317 from mandiant/fix-loop-viv
fix loop detection corner case
2023-02-15 11:50:26 +01:00
mr-tz
0d14c168a4 fix loop detection corner case 2023-02-15 11:41:54 +01:00
Capa Bot
00ecfe7a80 Sync capa-testfiles submodule 2023-02-15 10:22:12 +00:00
Willi Ballenthin
fd64b2c5d5 Merge pull request #1315 from mandiant/typing-address
freeze: better type annotations for Address value
2023-02-14 15:05:31 +01:00
Willi Ballenthin
514b4929b3 freeze: better type annotations for Address value 2023-02-14 09:47:57 +01:00
Capa Bot
4ea3475d2b Sync capa rules submodule 2023-02-13 09:50:39 +00:00
Capa Bot
15a276e3a5 Sync capa rules submodule 2023-02-13 09:47:05 +00:00
Capa Bot
f6e58ea212 Sync capa rules submodule 2023-02-10 10:08:30 +00:00
Capa Bot
1b191b5aea Sync capa-testfiles submodule 2023-02-10 08:52:58 +00:00
Moritz
c2346f41cb update to v5.0.0 (#1308) 2023-02-08 21:34:45 +01:00
Capa Bot
3f40f47104 Sync capa rules submodule 2023-02-08 08:57:54 +00:00
Capa Bot
3dfb7beb6b Sync capa rules submodule 2023-02-07 15:56:56 +00:00
Moritz
6a222a6139 Update black (#1307)
* build(deps-dev): bump black from 22.12.0 to 23.1.0

Bumps [black](https://github.com/psf/black) from 22.12.0 to 23.1.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/22.12.0...23.1.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* reformat black 23.1.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-07 15:50:15 +01:00
Capa Bot
b34864c55e Sync capa rules submodule 2023-02-07 14:49:39 +00:00
Capa Bot
26655315c7 Sync capa rules submodule 2023-02-07 14:48:39 +00:00
Capa Bot
8aaa8809e6 Sync capa-testfiles submodule 2023-02-07 11:21:49 +00:00
Capa Bot
cbac0e0d3b Sync capa rules submodule 2023-02-07 09:59:16 +00:00
Capa Bot
22b8c594b8 Sync capa-testfiles submodule 2023-02-06 20:47:00 +00:00
Capa Bot
7a8065b2bb Sync capa rules submodule 2023-02-06 17:13:11 +00:00
Capa Bot
6070479e0a Sync capa rules submodule 2023-02-06 17:12:33 +00:00
Moritz
fd70dc24df feat: store results to database and UI updates (#1292)
* feat: store results to database and UI updates

* feat: update result caching and UI

* use system rules cache and improve result cache validation

* improve buttons and status messages

* improve error messaging for invalid caches

---------

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2023-02-06 16:37:19 +01:00
Capa Bot
8cb8cfdb46 Sync capa-testfiles submodule 2023-02-06 15:21:58 +00:00
Capa Bot
79f25ec0a3 Sync capa rules submodule 2023-02-06 14:15:55 +00:00
Capa Bot
2235417a25 Sync capa-testfiles submodule 2023-02-06 14:07:24 +00:00
Capa Bot
ce449790df Sync capa-testfiles submodule 2023-02-06 14:03:55 +00:00
Capa Bot
79e36ab11d Sync capa-testfiles submodule 2023-02-06 13:52:53 +00:00
Capa Bot
dde3abdfa0 Sync capa-testfiles submodule 2023-02-06 09:07:31 +00:00
Mike Hunhoff
7ea166f98c explorer: fix UnboundLocal errors and improve render match by function (#1302) 2023-02-02 12:33:30 -07:00
Capa Bot
faceca6fec Sync capa rules submodule 2023-02-02 08:12:15 +00:00
Capa Bot
6589b2044b Sync capa rules submodule 2023-02-01 15:29:00 +00:00
Capa Bot
f00e44aba6 Sync capa-testfiles submodule 2023-02-01 15:28:22 +00:00
Capa Bot
6591b574a0 Sync capa rules submodule 2023-02-01 14:13:20 +00:00
Moritz
ca91051d1a Fix string length >= 4 and remove bytes/string overlaps (#1298)
* fix min string length >= 4

* feat: don't extract bytes for strings
2023-02-01 14:53:16 +01:00
Capa Bot
29f24de5d5 Sync capa rules submodule 2023-02-01 09:10:08 +00:00
Capa Bot
2014c64732 Sync capa rules submodule 2023-02-01 09:09:30 +00:00
Moritz
b5c6cdeaa1 Update ATT&CK and MBC lint data (#1297)
* sort by ID

* update ATT&CK/MBC lint data via script
2023-02-01 09:56:10 +01:00
Moritz
bf7c569060 Delete hook-smda.py (#1296) 2023-01-30 10:15:56 +01:00
Capa Bot
bbc0afd083 Sync capa rules submodule 2023-01-27 08:56:49 +00:00
Capa Bot
8857f92f7c Sync capa rules submodule 2023-01-26 08:15:31 +00:00
Willi Ballenthin
70f568b1cc Merge pull request #1291 from mandiant/rules-cache
cache rule set across invocations of capa
2023-01-25 17:52:34 +01:00
Capa Bot
c586166006 Sync capa-testfiles submodule 2023-01-25 16:45:08 +00:00
Moritz
96f266ce5e ci: pin GitHub Actions versions (#1295) 2023-01-25 17:34:28 +01:00
Willi Ballenthin
e5549d6ce8 Update capa/ida/plugin/form.py 2023-01-25 16:47:01 +01:00
Capa Bot
b60717bb8c Sync capa rules submodule 2023-01-24 14:35:01 +00:00
Willi Ballenthin
83eefd343c Update scripts/capa2yara.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-01-24 15:33:37 +01:00
Moritz
03e8be6368 Create scorecard.yml (#1294) 2023-01-24 14:15:53 +01:00
Capa Bot
a58e9e4df3 Sync capa rules submodule 2023-01-23 13:53:42 +00:00
Moritz
0a78187c69 optimize tests to speed them up (#1287)
* optimize tests to speed them up

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-01-23 11:25:04 +01:00
Willi Ballenthin
61112c2527 lint: fix pbar counts 2023-01-21 20:16:49 +01:00
Willi Ballenthin
67cfefd2df main: get_rules: remove progress bar 2023-01-21 19:38:23 +01:00
Willi Ballenthin
3dfd16c033 main: fix ValueError 2023-01-21 19:30:15 +01:00
Willi Ballenthin
67b9d2e1c0 black 2023-01-21 19:28:15 +01:00
Willi Ballenthin
a076a0c44e main: further document get_rules 2023-01-21 19:24:20 +01:00
Willi Ballenthin
f152729c79 explorer: use main.get_rules and simplify cache 2023-01-21 19:10:50 +01:00
Willi Ballenthin
3c0e36d5d4 ruleset: record number of source rules loaded 2023-01-21 19:10:35 +01:00
Willi Ballenthin
887f37b72c main: get_rules: accept callback to update status 2023-01-21 19:10:02 +01:00
Willi Ballenthin
e30dd08dec cache: add doc 2023-01-21 18:20:14 +01:00
Willi Ballenthin
2d1bbeda0c Merge branch 'rules-cache' of personal.github.com:mandiant/capa into rules-cache 2023-01-21 18:14:42 +01:00
Willi Ballenthin
68603a9cc7 Update scripts/cache-ruleset.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-01-23 12:13:07 +01:00
Willi Ballenthin
6c83db9977 Update scripts/cache-ruleset.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-01-23 12:12:57 +01:00
Willi Ballenthin
6d16cafbc8 cache: handle invalid caches 2023-01-21 18:14:12 +01:00
Willi Ballenthin
e503cedd8f main: pbar: realize the list so it has a length 2023-01-21 17:31:57 +01:00
Willi Ballenthin
1a498d1afc main: fix reference error 2023-01-20 16:21:44 +01:00
Willi Ballenthin
33a46cc633 ci: cache the ruleset 2023-01-20 16:19:46 +01:00
Willi Ballenthin
b3b9ec11dd pyinstaller: package up the cache directory, too 2023-01-20 16:11:00 +01:00
Willi Ballenthin
a7afdec2e1 cache: accept cache_dir parameter 2023-01-20 16:10:41 +01:00
Willi Ballenthin
56a0bedac9 scripts: add tool to cache a ruleset to a directory 2023-01-20 15:50:17 +01:00
Willi Ballenthin
f451fe68e1 pep8/mypy 2023-01-20 15:42:22 +01:00
Willi Ballenthin
946816e377 cache: improve variable name 2023-01-20 15:26:17 +01:00
Willi Ballenthin
99af09fce5 main: revert wording change, which was just churn 2023-01-20 15:24:34 +01:00
Willi Ballenthin
0888e5ad69 main: more doc 2023-01-20 15:22:43 +01:00
Willi Ballenthin
c423ccec67 add tests for ruleset caching 2023-01-20 15:20:26 +01:00
Willi Ballenthin
03f72f498e cache: use zlib to reduce cache size 2023-01-20 15:20:10 +01:00
Willi Ballenthin
fbd7c566f4 cache: add more helpers
to enable better testing
2023-01-20 15:19:48 +01:00
Willi Ballenthin
e09d35bbb9 main: fix rule content decoding 2023-01-20 15:01:05 +01:00
Willi Ballenthin
e644775ad1 changelog 2023-01-20 14:52:47 +01:00
Willi Ballenthin
6ad471a914 Merge branch 'master' into rules-cache 2023-01-20 14:51:32 +01:00
Willi Ballenthin
476ffabae9 rules: cache the ruleset to disk
ref: #1212
2023-01-20 14:50:00 +01:00
Willi Ballenthin
4b7a9e149f rules: move to directory structure 2023-01-20 13:27:30 +01:00
Capa Bot
49c18bd83d Sync capa rules submodule 2023-01-20 12:15:23 +00:00
Capa Bot
67717761bd Sync capa rules submodule 2023-01-20 12:15:02 +00:00
Capa Bot
b10196cdac Sync capa rules submodule 2023-01-20 11:12:04 +00:00
Moritz
fa0ddba436 add format to global features and code refactors (#1284)
* refactor: get format handling

* add format to global features
2023-01-19 13:31:00 +01:00
Capa Bot
0fb3be359f Sync capa rules submodule 2023-01-19 12:12:41 +00:00
Capa Bot
26662e99de Sync capa rules submodule 2023-01-19 12:11:19 +00:00
Willi Ballenthin
5513d4ca43 viv: insn: string: handle viv bug around substrings (#1273)
* viv: insn: string: handle viv bug around substrings

closes #1271

* use minimum string length 4

* update overlapping string test and fixup vivisect elf analysis missing function

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-01-19 13:02:53 +01:00
Capa Bot
2b07ec925c Sync capa rules submodule 2023-01-19 11:23:42 +00:00
Capa Bot
efb4c9d540 Sync capa rules submodule 2023-01-19 10:58:26 +00:00
Moritz
b8de9625ee fix: don't extract invalid calls from features (#1285) 2023-01-19 11:56:13 +01:00
Willi Ballenthin
607daa345e Merge pull request #1288 from mandiant/dependabot/pip/wcwidth-0.2.6
build(deps): bump wcwidth from 0.2.5 to 0.2.6
2023-01-19 11:43:35 +01:00
Capa Bot
35e6df6f6b Sync capa rules submodule 2023-01-18 15:10:43 +00:00
dependabot[bot]
cb1ef965d0 build(deps): bump wcwidth from 0.2.5 to 0.2.6
Bumps [wcwidth](https://github.com/jquast/wcwidth) from 0.2.5 to 0.2.6.
- [Release notes](https://github.com/jquast/wcwidth/releases)
- [Commits](https://github.com/jquast/wcwidth/compare/0.2.5...0.2.6)

---
updated-dependencies:
- dependency-name: wcwidth
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-16 14:03:54 +00:00
Capa Bot
2ab057a24d Sync capa rules submodule 2023-01-12 13:15:35 +00:00
Capa Bot
12f8588c03 Sync capa-testfiles submodule 2023-01-12 12:59:01 +00:00
Capa Bot
3571f35578 Sync capa rules submodule 2023-01-12 11:57:41 +00:00
Willi Ballenthin
803fe321d1 Merge pull request #1283 from mandiant/fix/issue-1282
better detect invalid rules
2023-01-12 12:56:25 +01:00
Willi Ballenthin
cf42670e97 Merge branch 'master' into fix/issue-1282 2023-01-12 12:31:11 +01:00
Willi Ballenthin
ac36b9d328 changelog 2023-01-12 10:39:36 +01:00
Willi Ballenthin
9a9f72f07a pep8 2023-01-12 10:38:52 +01:00
Willi Ballenthin
4b9a844c92 rules: catch invalid YAML exception 2023-01-12 10:38:26 +01:00
Moritz
a273ad31d4 make read consistent with file object behavior (#1281) 2023-01-11 17:17:04 +01:00
Willi Ballenthin
16f3164865 Merge pull request #1280 from mandiant/revert-1275-dependabot/pip/networkx-3.0
Revert "build(deps): bump networkx from 2.5.1 to 3.0"
2023-01-11 12:16:47 +01:00
Willi Ballenthin
5fb9de775f setup: document networkx dep version pin 2023-01-11 10:50:55 +01:00
Willi Ballenthin
05879dc02a Revert "build(deps): bump networkx from 2.5.1 to 3.0" 2023-01-11 10:49:04 +01:00
Willi Ballenthin
d5cb36151f Merge pull request #1275 from mandiant/dependabot/pip/networkx-3.0
build(deps): bump networkx from 2.5.1 to 3.0
2023-01-10 16:52:45 +01:00
Moritz
b6fd95c7b8 use positive error return code numbers (#1274) 2023-01-10 13:14:23 +01:00
Willi Ballenthin
8ce570cea7 Merge pull request #1276 from mandiant/dependabot/pip/termcolor-2.2.0
build(deps): bump termcolor from 2.1.1 to 2.2.0
2023-01-10 12:25:01 +01:00
Willi Ballenthin
5b82ed2fd9 Merge pull request #1270 from mandiant/fix/issue-1267
features: string: better __str__ embedded whitespace
2023-01-10 12:21:27 +01:00
Capa Bot
37a4dbf822 Sync capa rules submodule 2023-01-09 15:53:03 +00:00
dependabot[bot]
ef86160d88 build(deps): bump termcolor from 2.1.1 to 2.2.0
Bumps [termcolor](https://github.com/termcolor/termcolor) from 2.1.1 to 2.2.0.
- [Release notes](https://github.com/termcolor/termcolor/releases)
- [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md)
- [Commits](https://github.com/termcolor/termcolor/compare/2.1.1...2.2.0)

---
updated-dependencies:
- dependency-name: termcolor
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-09 14:03:48 +00:00
dependabot[bot]
5f31bdbb3e build(deps): bump networkx from 2.5.1 to 3.0
Bumps [networkx](https://github.com/networkx/networkx) from 2.5.1 to 3.0.
- [Release notes](https://github.com/networkx/networkx/releases)
- [Commits](https://github.com/networkx/networkx/compare/networkx-2.5.1...networkx-3.0)

---
updated-dependencies:
- dependency-name: networkx
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-09 14:03:44 +00:00
Capa Bot
810e2d70d3 Sync capa rules submodule 2023-01-09 13:38:25 +00:00
Moritz
85dd065f91 only show first lib match to reduce vverbose output noise (#1266)
* only show first lib match to reduce vverbose output noise

* improve rendering and wording
2023-01-09 14:14:08 +01:00
Capa Bot
2a61e357de Sync capa rules submodule 2023-01-09 13:08:27 +00:00
Willi Ballenthin
e34fdfae1a mypy 2023-01-09 13:01:41 +01:00
Willi Ballenthin
58e94a35cb features: string: better __str__ embedded whitespace 2023-01-09 10:51:08 +01:00
Capa Bot
93acf9feb4 Sync capa rules submodule 2023-01-09 08:50:03 +00:00
Moritz
0362148989 Merge pull request #1265 from mandiant/fix/extractor-logic
fix logic error from smda backend removal
2023-01-06 09:54:52 +01:00
mr-tz
985ea5ebdc fix logic error from smda backend removal 2023-01-05 12:27:27 +01:00
Capa Bot
64ebf14256 Sync capa rules submodule 2023-01-05 10:55:44 +00:00
Willi Ballenthin
cfebe5a5ba Merge pull request #1264 from mandiant/fix/issue-1263
render: verbose: fix rendering of scopes
2023-01-05 11:54:59 +01:00
Willi Ballenthin
99e0e45bfc changelog 2023-01-05 11:38:51 +01:00
Willi Ballenthin
83845078a7 render: verbose: fix rendering of scopes
closes #1263
2023-01-05 11:36:52 +01:00
Capa Bot
7c102509bd Sync capa rules submodule 2023-01-05 09:59:07 +00:00
Capa Bot
1af90b9db3 Sync capa rules submodule 2023-01-05 09:55:12 +00:00
Mike Hunhoff
d4de650f90 explorer: improve exception handling (#1262) 2023-01-04 13:28:15 -07:00
Capa Bot
5de0324441 Sync capa rules submodule 2023-01-04 16:59:55 +00:00
Moritz
5fa2a87747 fix dotnet and pe format handling (#1256) 2023-01-04 17:46:51 +01:00
Moritz
68ef9d7858 validate rule meta (#1257)
* validate rule meta
2023-01-04 17:46:25 +01:00
Mike Hunhoff
a286e066d1 explorer: refactor rule generator caching and matching (#1251)
* explorer: refactor rule generator caching and matching

* fix #1246

* fix #1159
2023-01-04 08:50:52 -07:00
Willi Ballenthin
94a712b820 Merge pull request #1213 from mandiant/fix-1062
remove SMDA backend
2023-01-04 14:48:41 +01:00
Moritz
c8aa73ac18 Merge pull request #1253 from mandiant/dependabot/pip/pydantic-1.10.4
build(deps): bump pydantic from 1.10.2 to 1.10.4
2023-01-04 11:17:31 +01:00
Capa Bot
a74b8e6328 Sync capa-testfiles submodule 2023-01-04 09:09:57 +00:00
Willi Ballenthin
ff773695d0 Merge pull request #1260 from jsoref/spelling
Spelling
2023-01-04 08:58:21 +01:00
Josh Soref
c4ebb0a31d spelling: unescaped
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
f9b3d6304c spelling: uncommitted
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
1c85f530b1 spelling: objects
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
d65d7bcd7e spelling: notifications
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
c11633c5db spelling: minimum
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
ea0a708f35 spelling: interesting
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
00254b93dc spelling: instruction
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
6932df3564 spelling: import
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
9e3a48aa8d spelling: globally
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
6e17462bd0 spelling: github
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
d29e7e6f3a spelling: further
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
049e222e88 spelling: falls through
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
caef7812a3 spelling: disassembly
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:32:39 -05:00
Josh Soref
68efa7316b spelling: dictionary
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:25:22 -05:00
Josh Soref
5396d5f99e spelling: contiguous
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:25:22 -05:00
Josh Soref
4576cbd0a1 spelling: committing
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:25:22 -05:00
Josh Soref
1fa9180fee spelling: beginning
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:25:22 -05:00
Josh Soref
801c80d7a2 spelling: alphanum
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-01-04 00:25:22 -05:00
mr-tz
eba1989c9f Merge branch 'master' into fix-1062 2023-01-03 18:46:41 +01:00
Mike Hunhoff
90591811df explorer: improve rules error messaging and documentation (#1249) 2023-01-03 09:09:05 -07:00
Capa Bot
c959506ae9 Sync capa rules submodule 2023-01-03 14:58:40 +00:00
Moritz
25f9029a82 Merge pull request #1255 from mandiant/ci/update-actions
update Actions
2023-01-03 11:56:58 +01:00
Capa Bot
4f75b3d9f6 Sync capa rules submodule 2023-01-03 10:46:49 +00:00
Capa Bot
974d79f2be Sync capa rules submodule 2023-01-03 10:42:41 +00:00
mr-tz
c0a8a91281 update Actions 2023-01-03 11:39:51 +01:00
Capa Bot
2219139605 Sync capa-testfiles submodule 2023-01-03 10:20:18 +00:00
Capa Bot
966e38babf Sync capa rules submodule 2023-01-03 10:19:17 +00:00
Capa Bot
5f39083df6 Sync capa-testfiles submodule 2023-01-03 10:17:36 +00:00
Capa Bot
565b002bfe Sync capa rules submodule 2023-01-02 17:33:19 +00:00
Capa Bot
1dd5a8dbf2 Sync capa rules submodule 2023-01-02 17:31:53 +00:00
dependabot[bot]
7ef17b8dee build(deps): bump pydantic from 1.10.2 to 1.10.4
Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.2 to 1.10.4.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v1.10.4/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v1.10.2...v1.10.4)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-02 14:05:03 +00:00
Moritz
d01a0e022d Merge pull request #1248 from mandiant/dependabot/pip/isort-5.11.4
build(deps-dev): bump isort from 5.11.3 to 5.11.4
2023-01-02 13:22:31 +01:00
Moritz
3258556d5d Merge pull request #1247 from mandiant/doc/rule-compat-info
update rule compatibility doc
2023-01-02 13:21:53 +01:00
Mike Hunhoff
5f77200108 explorer: assume 32-bit displacement for offsets (#1250)
* explorer: assume 32-bit displacement for offsets
2022-12-29 07:08:10 -07:00
dependabot[bot]
b12865f1e5 build(deps-dev): bump isort from 5.11.3 to 5.11.4
Bumps [isort](https://github.com/pycqa/isort) from 5.11.3 to 5.11.4.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.11.3...5.11.4)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-26 14:04:30 +00:00
mr-tz
ee90fc8761 update rule compatibility doc 2022-12-23 18:30:25 +01:00
Moritz
e6585ee526 Merge pull request #1245 from mandiant/doc/rule-releases
simplified rule release guidance
2022-12-22 15:37:06 +01:00
Mike Hunhoff
b68be0c2ce dotnet: emit namespace/class features for type references (#1242)
* dotnet: emit namespace/class features for type references

* dotnet: pre-compute .NET token caches
2022-12-21 15:59:29 -07:00
mr-tz
3b95ed0b5a simplified rule release guidance 2022-12-21 16:03:05 +01:00
Mike Hunhoff
50490e6a93 dotnet: emit namespace/class features for ldvirtftn/ldftn instructions (#1241)
* dotnet: emit namespace/class features for ldvirtftn/ldftn instructions

* dotnet: add unit tests for ldftn/ldvirtftn namespace/class features
2022-12-20 13:29:29 -07:00
Willi Ballenthin
d466345e4e Merge pull request #1239 from mandiant/dependabot/pip/isort-5.11.3
build(deps-dev): bump isort from 5.10.1 to 5.11.3
2022-12-20 13:42:24 +01:00
Mike Hunhoff
4ece47c64c dotnet: emit calls to/from MethodDef methods (#1236)
* dotnet: emit calls to/from MethodDef methods

* dotnet: update function.py copyright header
2022-12-19 15:06:16 -07:00
Moritz
2b85af0f88 explorer: update and remove outdated documentation (#1238) 2022-12-19 14:53:16 -07:00
Mike Hunhoff
e0491097b0 dotnet: emit API features for generic methods (#1231)
* dotnet: emit API features for generic methods

* dotnet: improve type checking

* dotnet: emit namespace/class features for generic methods

* dotnet: update for dnfile 0.13.0

* dotnet: refactor property extraction
2022-12-19 14:45:21 -07:00
dependabot[bot]
fa3d658f33 build(deps): bump dnfile from 0.12.0 to 0.13.0 (#1240)
Bumps [dnfile](https://github.com/malwarefrank/dnfile) from 0.12.0 to 0.13.0.
- [Release notes](https://github.com/malwarefrank/dnfile/releases)
- [Changelog](https://github.com/malwarefrank/dnfile/blob/master/HISTORY.rst)
- [Commits](https://github.com/malwarefrank/dnfile/compare/v0.12.0...v0.13.0)

---
updated-dependencies:
- dependency-name: dnfile
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 10:43:54 -07:00
dependabot[bot]
6dcd115765 build(deps-dev): bump isort from 5.10.1 to 5.11.3
Bumps [isort](https://github.com/pycqa/isort) from 5.10.1 to 5.11.3.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.10.1...5.11.3)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-19 14:02:58 +00:00
Willi Ballenthin
88cffee902 ci: bump action versions (#1233)
* ci: bump action versions

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2022-12-19 12:34:18 +01:00
Willi Ballenthin
b12d526a60 tests: use python 3.11 (#1191) 2022-12-19 11:12:42 +01:00
Mike Hunhoff
3af7fe0b08 dotnet: address unhandled exceptions through improved type checking (#1230)
* dotnet: bump dncil version

* dotnet: check #US stream valid before access

* dotnet: use assert statements to guard types
2022-12-15 12:55:57 -07:00
Willi Ballenthin
d7548c0b20 Merge pull request #1229 from mandiant/williballenthin-patch-2
setup: viv-utils 0.7.7
2022-12-15 12:03:48 +01:00
Willi Ballenthin
f79e16d1a6 Merge branch 'master' of https://github.com/mandiant/capa into williballenthin-patch-2 2022-12-15 10:07:36 +00:00
Willi Ballenthin
ad47ea3bab Merge pull request #1235 from mandiant/fix/issue-1234
stricter mypy checking
2022-12-15 10:54:03 +01:00
Willi Ballenthin
505910edb5 dotnet: remove duplicative validate_has_dotnet helper 2022-12-14 21:28:32 +01:00
Willi Ballenthin
aee0ec8016 features: cleanup mypy checking 2022-12-14 21:22:52 +01:00
Willi Ballenthin
613c185428 tests: fix broken test 2022-12-14 11:51:25 +01:00
Willi Ballenthin
501227f23f elf: fix missing attribute 2022-12-14 11:14:01 +01:00
Willi Ballenthin
56d075fd32 typing 2022-12-14 11:08:46 +01:00
Willi Ballenthin
9ae908c741 elf: better format attribution declarations 2022-12-14 10:57:27 +01:00
Willi Ballenthin
81500a4d1d black 2022-12-14 10:48:00 +01:00
Willi Ballenthin
b819033da0 lots of mypy 2022-12-14 10:37:39 +01:00
Willi Ballenthin
35243ef7a6 changelog 2022-12-13 13:23:46 +00:00
Willi Ballenthin
655c45d43f Merge pull request #1226 from mandiant/dependabot/pip/pycodestyle-2.10.0
build(deps-dev): bump pycodestyle from 2.9.1 to 2.10.0
2022-12-13 14:15:58 +01:00
Willi Ballenthin
34c4809f68 Merge pull request #1228 from mandiant/dependabot/pip/pyinstaller-5.7.0
build(deps-dev): bump pyinstaller from 5.5 to 5.7.0
2022-12-13 14:15:46 +01:00
dependabot[bot]
f9b6800831 build(deps-dev): bump pycodestyle from 2.9.1 to 2.10.0
Bumps [pycodestyle](https://github.com/PyCQA/pycodestyle) from 2.9.1 to 2.10.0.
- [Release notes](https://github.com/PyCQA/pycodestyle/releases)
- [Changelog](https://github.com/PyCQA/pycodestyle/blob/main/CHANGES.txt)
- [Commits](https://github.com/PyCQA/pycodestyle/compare/2.9.1...2.10.0)

---
updated-dependencies:
- dependency-name: pycodestyle
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-13 13:15:33 +00:00
Willi Ballenthin
b5254e3662 Merge pull request #1227 from mandiant/dependabot/pip/mypy-0.991
build(deps-dev): bump mypy from 0.982 to 0.991
2022-12-13 14:15:07 +01:00
Willi Ballenthin
148cb71839 Merge pull request #1225 from mandiant/dependabot/pip/black-22.12.0
build(deps-dev): bump black from 22.10.0 to 22.12.0
2022-12-13 14:14:23 +01:00
Willi Ballenthin
62700ca5d1 setup: bump viv-utils to 0.7.7 for py3.11 support 2022-12-13 14:07:51 +01:00
Willi Ballenthin
b1d6fcd6c8 mypy 2022-12-13 13:20:24 +01:00
Willi Ballenthin
8afebc1f17 ci: mypy: enable --check-untyped-defs 2022-12-13 13:20:01 +01:00
Mike Hunhoff
447cd95bc5 ida: add support for COFF and extern functions (#1223) 2022-12-12 16:36:44 -07:00
Willi Ballenthin
5224380947 setup: viv-utils 0.7.6
closes #1192
2022-12-12 18:02:07 +01:00
Moritz
7aeb685412 Merge pull request #1224 from mandiant/williballenthin-patch-2
tests: os: fix test
2022-12-12 16:43:58 +01:00
Capa Bot
b6911f8ad2 Sync capa rules submodule 2022-12-12 14:39:26 +00:00
dependabot[bot]
a7d06275c1 build(deps-dev): bump pyinstaller from 5.5 to 5.7.0
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.5 to 5.7.0.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v5.5...v5.7.0)

---
updated-dependencies:
- dependency-name: pyinstaller
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-12 14:02:58 +00:00
dependabot[bot]
d581eefcdf build(deps-dev): bump mypy from 0.982 to 0.991
Bumps [mypy](https://github.com/python/mypy) from 0.982 to 0.991.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.982...v0.991)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-12 14:02:53 +00:00
dependabot[bot]
47f58162c5 build(deps-dev): bump black from 22.10.0 to 22.12.0
Bumps [black](https://github.com/psf/black) from 22.10.0 to 22.12.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/22.10.0...22.12.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-12 14:02:41 +00:00
Willi Ballenthin
ee72ed4b53 tests: os: fix test 2022-12-12 14:06:17 +01:00
Capa Bot
5cd7f33d00 Sync capa-testfiles submodule 2022-12-12 12:29:44 +00:00
Willi Ballenthin
d6674c7548 Merge pull request #1222 from mandiant/fix/issue-1221
elf: better detect linux ELF files
2022-12-12 13:28:59 +01:00
Capa Bot
a46d7b3262 Sync capa-testfiles submodule 2022-12-12 12:18:01 +00:00
Willi Ballenthin
0f902124d1 elf: reduce logging verbosity 2022-12-12 11:43:48 +01:00
Willi Ballenthin
d4a218e268 elf: os: bug fixes 2022-12-12 11:41:01 +01:00
Willi Ballenthin
22bef146f8 tests: add OS detection tests 2022-12-12 11:40:43 +01:00
Willi Ballenthin
b26ed47ab8 tests: add OS detection tests 2022-12-12 11:40:32 +01:00
Willi Ballenthin
7ba08edffa changelog 2022-12-09 16:09:41 +01:00
Willi Ballenthin
c958a6a286 elf: black 2022-12-09 16:07:46 +01:00
William Ballenthin
1583fedba2 mypy 2022-12-09 17:34:44 +01:00
William Ballenthin
307a6fad4f elf: os: detect via so dependencies 2022-12-09 14:31:03 +01:00
William Ballenthin
958d5bcc6a elf: refactor OS detection 2022-12-09 12:56:09 +01:00
William Ballenthin
c5a9aa21bf wip: elf: better detect linux ELF files 2022-12-08 21:33:57 +01:00
Willi Ballenthin
13b5d7c179 Merge pull request #1220 from mandiant/disable-smda-tests
skip smda tests until we remove the backend
2022-12-08 12:07:16 +01:00
Capa Bot
bd84ee83a5 Sync capa rules submodule 2022-12-07 19:10:53 +00:00
mr-tz
97f633312f skip smda tests until we remove the backend 2022-12-07 16:44:52 +01:00
Willi Ballenthin
b290690b19 Merge pull request #1216 from mandiant/fix/issue-1215
add missing vverbose feature renderers
2022-12-07 15:12:10 +01:00
Willi Ballenthin
fc57ed76a0 Merge pull request #1218 from mandiant/fix/issue-1194
small explorer fixes
2022-12-07 15:11:02 +01:00
Willi Ballenthin
a6fdb71178 utils: use a single hex() implementation 2022-12-07 14:09:37 +00:00
Willi Ballenthin
fe2f668306 CHANGELOG 2022-12-07 13:41:10 +00:00
Willi Ballenthin
45d007fa9a explorer: fix UnboundLocalError
closes #1217
2022-12-07 13:39:55 +00:00
Willi Ballenthin
662ec11031 explorer: accept only plaintext to rule window
closes #1194
2022-12-07 13:38:50 +00:00
Willi Ballenthin
1d8a3486cd vverbose: prefer isinstance checks over strings
which also makes mypy happier
2022-12-07 13:14:05 +00:00
Willi Ballenthin
c195afa0b3 explorer: improve rendering of operand number/offsets 2022-12-07 13:07:24 +00:00
Willi Ballenthin
63e0d9b3f3 vverbose: render offer and operand number/offset features
closes #1215
2022-12-07 12:59:37 +00:00
Willi Ballenthin
659cbedc3c vverbose: dont show offset for format 2022-12-07 12:59:21 +00:00
Willi Ballenthin
0ebba2cd15 vverbose: guard against rendering basic blocks 2022-12-07 12:58:55 +00:00
Willi Ballenthin
1f091a4ccd tests: add tests demonstrating vverbose feature rendering 2022-12-07 12:58:10 +00:00
Willi Ballenthin
d1aafa3764 vverbose: render offset
closes #1215
2022-12-07 11:52:41 +00:00
Willi Ballenthin
faefe41ad5 Merge pull request #1214 from mandiant/fix/pylint-fixes
pylint fixes
2022-12-07 12:41:57 +01:00
Willi Ballenthin
473d0daf58 render: pylint 2022-12-07 11:41:05 +00:00
Willi Ballenthin
a10abfebde main: pylint 2022-12-06 16:23:10 +00:00
Willi Ballenthin
78172b5f5b rules: pylint 2022-12-06 16:06:08 +00:00
Willi Ballenthin
1caeb248ca pylint: fix old-style super calls 2022-12-06 16:02:21 +00:00
Willi Ballenthin
8527d02dc8 pylint fixes 2022-12-06 15:37:31 +00:00
Willi Ballenthin
0e73f26e88 CHANGELOG 2022-12-06 15:34:22 +00:00
Willi Ballenthin
ed24db4460 extractors: remove SMDA backend
closes #1210
closes #1062
2022-12-06 15:33:17 +00:00
Willi Ballenthin
127886144b Merge pull request #1209 from mandiant/williballenthin-patch-3
import-to-ida: use other md5 function
2022-12-06 13:07:35 +01:00
Willi Ballenthin
c83877ec74 mypy: ignore ida_nalt 2022-12-06 12:06:07 +00:00
Willi Ballenthin
8d6fcd9939 Merge pull request #1208 from mandiant/williballenthin-patch-2
import-to-ida: fix append comment
2022-12-06 13:03:55 +01:00
Willi Ballenthin
1dc5e40308 Merge pull request #1206 from mandiant/dependabot/pip/termcolor-2.1.1
build(deps): bump termcolor from 2.0.1 to 2.1.1
2022-12-06 12:58:04 +01:00
Willi Ballenthin
cc832d26aa import-to-ida: fix imports 2022-12-05 15:27:22 +00:00
Willi Ballenthin
9fcb70387d import-to-ida: use other md5 function
ref #1204
2022-12-05 16:17:11 +01:00
Willi Ballenthin
236ad883d4 changelog 2022-12-05 15:13:16 +00:00
Willi Ballenthin
12c9c466c7 import-to-ida: fix append comment
ref #1204
2022-12-05 16:02:40 +01:00
dependabot[bot]
5a1cb0e48d build(deps): bump termcolor from 2.0.1 to 2.1.1
Bumps [termcolor](https://github.com/termcolor/termcolor) from 2.0.1 to 2.1.1.
- [Release notes](https://github.com/termcolor/termcolor/releases)
- [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md)
- [Commits](https://github.com/termcolor/termcolor/compare/2.0.1...2.1.1)

---
updated-dependencies:
- dependency-name: termcolor
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-28 14:03:10 +00:00
Capa Bot
5196caabb5 Sync capa rules submodule 2022-11-22 12:35:27 +00:00
Capa Bot
0f99592903 Sync capa-testfiles submodule 2022-11-08 19:58:11 +00:00
Capa Bot
56e9645700 Sync capa rules submodule 2022-10-24 18:28:08 +00:00
Capa Bot
0d8c6cc0fd Sync capa rules submodule 2022-10-13 14:37:09 +00:00
Mike Hunhoff
20c7949be3 dotnet: emit features from newobj instruction (#1186) 2022-10-13 08:35:29 -06:00
Willi Ballenthin
7cc6773bf8 Merge pull request #1185 from mandiant/dependabot/pip/pyinstaller-5.5
build(deps-dev): bump pyinstaller from 5.4.1 to 5.5
2022-10-11 15:56:11 +02:00
Willi Ballenthin
055700a5d1 Merge pull request #1182 from mandiant/dependabot/pip/mypy-0.982
build(deps-dev): bump mypy from 0.971 to 0.982
2022-10-11 15:55:37 +02:00
Willi Ballenthin
85b14075cd address: explicitly resolve hash from int 2022-10-11 09:47:25 +00:00
Willi Ballenthin
149c3989f1 Merge pull request #1178 from mandiant/dependabot/pip/pytest-cov-4.0.0
build(deps-dev): bump pytest-cov from 3.0.0 to 4.0.0
2022-10-11 10:58:49 +02:00
dependabot[bot]
3b5a34f331 build(deps-dev): bump mypy from 0.971 to 0.982
Bumps [mypy](https://github.com/python/mypy) from 0.971 to 0.982.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.971...v0.982)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 06:18:59 +00:00
dependabot[bot]
b4fe2d8592 build(deps-dev): bump pytest-cov from 3.0.0 to 4.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v3.0.0...v4.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 06:18:47 +00:00
Moritz
67d06c73e0 Merge pull request #1183 from mandiant/dependabot/pip/types-tabulate-0.9.0.0
build(deps-dev): bump types-tabulate from 0.8.9 to 0.9.0.0
2022-10-11 08:18:21 +02:00
dependabot[bot]
81a942d7a1 build(deps-dev): bump pyinstaller from 5.4.1 to 5.5
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.4.1 to 5.5.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v5.4.1...v5.5)

---
updated-dependencies:
- dependency-name: pyinstaller
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 06:18:13 +00:00
Moritz
521473cd81 Merge pull request #1184 from mandiant/dependabot/pip/black-22.10.0
build(deps-dev): bump black from 22.8.0 to 22.10.0
2022-10-11 08:18:02 +02:00
Moritz
676d422511 Merge pull request #1181 from mandiant/dependabot/pip/tabulate-0.9.0
build(deps): bump tabulate from 0.8.9 to 0.9.0
2022-10-11 08:17:45 +02:00
dependabot[bot]
f2dbb531fe build(deps-dev): bump black from 22.8.0 to 22.10.0
Bumps [black](https://github.com/psf/black) from 22.8.0 to 22.10.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/22.8.0...22.10.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-10 15:09:31 +00:00
dependabot[bot]
84fce86152 build(deps-dev): bump types-tabulate from 0.8.9 to 0.9.0.0
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.8.9 to 0.9.0.0.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-tabulate
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-10 15:09:24 +00:00
dependabot[bot]
8307c66256 build(deps): bump tabulate from 0.8.9 to 0.9.0
Bumps [tabulate](https://github.com/astanin/python-tabulate) from 0.8.9 to 0.9.0.
- [Release notes](https://github.com/astanin/python-tabulate/releases)
- [Changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG)
- [Commits](https://github.com/astanin/python-tabulate/compare/v0.8.9...v0.9.0)

---
updated-dependencies:
- dependency-name: tabulate
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-10 15:09:17 +00:00
Capa Bot
ac71676d79 Sync capa rules submodule 2022-10-07 15:40:27 +00:00
Capa Bot
70e6d83259 Sync capa rules submodule 2022-10-03 15:28:44 +00:00
Capa Bot
3bbac4a35f Sync capa rules submodule 2022-10-03 15:17:03 +00:00
Capa Bot
87455ed6dd Sync capa-testfiles submodule 2022-09-20 19:34:29 +00:00
Mike Hunhoff
e1735f0a5e update pydantic models to guarantee type coercion (#1176)
* add CompoundStatement to fix Pydantic typing bug

* explorer: fix #1151

* explorer: support rendering operand number/offset
2022-09-20 08:38:19 -06:00
Capa Bot
8521f85742 Sync capa-testfiles submodule 2022-09-19 14:26:32 +00:00
Moritz
b1b15e2eef fix: do not overwrite __version__ (#1170) 2022-09-14 14:45:58 -06:00
Moritz
36e304839b Merge pull request #1173 from mandiant/dependabot/pip/pydantic-1.10.2
build(deps): bump pydantic from 1.10.1 to 1.10.2
2022-09-14 17:40:21 +02:00
Moritz
5a14a6d0cc Merge pull request #1172 from mandiant/dependabot/pip/termcolor-2.0.1
build(deps): bump termcolor from 1.1.0 to 2.0.1
2022-09-14 17:40:07 +02:00
Moritz
85901893a0 Merge pull request #1171 from mandiant/dependabot/pip/pyinstaller-5.4.1
build(deps-dev): bump pyinstaller from 5.3 to 5.4.1
2022-09-14 17:39:55 +02:00
dependabot[bot]
49d7f2a88f build(deps): bump pydantic from 1.10.1 to 1.10.2
Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-12 14:17:04 +00:00
dependabot[bot]
8d8c5f99c1 build(deps): bump termcolor from 1.1.0 to 2.0.1
Bumps [termcolor](https://github.com/termcolor/termcolor) from 1.1.0 to 2.0.1.
- [Release notes](https://github.com/termcolor/termcolor/releases)
- [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md)
- [Commits](https://github.com/termcolor/termcolor/compare/1.1.0...2.0.1)

---
updated-dependencies:
- dependency-name: termcolor
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-12 14:17:00 +00:00
dependabot[bot]
4069515cad build(deps-dev): bump pyinstaller from 5.3 to 5.4.1
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.3 to 5.4.1.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v5.3...v5.4.1)

---
updated-dependencies:
- dependency-name: pyinstaller
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-12 14:16:54 +00:00
Mike Hunhoff
3c1cd67f60 dotnet: support property feature extraction (#1168) 2022-09-09 12:09:41 -06:00
Capa Bot
580948e46b Sync capa rules submodule 2022-09-09 11:21:24 +00:00
Capa Bot
4ffd7b89f3 Sync capa rules submodule 2022-09-09 11:19:59 +00:00
Moritz
2441c18a85 fix: use int instead of Token to decouple extractor and features (#1158) 2022-09-08 11:09:17 -06:00
Moritz
ee89fa45b6 Update build.yml (#1157) 2022-09-08 10:58:29 -06:00
Moritz
3976e5858d feat: verify rule metadata format on load (#1160) 2022-09-08 10:56:59 -06:00
Capa Bot
4e542f9cff Sync capa rules submodule 2022-09-08 08:42:53 +00:00
Moritz
ce1ecfad4d Merge pull request #1164 from mandiant/dependabot/pip/psutil-5.9.2
build(deps-dev): bump psutil from 5.9.1 to 5.9.2
2022-09-06 17:40:59 +02:00
dependabot[bot]
d9d5aaffa1 build(deps-dev): bump psutil from 5.9.1 to 5.9.2
Bumps [psutil](https://github.com/giampaolo/psutil) from 5.9.1 to 5.9.2.
- [Release notes](https://github.com/giampaolo/psutil/releases)
- [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/release-5.9.1...release-5.9.2)

---
updated-dependencies:
- dependency-name: psutil
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-06 06:53:53 +00:00
Moritz
21809350f7 Merge pull request #1166 from mandiant/dependabot/pip/pydantic-1.10.1
build(deps): bump pydantic from 1.9.2 to 1.10.1
2022-09-06 08:53:46 +02:00
Moritz
418b063067 Merge pull request #1165 from mandiant/dependabot/pip/tqdm-4.64.1
build(deps): bump tqdm from 4.64.0 to 4.64.1
2022-09-06 08:53:30 +02:00
Moritz
dcf838872c Merge pull request #1163 from mandiant/dependabot/pip/pytest-7.1.3
build(deps-dev): bump pytest from 7.1.2 to 7.1.3
2022-09-06 08:53:07 +02:00
Moritz
456b32e6a8 Merge pull request #1162 from mandiant/dependabot/pip/black-22.8.0
build(deps-dev): bump black from 22.6.0 to 22.8.0
2022-09-06 08:52:51 +02:00
dependabot[bot]
acad9c5570 build(deps): bump pydantic from 1.9.2 to 1.10.1
Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.9.2 to 1.10.1.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v1.9.2...v1.10.1)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-05 16:32:53 +00:00
dependabot[bot]
4b2cfb4825 build(deps): bump tqdm from 4.64.0 to 4.64.1
Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.64.0 to 4.64.1.
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.64.0...v4.64.1)

---
updated-dependencies:
- dependency-name: tqdm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-05 16:32:49 +00:00
dependabot[bot]
7733562587 build(deps-dev): bump pytest from 7.1.2 to 7.1.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.2 to 7.1.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.1.2...7.1.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-05 16:32:34 +00:00
dependabot[bot]
eaa70fa80f build(deps-dev): bump black from 22.6.0 to 22.8.0
Bumps [black](https://github.com/psf/black) from 22.6.0 to 22.8.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/22.6.0...22.8.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-05 16:32:26 +00:00
Capa Bot
44843ea977 Sync capa rules submodule 2022-08-29 16:50:20 +00:00
Capa Bot
cac041b869 Sync capa-testfiles submodule 2022-08-24 10:47:31 +00:00
Moritz
49684e4c25 fix: display instruction items (#1155)
* fix: display instruction items

* fix: instruction item format
2022-08-23 17:12:51 +02:00
Mike Hunhoff
47268c2344 render: convert feature attributes to aliased dictionary for vverbose (#1152) 2022-08-18 12:15:52 -06:00
Moritz
da0a1e7903 Merge pull request #1149 from gdesmar/master
Fix maec.malware_category_ov typo in vverbose render
2022-08-18 11:31:40 +02:00
Moritz
eca1582678 Merge pull request #1148 from idiom/master
Add Optional attribute to argv property in Metadata model.
2022-08-18 11:31:23 +02:00
gdesmar
2049058b45 render: vverbose, fix maec.malware_category_ov typo 2022-08-16 18:40:51 +00:00
Moritz
c2b5e7116d Merge pull request #1146 from mandiant/dependabot/pip/dnfile-0.12.0
build(deps): bump dnfile from 0.11.0 to 0.12.0
2022-08-16 11:06:15 +02:00
dependabot[bot]
9c1b076a5f build(deps): bump dnfile from 0.11.0 to 0.12.0
Bumps [dnfile](https://github.com/malwarefrank/dnfile) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/malwarefrank/dnfile/releases)
- [Changelog](https://github.com/malwarefrank/dnfile/blob/master/HISTORY.rst)
- [Commits](https://github.com/malwarefrank/dnfile/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: dnfile
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-16 08:14:55 +00:00
Moritz
51f7e10cb6 Merge pull request #1145 from mandiant/dependabot/pip/pyelftools-0.29
build(deps): bump pyelftools from 0.28 to 0.29
2022-08-16 10:14:24 +02:00
Moritz
25ad6446ba Merge pull request #1144 from mandiant/dependabot/pip/pydantic-1.9.2
build(deps): bump pydantic from 1.9.1 to 1.9.2
2022-08-16 10:14:04 +02:00
idiom
1af5255501 Add Optional attribute to argv property in Metadata model. This resovles issue where a ValidationError is raised when argv is not in the passed matedata and set to None in from_capa. 2022-08-15 15:55:19 -04:00
dependabot[bot]
49d61db8f9 build(deps): bump pyelftools from 0.28 to 0.29
Bumps [pyelftools](https://github.com/eliben/pyelftools) from 0.28 to 0.29.
- [Release notes](https://github.com/eliben/pyelftools/releases)
- [Changelog](https://github.com/eliben/pyelftools/blob/master/CHANGES)
- [Commits](https://github.com/eliben/pyelftools/compare/v0.28...v0.29)

---
updated-dependencies:
- dependency-name: pyelftools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-15 14:23:21 +00:00
dependabot[bot]
601471c1e6 build(deps): bump pydantic from 1.9.1 to 1.9.2
Bumps [pydantic](https://github.com/samuelcolvin/pydantic) from 1.9.1 to 1.9.2.
- [Release notes](https://github.com/samuelcolvin/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/master/HISTORY.md)
- [Commits](https://github.com/samuelcolvin/pydantic/compare/v1.9.1...v1.9.2)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-15 14:23:17 +00:00
112 changed files with 6091 additions and 4036 deletions

View File

@@ -31,7 +31,7 @@ This project and everyone participating in it is governed by the [Capa Code of C
### Capa and its repositories
We host the capa project as three Github repositories:
We host the capa project as three GitHub repositories:
- [capa](https://github.com/mandiant/capa)
- [capa-rules](https://github.com/mandiant/capa-rules)
- [capa-testfiles](https://github.com/mandiant/capa-testfiles)

14
.github/mypy/mypy.ini vendored
View File

@@ -21,9 +21,6 @@ ignore_missing_imports = True
[mypy-flirt.*]
ignore_missing_imports = True
[mypy-smda.*]
ignore_missing_imports = True
[mypy-lief.*]
ignore_missing_imports = True
@@ -48,6 +45,9 @@ ignore_missing_imports = True
[mypy-ida_bytes.*]
ignore_missing_imports = True
[mypy-ida_nalt.*]
ignore_missing_imports = True
[mypy-ida_kernwin.*]
ignore_missing_imports = True
@@ -60,6 +60,9 @@ ignore_missing_imports = True
[mypy-ida_loader.*]
ignore_missing_imports = True
[mypy-ida_segment.*]
ignore_missing_imports = True
[mypy-PyQt5.*]
ignore_missing_imports = True
@@ -76,4 +79,7 @@ ignore_missing_imports = True
ignore_missing_imports = True
[mypy-dncil.*]
ignore_missing_imports = True
ignore_missing_imports = True
[mypy-netnode.*]
ignore_missing_imports = True

View File

@@ -1,5 +0,0 @@
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
import PyInstaller.utils.hooks
# ref: https://groups.google.com/g/pyinstaller/c/amWi0-66uZI/m/miPoKfWjBAAJ
binaries = PyInstaller.utils.hooks.collect_dynamic_libs("capstone")

View File

@@ -6,31 +6,6 @@ import subprocess
import wcwidth
# git output will look like:
#
# tags/v1.0.0-0-g3af38dc
# ------- tag
# - commits since
# g------- git hash fragment
version = (
subprocess.check_output(["git", "describe", "--always", "--tags", "--long"])
.decode("utf-8")
.strip()
.replace("tags/", "")
)
# when invoking pyinstaller from the project root, this gets run from the project root.
with open("./capa/version.py", "r", encoding="utf-8") as f:
lines = f.read()
# version.py contains the version string and other helper functions
# here we manually replace the version value substring with the result of the above git output
VERSION_DEF = "__version__ = "
s = lines.index(VERSION_DEF)
e = s + len(VERSION_DEF)
off_rest_file = e + lines[e:].index("\n")
lines = lines[s:e] + f'"{version}"' + lines[off_rest_file:]
with open("./capa/version.py", "w", encoding="utf-8") as f:
f.write(lines)
a = Analysis(
# when invoking pyinstaller from the project root,
# this gets invoked from the directory of the spec file,
@@ -44,6 +19,7 @@ a = Analysis(
# i.e. ./.github/pyinstaller
("../../rules", "rules"),
("../../sigs", "sigs"),
("../../cache", "cache"),
# capa.render.default uses tabulate that depends on wcwidth.
# it seems wcwidth uses a json file `version.json`
# and this doesn't get picked up by pyinstaller automatically.

View File

@@ -19,21 +19,21 @@ jobs:
# use old linux so that the shared library versioning is more portable
artifact_name: capa
asset_name: linux
- os: windows-2022
- os: windows-2019
artifact_name: capa.exe
asset_name: windows
- os: macos-10.15
- os: macos-11
# use older macOS for assumed better portability
artifact_name: capa
asset_name: macos
steps:
- name: Checkout capa
uses: actions/checkout@v2
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
submodules: true
# using Python 3.8 to support running across multiple operating systems including Windows 7
- name: Set up Python 3.8
uses: actions/setup-python@v2
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
with:
python-version: 3.8
- if: matrix.os == 'ubuntu-18.04'
@@ -42,6 +42,8 @@ jobs:
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/
- name: Build standalone executable
run: pyinstaller --log-level DEBUG .github/pyinstaller/pyinstaller.spec
- name: Does it run (PE)?
@@ -50,7 +52,7 @@ jobs:
run: dist/capa "tests/data/499c2a85f6e8142c3f48d4251c9c7cd6.raw32"
- name: Does it run (ELF)?
run: dist/capa "tests/data/7351f8a40c5450557b24622417fc478d.elf_"
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ${{ matrix.asset_name }}
path: dist/${{ matrix.artifact_name }}
@@ -74,11 +76,11 @@ jobs:
asset_name: windows
steps:
- name: Download ${{ matrix.asset_name }}
uses: actions/download-artifact@v2
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: ${{ matrix.asset_name }}
- name: Set executable flag
if: matrix.os != 'windows-2022'
if: matrix.os != 'windows-2022'
run: chmod +x ${{ matrix.artifact_name }}
- name: Run capa
run: ./${{ matrix.artifact_name }} -h
@@ -100,7 +102,7 @@ jobs:
artifact_name: capa
steps:
- name: Download ${{ matrix.asset_name }}
uses: actions/download-artifact@v2
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: ${{ matrix.asset_name }}
- name: Set executable flag
@@ -110,7 +112,7 @@ jobs:
- name: Zip ${{ matrix.artifact_name }} into ${{ env.zip_name }}
run: zip ${{ env.zip_name }} ${{ matrix.artifact_name }}
- name: Upload ${{ env.zip_name }} to GH Release
uses: svenstaro/upload-release-action@v2
uses: svenstaro/upload-release-action@2728235f7dc9ff598bd86ce3c274b74f802d2208 # v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN}}
file: ${{ env.zip_name }}

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Get changed files
id: files
uses: Ana06/get-changed-files@v1.2
uses: Ana06/get-changed-files@e0c398b7065a8d84700c471b6afc4116d1ba4e96 # v2.2.0
- name: check changelog updated
id: changelog_updated
env:
@@ -27,14 +27,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@v0.1.0
uses: Ana06/automatic-pull-request-review@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.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@v0.1.0
uses: Ana06/automatic-pull-request-review@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
event: DISMISS

View File

@@ -11,9 +11,9 @@ jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
with:
python-version: '3.7'
- name: Install dependencies
@@ -27,4 +27,3 @@ jobs:
run: |
python setup.py sdist bdist_wheel
twine upload --skip-existing dist/*

72
.github/workflows/scorecard.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '43 4 * * 3'
push:
branches: [ "master" ]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
# Uncomment the permissions below if installing in a private repository.
# contents: read
# actions: read
steps:
- name: "Checkout code"
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecard on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# 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
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # v2.1.27
with:
sarif_file: results.sarif

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout capa-rules
uses: actions/checkout@v2
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
repository: mandiant/capa-rules
token: ${{ secrets.CAPA_TOKEN }}
@@ -23,7 +23,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@master
uses: ad-m/github-push-action@0fafdd62b84042d49ec0cb92d9cac7f7ce4ec79e # master
with:
repository: mandiant/capa-rules
github_token: ${{ secrets.CAPA_TOKEN }}

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout capa
uses: actions/checkout@v2
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
# The sync GH action in capa-rules relies on a single '- *$' in the CHANGELOG file
- name: Ensure CHANGELOG has '- *$'
run: |
@@ -26,9 +26,9 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout capa
uses: actions/checkout@v2
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Set up Python 3.8
uses: actions/setup-python@v2
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
with:
python-version: "3.8"
- name: Install dependencies
@@ -40,17 +40,17 @@ jobs:
- name: Lint with pycodestyle
run: pycodestyle --show-source capa/ scripts/ tests/
- name: Check types with mypy
run: mypy --config-file .github/mypy/mypy.ini capa/ scripts/ tests/
run: mypy --config-file .github/mypy/mypy.ini --check-untyped-defs capa/ scripts/ tests/
rule_linter:
runs-on: ubuntu-20.04
steps:
- name: Checkout capa with submodules
uses: actions/checkout@v2
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
submodules: recursive
- name: Set up Python 3.8
uses: actions/setup-python@v2
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
with:
python-version: "3.8"
- name: Install capa
@@ -67,7 +67,7 @@ jobs:
matrix:
os: [ubuntu-20.04, windows-2019, macos-11]
# across all operating systems
python-version: ["3.7", "3.10"]
python-version: ["3.7", "3.11"]
include:
# on Ubuntu run these as well
- os: ubuntu-20.04
@@ -76,11 +76,11 @@ jobs:
python-version: "3.9"
steps:
- name: Checkout capa with submodules
uses: actions/checkout@v2
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
with:
python-version: ${{ matrix.python-version }}
- name: Install pyyaml

View File

@@ -3,23 +3,174 @@
## master (unreleased)
### New Features
- dotnet: add support for basic blocks #1326 @mike-hunhoff
### Breaking Changes
### New Rules (0)
### New Rules (9)
-
- persistence/scheduled-tasks/schedule-task-via-at joren485
- data-manipulation/prng/generate-random-numbers-via-rtlgenrandom william.ballenthin@mandiant.com
- communication/ip/convert-ip-address-from-string @mr-tz
- data-manipulation/compression/compress-data-via-zlib-inflate-or-deflate blas.kojusner@mandiant.com
- executable/installer/dotnet/packaged-as-single-file-dotnet-application michael.hunhoff@mandiant.com
- communication/socket/create-raw-socket blas.kojusner@mandiant.com
- communication/http/reference-http-user-agent-string @mr-tz
- communication/http/get-http-content-length william.ballenthin@mandiant.com
- nursery/move-directory michael.hunhoff@mandiant.com
-
### Bug Fixes
- extractor: fix vivisect loop detection corner case #1310 @mr-tz
### capa explorer IDA Pro plugin
### Development
### Raw diffs
- [capa v4.0.1...master](https://github.com/mandiant/capa/compare/v4.0.1...master)
- [capa-rules v4.0.1...master](https://github.com/mandiant/capa-rules/compare/v4.0.1...master)
- [capa v5.0.0...master](https://github.com/mandiant/capa/compare/v5.0.0...master)
- [capa-rules v5.0.0...master](https://github.com/mandiant/capa-rules/compare/v5.0.0...master)
## v5.0.0 (2023-02-08)
This capa version comes with major improvements and additions to better handle .NET binaries. To showcase this we've updated and added over 30 .NET rules.
Additionally, capa now caches its rule set for better performance. The capa explorer also caches its analysis results, so that multiple IDA Pro or plugin invocations don't need to repeat the same analysis.
We have removed the SMDA backend and changed the program return codes to be positive numbers.
Other improvements to highlight include better ELF OS detection, various rendering bug fixes, and enhancements to the feature extraction. We've also added support for Python 3.11.
Thanks for all the support, especially to @jsoref, @bkojusner, @edeca, @richardweiss80, @joren485, @ryantxu1, @mwilliams31, @anushkavirgaonkar, @MalwareMechanic, @Still34, @dzbeck, @johnk3r, and everyone else who submitted bugs and provided feedback!
### New Features
- verify rule metadata format on load #1160 @mr-tz
- dotnet: emit property features #1168 @anushkavirgaonkar
- dotnet: emit API features for objects created via the newobj instruction #1186 @mike-hunhoff
- dotnet: emit API features for generic methods #1231 @mike-hunhoff
- Python 3.11 support #1192 @williballenthin
- dotnet: emit calls to/from MethodDef methods #1236 @mike-hunhoff
- dotnet: emit namespace/class features for ldvirtftn/ldftn instructions #1241 @mike-hunhoff
- dotnet: emit namespace/class features for type references #1242 @mike-hunhoff
- dotnet: extract dotnet and pe format #1187 @mr-tz
- don't render all library rule matches in vverbose output #1174 @mr-tz
- cache the rule set across invocations for better performance #1212 @williballenthin
- update ATT&CK/MBC data for linting #1297 @mr-tz
### Breaking Changes
- remove SMDA backend #1062 @williballenthin
- error return codes are now positive numbers #1269 @mr-tz
### New Rules (77)
- collection/use-dotnet-library-sharpclipboard @johnk3r
- data-manipulation/encryption/aes/use-dotnet-library-encryptdecryptutils @johnk3r
- data-manipulation/json/use-dotnet-library-newtonsoftjson @johnk3r
- data-manipulation/svg/use-dotnet-library-sharpvectors @johnk3r
- executable/resource/embed-dependencies-as-resources-using-fodycostura @johnk3r @mr-tz
- communication/ftp/send/send-file-using-ftp michael.hunhof@mandiant.com anushka.virgaonkar@mandiant.com
- nursery/extract-zip-archive anushka.virgaonkar@mandiant.com
- nursery/allocate-unmanaged-memory-in-dotnet michael.hunhoff@mandiant.com
- nursery/check-file-extension-in-dotnet michael.hunhoff@mandiant.com
- nursery/decode-data-using-base64-in-dotnet michael.hunhoff@mandiant.com
- nursery/deserialize-json-in-dotnet michael.hunhoff@mandiant.com
- nursery/find-data-using-regex-in-dotnet michael.hunhoff@mandiant.com
- nursery/generate-random-filename-in-dotnet michael.hunhoff@mandiant.com
- nursery/get-os-version-in-dotnet michael.hunhoff@mandiant.com
- nursery/load-xml-in-dotnet michael.hunhoff@mandiant.com
- nursery/manipulate-unmanaged-memory-in-dotnet michael.hunhoff@mandiant.com
- nursery/save-image-in-dotnet michael.hunhoff@mandiant.com
- nursery/send-email-in-dotnet michael.hunhoff@mandiant.com
- nursery/serialize-json-in-dotnet michael.hunhoff@mandiant.com
- nursery/set-http-user-agent-in-dotnet michael.hunhoff@mandiant.com
- nursery/compile-csharp-in-dotnet michael.hunhoff@mandiant.com
- nursery/compile-visual-basic-in-dotnet michael.hunhoff@mandiant.com
- nursery/compress-data-using-gzip-in-dotnet michael.hunhoff@mandiant.com
- nursery/execute-sqlite-statement-in-dotnet michael.hunhoff@mandiant.com
- nursery/execute-via-asynchronous-task-in-dotnet michael.hunhoff@mandiant.com
- nursery/execute-via-timer-in-dotnet michael.hunhoff@mandiant.com
- nursery/execute-wmi-query-in-dotnet michael.hunhoff@mandiant.com
- nursery/manipulate-network-credentials-in-dotnet michael.hunhoff@mandiant.com
- nursery/encrypt-data-using-aes william.ballenthin@mandiant.com Ivan Kwiatkowski (@JusticeRage)
- host-interaction/uac/bypass/bypass-uac-via-rpc david.cannings@pwc.com david@edeca.net
- nursery/check-for-vm-using-instruction-vpcext richard.weiss@mandiant.com
- nursery/get-windows-directory-from-kuser_shared_data david.cannings@pwc.com
- nursery/encrypt-data-using-openssl-dsa Ana06
- nursery/encrypt-data-using-openssl-ecdsa Ana06
- nursery/encrypt-data-using-openssl-rsa Ana06
- runtime/dotnet/execute-via-dotnet-startup-hook william.ballenthin@mandiant.com
- host-interaction/console/manipulate-console-buffer william.ballenthin@mandiant.com michael.hunhoff@mandiant.com
- nursery/access-wmi-data-in-dotnet michael.hunhoff@mandiant.com
- nursery/allocate-unmanaged-memory-via-dotnet michael.hunhoff@mandiant.com
- nursery/generate-random-bytes-in-dotnet michael.hunhoff@mandiant.com
- nursery/manipulate-console-window michael.hunhoff@mandiant.com
- nursery/obfuscated-with-koivm michael.hunhoff@mandiant.com
- nursery/implement-com-dll moritz.raabe@mandiant.com
- nursery/linked-against-libsodium @mr-tz
- compiler/nuitka/compiled-with-nuitka @williballenthin
- nursery/authenticate-data-with-md5-mac william.ballenthin@mandiant.com
- nursery/resolve-function-by-djb2-hash still@teamt5.org
- host-interaction/mutex/create-semaphore-on-linux @ramen0x3f
- host-interaction/mutex/lock-semaphore-on-linux @ramen0x3f
- host-interaction/mutex/unlock-semaphore-on-linux @ramen0x3f
- data-manipulation/hashing/sha384/hash-data-using-sha384 william.ballenthin@mandiant.com
- data-manipulation/hashing/sha512/hash-data-using-sha512 william.ballenthin@mandiant.com
- nursery/decode-data-using-url-encoding michael.hunhoff@mandiant.com
- nursery/manipulate-user-privileges michael.hunhoff@mandiant.com
- lib/get-os-version @mr-tz
- nursery/decrypt-data-using-tea william.ballenthin@mandiant.com
- nursery/encrypt-data-using-tea william.ballenthin@mandiant.com
- nursery/hash-data-using-whirlpool william.ballenthin@mandiant.com
- nursery/reference-base58-string william.ballenthin@mandiant.com
- communication/mailslot/create-mailslot william.ballenthin@mandiant.com
- executable/resource/access-dotnet-resource @mr-tz
- linking/static/linked-against-cpp-standard-library @mr-tz
- data-manipulation/compression/compress-data-using-lzo david@edeca.net david.cannings@pwc.com
- data-manipulation/compression/decompress-data-using-lzo david@edeca.net david.cannings@pwc.com
- communication/socket/tcp/create-tcp-socket-via-raw-afd-driver william.ballenthin@mandiant.com
- host-interaction/process/map-section-object william.ballenthin@mandiant.com
- lib/create-or-open-section-object william.ballenthin@mandiant.com
- load-code/dotnet/execute-dotnet-assembly-via-clr-host blas.kojusner@mandiant.com
- load-code/execute-vbscript-javascript-or-jscript-in-memory blas.kojusner@mandiant.com
- host-interaction/file-system/reference-absolute-stream-path-on-windows blas.kojusner@mandiant.com
- nursery/generate-method-via-reflection-in-dotnet michael.hunhoff@mandiant.com
- nursery/unmanaged-call-via-dynamic-pinvoke-in-dotnet michael.hunhoff@mandiant.com
### Bug Fixes
- render: convert feature attributes to aliased dictionary for vverbose #1152 @mike-hunhoff
- decouple Token dependency / extractor and features #1139 @mr-tz
- update pydantic model to guarantee type coercion #1176 @mike-hunhoff
- do not overwrite version in version.py during PyInstaller build #1169 @mr-tz
- render: fix vverbose rendering of offsets #1215 @williballenthin
- elf: better detect OS via GLIBC ABI version needed and dependencies #1221 @williballenthin
- dotnet: address unhandled exceptions with improved type checking #1230 @mike-hunhoff
- fix import-to-ida script formatting #1208 @williballenthin
- render: fix verbose rendering of scopes #1263 @williballenthin
- rules: better detect invalid rules #1282 @williballenthin
- show-features: better render strings with embedded whitespace #1267 @williballenthin
- handle vivisect bug around strings at instruction level, use min length 4 #1271 @williballenthin @mr-tz
- extractor: guard against invalid "calls from" features #1177 @mr-tz
- extractor: add format to global features #1258 @mr-tz
- extractor: discover all strings with length >= 4 #1280 @mr-tz
- extractor: don't extract byte features for strings #1293 @mr-tz
### capa explorer IDA Pro plugin
- fix: display instruction items #1154 @mr-tz
- fix: accept only plaintext pasted content #1194 @williballenthin
- fix: UnboundLocalError #1217 @williballenthin
- extractor: add support for COFF files and extern functions #1223 @mike-hunhoff
- doc: improve error messaging and documentation related to capa rule set #1249 @mike-hunhoff
- fix: assume 32-bit displacement for offsets #1250 @mike-hunhoff
- generator: refactor caching and matching #1251 @mike-hunhoff
- fix: improve exception handling to prevent IDA from locking up when errors occur #1262 @mike-hunhoff
- verify rule metadata using Pydantic #1167 @mr-tz
- extractor: make read consistent with file object behavior #1254 @mr-tz
- fix: UnboundLocalError x2 #1302 @mike-hunhoff
- cache capa results across IDA sessions #1279 @mr-tz
### Raw diffs
- [capa v4.0.1...v5.0.0](https://github.com/mandiant/capa/compare/v4.0.1...v5.0.0)
- [capa-rules v4.0.1...v5.0.0](https://github.com/mandiant/capa-rules/compare/v4.0.1...v5.0.0)
## v4.0.1 (2022-08-15)
@@ -1245,7 +1396,7 @@ Download a standalone binary below and checkout the readme [here on GitHub](http
- setup: pin vivisect version @williballenthin
- setup: bump vivisect dependency version @williballenthin
- setup: set Python project name to `flare-capa` @williballenthin
- ci: run tests and linter via Github Actions @Ana06
- ci: run tests and linter via GitHub Actions @Ana06
- hooks: run style checkers and hide stashed output @Ana06
- linter: ignore period in rule filename @williballenthin
- linter: warn on nursery rule with no changes needed @williballenthin

View File

@@ -2,7 +2,7 @@
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flare-capa)](https://pypi.org/project/flare-capa)
[![Last release](https://img.shields.io/github/v/release/mandiant/capa)](https://github.com/mandiant/capa/releases)
[![Number of rules](https://img.shields.io/badge/rules-703-blue.svg)](https://github.com/mandiant/capa-rules)
[![Number of rules](https://img.shields.io/badge/rules-779-blue.svg)](https://github.com/mandiant/capa-rules)
[![CI status](https://github.com/mandiant/capa/workflows/CI/badge.svg)](https://github.com/mandiant/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
[![Downloads](https://img.shields.io/github/downloads/mandiant/capa/total)](https://github.com/mandiant/capa/releases)
[![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.txt)

View File

@@ -8,7 +8,7 @@
import copy
import collections
from typing import TYPE_CHECKING, Set, Dict, List, Tuple, Mapping, Iterable
from typing import TYPE_CHECKING, Set, Dict, List, Tuple, Union, Mapping, Iterable, Iterator, cast
import capa.perf
import capa.features.common
@@ -38,7 +38,7 @@ class Statement:
"""
def __init__(self, description=None):
super(Statement, self).__init__()
super().__init__()
self.name = self.__class__.__name__
self.description = description
@@ -60,17 +60,24 @@ class Statement:
"""
raise NotImplementedError()
def get_children(self):
def get_children(self) -> Iterator[Union["Statement", Feature]]:
if hasattr(self, "child"):
yield self.child
# this really confuses mypy because the property may not exist
# since its defined in the subclasses.
child = self.child # type: ignore
assert isinstance(child, (Statement, Feature))
yield child
if hasattr(self, "children"):
for child in getattr(self, "children"):
assert isinstance(child, (Statement, Feature))
yield child
def replace_child(self, existing, new):
if hasattr(self, "child"):
if self.child is existing:
# this really confuses mypy because the property may not exist
# since its defined in the subclasses.
if self.child is existing: # type: ignore
self.child = new
if hasattr(self, "children"):
@@ -90,7 +97,7 @@ class And(Statement):
"""
def __init__(self, children, description=None):
super(And, self).__init__(description=description)
super().__init__(description=description)
self.children = children
def evaluate(self, ctx, short_circuit=True):
@@ -123,7 +130,7 @@ class Or(Statement):
"""
def __init__(self, children, description=None):
super(Or, self).__init__(description=description)
super().__init__(description=description)
self.children = children
def evaluate(self, ctx, short_circuit=True):
@@ -150,7 +157,7 @@ class Not(Statement):
"""match only if the child evaluates to False."""
def __init__(self, child, description=None):
super(Not, self).__init__(description=description)
super().__init__(description=description)
self.child = child
def evaluate(self, ctx, short_circuit=True):
@@ -172,7 +179,7 @@ class Some(Statement):
"""
def __init__(self, count, children, description=None):
super(Some, self).__init__(description=description)
super().__init__(description=description)
self.count = count
self.children = children
@@ -208,7 +215,7 @@ class Range(Statement):
"""match if the child is contained in the ctx set with a count in the given range."""
def __init__(self, child, min=None, max=None, description=None):
super(Range, self).__init__(description=description)
super().__init__(description=description)
self.child = child
self.min = min if min is not None else 0
self.max = max if max is not None else (1 << 64 - 1)
@@ -237,7 +244,7 @@ class Subscope(Statement):
"""
def __init__(self, scope, child, description=None):
super(Subscope, self).__init__(description=description)
super().__init__(description=description)
self.scope = scope
self.child = child

View File

@@ -1,7 +1,5 @@
import abc
from dncil.clr.token import Token
class Address(abc.ABC):
@abc.abstractmethod
@@ -34,6 +32,9 @@ class AbsoluteVirtualAddress(int, Address):
def __repr__(self):
return f"absolute(0x{self:x})"
def __hash__(self):
return int.__hash__(self)
class RelativeVirtualAddress(int, Address):
"""a memory address relative to a base address"""
@@ -41,6 +42,9 @@ class RelativeVirtualAddress(int, Address):
def __repr__(self):
return f"relative(0x{self:x})"
def __hash__(self):
return int.__hash__(self)
class FileOffsetAddress(int, Address):
"""an address relative to the start of a file"""
@@ -52,52 +56,45 @@ class FileOffsetAddress(int, Address):
def __repr__(self):
return f"file(0x{self:x})"
def __hash__(self):
return int.__hash__(self)
class DNTokenAddress(Address):
class DNTokenAddress(int, Address):
"""a .NET token"""
def __init__(self, token: Token):
self.token = token
def __eq__(self, other):
return self.token.value == other.token.value
def __lt__(self, other):
return self.token.value < other.token.value
def __hash__(self):
return hash(self.token.value)
def __new__(cls, token: int):
return int.__new__(cls, token)
def __repr__(self):
return f"token(0x{self.token.value:x})"
return f"token(0x{self:x})"
def __index__(self):
# returns the object converted to an integer
return self.token.value
def __hash__(self):
return int.__hash__(self)
class DNTokenOffsetAddress(Address):
"""an offset into an object specified by a .NET token"""
def __init__(self, token: Token, offset: int):
def __init__(self, token: int, offset: int):
assert offset >= 0
self.token = token
self.offset = offset
def __eq__(self, other):
return (self.token.value, self.offset) == (other.token.value, other.offset)
return (self.token, self.offset) == (other.token, other.offset)
def __lt__(self, other):
return (self.token.value, self.offset) < (other.token.value, other.offset)
return (self.token, self.offset) < (other.token, other.offset)
def __hash__(self):
return hash((self.token.value, self.offset))
return hash((self.token, self.offset))
def __repr__(self):
return f"token(0x{self.token.value:x})+(0x{self.offset:x})"
return f"token(0x{self.token:x})+(0x{self.offset:x})"
def __index__(self):
return self.token.value + self.offset
return self.token + self.offset
class _NoAddress(Address):

View File

@@ -11,7 +11,7 @@ from capa.features.common import Feature
class BasicBlock(Feature):
def __init__(self, description=None):
super(BasicBlock, self).__init__(None, description=description)
super().__init__(0, description=description)
def __str__(self):
return "basic block"

View File

@@ -9,9 +9,10 @@
import re
import abc
import codecs
import typing
import logging
import collections
from typing import TYPE_CHECKING, Set, Dict, List, Union, Optional, Sequence
from typing import TYPE_CHECKING, Set, Dict, List, Union, Optional
if TYPE_CHECKING:
# circular import, otherwise
@@ -29,6 +30,14 @@ MAX_BYTES_FEATURE_SIZE = 0x100
THUNK_CHAIN_DEPTH_DELTA = 5
class FeatureAccess:
READ = "read"
WRITE = "write"
VALID_FEATURE_ACCESS = (FeatureAccess.READ, FeatureAccess.WRITE)
def bytes_to_str(b: bytes) -> str:
return str(codecs.encode(b, "hex").decode("utf-8"))
@@ -73,7 +82,7 @@ class Result:
children: List["Result"],
locations: Optional[Set[Address]] = None,
):
super(Result, self).__init__()
super().__init__()
self.success = success
self.statement = statement
self.children = children
@@ -92,15 +101,19 @@ class Result:
class Feature(abc.ABC):
def __init__(self, value: Union[str, int, float, bytes], description=None):
def __init__(
self,
value: Union[str, int, float, bytes],
description: Optional[str] = None,
):
"""
Args:
value (any): the value of the feature, such as the number or string.
description (str): a human-readable description that explains the feature value.
"""
super(Feature, self).__init__()
self.name = self.__class__.__name__.lower()
super().__init__()
self.name = self.__class__.__name__.lower()
self.value = value
self.description = description
@@ -119,23 +132,28 @@ class Feature(abc.ABC):
< capa.features.freeze.features.feature_from_capa(other).json()
)
def get_name_str(self) -> str:
"""
render the name of this feature, for use by `__str__` and friends.
subclasses should override to customize the rendering.
"""
return self.name
def get_value_str(self) -> str:
"""
render the value of this feature, for use by `__str__` and friends.
subclasses should override to customize the rendering.
Returns: any
"""
return str(self.value)
def __str__(self):
if self.value is not None:
if self.description:
return "%s(%s = %s)" % (self.name, self.get_value_str(), self.description)
return "%s(%s = %s)" % (self.get_name_str(), self.get_value_str(), self.description)
else:
return "%s(%s)" % (self.name, self.get_value_str())
return "%s(%s)" % (self.get_name_str(), self.get_value_str())
else:
return "%s" % self.name
return "%s" % self.get_name_str()
def __repr__(self):
return str(self)
@@ -148,33 +166,37 @@ class Feature(abc.ABC):
class MatchedRule(Feature):
def __init__(self, value: str, description=None):
super(MatchedRule, self).__init__(value, description=description)
super().__init__(value, description=description)
self.name = "match"
class Characteristic(Feature):
def __init__(self, value: str, description=None):
super(Characteristic, self).__init__(value, description=description)
super().__init__(value, description=description)
class String(Feature):
def __init__(self, value: str, description=None):
super(String, self).__init__(value, description=description)
super().__init__(value, description=description)
def get_value_str(self) -> str:
assert isinstance(self.value, str)
return escape_string(self.value)
class Class(Feature):
def __init__(self, value: str, description=None):
super(Class, self).__init__(value, description=description)
super().__init__(value, description=description)
class Namespace(Feature):
def __init__(self, value: str, description=None):
super(Namespace, self).__init__(value, description=description)
super().__init__(value, description=description)
class Substring(String):
def __init__(self, value: str, description=None):
super(Substring, self).__init__(value, description=description)
super().__init__(value, description=description)
self.value = value
def evaluate(self, ctx, short_circuit=True):
@@ -183,8 +205,9 @@ class Substring(String):
# mapping from string value to list of locations.
# will unique the locations later on.
matches = collections.defaultdict(list)
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
assert isinstance(self.value, str)
for feature, locations in ctx.items():
if not isinstance(feature, (String,)):
continue
@@ -194,32 +217,32 @@ class Substring(String):
raise ValueError("unexpected feature value type")
if self.value in feature.value:
matches[feature.value].extend(locations)
matches[feature.value].update(locations)
if short_circuit:
# we found one matching string, thats sufficient to match.
# don't collect other matching strings in this mode.
break
if matches:
# finalize: defaultdict -> dict
# which makes json serialization easier
matches = dict(matches)
# collect all locations
locations = set()
for s in matches.keys():
matches[s] = list(set(matches[s]))
locations.update(matches[s])
for locs in matches.values():
locations.update(locs)
# unlike other features, we cannot return put a reference to `self` directly in a `Result`.
# this is because `self` may match on many strings, so we can't stuff the matched value into it.
# instead, return a new instance that has a reference to both the substring and the matched values.
return Result(True, _MatchedSubstring(self, matches), [], locations=locations)
return Result(True, _MatchedSubstring(self, dict(matches)), [], locations=locations)
else:
return Result(False, _MatchedSubstring(self, {}), [])
def get_value_str(self) -> str:
assert isinstance(self.value, str)
return escape_string(self.value)
def __str__(self):
return "substring(%s)" % self.value
assert isinstance(self.value, str)
return "substring(%s)" % escape_string(self.value)
class _MatchedSubstring(Substring):
@@ -236,7 +259,7 @@ class _MatchedSubstring(Substring):
substring: the substring feature that matches.
match: mapping from matching string to its locations.
"""
super(_MatchedSubstring, self).__init__(str(substring.value), description=substring.description)
super().__init__(str(substring.value), description=substring.description)
# we want this to collide with the name of `Substring` above,
# so that it works nicely with the renderers.
self.name = "substring"
@@ -244,6 +267,7 @@ class _MatchedSubstring(Substring):
self.matches = matches
def __str__(self):
assert isinstance(self.value, str)
return 'substring("%s", matches = %s)' % (
self.value,
", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys())),
@@ -252,7 +276,7 @@ class _MatchedSubstring(Substring):
class Regex(String):
def __init__(self, value: str, description=None):
super(Regex, self).__init__(value, description=description)
super().__init__(value, description=description)
self.value = value
pat = self.value[len("/") : -len("/")]
@@ -262,12 +286,12 @@ class Regex(String):
flags |= re.IGNORECASE
try:
self.re = re.compile(pat, flags)
except re.error:
except re.error as exc:
if value.endswith("/i"):
value = value[: -len("i")]
raise ValueError(
"invalid regular expression: %s it should use Python syntax, try it at https://pythex.org" % value
)
) from exc
def evaluate(self, ctx, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
@@ -275,7 +299,7 @@ class Regex(String):
# mapping from string value to list of locations.
# will unique the locations later on.
matches = collections.defaultdict(list)
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
for feature, locations in ctx.items():
if not isinstance(feature, (String,)):
@@ -290,32 +314,28 @@ class Regex(String):
# using this mode cleans is more convenient for rule authors,
# so that they don't have to prefix/suffix their terms like: /.*foo.*/.
if self.re.search(feature.value):
matches[feature.value].extend(locations)
matches[feature.value].update(locations)
if short_circuit:
# we found one matching string, thats sufficient to match.
# don't collect other matching strings in this mode.
break
if matches:
# finalize: defaultdict -> dict
# which makes json serialization easier
matches = dict(matches)
# collect all locations
locations = set()
for s in matches.keys():
matches[s] = list(set(matches[s]))
locations.update(matches[s])
for locs in matches.values():
locations.update(locs)
# unlike other features, we cannot return put a reference to `self` directly in a `Result`.
# this is because `self` may match on many strings, so we can't stuff the matched value into it.
# instead, return a new instance that has a reference to both the regex and the matched values.
# see #262.
return Result(True, _MatchedRegex(self, matches), [], locations=locations)
return Result(True, _MatchedRegex(self, dict(matches)), [], locations=locations)
else:
return Result(False, _MatchedRegex(self, {}), [])
def __str__(self):
assert isinstance(self.value, str)
return "regex(string =~ %s)" % self.value
@@ -333,7 +353,7 @@ class _MatchedRegex(Regex):
regex: the regex feature that matches.
matches: mapping from matching string to its locations.
"""
super(_MatchedRegex, self).__init__(str(regex.value), description=regex.description)
super().__init__(str(regex.value), description=regex.description)
# we want this to collide with the name of `Regex` above,
# so that it works nicely with the renderers.
self.name = "regex"
@@ -341,6 +361,7 @@ class _MatchedRegex(Regex):
self.matches = matches
def __str__(self):
assert isinstance(self.value, str)
return "regex(string =~ %s, matches = %s)" % (
self.value,
", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys())),
@@ -356,23 +377,26 @@ class StringFactory:
class Bytes(Feature):
def __init__(self, value: bytes, description=None):
super(Bytes, self).__init__(value, description=description)
super().__init__(value, description=description)
self.value = value
def evaluate(self, ctx, **kwargs):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.bytes"] += 1
assert isinstance(self.value, bytes)
for feature, locations in ctx.items():
if not isinstance(feature, (Bytes,)):
continue
assert isinstance(feature.value, bytes)
if feature.value.startswith(self.value):
return Result(True, self, [], locations=locations)
return Result(False, self, [])
def get_value_str(self):
assert isinstance(self.value, bytes)
return hex_string(bytes_to_str(self.value))
@@ -386,7 +410,7 @@ VALID_ARCH = (ARCH_I386, ARCH_AMD64, ARCH_ANY)
class Arch(Feature):
def __init__(self, value: str, description=None):
super(Arch, self).__init__(value, description=description)
super().__init__(value, description=description)
self.name = "arch"
@@ -401,7 +425,7 @@ VALID_OS.update({OS_WINDOWS, OS_LINUX, OS_MACOS, OS_ANY})
class OS(Feature):
def __init__(self, value: str, description=None):
super(OS, self).__init__(value, description=description)
super().__init__(value, description=description)
self.name = "os"
@@ -419,7 +443,7 @@ FORMAT_UNKNOWN = "unknown"
class Format(Feature):
def __init__(self, value: str, description=None):
super(Format, self).__init__(value, description=description)
super().__init__(value, description=description)
self.name = "format"

View File

@@ -87,7 +87,7 @@ class FeatureExtractor:
# for example, the Vivisect feature extract might require the vw and/or path.
# this base class doesn't know what to do with that info, though.
#
super(FeatureExtractor, self).__init__()
super().__init__()
@abc.abstractmethod
def get_base_address(self) -> Union[AbsoluteVirtualAddress, capa.features.address._NoAddress]:

View File

@@ -9,6 +9,7 @@ import pefile
import capa.features
import capa.features.extractors.elf
import capa.features.extractors.pefile
import capa.features.extractors.strings
from capa.features.common import OS, FORMAT_PE, FORMAT_ELF, OS_WINDOWS, FORMAT_FREEZE, Arch, Format, String, Feature
from capa.features.freeze import is_freeze
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress
@@ -63,7 +64,7 @@ def extract_arch(buf) -> Iterator[Tuple[Feature, Address]]:
# 2. handling a new file format (e.g. macho)
#
# for (1) we can't do much - its shellcode and all bets are off.
# we could maybe accept a futher CLI argument to specify the arch,
# we could maybe accept a further CLI argument to specify the arch,
# but i think this would be rarely used.
# rules that rely on arch conditions will fail to match on shellcode.
#
@@ -91,7 +92,7 @@ def extract_os(buf) -> Iterator[Tuple[Feature, Address]]:
# 2. handling a new file format (e.g. macho)
#
# for (1) we can't do much - its shellcode and all bets are off.
# we could maybe accept a futher CLI argument to specify the OS,
# we could maybe accept a further CLI argument to specify the OS,
# but i think this would be rarely used.
# rules that rely on OS conditions will fail to match on shellcode.
#

View File

@@ -0,0 +1,45 @@
# 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 Tuple, Iterator
from dncil.cil.instruction import Instruction
from capa.features.common import Feature, Characteristic
from capa.features.address import Address
from capa.features.basicblock import BasicBlock
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
def extract_bb_stackstring(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract stackstring indicators from basic block"""
raise NotImplementedError
def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract tight loop indicators from a basic block"""
first: Instruction = bbh.inner.instructions[0]
last: Instruction = bbh.inner.instructions[-1]
if any((last.is_br(), last.is_cond_br(), last.is_leave())):
if last.operand == first.offset:
yield Characteristic("tight loop"), bbh.address
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract basic block features"""
for bb_handler in BASIC_BLOCK_HANDLERS:
for feature, addr in bb_handler(fh, bbh):
yield feature, addr
yield BasicBlock(), bbh.address
BASIC_BLOCK_HANDLERS = (
extract_bb_tight_loop,
# extract_bb_stackstring,
)

View File

@@ -8,27 +8,79 @@
from __future__ import annotations
from typing import List, Tuple, Iterator
from typing import Set, Dict, List, Tuple, Union, Iterator, Optional
import dnfile
from dncil.clr.token import Token
from dncil.cil.opcode import OpCodes
from dncil.cil.instruction import Instruction
import capa.features.extractors
import capa.features.extractors.dotnetfile
import capa.features.extractors.dnfile.file
import capa.features.extractors.dnfile.insn
import capa.features.extractors.dnfile.function
import capa.features.extractors.dnfile.basicblock
from capa.features.common import Feature
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress
from capa.features.extractors.dnfile.types import DnType, DnBasicBlock, DnUnmanagedMethod
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
from capa.features.extractors.dnfile.helpers import get_dotnet_managed_method_bodies
from capa.features.extractors.dnfile.helpers import (
get_dotnet_types,
get_dotnet_fields,
get_dotnet_managed_imports,
get_dotnet_managed_methods,
get_dotnet_unmanaged_imports,
get_dotnet_managed_method_bodies,
)
class DnFileFeatureExtractorCache:
def __init__(self, pe: dnfile.dnPE):
self.imports: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
self.native_imports: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
self.methods: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
self.fields: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
self.types: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
for import_ in get_dotnet_managed_imports(pe):
self.imports[import_.token] = import_
for native_import in get_dotnet_unmanaged_imports(pe):
self.native_imports[native_import.token] = native_import
for method in get_dotnet_managed_methods(pe):
self.methods[method.token] = method
for field in get_dotnet_fields(pe):
self.fields[field.token] = field
for type_ in get_dotnet_types(pe):
self.types[type_.token] = type_
def get_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.imports.get(token, None)
def get_native_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.native_imports.get(token, None)
def get_method(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.methods.get(token, None)
def get_field(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.fields.get(token, None)
def get_type(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.types.get(token, None)
class DnfileFeatureExtractor(FeatureExtractor):
def __init__(self, path: str):
super(DnfileFeatureExtractor, self).__init__()
super().__init__()
self.pe: dnfile.dnPE = dnfile.dnPE(path)
# pre-compute .NET token lookup tables; each .NET method has access to this cache for feature extraction
# most relevant at instruction scope
self.token_cache: DnFileFeatureExtractorCache = DnFileFeatureExtractorCache(self.pe)
# pre-compute these because we'll yield them at *every* scope.
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_format())
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_os(pe=self.pe))
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe))
@@ -42,28 +94,140 @@ class DnfileFeatureExtractor(FeatureExtractor):
yield from capa.features.extractors.dnfile.file.extract_features(self.pe)
def get_functions(self) -> Iterator[FunctionHandle]:
for token, f in get_dotnet_managed_method_bodies(self.pe):
yield FunctionHandle(address=DNTokenAddress(Token(token)), inner=f, ctx={"pe": self.pe})
# create a method lookup table
methods: Dict[Address, FunctionHandle] = {}
for token, method in get_dotnet_managed_method_bodies(self.pe):
fh: FunctionHandle = FunctionHandle(
address=DNTokenAddress(token),
inner=method,
ctx={
"pe": self.pe,
"calls_from": set(),
"calls_to": set(),
"blocks": list(),
"cache": self.token_cache,
},
)
def extract_function_features(self, f):
# TODO
yield from []
# method tokens should be unique
assert fh.address not in methods.keys()
methods[fh.address] = fh
def get_basic_blocks(self, f) -> Iterator[BBHandle]:
# each dotnet method is considered 1 basic block
yield BBHandle(
address=f.address,
inner=f.inner,
)
# calculate unique calls to/from each method
for fh in methods.values():
for insn in fh.inner.instructions:
if insn.opcode not in (
OpCodes.Call,
OpCodes.Callvirt,
OpCodes.Jmp,
OpCodes.Newobj,
):
continue
address: DNTokenAddress = DNTokenAddress(insn.operand.value)
# record call to destination method; note: we only consider MethodDef methods for destinations
dest: Optional[FunctionHandle] = methods.get(address, None)
if dest is not None:
dest.ctx["calls_to"].add(fh.address)
# record call from source method; note: we record all unique calls from a MethodDef method, not just
# those calls to other MethodDef methods e.g. calls to imported MemberRef methods
fh.ctx["calls_from"].add(address)
# calculate basic blocks
for fh in methods.values():
# calculate basic block leaders where,
# 1. The first instruction of the intermediate code is a leader
# 2. Instructions that are targets of unconditional or conditional jump/goto statements are leaders
# 3. Instructions that immediately follow unconditional or conditional jump/goto statements are considered leaders
# https://www.geeksforgeeks.org/basic-blocks-in-compiler-design/
leaders: Set[int] = set()
for idx, insn in enumerate(fh.inner.instructions):
if idx == 0:
# add #1
leaders.add(insn.offset)
if any((insn.is_br(), insn.is_cond_br(), insn.is_leave())):
# add #2
leaders.add(insn.operand)
# add #3
try:
leaders.add(fh.inner.instructions[idx + 1].offset)
except IndexError:
# may encounter branch at end of method
continue
# build basic blocks using leaders
bb_curr: Optional[DnBasicBlock] = None
for idx, insn in enumerate(fh.inner.instructions):
if insn.offset in leaders:
# new leader, new basic block
bb_curr = DnBasicBlock(instructions=[insn])
fh.ctx["blocks"].append(bb_curr)
continue
assert bb_curr is not None
bb_curr.instructions.append(insn)
# create mapping of first instruction to basic block
bb_map: Dict[int, DnBasicBlock] = {}
for bb in fh.ctx["blocks"]:
if len(bb.instructions) == 0:
# TODO: consider error?
continue
bb_map[bb.instructions[0].offset] = bb
# connect basic blocks
for idx, bb in enumerate(fh.ctx["blocks"]):
if len(bb.instructions) == 0:
# TODO: consider error?
continue
last = bb.instructions[-1]
# connect branches to other basic blocks
if any((last.is_br(), last.is_cond_br(), last.is_leave())):
bb_branch: Optional[DnBasicBlock] = bb_map.get(last.operand, None)
if bb_branch is not None:
# TODO: consider None error?
bb.succs.append(bb_branch)
bb_branch.preds.append(bb)
if any((last.is_br(), last.is_leave())):
# no fallthrough
continue
# connect fallthrough
try:
bb_next: DnBasicBlock = fh.ctx["blocks"][idx + 1]
bb.succs.append(bb_next)
bb_next.preds.append(bb)
except IndexError:
continue
yield from methods.values()
def extract_function_features(self, fh) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.dnfile.function.extract_features(fh)
def get_basic_blocks(self, fh) -> Iterator[BBHandle]:
for bb in fh.ctx["blocks"]:
yield BBHandle(
address=DNTokenOffsetAddress(
fh.address, bb.instructions[0].offset - (fh.inner.offset + fh.inner.header_size)
),
inner=bb,
)
def extract_basic_block_features(self, fh, bbh):
# we don't support basic block features
yield from []
yield from capa.features.extractors.dnfile.basicblock.extract_features(fh, bbh)
def get_instructions(self, fh, bbh):
for insn in bbh.inner.instructions:
yield InsnHandle(
address=DNTokenOffsetAddress(bbh.address.token, insn.offset - (fh.inner.offset + fh.inner.header_size)),
address=DNTokenOffsetAddress(fh.address, insn.offset - (fh.inner.offset + fh.inner.header_size)),
inner=insn,
)

View File

@@ -48,7 +48,7 @@ def extract_file_class_features(pe: dnfile.dnPE) -> Iterator[Tuple[Class, Addres
def extract_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
for file_handler in FILE_HANDLERS:
for (feature, address) in file_handler(pe):
for feature, address in file_handler(pe):
yield feature, address

View File

@@ -0,0 +1,62 @@
# 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 __future__ import annotations
import logging
from typing import Tuple, Iterator
from capa.features.common import Feature, Characteristic
from capa.features.address import Address
from capa.features.extractors import loops
from capa.features.extractors.base_extractor import FunctionHandle
logger = logging.getLogger(__name__)
def extract_function_calls_to(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
"""extract callers to a function"""
for dest in fh.ctx["calls_to"]:
yield Characteristic("calls to"), dest
def extract_function_calls_from(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
"""extract callers from a function"""
for src in fh.ctx["calls_from"]:
yield Characteristic("calls from"), src
def extract_recursive_call(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
"""extract recursive function call"""
if fh.address in fh.ctx["calls_to"]:
yield Characteristic("recursive call"), fh.address
def extract_function_loop(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
"""extract loop indicators from a function"""
edges = []
for bb in fh.ctx["blocks"]:
for succ in bb.succs:
edges.append((bb.instructions[0].offset, succ.instructions[0].offset))
if loops.has_loop(edges):
yield Characteristic("loop"), fh.address
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
for func_handler in FUNCTION_HANDLERS:
for feature, addr in func_handler(fh):
yield feature, addr
FUNCTION_HANDLERS = (
extract_function_calls_to,
extract_function_calls_from,
extract_recursive_call,
extract_function_loop,
)

View File

@@ -9,7 +9,7 @@
from __future__ import annotations
import logging
from typing import Any, Tuple, Iterator, Optional
from typing import Dict, Tuple, Union, Iterator, Optional
import dnfile
from dncil.cil.body import CilMethodBody
@@ -17,10 +17,10 @@ from dncil.cil.error import MethodBodyFormatError
from dncil.clr.token import Token, StringToken, InvalidToken
from dncil.cil.body.reader import CilMethodBodyReaderBase
logger = logging.getLogger(__name__)
from capa.features.common import FeatureAccess
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
# key indexes to dotnet metadata tables
DOTNET_META_TABLES_BY_INDEX = {table.value: table.name for table in dnfile.enums.MetadataTables}
logger = logging.getLogger(__name__)
class DnfileMethodBodyReader(CilMethodBodyReaderBase):
@@ -41,90 +41,20 @@ class DnfileMethodBodyReader(CilMethodBodyReaderBase):
return self.offset
class DnClass(object):
def __init__(self, token: int, namespace: str, classname: str):
self.token: int = token
self.namespace: str = namespace
self.classname: str = classname
def __hash__(self):
return hash((self.token,))
def __eq__(self, other):
return self.token == other.token
def __str__(self):
return DnClass.format_name(self.namespace, self.classname)
def __repr__(self):
return str(self)
@staticmethod
def format_name(namespace: str, classname: str):
name: str = classname
if namespace:
# like System.IO.File::OpenRead
name = f"{namespace}.{name}"
return name
class DnMethod(DnClass):
def __init__(self, token: int, namespace: str, classname: str, methodname: str):
super(DnMethod, self).__init__(token, namespace, classname)
self.methodname: str = methodname
def __str__(self):
return DnMethod.format_name(self.namespace, self.classname, self.methodname)
@staticmethod
def format_name(namespace: str, classname: str, methodname: str): # type: ignore
# like File::OpenRead
name: str = f"{classname}::{methodname}"
if namespace:
# like System.IO.File::OpenRead
name = f"{namespace}.{name}"
return name
class DnUnmanagedMethod:
def __init__(self, token: int, modulename: str, methodname: str):
self.token: int = token
self.modulename: str = modulename
self.methodname: str = methodname
def __hash__(self):
return hash((self.token,))
def __eq__(self, other):
return self.token == other.token
def __str__(self):
return DnUnmanagedMethod.format_name(self.modulename, self.methodname)
def __repr__(self):
return str(self)
@staticmethod
def format_name(modulename, methodname):
return f"{modulename}.{methodname}"
def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Any:
def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Union[dnfile.base.MDTableRow, InvalidToken, str]:
"""map generic token to string or table row"""
assert pe.net is not None
assert pe.net.mdtables is not None
if isinstance(token, StringToken):
user_string: Optional[str] = read_dotnet_user_string(pe, token)
if user_string is None:
return InvalidToken(token.value)
return user_string
table_name: str = DOTNET_META_TABLES_BY_INDEX.get(token.table, "")
if not table_name:
# table_index is not valid
return InvalidToken(token.value)
table: Any = getattr(pe.net.mdtables, table_name, None)
table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(token.table, None)
if table is None:
# table index is valid but table is not present
# table index is not valid
return InvalidToken(token.value)
try:
@@ -139,16 +69,23 @@ def read_dotnet_method_body(pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow) -
try:
return CilMethodBody(DnfileMethodBodyReader(pe, row))
except MethodBodyFormatError as e:
logger.warn("failed to parse managed method body @ 0x%08x (%s)" % (row.Rva, e))
logger.debug("failed to parse managed method body @ 0x%08x (%s)", row.Rva, e)
return None
def read_dotnet_user_string(pe: dnfile.dnPE, token: StringToken) -> Optional[str]:
"""read user string from #US stream"""
assert pe.net is not None
if pe.net.user_strings is None:
# stream may not exist (seen in obfuscated .NET)
logger.debug("#US stream does not exist for stream index 0x%08x", token.rid)
return None
try:
user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get_us(token.rid)
except UnicodeDecodeError as e:
logger.warn("failed to decode #US stream index 0x%08x (%s)" % (token.rid, e))
logger.debug("failed to decode #US stream index 0x%08x (%s)", token.rid, e)
return None
if user_string is None:
@@ -157,7 +94,7 @@ def read_dotnet_user_string(pe: dnfile.dnPE, token: StringToken) -> Optional[str
return user_string.value
def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnMethod]:
def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnType]:
"""get managed imports from MemberRef table
see https://www.ntcore.com/files/dotnetformat.htm
@@ -171,15 +108,76 @@ def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnMethod]:
TypeName (index into String heap)
TypeNamespace (index into String heap)
"""
for (rid, row) in enumerate(iter_dotnet_table(pe, "MemberRef")):
if not isinstance(row.Class.row, dnfile.mdtable.TypeRefRow):
for rid, member_ref in iter_dotnet_table(pe, dnfile.mdtable.MemberRef.number):
assert isinstance(member_ref, dnfile.mdtable.MemberRefRow)
if not isinstance(member_ref.Class.row, dnfile.mdtable.TypeRefRow):
# only process class imports from TypeRef table
continue
token: int = calculate_dotnet_token_value(pe.net.mdtables.MemberRef.number, rid + 1)
yield DnMethod(token, row.Class.row.TypeNamespace, row.Class.row.TypeName, row.Name)
token: int = calculate_dotnet_token_value(dnfile.mdtable.MemberRef.number, rid)
access: Optional[str]
# assume .NET imports starting with get_/set_ are used to access a property
if member_ref.Name.startswith("get_"):
access = FeatureAccess.READ
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:]
yield DnType(
token,
member_ref.Class.row.TypeName,
namespace=member_ref.Class.row.TypeNamespace,
member=member_ref_name,
access=access,
)
def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnMethod]:
def get_dotnet_methoddef_property_accessors(pe: dnfile.dnPE) -> Iterator[Tuple[int, str]]:
"""get MethodDef methods used to access properties
see https://www.ntcore.com/files/dotnetformat.htm
24 - MethodSemantics Table
Links Events and Properties to specific methods. For example one Event can be associated to more methods. A property uses this table to associate get/set methods.
Semantics (a 2-byte bitmask of type MethodSemanticsAttributes)
Method (index into the MethodDef table)
Association (index into the Event or Property table; more precisely, a HasSemantics coded index)
"""
for rid, method_semantics in iter_dotnet_table(pe, dnfile.mdtable.MethodSemantics.number):
assert isinstance(method_semantics, dnfile.mdtable.MethodSemanticsRow)
if method_semantics.Association.row is None:
logger.debug("MethodSemantics[0x%X] Association row is None", rid)
continue
if isinstance(method_semantics.Association.row, dnfile.mdtable.EventRow):
# ignore events
logger.debug("MethodSemantics[0x%X] ignoring Event", rid)
continue
if method_semantics.Method.table is None:
logger.debug("MethodSemantics[0x%X] Method table is None", rid)
continue
token: int = calculate_dotnet_token_value(
method_semantics.Method.table.number, method_semantics.Method.row_index
)
if method_semantics.Semantics.msSetter:
yield token, FeatureAccess.WRITE
elif method_semantics.Semantics.msGetter:
yield token, FeatureAccess.READ
def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]:
"""get managed method names from TypeDef table
see https://www.ntcore.com/files/dotnetformat.htm
@@ -188,29 +186,74 @@ def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnMethod]:
Each row represents a class in the current assembly.
TypeName (index into String heap)
TypeNamespace (index into String heap)
MethodList (index into MethodDef table; it marks the first of a continguous run of Methods owned by this Type)
MethodList (index into MethodDef table; it marks the first of a contiguous run of Methods owned by this Type)
"""
for row in iter_dotnet_table(pe, "TypeDef"):
for index in row.MethodList:
token = calculate_dotnet_token_value(index.table.number, index.row_index)
yield DnMethod(token, row.TypeNamespace, row.TypeName, index.row.Name)
accessor_map: Dict[int, str] = {}
for methoddef, methoddef_access in get_dotnet_methoddef_property_accessors(pe):
accessor_map[methoddef] = methoddef_access
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
for idx, method in enumerate(typedef.MethodList):
if method.table is None:
logger.debug("TypeDef[0x%X] MethodList[0x%X] table is None", rid, idx)
continue
if method.row is None:
logger.debug("TypeDef[0x%X] MethodList[0x%X] row is None", rid, idx)
continue
token: int = calculate_dotnet_token_value(method.table.number, method.row_index)
access: Optional[str] = accessor_map.get(token, None)
method_name: str = method.row.Name
if method_name.startswith(("get_", "set_")):
# remove get_/set_
method_name = method_name[4:]
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=method_name, access=access)
def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]:
"""get fields from TypeDef table
see https://www.ntcore.com/files/dotnetformat.htm
02 - TypeDef Table
Each row represents a class in the current assembly.
TypeName (index into String heap)
TypeNamespace (index into String heap)
FieldList (index into Field table; it marks the first of a contiguous run of Fields owned by this Type)
"""
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
for idx, field in enumerate(typedef.FieldList):
if field.table is None:
logger.debug("TypeDef[0x%X] FieldList[0x%X] table is None", rid, idx)
continue
if field.row is None:
logger.debug("TypeDef[0x%X] FieldList[0x%X] row is None", rid, idx)
continue
token: int = calculate_dotnet_token_value(field.table.number, field.row_index)
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=field.row.Name)
def get_dotnet_managed_method_bodies(pe: dnfile.dnPE) -> Iterator[Tuple[int, CilMethodBody]]:
"""get managed methods from MethodDef table"""
if not hasattr(pe.net.mdtables, "MethodDef"):
return
for rid, method_def in iter_dotnet_table(pe, dnfile.mdtable.MethodDef.number):
assert isinstance(method_def, dnfile.mdtable.MethodDefRow)
for (rid, row) in enumerate(pe.net.mdtables.MethodDef):
if not row.ImplFlags.miIL or any((row.Flags.mdAbstract, row.Flags.mdPinvokeImpl)):
if not method_def.ImplFlags.miIL or any((method_def.Flags.mdAbstract, method_def.Flags.mdPinvokeImpl)):
# skip methods that do not have a method body
continue
body: Optional[CilMethodBody] = read_dotnet_method_body(pe, row)
body: Optional[CilMethodBody] = read_dotnet_method_body(pe, method_def)
if body is None:
logger.debug("MethodDef[0x%X] method body is None", rid)
continue
token: int = calculate_dotnet_token_value(dnfile.enums.MetadataTables.MethodDef.value, rid + 1)
token: int = calculate_dotnet_token_value(dnfile.mdtable.MethodDef.number, rid)
yield token, body
@@ -225,37 +268,68 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod]
ImportName (index into the String heap)
ImportScope (index into the ModuleRef table)
"""
for row in iter_dotnet_table(pe, "ImplMap"):
modulename: str = row.ImportScope.row.Name
methodname: str = row.ImportName
for rid, impl_map in iter_dotnet_table(pe, dnfile.mdtable.ImplMap.number):
assert isinstance(impl_map, dnfile.mdtable.ImplMapRow)
module: str
if impl_map.ImportScope.row is None:
logger.debug("ImplMap[0x%X] ImportScope row is None", rid)
module = ""
else:
module = impl_map.ImportScope.row.Name
method: str = impl_map.ImportName
member_forward_table: int
if impl_map.MemberForwarded.table is None:
logger.debug("ImplMap[0x%X] MemberForwarded table is None", rid)
continue
else:
member_forward_table = impl_map.MemberForwarded.table.number
member_forward_row: int = impl_map.MemberForwarded.row_index
# ECMA says "Each row of the ImplMap table associates a row in the MethodDef table (MemberForwarded) with the
# name of a routine (ImportName) in some unmanaged DLL (ImportScope)"; so we calculate and map the MemberForwarded
# MethodDef table token to help us later record native import method calls made from CIL
token: int = calculate_dotnet_token_value(row.MemberForwarded.table.number, row.MemberForwarded.row_index)
token: int = calculate_dotnet_token_value(member_forward_table, member_forward_row)
# like Kernel32.dll
if modulename and "." in modulename:
modulename = modulename.split(".")[0]
if module and "." in module:
module = module.split(".")[0]
# like kernel32.CreateFileA
yield DnUnmanagedMethod(token, modulename, methodname)
yield DnUnmanagedMethod(token, module, method)
def get_dotnet_types(pe: dnfile.dnPE) -> Iterator[DnType]:
"""get .NET types from TypeDef and TypeRef tables"""
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
typedef_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
yield DnType(typedef_token, typedef.TypeName, namespace=typedef.TypeNamespace)
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
typeref_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
yield DnType(typeref_token, typeref.TypeName, namespace=typeref.TypeNamespace)
def calculate_dotnet_token_value(table: int, rid: int) -> int:
return ((table & 0xFF) << Token.TABLE_SHIFT) | (rid & Token.RID_MASK)
def is_dotnet_table_valid(pe: dnfile.dnPE, table_name: str) -> bool:
return bool(getattr(pe.net.mdtables, table_name, None))
def is_dotnet_mixed_mode(pe: dnfile.dnPE) -> bool:
assert pe.net is not None
assert pe.net.Flags is not None
return not bool(pe.net.Flags.CLR_ILONLY)
def iter_dotnet_table(pe: dnfile.dnPE, name: str) -> Iterator[Any]:
if not is_dotnet_table_valid(pe, name):
return
for row in getattr(pe.net.mdtables, name):
yield row
def iter_dotnet_table(pe: dnfile.dnPE, table_index: int) -> Iterator[Tuple[int, dnfile.base.MDTableRow]]:
assert pe.net is not None
assert pe.net.mdtables is not None
for rid, row in enumerate(pe.net.mdtables.tables.get(table_index, [])):
# .NET tables are 1-indexed
yield rid + 1, row

View File

@@ -8,175 +8,220 @@
from __future__ import annotations
from typing import Any, Dict, Tuple, Union, Iterator, Optional
import logging
from typing import TYPE_CHECKING, Any, Dict, Tuple, Union, Iterator, Optional
if TYPE_CHECKING:
from capa.features.extractors.dnfile.extractor import DnFileFeatureExtractorCache
import dnfile
from dncil.cil.body import CilMethodBody
from dncil.clr.token import Token, StringToken, InvalidToken
from dncil.cil.opcode import OpCodes
from dncil.cil.instruction import Instruction
import capa.features.extractors.helpers
from capa.features.insn import API, Number
from capa.features.common import Class, String, Feature, Namespace, Characteristic
from capa.features.insn import API, Number, Property
from capa.features.common import Class, String, Feature, Namespace, FeatureAccess, Characteristic
from capa.features.address import Address
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
from capa.features.extractors.dnfile.helpers import (
DnClass,
DnMethod,
DnUnmanagedMethod,
resolve_dotnet_token,
read_dotnet_user_string,
get_dotnet_managed_imports,
get_dotnet_managed_methods,
get_dotnet_unmanaged_imports,
calculate_dotnet_token_value,
)
def get_managed_imports(ctx: Dict) -> Dict:
if "managed_imports_cache" not in ctx:
ctx["managed_imports_cache"] = {}
for method in get_dotnet_managed_imports(ctx["pe"]):
ctx["managed_imports_cache"][method.token] = method
return ctx["managed_imports_cache"]
logger = logging.getLogger(__name__)
def get_unmanaged_imports(ctx: Dict) -> Dict:
if "unmanaged_imports_cache" not in ctx:
ctx["unmanaged_imports_cache"] = {}
for imp in get_dotnet_unmanaged_imports(ctx["pe"]):
ctx["unmanaged_imports_cache"][imp.token] = imp
return ctx["unmanaged_imports_cache"]
def get_callee(
pe: dnfile.dnPE, cache: DnFileFeatureExtractorCache, token: Token
) -> Optional[Union[DnType, DnUnmanagedMethod]]:
"""map .NET token to un/managed (generic) method"""
token_: int
if token.table == dnfile.mdtable.MethodSpec.number:
# map MethodSpec to MethodDef or MemberRef
row: Union[dnfile.base.MDTableRow, InvalidToken, str] = resolve_dotnet_token(pe, token)
assert isinstance(row, dnfile.mdtable.MethodSpecRow)
if row.Method.table is None:
logger.debug("MethodSpec[0x%X] Method table is None", token.rid)
return None
def get_methods(ctx: Dict) -> Dict:
if "methods_cache" not in ctx:
ctx["methods_cache"] = {}
for method in get_dotnet_managed_methods(ctx["pe"]):
ctx["methods_cache"][method.token] = method
return ctx["methods_cache"]
token_ = calculate_dotnet_token_value(row.Method.table.number, row.Method.row_index)
else:
token_ = token.value
def get_callee(ctx: Dict, token: int) -> Union[DnMethod, DnUnmanagedMethod, None]:
"""map dotnet token to un/managed method"""
callee: Union[DnMethod, DnUnmanagedMethod, None] = get_managed_imports(ctx).get(token, None)
if not callee:
callee: Optional[Union[DnType, DnUnmanagedMethod]] = cache.get_import(token_)
if callee is None:
# we must check unmanaged imports before managed methods because we map forwarded managed methods
# to their unmanaged imports; we prefer a forwarded managed method be mapped to its unmanaged import for analysis
callee = get_unmanaged_imports(ctx).get(token, None)
if not callee:
callee = get_methods(ctx).get(token, None)
callee = cache.get_native_import(token_)
if callee is None:
callee = cache.get_method(token_)
return callee
def extract_insn_api_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction API features"""
insn: Instruction = ih.inner
if insn.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
if ih.inner.opcode not in (
OpCodes.Call,
OpCodes.Callvirt,
OpCodes.Jmp,
OpCodes.Newobj,
):
return
callee: Union[DnMethod, DnUnmanagedMethod, None] = get_callee(fh.ctx, insn.operand.value)
if callee is None:
return
if isinstance(callee, DnUnmanagedMethod):
callee: Optional[Union[DnType, DnUnmanagedMethod]] = get_callee(fh.ctx["pe"], fh.ctx["cache"], ih.inner.operand)
if isinstance(callee, DnType):
# ignore methods used to access properties
if callee.access is None:
# like System.IO.File::Delete
yield API(str(callee)), ih.address
elif isinstance(callee, DnUnmanagedMethod):
# like kernel32.CreateFileA
for name in capa.features.extractors.helpers.generate_symbols(callee.modulename, callee.methodname):
for name in capa.features.extractors.helpers.generate_symbols(callee.module, callee.method):
yield API(name), ih.address
else:
# like System.IO.File::Delete
yield API(str(callee)), ih.address
def extract_insn_class_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Class, Address]]:
"""parse instruction class features"""
if ih.inner.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
return
def extract_insn_property_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction property features"""
name: Optional[str] = None
access: Optional[str] = None
row: Any = resolve_dotnet_token(fh.ctx["pe"], Token(ih.inner.operand.value))
if ih.inner.opcode in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp):
# property access via MethodDef or MemberRef
callee: Optional[Union[DnType, DnUnmanagedMethod]] = get_callee(fh.ctx["pe"], fh.ctx["cache"], ih.inner.operand)
if isinstance(callee, DnType):
if callee.access is not None:
name = str(callee)
access = callee.access
if not isinstance(row, dnfile.mdtable.MemberRefRow):
return
if not isinstance(row.Class.row, (dnfile.mdtable.TypeRefRow, dnfile.mdtable.TypeDefRow)):
return
elif ih.inner.opcode in (OpCodes.Ldfld, OpCodes.Ldflda, OpCodes.Ldsfld, OpCodes.Ldsflda):
# property read via Field
read_field: Optional[Union[DnType, DnUnmanagedMethod]] = fh.ctx["cache"].get_field(ih.inner.operand.value)
if read_field is not None:
name = str(read_field)
access = FeatureAccess.READ
yield Class(DnClass.format_name(row.Class.row.TypeNamespace, row.Class.row.TypeName)), ih.address
elif ih.inner.opcode in (OpCodes.Stfld, OpCodes.Stsfld):
# property write via Field
write_field: Optional[Union[DnType, DnUnmanagedMethod]] = fh.ctx["cache"].get_field(ih.inner.operand.value)
if write_field is not None:
name = str(write_field)
access = FeatureAccess.WRITE
if name is not None:
if access is not None:
yield Property(name, access=access), ih.address
yield Property(name), ih.address
def extract_insn_namespace_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Namespace, Address]]:
"""parse instruction namespace features"""
if ih.inner.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
return
def extract_insn_namespace_class_features(
fh: FunctionHandle, bh, ih: InsnHandle
) -> Iterator[Tuple[Union[Namespace, Class], Address]]:
"""parse instruction namespace and class features"""
type_: Optional[Union[DnType, DnUnmanagedMethod]] = None
row: Any = resolve_dotnet_token(fh.ctx["pe"], Token(ih.inner.operand.value))
if ih.inner.opcode in (
OpCodes.Call,
OpCodes.Callvirt,
OpCodes.Jmp,
OpCodes.Ldvirtftn,
OpCodes.Ldftn,
OpCodes.Newobj,
):
# method call - includes managed methods (MethodDef, TypeRef) and properties (MethodSemantics, TypeRef)
type_ = get_callee(fh.ctx["pe"], fh.ctx["cache"], ih.inner.operand)
if not isinstance(row, dnfile.mdtable.MemberRefRow):
return
if not isinstance(row.Class.row, (dnfile.mdtable.TypeRefRow, dnfile.mdtable.TypeDefRow)):
return
if not row.Class.row.TypeNamespace:
return
elif ih.inner.opcode in (
OpCodes.Ldfld,
OpCodes.Ldflda,
OpCodes.Ldsfld,
OpCodes.Ldsflda,
OpCodes.Stfld,
OpCodes.Stsfld,
):
# field access
type_ = fh.ctx["cache"].get_field(ih.inner.operand.value)
yield Namespace(row.Class.row.TypeNamespace), ih.address
# ECMA 335 VI.C.4.10
elif ih.inner.opcode in (
OpCodes.Initobj,
OpCodes.Box,
OpCodes.Castclass,
OpCodes.Cpobj,
OpCodes.Isinst,
OpCodes.Ldelem,
OpCodes.Ldelema,
OpCodes.Ldobj,
OpCodes.Mkrefany,
OpCodes.Newarr,
OpCodes.Refanyval,
OpCodes.Sizeof,
OpCodes.Stobj,
OpCodes.Unbox,
OpCodes.Constrained,
OpCodes.Stelem,
OpCodes.Unbox_Any,
):
# type access
type_ = fh.ctx["cache"].get_type(ih.inner.operand.value)
if isinstance(type_, DnType):
yield Class(DnType.format_name(type_.class_, namespace=type_.namespace)), ih.address
if type_.namespace:
yield Namespace(type_.namespace), ih.address
def extract_insn_number_features(fh, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction number features"""
insn: Instruction = ih.inner
if insn.is_ldc():
yield Number(insn.get_ldc()), ih.address
if ih.inner.is_ldc():
yield Number(ih.inner.get_ldc()), ih.address
def extract_insn_string_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction string features"""
f: CilMethodBody = fh.inner
insn: Instruction = ih.inner
if not insn.is_ldstr():
if not ih.inner.is_ldstr():
return
if not isinstance(insn.operand, StringToken):
if not isinstance(ih.inner.operand, StringToken):
return
user_string: Optional[str] = read_dotnet_user_string(fh.ctx["pe"], insn.operand)
user_string: Optional[str] = read_dotnet_user_string(fh.ctx["pe"], ih.inner.operand)
if user_string is None:
return
yield String(user_string), ih.address
if len(user_string) >= 4:
yield String(user_string), ih.address
def extract_unmanaged_call_characteristic_features(
fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Characteristic, Address]]:
insn: Instruction = ih.inner
if insn.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
if ih.inner.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp):
return
token: Any = resolve_dotnet_token(fh.ctx["pe"], insn.operand)
if isinstance(token, InvalidToken):
return
if not isinstance(token, dnfile.mdtable.MethodDefRow):
row: Union[str, InvalidToken, dnfile.base.MDTableRow] = resolve_dotnet_token(fh.ctx["pe"], ih.inner.operand)
if not isinstance(row, dnfile.mdtable.MethodDefRow):
return
if any((token.Flags.mdPinvokeImpl, token.ImplFlags.miUnmanaged, token.ImplFlags.miNative)):
if any((row.Flags.mdPinvokeImpl, row.ImplFlags.miUnmanaged, row.ImplFlags.miNative)):
yield Characteristic("unmanaged call"), ih.address
def extract_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract instruction features"""
for inst_handler in INSTRUCTION_HANDLERS:
for (feature, addr) in inst_handler(fh, bbh, ih):
for feature, addr in inst_handler(fh, bbh, ih):
assert isinstance(addr, Address)
yield feature, addr
INSTRUCTION_HANDLERS = (
extract_insn_api_features,
extract_insn_property_features,
extract_insn_number_features,
extract_insn_string_features,
extract_insn_namespace_features,
extract_insn_class_features,
extract_insn_namespace_class_features,
extract_unmanaged_call_characteristic_features,
)

View File

@@ -0,0 +1,84 @@
# 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 TYPE_CHECKING, Dict, List, Optional
if TYPE_CHECKING:
from dncil.cil.instruction import Instruction
class DnType(object):
def __init__(self, token: int, class_: str, namespace: str = "", member: str = "", access: Optional[str] = None):
self.token: int = token
self.access: Optional[str] = access
self.namespace: str = namespace
self.class_: str = class_
if member == ".ctor":
member = "ctor"
if member == ".cctor":
member = "cctor"
self.member: str = member
def __hash__(self):
return hash((self.token, self.access, self.namespace, self.class_, self.member))
def __eq__(self, other):
return (
self.token == other.token
and self.access == other.access
and self.namespace == other.namespace
and self.class_ == other.class_
and self.member == other.member
)
def __str__(self):
return DnType.format_name(self.class_, namespace=self.namespace, member=self.member)
def __repr__(self):
return str(self)
@staticmethod
def format_name(class_: str, namespace: str = "", member: str = ""):
# like File::OpenRead
name: str = f"{class_}::{member}" if member else class_
if namespace:
# like System.IO.File::OpenRead
name = f"{namespace}.{name}"
return name
class DnUnmanagedMethod:
def __init__(self, token: int, module: str, method: str):
self.token: int = token
self.module: str = module
self.method: str = method
def __hash__(self):
return hash((self.token, self.module, self.method))
def __eq__(self, other):
return self.token == other.token and self.module == other.module and self.method == other.method
def __str__(self):
return DnUnmanagedMethod.format_name(self.module, self.method)
def __repr__(self):
return str(self)
@staticmethod
def format_name(module, method):
return f"{module}.{method}"
class DnBasicBlock:
def __init__(self, preds=None, succs=None, instructions=None):
self.succs: List[DnBasicBlock] = succs or []
self.preds: List[DnBasicBlock] = preds or []
self.instructions: List[Instruction] = instructions or []

View File

@@ -4,7 +4,18 @@ from typing import Tuple, Iterator
import dnfile
import pefile
from capa.features.common import OS, OS_ANY, ARCH_ANY, ARCH_I386, ARCH_AMD64, FORMAT_DOTNET, Arch, Format, Feature
from capa.features.common import (
OS,
OS_ANY,
ARCH_ANY,
ARCH_I386,
FORMAT_PE,
ARCH_AMD64,
FORMAT_DOTNET,
Arch,
Format,
Feature,
)
from capa.features.address import NO_ADDRESS, Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import FeatureExtractor
@@ -12,6 +23,7 @@ logger = logging.getLogger(__name__)
def extract_file_format(**kwargs) -> Iterator[Tuple[Feature, Address]]:
yield Format(FORMAT_PE), NO_ADDRESS
yield Format(FORMAT_DOTNET), NO_ADDRESS
@@ -19,9 +31,12 @@ def extract_file_os(**kwargs) -> Iterator[Tuple[Feature, Address]]:
yield OS(OS_ANY), NO_ADDRESS
def extract_file_arch(pe, **kwargs) -> Iterator[Tuple[Feature, Address]]:
def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Feature, Address]]:
# to distinguish in more detail, see https://stackoverflow.com/a/23614024/10548020
# .NET 4.5 added option: any CPU, 32-bit preferred
assert pe.net is not None
assert pe.net.Flags is not None
if pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE:
yield Arch(ARCH_I386), NO_ADDRESS
elif not pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS:
@@ -60,7 +75,7 @@ GLOBAL_HANDLERS = (
class DnfileFeatureExtractor(FeatureExtractor):
def __init__(self, path: str):
super(DnfileFeatureExtractor, self).__init__()
super().__init__()
self.path: str = path
self.pe: dnfile.dnPE = dnfile.dnPE(path)
@@ -71,6 +86,9 @@ class DnfileFeatureExtractor(FeatureExtractor):
# self.pe.net.Flags.CLT_NATIVE_ENTRYPOINT
# True: native EP: Token
# False: managed EP: RVA
assert self.pe.net is not None
assert self.pe.net.struct is not None
return self.pe.net.struct.EntryPointTokenOrRva
def extract_global_features(self):
@@ -83,13 +101,29 @@ class DnfileFeatureExtractor(FeatureExtractor):
return bool(self.pe.net)
def is_mixed_mode(self) -> bool:
assert self.pe is not None
assert self.pe.net is not None
assert self.pe.net.Flags is not None
return not bool(self.pe.net.Flags.CLR_ILONLY)
def get_runtime_version(self) -> Tuple[int, int]:
assert self.pe is not None
assert self.pe.net is not None
assert self.pe.net.struct is not None
return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion
def get_meta_version_string(self) -> str:
return self.pe.net.metadata.struct.Version.rstrip(b"\x00").decode("utf-8")
assert self.pe.net is not None
assert self.pe.net.metadata is not None
assert self.pe.net.metadata.struct is not None
assert self.pe.net.metadata.struct.Version is not None
vbuf = self.pe.net.metadata.struct.Version
assert isinstance(vbuf, bytes)
return vbuf.rstrip(b"\x00").decode("utf-8")
def get_functions(self):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")

View File

@@ -1,10 +1,8 @@
import logging
import itertools
from typing import Tuple, Iterator
from typing import Tuple, Iterator, cast
import dnfile
import pefile
from dncil.clr.token import Token
import capa.features.extractors.helpers
from capa.features.file import Import, FunctionName
@@ -13,6 +11,7 @@ from capa.features.common import (
OS_ANY,
ARCH_ANY,
ARCH_I386,
FORMAT_PE,
ARCH_AMD64,
FORMAT_DOTNET,
Arch,
@@ -23,11 +22,10 @@ from capa.features.common import (
Namespace,
Characteristic,
)
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress
from capa.features.extractors.base_extractor import FeatureExtractor
from capa.features.extractors.dnfile.helpers import (
DnClass,
DnMethod,
DnType,
iter_dotnet_table,
is_dotnet_mixed_mode,
get_dotnet_managed_imports,
@@ -40,23 +38,24 @@ 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
def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Import, Address]]:
for method in get_dotnet_managed_imports(pe):
# like System.IO.File::OpenRead
yield Import(str(method)), DNTokenAddress(Token(method.token))
yield Import(str(method)), DNTokenAddress(method.token)
for imp in get_dotnet_unmanaged_imports(pe):
# like kernel32.CreateFileA
for name in capa.features.extractors.helpers.generate_symbols(imp.modulename, imp.methodname):
yield Import(name), DNTokenAddress(Token(imp.token))
for name in capa.features.extractors.helpers.generate_symbols(imp.module, imp.method):
yield Import(name), DNTokenAddress(imp.token)
def extract_file_function_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[FunctionName, Address]]:
for method in get_dotnet_managed_methods(pe):
yield FunctionName(str(method)), DNTokenAddress(Token(method.token))
yield FunctionName(str(method)), DNTokenAddress(method.token)
def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Namespace, Address]]:
@@ -65,11 +64,15 @@ def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple
# namespaces may be referenced multiple times, so we need to filter
namespaces = set()
for row in iter_dotnet_table(pe, "TypeDef"):
namespaces.add(row.TypeNamespace)
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)
for row in iter_dotnet_table(pe, "TypeRef"):
namespaces.add(row.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 may be empty, discard
namespaces.discard("")
@@ -81,13 +84,19 @@ def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple
def extract_file_class_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Class, Address]]:
"""emit class features from TypeRef and TypeDef tables"""
for (rid, row) in enumerate(iter_dotnet_table(pe, "TypeDef")):
token = calculate_dotnet_token_value(pe.net.mdtables.TypeDef.number, rid + 1)
yield Class(DnClass.format_name(row.TypeNamespace, row.TypeName)), DNTokenAddress(Token(token))
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
# emit internal .NET classes
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
for (rid, row) in enumerate(iter_dotnet_table(pe, "TypeRef")):
token = calculate_dotnet_token_value(pe.net.mdtables.TypeRef.number, rid + 1)
yield Class(DnClass.format_name(row.TypeNamespace, row.TypeName)), DNTokenAddress(Token(token))
token = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
yield Class(DnType.format_name(typedef.TypeName, namespace=typedef.TypeNamespace)), DNTokenAddress(token)
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
# emit external .NET classes
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
token = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
yield Class(DnType.format_name(typeref.TypeName, namespace=typeref.TypeNamespace)), DNTokenAddress(token)
def extract_file_os(**kwargs) -> Iterator[Tuple[OS, Address]]:
@@ -97,6 +106,9 @@ def extract_file_os(**kwargs) -> Iterator[Tuple[OS, Address]]:
def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Arch, Address]]:
# to distinguish in more detail, see https://stackoverflow.com/a/23614024/10548020
# .NET 4.5 added option: any CPU, 32-bit preferred
assert pe.net is not None
assert pe.net.Flags is not None
if pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE:
yield Arch(ARCH_I386), NO_ADDRESS
elif not pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS:
@@ -147,7 +159,7 @@ GLOBAL_HANDLERS = (
class DotnetFileFeatureExtractor(FeatureExtractor):
def __init__(self, path: str):
super(DotnetFileFeatureExtractor, self).__init__()
super().__init__()
self.path: str = path
self.pe: dnfile.dnPE = dnfile.dnPE(path)
@@ -158,6 +170,9 @@ class DotnetFileFeatureExtractor(FeatureExtractor):
# self.pe.net.Flags.CLT_NATIVE_ENTRYPOINT
# True: native EP: Token
# False: managed EP: RVA
assert self.pe.net is not None
assert self.pe.net.struct is not None
return self.pe.net.struct.EntryPointTokenOrRva
def extract_global_features(self):
@@ -173,10 +188,23 @@ class DotnetFileFeatureExtractor(FeatureExtractor):
return is_dotnet_mixed_mode(self.pe)
def get_runtime_version(self) -> Tuple[int, int]:
assert self.pe.net is not None
assert self.pe.net.struct is not None
assert self.pe.net.struct.MajorRuntimeVersion is not None
assert self.pe.net.struct.MinorRuntimeVersion is not None
return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion
def get_meta_version_string(self) -> str:
return self.pe.net.metadata.struct.Version.rstrip(b"\x00").decode("utf-8")
assert self.pe.net is not None
assert self.pe.net.metadata is not None
assert self.pe.net.metadata.struct is not None
assert self.pe.net.metadata.struct.Version is not None
vbuf = self.pe.net.metadata.struct.Version
assert isinstance(vbuf, bytes)
return vbuf.rstrip(b"\x00").decode("utf-8")
def get_functions(self):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")

View File

@@ -7,8 +7,11 @@
# See the License for the specific language governing permissions and limitations under the License.
import struct
import logging
import itertools
import collections
from enum import Enum
from typing import BinaryIO
from typing import Set, Dict, List, Tuple, BinaryIO, Iterator, Optional
from dataclasses import dataclass
logger = logging.getLogger(__name__)
@@ -21,6 +24,12 @@ def align(v, alignment):
return v + (alignment - remainder)
def read_cstr(buf, offset):
s = buf[offset:]
s, _, _ = s.partition(b"\x00")
return s.decode("utf-8")
class CorruptElfFile(ValueError):
pass
@@ -60,52 +69,94 @@ GNU_ABI_TAG = {
}
def detect_elf_os(f) -> str:
"""
f: type Union[BinaryIO, IDAIO]
"""
f.seek(0x0)
file_header = f.read(0x40)
@dataclass
class Phdr:
type: int
offset: int
vaddr: int
paddr: int
filesz: int
buf: bytes
# we'll set this to the detected OS
# prefer the first heuristics,
# but rather than short circuiting,
# we'll still parse out the remainder, for debugging.
ret = None
if not file_header.startswith(b"\x7fELF"):
raise CorruptElfFile("missing magic header")
@dataclass
class Shdr:
name: int
type: int
flags: int
addr: int
offset: int
size: int
link: int
buf: bytes
ei_class, ei_data = struct.unpack_from("BB", file_header, 4)
logger.debug("ei_class: 0x%02x ei_data: 0x%02x", ei_class, ei_data)
if ei_class == 1:
bitness = 32
elif ei_class == 2:
bitness = 64
else:
raise CorruptElfFile("invalid ei_class: 0x%02x" % ei_class)
if ei_data == 1:
endian = "<"
elif ei_data == 2:
endian = ">"
else:
raise CorruptElfFile("not an ELF file: invalid ei_data: 0x%02x" % ei_data)
class ELF:
def __init__(self, f: BinaryIO):
self.f = f
if bitness == 32:
(e_phoff, e_shoff) = struct.unpack_from(endian + "II", file_header, 0x1C)
e_phentsize, e_phnum = struct.unpack_from(endian + "HH", file_header, 0x2A)
e_shentsize, e_shnum = struct.unpack_from(endian + "HH", file_header, 0x2E)
elif bitness == 64:
(e_phoff, e_shoff) = struct.unpack_from(endian + "QQ", file_header, 0x20)
e_phentsize, e_phnum = struct.unpack_from(endian + "HH", file_header, 0x36)
e_shentsize, e_shnum = struct.unpack_from(endian + "HH", file_header, 0x3A)
else:
raise NotImplementedError()
# these will all be initialized in `_parse()`
self.bitness: int
self.endian: str
self.e_phentsize: int
self.e_phnum: int
self.e_shentsize: int
self.e_shnum: int
self.phbuf: bytes
self.shbuf: bytes
logger.debug("e_phoff: 0x%02x e_phentsize: 0x%02x e_phnum: %d", e_phoff, e_phentsize, e_phnum)
self._parse()
def _parse(self):
self.f.seek(0x0)
self.file_header = self.f.read(0x40)
if not self.file_header.startswith(b"\x7fELF"):
raise CorruptElfFile("missing magic header")
ei_class, ei_data = struct.unpack_from("BB", self.file_header, 4)
logger.debug("ei_class: 0x%02x ei_data: 0x%02x", ei_class, ei_data)
if ei_class == 1:
self.bitness = 32
elif ei_class == 2:
self.bitness = 64
else:
raise CorruptElfFile("invalid ei_class: 0x%02x" % ei_class)
if ei_data == 1:
self.endian = "<"
elif ei_data == 2:
self.endian = ">"
else:
raise CorruptElfFile("not an ELF file: invalid ei_data: 0x%02x" % ei_data)
if self.bitness == 32:
e_phoff, e_shoff = struct.unpack_from(self.endian + "II", self.file_header, 0x1C)
self.e_phentsize, self.e_phnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x2A)
self.e_shentsize, self.e_shnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x2E)
elif self.bitness == 64:
e_phoff, e_shoff = struct.unpack_from(self.endian + "QQ", self.file_header, 0x20)
self.e_phentsize, self.e_phnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x36)
self.e_shentsize, self.e_shnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x3A)
else:
raise NotImplementedError()
logger.debug("e_phoff: 0x%02x e_phentsize: 0x%02x e_phnum: %d", e_phoff, self.e_phentsize, self.e_phnum)
self.f.seek(e_phoff)
program_header_size = self.e_phnum * self.e_phentsize
self.phbuf = self.f.read(program_header_size)
if len(self.phbuf) != program_header_size:
logger.warning("failed to read program headers")
self.e_phnum = 0
self.f.seek(e_shoff)
section_header_size = self.e_shnum * self.e_shentsize
self.shbuf = self.f.read(section_header_size)
if len(self.shbuf) != section_header_size:
logger.warning("failed to read section headers")
self.e_shnum = 0
(ei_osabi,) = struct.unpack_from(endian + "B", file_header, 7)
OSABI = {
# via pyelftools: https://github.com/eliben/pyelftools/blob/0664de05ed2db3d39041e2d51d19622a8ef4fb0f/elftools/elf/enums.py#L35-L58
# some candidates are commented out because the are not useful values,
@@ -133,218 +184,598 @@ def detect_elf_os(f) -> str:
# 97: "ARM", # not an OS
# 255: "STANDALONE", # not an OS
}
logger.debug("ei_osabi: 0x%02x (%s)", ei_osabi, OSABI.get(ei_osabi, "unknown"))
# os_osabi == 0 is commonly set even when the OS is not SYSV.
# other values are unused or unknown.
if ei_osabi in OSABI and ei_osabi != 0x0:
# subsequent strategies may overwrite this value
ret = OSABI[ei_osabi]
@property
def ei_osabi(self) -> Optional[OS]:
(ei_osabi,) = struct.unpack_from(self.endian + "B", self.file_header, 7)
return ELF.OSABI.get(ei_osabi)
f.seek(e_phoff)
program_header_size = e_phnum * e_phentsize
program_headers = f.read(program_header_size)
if len(program_headers) != program_header_size:
logger.warning("failed to read program headers")
e_phnum = 0
MACHINE = {
# via https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
1: "M32",
2: "SPARC",
3: "i386",
4: "68K",
5: "88K",
6: "486",
7: "860",
8: "MIPS",
9: "S370",
10: "MIPS_RS3_LE",
11: "RS6000",
15: "PA_RISC",
16: "nCUBE",
17: "VPP500",
18: "SPARC32PLUS",
19: "960",
20: "PPC",
21: "PPC64",
22: "S390",
23: "SPU",
36: "V800",
37: "FR20",
38: "RH32",
39: "RCE",
40: "ARM",
41: "ALPHA",
42: "SH",
43: "SPARCV9",
44: "TRICORE",
45: "ARC",
46: "H8_300",
47: "H8_300H",
48: "H8S",
49: "H8_500",
50: "IA_64",
51: "MIPS_X",
52: "COLDFIRE",
53: "68HC12",
54: "MMA",
55: "PCP",
56: "NCPU",
57: "NDR1",
58: "STARCORE",
59: "ME16",
60: "ST100",
61: "TINYJ",
62: "amd64",
63: "PDSP",
64: "PDP10",
65: "PDP11",
66: "FX66",
67: "ST9PLUS",
68: "ST7",
69: "68HC16",
70: "68HC11",
71: "68HC08",
72: "68HC05",
73: "SVX",
74: "ST19",
75: "VAX",
76: "CRIS",
77: "JAVELIN",
78: "FIREPATH",
79: "ZSP",
80: "MMIX",
81: "HUANY",
82: "PRISM",
83: "AVR",
84: "FR30",
85: "D10V",
86: "D30V",
87: "V850",
88: "M32R",
89: "MN10300",
90: "MN10200",
91: "PJ",
92: "OPENRISC",
93: "ARC_A5",
94: "XTENSA",
95: "VIDEOCORE",
96: "TMM_GPP",
97: "NS32K",
98: "TPC",
99: "SNP1K",
100: "ST200",
}
# search for PT_NOTE sections that specify an OS
# for example, on Linux there is a GNU section with minimum kernel version
for i in range(e_phnum):
offset = i * e_phentsize
phent = program_headers[offset : offset + e_phentsize]
@property
def e_machine(self) -> Optional[str]:
(e_machine,) = struct.unpack_from(self.endian + "H", self.file_header, 0x12)
return ELF.MACHINE.get(e_machine)
PT_NOTE = 0x4
def parse_program_header(self, i) -> Phdr:
phent_offset = i * self.e_phentsize
phent = self.phbuf[phent_offset : phent_offset + self.e_phentsize]
(p_type,) = struct.unpack_from(endian + "I", phent, 0x0)
(p_type,) = struct.unpack_from(self.endian + "I", phent, 0x0)
logger.debug("ph:p_type: 0x%04x", p_type)
if p_type != PT_NOTE:
continue
if bitness == 32:
p_offset, _, _, p_filesz = struct.unpack_from(endian + "IIII", phent, 0x4)
elif bitness == 64:
p_offset, _, _, p_filesz = struct.unpack_from(endian + "QQQQ", phent, 0x8)
if self.bitness == 32:
p_offset, p_vaddr, p_paddr, p_filesz = struct.unpack_from(self.endian + "IIII", phent, 0x4)
elif self.bitness == 64:
p_offset, p_vaddr, p_paddr, p_filesz = struct.unpack_from(self.endian + "QQQQ", phent, 0x8)
else:
raise NotImplementedError()
logger.debug("ph:p_offset: 0x%02x p_filesz: 0x%04x", p_offset, p_filesz)
f.seek(p_offset)
note = f.read(p_filesz)
if len(note) != p_filesz:
logger.warning("failed to read note content")
continue
self.f.seek(p_offset)
buf = self.f.read(p_filesz)
if len(buf) != p_filesz:
raise ValueError("failed to read program header content")
namesz, descsz, type_ = struct.unpack_from(endian + "III", note, 0x0)
name_offset = 0xC
desc_offset = name_offset + align(namesz, 0x4)
return Phdr(p_type, p_offset, p_vaddr, p_paddr, p_filesz, buf)
logger.debug("ph:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, descsz, type_)
name = note[name_offset : name_offset + namesz].partition(b"\x00")[0].decode("ascii")
logger.debug("name: %s", name)
if type_ != 1:
continue
if name == "GNU":
if descsz < 16:
@property
def program_headers(self):
for i in range(self.e_phnum):
try:
yield self.parse_program_header(i)
except ValueError:
continue
desc = note[desc_offset : desc_offset + descsz]
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(endian + "IIII", desc, 0x0)
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
def parse_section_header(self, i) -> Shdr:
shent_offset = i * self.e_shentsize
shent = self.shbuf[shent_offset : shent_offset + self.e_shentsize]
if abi_tag in GNU_ABI_TAG:
# update only if not set
# so we can get the debugging output of subsequent strategies
ret = GNU_ABI_TAG[abi_tag] if not ret else ret
logger.debug("abi tag: %s earliest compatible kernel: %d.%d.%d", ret, kmajor, kminor, kpatch)
elif name == "OpenBSD":
logger.debug("note owner: %s", "OPENBSD")
ret = OS.OPENBSD if not ret else ret
elif name == "NetBSD":
logger.debug("note owner: %s", "NETBSD")
ret = OS.NETBSD if not ret else ret
elif name == "FreeBSD":
logger.debug("note owner: %s", "FREEBSD")
ret = OS.FREEBSD if not ret else ret
# search for recognizable dynamic linkers (interpreters)
# for example, on linux, we see file paths like: /lib64/ld-linux-x86-64.so.2
for i in range(e_phnum):
offset = i * e_phentsize
phent = program_headers[offset : offset + e_phentsize]
PT_INTERP = 0x3
(p_type,) = struct.unpack_from(endian + "I", phent, 0x0)
if p_type != PT_INTERP:
continue
if bitness == 32:
p_offset, _, _, p_filesz = struct.unpack_from(endian + "IIII", phent, 0x4)
elif bitness == 64:
p_offset, _, _, p_filesz = struct.unpack_from(endian + "QQQQ", phent, 0x8)
if self.bitness == 32:
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link = struct.unpack_from(
self.endian + "IIIIIII", shent, 0x0
)
elif self.bitness == 64:
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link = struct.unpack_from(
self.endian + "IIQQQQI", shent, 0x0
)
else:
raise NotImplementedError()
f.seek(p_offset)
interp = f.read(p_filesz)
if len(interp) != p_filesz:
logger.warning("failed to read interp content")
continue
linker = interp.partition(b"\x00")[0].decode("ascii")
logger.debug("linker: %s", linker)
if "ld-linux" in linker:
# update only if not set
# so we can get the debugging output of subsequent strategies
ret = OS.LINUX if ret is None else ret
f.seek(e_shoff)
section_header_size = e_shnum * e_shentsize
section_headers = f.read(section_header_size)
if len(section_headers) != section_header_size:
logger.warning("failed to read section headers")
e_shnum = 0
# search for notes stored in sections that aren't visible in program headers.
# e.g. .note.Linux in Linux kernel modules.
for i in range(e_shnum):
offset = i * e_shentsize
shent = section_headers[offset : offset + e_shentsize]
if bitness == 32:
sh_name, sh_type, _, sh_addr, sh_offset, sh_size = struct.unpack_from(endian + "IIIIII", shent, 0x0)
elif bitness == 64:
sh_name, sh_type, _, sh_addr, sh_offset, sh_size = struct.unpack_from(endian + "IIQQQQ", shent, 0x0)
else:
raise NotImplementedError()
SHT_NOTE = 0x7
if sh_type != SHT_NOTE:
continue
logger.debug("sh:sh_offset: 0x%02x sh_size: 0x%04x", sh_offset, sh_size)
f.seek(sh_offset)
note = f.read(sh_size)
if len(note) != sh_size:
logger.warning("failed to read note content")
continue
self.f.seek(sh_offset)
buf = self.f.read(sh_size)
if len(buf) != sh_size:
raise ValueError("failed to read section header content")
namesz, descsz, type_ = struct.unpack_from(endian + "III", note, 0x0)
name_offset = 0xC
desc_offset = name_offset + align(namesz, 0x4)
return Shdr(sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, buf)
logger.debug("sh:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, descsz, type_)
name = note[name_offset : name_offset + namesz].partition(b"\x00")[0].decode("ascii")
logger.debug("name: %s", name)
if name == "Linux":
logger.debug("note owner: %s", "LINUX")
ret = OS.LINUX if not ret else ret
elif name == "OpenBSD":
logger.debug("note owner: %s", "OPENBSD")
ret = OS.OPENBSD if not ret else ret
elif name == "NetBSD":
logger.debug("note owner: %s", "NETBSD")
ret = OS.NETBSD if not ret else ret
elif name == "FreeBSD":
logger.debug("note owner: %s", "FREEBSD")
ret = OS.FREEBSD if not ret else ret
elif name == "GNU":
if descsz < 16:
@property
def section_headers(self):
for i in range(self.e_shnum):
try:
yield self.parse_section_header(i)
except ValueError:
continue
desc = note[desc_offset : desc_offset + descsz]
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(endian + "IIII", desc, 0x0)
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
@property
def linker(self):
PT_INTERP = 0x3
for phdr in self.program_headers:
if phdr.type != PT_INTERP:
continue
if abi_tag in GNU_ABI_TAG:
# update only if not set
# so we can get the debugging output of subsequent strategies
ret = GNU_ABI_TAG[abi_tag] if not ret else ret
logger.debug("abi tag: %s earliest compatible kernel: %d.%d.%d", ret, kmajor, kminor, kpatch)
return read_cstr(phdr.buf, 0)
@property
def versions_needed(self) -> Dict[str, Set[str]]:
# symbol version requirements are stored in the .gnu.version_r section,
# which has type SHT_GNU_verneed (0x6ffffffe).
#
# this contains a linked list of ElfXX_Verneed structs,
# each referencing a linked list of ElfXX_Vernaux structs.
# strings are stored in the section referenced by the sh_link field of the section header.
# each Verneed struct contains a reference to the name of the library,
# each Vernaux struct contains a reference to the name of a symbol.
SHT_GNU_VERNEED = 0x6FFFFFFE
for shdr in self.section_headers:
if shdr.type != SHT_GNU_VERNEED:
continue
# the linked section contains strings referenced by the verneed structures.
linked_shdr = self.parse_section_header(shdr.link)
versions_needed = collections.defaultdict(set)
# read verneed structures from the start of the section
# until the vn_next link is 0x0.
# each entry describes a shared object that is required by this binary.
vn_offset = 0x0
while True:
# ElfXX_Verneed layout is the same on 32 and 64 bit
vn_version, vn_cnt, vn_file, vn_aux, vn_next = struct.unpack_from(
self.endian + "HHIII", shdr.buf, vn_offset
)
if vn_version != 1:
# unexpected format, don't try to keep parsing
break
# shared object names, like: "libdl.so.2"
so_name = read_cstr(linked_shdr.buf, vn_file)
# read vernaux structures linked from the verneed structure.
# there should be vn_cnt of these.
# each entry describes an ABI name required by the shared object.
vna_offset = vn_offset + vn_aux
for i in range(vn_cnt):
# ElfXX_Vernaux layout is the same on 32 and 64 bit
_, _, _, vna_name, vna_next = struct.unpack_from(self.endian + "IHHII", shdr.buf, vna_offset)
# ABI names, like: "GLIBC_2.2.5"
abi = read_cstr(linked_shdr.buf, vna_name)
versions_needed[so_name].add(abi)
vna_offset += vna_next
vn_offset += vn_next
if vn_next == 0:
break
return dict(versions_needed)
return {}
@property
def dynamic_entries(self) -> Iterator[Tuple[int, int]]:
"""
read the entries from the dynamic section,
yielding the tag and value for each entry.
"""
DT_NULL = 0x0
PT_DYNAMIC = 0x2
for phdr in self.program_headers:
if phdr.type != PT_DYNAMIC:
continue
offset = 0x0
while True:
if self.bitness == 32:
d_tag, d_val = struct.unpack_from(self.endian + "II", phdr.buf, offset)
offset += 8
elif self.bitness == 64:
d_tag, d_val = struct.unpack_from(self.endian + "QQ", phdr.buf, offset)
offset += 16
else:
raise NotImplementedError()
if d_tag == DT_NULL:
break
yield d_tag, d_val
@property
def strtab(self) -> Optional[bytes]:
"""
fetch the bytes of the string table
referenced by the dynamic section.
"""
DT_STRTAB = 0x5
DT_STRSZ = 0xA
strtab_addr = None
strtab_size = None
for d_tag, d_val in self.dynamic_entries:
if d_tag == DT_STRTAB:
strtab_addr = d_val
for d_tag, d_val in self.dynamic_entries:
if d_tag == DT_STRSZ:
strtab_size = d_val
if strtab_addr is None:
return None
if strtab_size is None:
return None
strtab_offset = None
for shdr in self.section_headers:
if shdr.addr <= strtab_addr < shdr.addr + shdr.size:
strtab_offset = shdr.offset + (strtab_addr - shdr.addr)
if strtab_offset is None:
return None
self.f.seek(strtab_offset)
strtab_buf = self.f.read(strtab_size)
if len(strtab_buf) != strtab_size:
return None
return strtab_buf
@property
def needed(self) -> Iterator[str]:
"""
read the names of DT_NEEDED entries from the dynamic section,
which correspond to dependencies on other shared objects,
like: `libpthread.so.0`
"""
DT_NEEDED = 0x1
strtab = self.strtab
if not strtab:
return
for d_tag, d_val in self.dynamic_entries:
if d_tag != DT_NEEDED:
continue
yield read_cstr(strtab, d_val)
@dataclass
class ABITag:
os: OS
kmajor: int
kminor: int
kpatch: int
class PHNote:
def __init__(self, endian: str, buf: bytes):
self.endian = endian
self.buf = buf
# these will be initialized in `_parse()`
self.type_: int
self.descsz: int
self.name: str
self._parse()
def _parse(self):
namesz, self.descsz, self.type_ = struct.unpack_from(self.endian + "III", self.buf, 0x0)
name_offset = 0xC
self.desc_offset = name_offset + align(namesz, 0x4)
logger.debug("ph:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, self.descsz, self.type_)
self.name = self.buf[name_offset : name_offset + namesz].partition(b"\x00")[0].decode("ascii")
logger.debug("name: %s", self.name)
@property
def abi_tag(self) -> Optional[ABITag]:
if self.type_ != 1:
# > The type field shall be 1.
# Linux Standard Base Specification 1.2
# ref: https://refspecs.linuxfoundation.org/LSB_1.2.0/gLSB/noteabitag.html
return None
if self.name != "GNU":
return None
if self.descsz < 16:
return None
desc = self.buf[self.desc_offset : self.desc_offset + self.descsz]
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(self.endian + "IIII", desc, 0x0)
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
os = GNU_ABI_TAG.get(abi_tag)
if not os:
return None
logger.debug("abi tag: %s earliest compatible kernel: %d.%d.%d", os, kmajor, kminor, kpatch)
return ABITag(os, kmajor, kminor, kpatch)
class SHNote:
def __init__(self, endian: str, buf: bytes):
self.endian = endian
self.buf = buf
# these will be initialized in `_parse()`
self.type_: int
self.descsz: int
self.name: str
self._parse()
def _parse(self):
namesz, self.descsz, self.type_ = struct.unpack_from(self.endian + "III", self.buf, 0x0)
name_offset = 0xC
self.desc_offset = name_offset + align(namesz, 0x4)
logger.debug("sh:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, self.descsz, self.type_)
name_buf = self.buf[name_offset : name_offset + namesz]
self.name = read_cstr(name_buf, 0x0)
logger.debug("sh:name: %s", self.name)
@property
def abi_tag(self) -> Optional[ABITag]:
if self.name != "GNU":
return None
if self.descsz < 16:
return None
desc = self.buf[self.desc_offset : self.desc_offset + self.descsz]
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(self.endian + "IIII", desc, 0x0)
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
os = GNU_ABI_TAG.get(abi_tag)
if not os:
return None
logger.debug("abi tag: %s earliest compatible kernel: %d.%d.%d", os, kmajor, kminor, kpatch)
return ABITag(os, kmajor, kminor, kpatch)
def guess_os_from_osabi(elf) -> Optional[OS]:
return elf.ei_osabi
def guess_os_from_ph_notes(elf) -> Optional[OS]:
# search for PT_NOTE sections that specify an OS
# for example, on Linux there is a GNU section with minimum kernel version
PT_NOTE = 0x4
for phdr in elf.program_headers:
if phdr.type != PT_NOTE:
continue
note = PHNote(elf.endian, phdr.buf)
if note.type_ != 1:
# > The type field shall be 1.
# Linux Standard Base Specification 1.2
# ref: https://refspecs.linuxfoundation.org/LSB_1.2.0/gLSB/noteabitag.html
continue
if note.name == "Linux":
logger.debug("note owner: %s", "LINUX")
return OS.LINUX
elif note.name == "OpenBSD":
logger.debug("note owner: %s", "OPENBSD")
return OS.OPENBSD
elif note.name == "NetBSD":
logger.debug("note owner: %s", "NETBSD")
return OS.NETBSD
elif note.name == "FreeBSD":
logger.debug("note owner: %s", "FREEBSD")
return OS.FREEBSD
elif note.name == "GNU":
abi_tag = note.abi_tag
if abi_tag:
return abi_tag.os
else:
# cannot make a guess about the OS, but probably linux or hurd
pass
return None
def guess_os_from_sh_notes(elf) -> Optional[OS]:
# search for notes stored in sections that aren't visible in program headers.
# e.g. .note.Linux in Linux kernel modules.
SHT_NOTE = 0x7
for shdr in elf.section_headers:
if shdr.type != SHT_NOTE:
continue
note = SHNote(elf.endian, shdr.buf)
if note.name == "Linux":
logger.debug("note owner: %s", "LINUX")
return OS.LINUX
elif note.name == "OpenBSD":
logger.debug("note owner: %s", "OPENBSD")
return OS.OPENBSD
elif note.name == "NetBSD":
logger.debug("note owner: %s", "NETBSD")
return OS.NETBSD
elif note.name == "FreeBSD":
logger.debug("note owner: %s", "FREEBSD")
return OS.FREEBSD
elif note.name == "GNU":
abi_tag = note.abi_tag
if abi_tag:
return abi_tag.os
else:
# cannot make a guess about the OS, but probably linux or hurd
pass
return None
def guess_os_from_linker(elf) -> Optional[OS]:
# search for recognizable dynamic linkers (interpreters)
# for example, on linux, we see file paths like: /lib64/ld-linux-x86-64.so.2
linker = elf.linker
if linker and "ld-linux" in elf.linker:
return OS.LINUX
return None
def guess_os_from_abi_versions_needed(elf) -> Optional[OS]:
# then lets look for GLIBC symbol versioning requirements.
# this will let us guess about linux/hurd in some cases.
versions_needed = elf.versions_needed
if any(map(lambda abi: abi.startswith("GLIBC"), itertools.chain(*versions_needed.values()))):
# there are any GLIBC versions needed
if elf.e_machine != "i386":
# GLIBC runs on Linux and Hurd.
# for Hurd, its *only* on i386.
# so if we're not on i386, then we're on Linux.
return OS.LINUX
else:
# we're on i386, so we could be on either Linux or Hurd.
linker = elf.linker
if linker and "ld-linux" in linker:
return OS.LINUX
elif linker and "/ld.so" in linker:
return OS.HURD
else:
# we don't have any good guesses based on versions needed
pass
return None
def guess_os_from_needed_dependencies(elf) -> Optional[OS]:
for needed in elf.needed:
if needed.startswith("libmachuser.so"):
return OS.HURD
if needed.startswith("libhurduser.so"):
return OS.HURD
return None
def detect_elf_os(f) -> str:
"""
f: type Union[BinaryIO, IDAIO]
"""
elf = ELF(f)
osabi_guess = guess_os_from_osabi(elf)
logger.debug("guess: osabi: %s", osabi_guess)
ph_notes_guess = guess_os_from_ph_notes(elf)
logger.debug("guess: ph notes: %s", ph_notes_guess)
sh_notes_guess = guess_os_from_sh_notes(elf)
logger.debug("guess: sh notes: %s", sh_notes_guess)
linker_guess = guess_os_from_linker(elf)
logger.debug("guess: linker: %s", linker_guess)
abi_versions_needed_guess = guess_os_from_abi_versions_needed(elf)
logger.debug("guess: ABI versions needed: %s", abi_versions_needed_guess)
needed_dependencies_guess = guess_os_from_needed_dependencies(elf)
logger.debug("guess: needed dependencies: %s", needed_dependencies_guess)
ret = None
if osabi_guess:
ret = osabi_guess
elif ph_notes_guess:
ret = ph_notes_guess
elif sh_notes_guess:
ret = sh_notes_guess
elif linker_guess:
ret = linker_guess
elif abi_versions_needed_guess:
ret = abi_versions_needed_guess
elif needed_dependencies_guess:
ret = needed_dependencies_guess
return ret.value if ret is not None else "unknown"
class Arch(str, Enum):
I386 = "i386"
AMD64 = "amd64"
def detect_elf_arch(f: BinaryIO) -> str:
f.seek(0x0)
file_header = f.read(0x40)
if not file_header.startswith(b"\x7fELF"):
raise CorruptElfFile("missing magic header")
(ei_data,) = struct.unpack_from("B", file_header, 5)
logger.debug("ei_data: 0x%02x", ei_data)
if ei_data == 1:
endian = "<"
elif ei_data == 2:
endian = ">"
else:
raise CorruptElfFile("not an ELF file: invalid ei_data: 0x%02x" % ei_data)
(ei_machine,) = struct.unpack_from(endian + "H", file_header, 0x12)
logger.debug("ei_machine: 0x%02x", ei_machine)
EM_386 = 0x3
EM_X86_64 = 0x3E
if ei_machine == EM_386:
return Arch.I386
elif ei_machine == EM_X86_64:
return Arch.AMD64
else:
# not really unknown, but unsupport at the moment:
# https://github.com/eliben/pyelftools/blob/ab444d982d1849191e910299a985989857466620/elftools/elf/enums.py#L73
return "unknown"
return ELF(f).e_machine or "unknown"

View File

@@ -7,7 +7,6 @@
# See the License for the specific language governing permissions and limitations under the License.
import io
import logging
import contextlib
from typing import Tuple, Iterator
from elftools.elf.elffile import ELFFile, SymbolTableSection
@@ -16,7 +15,6 @@ import capa.features.extractors.common
from capa.features.file import Import, Section
from capa.features.common import OS, FORMAT_ELF, Arch, Format, Feature
from capa.features.address import NO_ADDRESS, FileOffsetAddress, AbsoluteVirtualAddress
from capa.features.extractors.elf import Arch as ElfArch
from capa.features.extractors.base_extractor import FeatureExtractor
logger = logging.getLogger(__name__)
@@ -26,17 +24,17 @@ def extract_file_import_names(elf, **kwargs):
# see https://github.com/eliben/pyelftools/blob/0664de05ed2db3d39041e2d51d19622a8ef4fb0f/scripts/readelf.py#L372
symbol_tables = [(idx, s) for idx, s in enumerate(elf.iter_sections()) if isinstance(s, SymbolTableSection)]
for section_index, section in symbol_tables:
for _, section in symbol_tables:
if not isinstance(section, SymbolTableSection):
continue
if section["sh_entsize"] == 0:
logger.debug("Symbol table '%s' has a sh_entsize of zero!" % (section.name))
logger.debug("Symbol table '%s' has a sh_entsize of zero!", section.name)
continue
logger.debug("Symbol table '%s' contains %s entries:" % (section.name, section.num_symbols()))
logger.debug("Symbol table '%s' contains %s entries:", section.name, section.num_symbols())
for nsym, symbol in enumerate(section.iter_symbols()):
for _, symbol in enumerate(section.iter_symbols()):
if symbol.name and symbol.entry.st_info.type == "STT_FUNC":
# TODO symbol address
# TODO symbol version info?
@@ -73,9 +71,9 @@ def extract_file_arch(elf, **kwargs):
# TODO merge with capa.features.extractors.elf.detect_elf_arch()
arch = elf.get_machine_arch()
if arch == "x86":
yield Arch(ElfArch.I386), NO_ADDRESS
yield Arch("i386"), NO_ADDRESS
elif arch == "x64":
yield Arch(ElfArch.AMD64), NO_ADDRESS
yield Arch("amd64"), NO_ADDRESS
else:
logger.warning("unsupported architecture: %s", arch)
@@ -110,7 +108,7 @@ GLOBAL_HANDLERS = (
class ElfFeatureExtractor(FeatureExtractor):
def __init__(self, path: str):
super(ElfFeatureExtractor, self).__init__()
super().__init__()
self.path = path
with open(self.path, "rb") as f:
self.elf = ELFFile(io.BytesIO(f.read()))
@@ -153,8 +151,8 @@ class ElfFeatureExtractor(FeatureExtractor):
def extract_insn_features(self, f, bb, insn):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
def is_library_function(self, va):
def is_library_function(self, addr):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
def get_function_name(self, va):
def get_function_name(self, addr):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")

View File

@@ -112,7 +112,6 @@ def carve_pe(pbytes: bytes, offset: int = 0) -> Iterator[Tuple[int, int]]:
todo = [(off, mzx, pex, key) for (off, mzx, pex, key) in todo if off != -1]
while len(todo):
off, mzx, pex, key = todo.pop()
# The MZ header has one field we will check

View File

@@ -95,7 +95,7 @@ def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[F
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract basic block features"""
for bb_handler in BASIC_BLOCK_HANDLERS:
for (feature, addr) in bb_handler(fh, bbh):
for feature, addr in bb_handler(fh, bbh):
yield feature, addr
yield BasicBlock(), bbh.address

View File

@@ -23,8 +23,9 @@ from capa.features.extractors.base_extractor import BBHandle, InsnHandle, Functi
class IdaFeatureExtractor(FeatureExtractor):
def __init__(self):
super(IdaFeatureExtractor, self).__init__()
super().__init__()
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.ida.file.extract_file_format())
self.global_features.extend(capa.features.extractors.ida.global_.extract_os())
self.global_features.extend(capa.features.extractors.ida.global_.extract_arch())

View File

@@ -39,7 +39,7 @@ def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]:
]
todo = []
for (mzx, pex, i) in mz_xor:
for mzx, pex, i in mz_xor:
for off in capa.features.extractors.ida.helpers.find_byte_sequence(seg.start_ea, seg.end_ea, mzx):
todo.append((off, mzx, pex, i))
@@ -73,13 +73,13 @@ def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]:
- Check 'Load resource sections' when opening binary in IDA manually
"""
for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True):
for (ea, _) in check_segment_for_pe(seg):
for ea, _ in check_segment_for_pe(seg):
yield Characteristic("embedded pe"), FileOffsetAddress(ea)
def extract_file_export_names() -> Iterator[Tuple[Feature, Address]]:
"""extract function exports"""
for (_, _, ea, name) in idautils.Entries():
for _, _, ea, name in idautils.Entries():
yield Export(name), AbsoluteVirtualAddress(ea)
@@ -94,7 +94,7 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
- modulename.importname
- importname
"""
for (ea, info) in capa.features.extractors.ida.helpers.get_file_imports().items():
for ea, info in capa.features.extractors.ida.helpers.get_file_imports().items():
addr = AbsoluteVirtualAddress(ea)
if info[1] and info[2]:
# e.g. in mimikatz: ('cabinet', 'FCIAddFile', 11L)
@@ -115,6 +115,9 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield Import(name), addr
for ea, info in capa.features.extractors.ida.helpers.get_file_externs().items():
yield Import(info[1]), AbsoluteVirtualAddress(ea)
def extract_file_section_names() -> Iterator[Tuple[Feature, Address]]:
"""extract section names
@@ -165,7 +168,7 @@ def extract_file_function_names() -> Iterator[Tuple[Feature, Address]]:
def extract_file_format() -> Iterator[Tuple[Feature, Address]]:
file_info = idaapi.get_inf_structure()
if file_info.filetype == idaapi.f_PE:
if file_info.filetype in (idaapi.f_PE, idaapi.f_COFF):
yield Format(FORMAT_PE), NO_ADDRESS
elif file_info.filetype == idaapi.f_ELF:
yield Format(FORMAT_ELF), NO_ADDRESS
@@ -173,7 +176,7 @@ def extract_file_format() -> Iterator[Tuple[Feature, Address]]:
# no file type to return when processing a binary file, but we want to continue processing
return
else:
raise NotImplementedError("file format: %d" % file_info.filetype)
raise NotImplementedError("unexpected file format: %d" % file_info.filetype)
def extract_features() -> Iterator[Tuple[Feature, Address]]:

View File

@@ -45,7 +45,7 @@ def extract_recursive_call(fh: FunctionHandle):
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
for func_handler in FUNCTION_HANDLERS:
for (feature, addr) in func_handler(fh):
for feature, addr in func_handler(fh):
yield feature, addr

View File

@@ -31,7 +31,7 @@ def extract_os() -> Iterator[Tuple[Feature, Address]]:
# 2. handling a new file format (e.g. macho)
#
# for (1) we can't do much - its shellcode and all bets are off.
# we could maybe accept a futher CLI argument to specify the OS,
# we could maybe accept a further CLI argument to specify the OS,
# but i think this would be rarely used.
# rules that rely on OS conditions will fail to match on shellcode.
#

View File

@@ -5,12 +5,13 @@
# 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
from typing import Any, Dict, Tuple, Iterator, Optional
import idc
import idaapi
import idautils
import ida_bytes
import ida_segment
from capa.features.address import AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import FunctionHandle
@@ -35,7 +36,7 @@ def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
def get_functions(
start: int = None, end: int = None, skip_thunks: bool = False, skip_libs: bool = False
start: Optional[int] = None, end: Optional[int] = None, skip_thunks: bool = False, skip_libs: bool = False
) -> Iterator[FunctionHandle]:
"""get functions, range optional
@@ -109,6 +110,19 @@ def get_file_imports() -> Dict[int, Tuple[str, str, int]]:
return imports
def get_file_externs() -> Dict[int, Tuple[str, str, int]]:
externs = {}
for seg in get_segments(skip_header_segments=True):
if not (seg.type == ida_segment.SEG_XTRN):
continue
for ea in idautils.Functions(seg.start_ea, seg.end_ea):
externs[ea] = ("", idaapi.get_func_name(ea), -1)
return externs
def get_instructions_in_range(start: int, end: int) -> Iterator[idaapi.insn_t]:
"""yield instructions in range
@@ -183,7 +197,7 @@ def read_bytes_at(ea: int, count: int) -> bytes:
def find_string_at(ea: int, min_: int = 4) -> str:
"""check if ASCII string exists at a given virtual address"""
found = idaapi.get_strlit_contents(ea, -1, idaapi.STRTYPE_C)
if found and len(found) > min_:
if found and len(found) >= min_:
try:
found = found.decode("ascii")
# hacky check for IDA bug; get_strlit_contents also reads Unicode as
@@ -207,7 +221,8 @@ def get_op_phrase_info(op: idaapi.op_t) -> Dict:
return {}
scale = 1 << ((op.specflag2 & 0xC0) >> 6)
offset = op.addr
# IDA ea_t may be 32- or 64-bit; we assume displacement can only be 32-bit
offset = op.addr & 0xFFFFFFFF
if op.specflag1 == 0:
index = None
@@ -273,7 +288,7 @@ def is_frame_register(reg: int) -> bool:
return reg in (idautils.procregs.sp.reg, idautils.procregs.bp.reg)
def get_insn_ops(insn: idaapi.insn_t, target_ops: Tuple[Any] = None) -> idaapi.op_t:
def get_insn_ops(insn: idaapi.insn_t, target_ops: Optional[Tuple[Any]] = None) -> idaapi.op_t:
"""yield op_t for instruction, filter on type if specified"""
for op in insn.ops:
if op.type == idaapi.o_void:

View File

@@ -23,13 +23,19 @@ from capa.features.extractors.base_extractor import BBHandle, InsnHandle, Functi
SECURITY_COOKIE_BYTES_DELTA = 0x40
def get_imports(ctx: Dict[str, Any]) -> Dict[str, Any]:
def get_imports(ctx: Dict[str, Any]) -> Dict[int, Any]:
if "imports_cache" not in ctx:
ctx["imports_cache"] = capa.features.extractors.ida.helpers.get_file_imports()
return ctx["imports_cache"]
def check_for_api_call(ctx: Dict[str, Any], insn: idaapi.insn_t) -> Iterator[str]:
def get_externs(ctx: Dict[str, Any]) -> Dict[int, Any]:
if "externs_cache" not in ctx:
ctx["externs_cache"] = capa.features.extractors.ida.helpers.get_file_externs()
return ctx["externs_cache"]
def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[Any]:
"""check instruction for API call"""
info = ()
ref = insn.ea
@@ -46,7 +52,7 @@ def check_for_api_call(ctx: Dict[str, Any], insn: idaapi.insn_t) -> Iterator[str
except IndexError:
break
info = get_imports(ctx).get(ref, ())
info = funcs.get(ref, ())
if info:
break
@@ -55,7 +61,7 @@ def check_for_api_call(ctx: Dict[str, Any], insn: idaapi.insn_t) -> Iterator[str
break
if info:
yield "%s.%s" % (info[0], info[1])
yield info
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
@@ -70,11 +76,17 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
if not insn.get_canon_mnem() in ("call", "jmp"):
return
for api in check_for_api_call(fh.ctx, insn):
dll, _, symbol = api.rpartition(".")
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
# check calls to imported functions
for api in check_for_api_call(insn, get_imports(fh.ctx)):
# tuple (<module>, <function>, <ordinal>)
for name in capa.features.extractors.helpers.generate_symbols(api[0], api[1]):
yield API(name), ih.address
# check calls to extern functions
for api in check_for_api_call(insn, get_externs(fh.ctx)):
# tuple (<module>, <function>, <ordinal>)
yield API(api[1]), ih.address
# extract IDA/FLIRT recognized API functions
targets = tuple(idautils.CodeRefsFrom(insn.ea, False))
if not targets:
@@ -160,7 +172,9 @@ def extract_insn_bytes_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandl
if ref != insn.ea:
extracted_bytes = capa.features.extractors.ida.helpers.read_bytes_at(ref, MAX_BYTES_FEATURE_SIZE)
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
yield Bytes(extracted_bytes), ih.address
if not capa.features.extractors.ida.helpers.find_string_at(insn.ea):
# don't extract byte features for obvious strings
yield Bytes(extracted_bytes), ih.address
def extract_insn_string_features(
@@ -201,7 +215,11 @@ def extract_insn_offset_features(
continue
p_info = capa.features.extractors.ida.helpers.get_op_phrase_info(op)
op_off = p_info.get("offset", 0)
op_off = p_info.get("offset", None)
if op_off is None:
continue
if idaapi.is_mapped(op_off):
# Ignore:
# mov esi, dword_1005B148[esi]
@@ -464,7 +482,7 @@ def extract_function_indirect_call_characteristic_features(
def extract_features(f: FunctionHandle, bbh: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract instruction features"""
for inst_handler in INSTRUCTION_HANDLERS:
for (feature, ea) in inst_handler(f, bbh, insn):
for feature, ea in inst_handler(f, bbh, insn):
yield feature, ea

View File

@@ -52,26 +52,21 @@ class NullFeatureExtractor(FeatureExtractor):
yield FunctionHandle(address, None)
def extract_function_features(self, f):
for address, feature in self.functions.get(f.address, {}).features:
for address, feature in self.functions[f.address].features:
yield feature, address
def get_basic_blocks(self, f):
for address in sorted(self.functions.get(f.address, {}).basic_blocks.keys()):
for address in sorted(self.functions[f.address].basic_blocks.keys()):
yield BBHandle(address, None)
def extract_basic_block_features(self, f, bb):
for address, feature in self.functions.get(f.address, {}).basic_blocks.get(bb.address, {}).features:
for address, feature in self.functions[f.address].basic_blocks[bb.address].features:
yield feature, address
def get_instructions(self, f, bb):
for address in sorted(self.functions.get(f.address, {}).basic_blocks.get(bb.address, {}).instructions.keys()):
for address in sorted(self.functions[f.address].basic_blocks[bb.address].instructions.keys()):
yield InsnHandle(address, None)
def extract_insn_features(self, f, bb, insn):
for address, feature in (
self.functions.get(f.address, {})
.basic_blocks.get(bb.address, {})
.instructions.get(insn.address, {})
.features
):
for address, feature in self.functions[f.address].basic_blocks[bb.address].instructions[insn.address].features:
yield feature, address

View File

@@ -133,7 +133,8 @@ def extract_file_features(pe, buf):
"""
for file_handler in FILE_HANDLERS:
for feature, va in file_handler(pe=pe, buf=buf):
# file_handler: type: (pe, bytes) -> Iterable[Tuple[Feature, Address]]
for feature, va in file_handler(pe=pe, buf=buf): # type: ignore
yield feature, va
@@ -160,7 +161,8 @@ def extract_global_features(pe, buf):
Tuple[Feature, VA]: a feature and its location.
"""
for handler in GLOBAL_HANDLERS:
for feature, va in handler(pe=pe, buf=buf):
# file_handler: type: (pe, bytes) -> Iterable[Tuple[Feature, Address]]
for feature, va in handler(pe=pe, buf=buf): # type: ignore
yield feature, va
@@ -172,7 +174,7 @@ GLOBAL_HANDLERS = (
class PefileFeatureExtractor(FeatureExtractor):
def __init__(self, path: str):
super(PefileFeatureExtractor, self).__init__()
super().__init__()
self.path = path
self.pe = pefile.PE(path)

View File

@@ -1,133 +0,0 @@
import string
import struct
from typing import Tuple, Iterator
from capa.features.common import Feature, Characteristic
from capa.features.address import Address
from capa.features.basicblock import BasicBlock
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
def _bb_has_tight_loop(f, bb):
"""
parse tight loops, true if last instruction in basic block branches to bb start
"""
return bb.offset in f.blockrefs[bb.offset] if bb.offset in f.blockrefs else False
def extract_bb_tight_loop(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""check basic block for tight loop indicators"""
if _bb_has_tight_loop(f.inner, bb.inner):
yield Characteristic("tight loop"), bb.address
def _bb_has_stackstring(f, bb):
"""
extract potential stackstring creation, using the following heuristics:
- basic block contains enough moves of constant bytes to the stack
"""
count = 0
for instr in bb.getInstructions():
if is_mov_imm_to_stack(instr):
count += get_printable_len(instr.getDetailed())
if count > MIN_STACKSTRING_LEN:
return True
return False
def get_operands(smda_ins):
return [o.strip() for o in smda_ins.operands.split(",")]
def extract_stackstring(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""check basic block for stackstring indicators"""
if _bb_has_stackstring(f.inner, bb.inner):
yield Characteristic("stack string"), bb.address
def is_mov_imm_to_stack(smda_ins):
"""
Return if instruction moves immediate onto stack
"""
if not smda_ins.mnemonic.startswith("mov"):
return False
try:
dst, src = get_operands(smda_ins)
except ValueError:
# not two operands
return False
try:
int(src, 16)
except ValueError:
return False
if not any(regname in dst for regname in ["ebp", "rbp", "esp", "rsp"]):
return False
return True
def is_printable_ascii(chars):
return all(c < 127 and chr(c) in string.printable for c in chars)
def is_printable_utf16le(chars):
if all(c == 0x00 for c in chars[1::2]):
return is_printable_ascii(chars[::2])
def get_printable_len(instr):
"""
Return string length if all operand bytes are ascii or utf16-le printable
Works on a capstone instruction
"""
# should have exactly two operands for mov immediate
if len(instr.operands) != 2:
return 0
op_value = instr.operands[1].value.imm
if instr.imm_size == 1:
chars = struct.pack("<B", op_value & 0xFF)
elif instr.imm_size == 2:
chars = struct.pack("<H", op_value & 0xFFFF)
elif instr.imm_size == 4:
chars = struct.pack("<I", op_value & 0xFFFFFFFF)
elif instr.imm_size == 8:
chars = struct.pack("<Q", op_value & 0xFFFFFFFFFFFFFFFF)
else:
raise ValueError("Unhandled operand data type 0x%x." % instr.imm_size)
if is_printable_ascii(chars):
return instr.imm_size
if is_printable_utf16le(chars):
return instr.imm_size // 2
return 0
def extract_features(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract features from the given basic block.
args:
f: the function from which to extract features
bb: the basic block to process.
yields:
Tuple[Feature, Address]: the features and their location found in this basic block.
"""
yield BasicBlock(), bb.address
for bb_handler in BASIC_BLOCK_HANDLERS:
for feature, addr in bb_handler(f, bb):
yield feature, addr
BASIC_BLOCK_HANDLERS = (
extract_bb_tight_loop,
extract_stackstring,
)

View File

@@ -1,57 +0,0 @@
from typing import List, Tuple
from smda.common.SmdaReport import SmdaReport
import capa.features.extractors.common
import capa.features.extractors.smda.file
import capa.features.extractors.smda.insn
import capa.features.extractors.smda.global_
import capa.features.extractors.smda.function
import capa.features.extractors.smda.basicblock
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
class SmdaFeatureExtractor(FeatureExtractor):
def __init__(self, smda_report: SmdaReport, path):
super(SmdaFeatureExtractor, self).__init__()
self.smda_report = smda_report
self.path = path
with open(self.path, "rb") as f:
self.buf = f.read()
# pre-compute these because we'll yield them at *every* scope.
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.common.extract_os(self.buf))
self.global_features.extend(capa.features.extractors.smda.global_.extract_arch(self.smda_report))
def get_base_address(self):
return AbsoluteVirtualAddress(self.smda_report.base_addr)
def extract_global_features(self):
yield from self.global_features
def extract_file_features(self):
yield from capa.features.extractors.smda.file.extract_features(self.smda_report, self.buf)
def get_functions(self):
for function in self.smda_report.getFunctions():
yield FunctionHandle(address=AbsoluteVirtualAddress(function.offset), inner=function)
def extract_function_features(self, fh):
yield from capa.features.extractors.smda.function.extract_features(fh)
def get_basic_blocks(self, fh):
for bb in fh.inner.getBlocks():
yield BBHandle(address=AbsoluteVirtualAddress(bb.offset), inner=bb)
def extract_basic_block_features(self, fh, bbh):
yield from capa.features.extractors.smda.basicblock.extract_features(fh, bbh)
def get_instructions(self, fh, bbh):
for smda_ins in bbh.inner.getInstructions():
yield InsnHandle(address=AbsoluteVirtualAddress(smda_ins.offset), inner=smda_ins)
def extract_insn_features(self, fh, bbh, ih):
yield from capa.features.extractors.smda.insn.extract_features(fh, bbh, ih)

View File

@@ -1,103 +0,0 @@
# if we have SMDA we definitely have lief
import lief
import capa.features.extractors.common
import capa.features.extractors.helpers
import capa.features.extractors.strings
from capa.features.file import Export, Import, Section
from capa.features.common import String, Characteristic
from capa.features.address import FileOffsetAddress, AbsoluteVirtualAddress
def extract_file_embedded_pe(buf, **kwargs):
for offset, _ in capa.features.extractors.helpers.carve_pe(buf, 1):
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
def extract_file_export_names(buf, **kwargs):
lief_binary = lief.parse(buf)
if lief_binary is not None:
for function in lief_binary.exported_functions:
yield Export(function.name), AbsoluteVirtualAddress(function.address)
def extract_file_import_names(smda_report, buf):
# extract import table info via LIEF
lief_binary = lief.parse(buf)
if not isinstance(lief_binary, lief.PE.Binary):
return
for imported_library in lief_binary.imports:
library_name = imported_library.name.lower()
library_name = library_name[:-4] if library_name.endswith(".dll") else library_name
for func in imported_library.entries:
va = func.iat_address + smda_report.base_addr
if func.name:
for name in capa.features.extractors.helpers.generate_symbols(library_name, func.name):
yield Import(name), AbsoluteVirtualAddress(va)
elif func.is_ordinal:
for name in capa.features.extractors.helpers.generate_symbols(library_name, "#%s" % func.ordinal):
yield Import(name), AbsoluteVirtualAddress(va)
def extract_file_section_names(buf, **kwargs):
lief_binary = lief.parse(buf)
if not isinstance(lief_binary, lief.PE.Binary):
return
if lief_binary and lief_binary.sections:
base_address = lief_binary.optional_header.imagebase
for section in lief_binary.sections:
yield Section(section.name), AbsoluteVirtualAddress(base_address + section.virtual_address)
def extract_file_strings(buf, **kwargs):
"""
extract ASCII and UTF-16 LE strings from file
"""
for s in capa.features.extractors.strings.extract_ascii_strings(buf):
yield String(s.s), FileOffsetAddress(s.offset)
for s in capa.features.extractors.strings.extract_unicode_strings(buf):
yield String(s.s), FileOffsetAddress(s.offset)
def extract_file_function_names(smda_report, **kwargs):
"""
extract the names of statically-linked library functions.
"""
if False:
# using a `yield` here to force this to be a generator, not function.
yield NotImplementedError("SMDA doesn't have library matching")
return
def extract_file_format(buf, **kwargs):
yield from capa.features.extractors.common.extract_format(buf)
def extract_features(smda_report, buf):
"""
extract file features from given workspace
args:
smda_report (smda.common.SmdaReport): a SmdaReport
buf: the raw bytes of the sample
yields:
Tuple[Feature, VA]: a feature and its location.
"""
for file_handler in FILE_HANDLERS:
for feature, addr in file_handler(smda_report=smda_report, buf=buf):
yield feature, addr
FILE_HANDLERS = (
extract_file_embedded_pe,
extract_file_export_names,
extract_file_import_names,
extract_file_section_names,
extract_file_strings,
extract_file_function_names,
extract_file_format,
)

View File

@@ -1,42 +0,0 @@
from typing import Tuple, Iterator
from capa.features.common import Feature, Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors import loops
from capa.features.extractors.base_extractor import FunctionHandle
def extract_function_calls_to(f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
for inref in f.inner.inrefs:
yield Characteristic("calls to"), AbsoluteVirtualAddress(inref)
def extract_function_loop(f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse if a function has a loop
"""
edges = []
for bb_from, bb_tos in f.inner.blockrefs.items():
for bb_to in bb_tos:
edges.append((bb_from, bb_to))
if edges and loops.has_loop(edges):
yield Characteristic("loop"), f.address
def extract_features(f: FunctionHandle):
"""
extract features from the given function.
args:
f: the function from which to extract features
yields:
Tuple[Feature, Address]: the features and their location found in this function.
"""
for func_handler in FUNCTION_HANDLERS:
for feature, addr in func_handler(f):
yield feature, addr
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)

View File

@@ -1,21 +0,0 @@
import logging
from capa.features.common import ARCH_I386, ARCH_AMD64, Arch
from capa.features.address import NO_ADDRESS
logger = logging.getLogger(__name__)
def extract_arch(smda_report):
if smda_report.architecture == "intel":
if smda_report.bitness == 32:
yield Arch(ARCH_I386), NO_ADDRESS
elif smda_report.bitness == 64:
yield Arch(ARCH_AMD64), NO_ADDRESS
else:
# we likely end up here:
# 1. handling a new architecture (e.g. aarch64)
#
# for (1), this logic will need to be updated as the format is implemented.
logger.debug("unsupported architecture: %s", smda_report.architecture)
return

View File

@@ -1,455 +0,0 @@
import re
import string
import struct
from typing import Tuple, Iterator
import smda
import capa.features.extractors.helpers
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
# byte range within the first and returning basic blocks, this helps to reduce FP features
SECURITY_COOKIE_BYTES_DELTA = 0x40
PATTERN_HEXNUM = re.compile(r"[+\-] (?P<num>0x[a-fA-F0-9]+)")
PATTERN_SINGLENUM = re.compile(r"[+\-] (?P<num>[0-9])")
def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse API features from the given instruction."""
f: smda.Function = fh.inner
insn: smda.Insn = ih.inner
if ih.address in f.apirefs:
api_entry = f.apirefs[ih.address]
# reformat
dll_name, api_name = api_entry.split("!")
dll_name = dll_name.split(".")[0]
dll_name = dll_name.lower()
for name in capa.features.extractors.helpers.generate_symbols(dll_name, api_name):
yield API(name), ih.address
elif ih.address in f.outrefs:
current_function = f
current_instruction = insn
for index in range(THUNK_CHAIN_DEPTH_DELTA):
if current_function and len(current_function.outrefs[current_instruction.offset]) == 1:
target = current_function.outrefs[current_instruction.offset][0]
referenced_function = current_function.smda_report.getFunction(target)
if referenced_function:
# TODO SMDA: implement this function for both jmp and call, checking if function has 1 instruction which refs an API
if referenced_function.isApiThunk():
api_entry = (
referenced_function.apirefs[target] if target in referenced_function.apirefs else None
)
if api_entry:
# reformat
dll_name, api_name = api_entry.split("!")
dll_name = dll_name.split(".")[0]
dll_name = dll_name.lower()
for name in capa.features.extractors.helpers.generate_symbols(dll_name, api_name):
yield API(name), ih.address
elif referenced_function.num_instructions == 1 and referenced_function.num_outrefs == 1:
current_function = referenced_function
current_instruction = [i for i in referenced_function.getInstructions()][0]
else:
return
def extract_insn_number_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse number features from the given instruction."""
# example:
#
# push 3136B0h ; dwControlCode
f: smda.Function = fh.inner
insn: smda.Insn = ih.inner
operands = [o.strip() for o in insn.operands.split(",")]
if insn.mnemonic == "add" and operands[0] in ["esp", "rsp"]:
# skip things like:
#
# .text:00401140 call sub_407E2B
# .text:00401145 add esp, 0Ch
return
for i, operand in enumerate(operands):
try:
# The result of bitwise operations is calculated as though carried out
# in twos complement with an infinite number of sign bits
value = int(operand, 16) & ((1 << f.smda_report.bitness) - 1)
except ValueError:
continue
else:
yield Number(value), ih.address
yield OperandNumber(i, value), ih.address
if insn.mnemonic == "add" and 0 < value < MAX_STRUCTURE_SIZE:
# for pattern like:
#
# add eax, 0x10
#
# assume 0x10 is also an offset (imagine eax is a pointer).
yield Offset(value), ih.address
yield OperandOffset(i, value), ih.address
def read_bytes(smda_report, va, num_bytes=None):
"""
read up to MAX_BYTES_FEATURE_SIZE from the given address.
"""
rva = va - smda_report.base_addr
if smda_report.buffer is None:
raise ValueError("buffer is empty")
buffer_end = len(smda_report.buffer)
max_bytes = num_bytes if num_bytes is not None else MAX_BYTES_FEATURE_SIZE
if rva + max_bytes > buffer_end:
return smda_report.buffer[rva:]
else:
return smda_report.buffer[rva : rva + max_bytes]
def derefs(smda_report, p):
"""
recursively follow the given pointer, yielding the valid memory addresses along the way.
useful when you may have a pointer to string, or pointer to pointer to string, etc.
this is a "do what i mean" type of helper function.
based on the implementation in viv/insn.py
"""
depth = 0
while True:
if not smda_report.isAddrWithinMemoryImage(p):
return
yield p
bytes_ = read_bytes(smda_report, p, num_bytes=4)
val = struct.unpack("I", bytes_)[0]
# sanity: pointer points to self
if val == p:
return
# sanity: avoid chains of pointers that are unreasonably deep
depth += 1
if depth > 10:
return
p = val
def extract_insn_bytes_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse byte sequence features from the given instruction.
example:
# push offset iid_004118d4_IShellLinkA ; riid
"""
f: smda.Function = fh.inner
insn: smda.Insn = ih.inner
for data_ref in insn.getDataRefs():
for v in derefs(f.smda_report, data_ref):
bytes_read = read_bytes(f.smda_report, v)
if bytes_read is None:
continue
if capa.features.extractors.helpers.all_zeros(bytes_read):
continue
yield Bytes(bytes_read), ih.address
def detect_ascii_len(smda_report, offset):
if smda_report.buffer is None:
return 0
ascii_len = 0
rva = offset - smda_report.base_addr
char = smda_report.buffer[rva]
while char < 127 and chr(char) in string.printable:
ascii_len += 1
rva += 1
char = smda_report.buffer[rva]
if char == 0:
return ascii_len
return 0
def detect_unicode_len(smda_report, offset):
if smda_report.buffer is None:
return 0
unicode_len = 0
rva = offset - smda_report.base_addr
char = smda_report.buffer[rva]
second_char = smda_report.buffer[rva + 1]
while char < 127 and chr(char) in string.printable and second_char == 0:
unicode_len += 2
rva += 2
char = smda_report.buffer[rva]
second_char = smda_report.buffer[rva + 1]
if char == 0 and second_char == 0:
return unicode_len
return 0
def read_string(smda_report, offset):
alen = detect_ascii_len(smda_report, offset)
if alen > 1:
return read_bytes(smda_report, offset, alen).decode("utf-8")
ulen = detect_unicode_len(smda_report, offset)
if ulen > 2:
return read_bytes(smda_report, offset, ulen).decode("utf-16")
def extract_insn_string_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse string features from the given instruction."""
# example:
#
# push offset aAcr ; "ACR > "
f: smda.Function = fh.inner
insn: smda.Insn = ih.inner
for data_ref in insn.getDataRefs():
for v in derefs(f.smda_report, data_ref):
string_read = read_string(f.smda_report, v)
if string_read:
yield String(string_read.rstrip("\x00")), ih.address
def extract_insn_offset_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse structure offset features from the given instruction."""
# examples:
#
# mov eax, [esi + 4]
# mov eax, [esi + ecx + 16384]
insn: smda.Insn = ih.inner
operands = [o.strip() for o in insn.operands.split(",")]
for i, operand in enumerate(operands):
if "esp" in operand or "ebp" in operand or "rbp" in operand:
continue
number = 0
number_hex = re.search(PATTERN_HEXNUM, operand)
number_int = re.search(PATTERN_SINGLENUM, operand)
if number_hex:
number = int(number_hex.group("num"), 16)
number = -1 * number if number_hex.group().startswith("-") else number
elif number_int:
number = int(number_int.group("num"))
number = -1 * number if number_int.group().startswith("-") else number
if "ptr" not in operand:
if (
insn.mnemonic == "lea"
and i == 1
and (operand.count("+") + operand.count("-")) == 1
and operand.count("*") == 0
):
# for pattern like:
#
# lea eax, [ebx + 1]
#
# assume 1 is also an offset (imagine ebx is a zero register).
yield Number(number), ih.address
yield OperandNumber(i, number), ih.address
continue
yield Offset(number), ih.address
yield OperandOffset(i, number), ih.address
def is_security_cookie(f, bb, insn):
"""
check if an instruction is related to security cookie checks
"""
# security cookie check should use SP or BP
operands = [o.strip() for o in insn.operands.split(",")]
if operands[1] not in ["esp", "ebp", "rsp", "rbp"]:
return False
for index, block in enumerate(f.getBlocks()):
# expect security cookie init in first basic block within first bytes (instructions)
block_instructions = [i for i in block.getInstructions()]
if index == 0 and insn.offset < (block_instructions[0].offset + SECURITY_COOKIE_BYTES_DELTA):
return True
# ... or within last bytes (instructions) before a return
if block_instructions[-1].mnemonic.startswith("ret") and insn.offset > (
block_instructions[-1].offset - SECURITY_COOKIE_BYTES_DELTA
):
return True
return False
def extract_insn_nzxor_characteristic_features(
fh: FunctionHandle, bh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse non-zeroing XOR instruction from the given instruction.
ignore expected non-zeroing XORs, e.g. security cookies.
"""
f: smda.Function = fh.inner
bb: smda.BasicBlock = bh.inner
insn: smda.Insn = ih.inner
if insn.mnemonic not in ("xor", "xorpd", "xorps", "pxor"):
return
operands = [o.strip() for o in insn.operands.split(",")]
if operands[0] == operands[1]:
return
if is_security_cookie(f, bb, insn):
return
yield Characteristic("nzxor"), ih.address
def extract_insn_mnemonic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse mnemonic features from the given instruction."""
yield Mnemonic(ih.inner.mnemonic), ih.address
def extract_insn_obfs_call_plus_5_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse call $+5 instruction from the given instruction.
"""
insn: smda.Insn = ih.inner
if insn.mnemonic != "call":
return
if not insn.operands.startswith("0x"):
return
if int(insn.operands, 16) == insn.offset + 5:
yield Characteristic("call $+5"), ih.address
def extract_insn_peb_access_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse peb access from the given function. fs:[0x30] on x86, gs:[0x60] on x64
"""
insn: smda.Insn = ih.inner
if insn.mnemonic not in ["push", "mov"]:
return
operands = [o.strip() for o in insn.operands.split(",")]
for operand in operands:
if "fs:" in operand and "0x30" in operand:
yield Characteristic("peb access"), ih.address
elif "gs:" in operand and "0x60" in operand:
yield Characteristic("peb access"), ih.address
def extract_insn_segment_access_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse the instruction for access to fs or gs"""
insn: smda.Insn = ih.inner
operands = [o.strip() for o in insn.operands.split(",")]
for operand in operands:
if "fs:" in operand:
yield Characteristic("fs access"), ih.address
elif "gs:" in operand:
yield Characteristic("gs access"), ih.address
def extract_insn_cross_section_cflow(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
inspect the instruction for a CALL or JMP that crosses section boundaries.
"""
f: smda.Function = fh.inner
insn: smda.Insn = ih.inner
if insn.mnemonic in ["call", "jmp"]:
if ih.address in f.apirefs:
return
smda_report = insn.smda_function.smda_report
if ih.address in f.outrefs:
for target in f.outrefs[ih.address]:
if smda_report.getSection(ih.address) != smda_report.getSection(target):
yield Characteristic("cross section flow"), ih.address
elif insn.operands.startswith("0x"):
target = int(insn.operands, 16)
if smda_report.getSection(ih.address) != smda_report.getSection(target):
yield Characteristic("cross section flow"), ih.address
# this is a feature that's most relevant at the function scope,
# however, its most efficient to extract at the instruction scope.
def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
f: smda.Function = fh.inner
insn: smda.Insn = ih.inner
if insn.mnemonic != "call":
return
if ih.address in f.outrefs:
for outref in f.outrefs[ih.address]:
yield Characteristic("calls from"), AbsoluteVirtualAddress(outref)
if outref == f.offset:
# if we found a jump target and it's the function address
# mark as recursive
yield Characteristic("recursive call"), AbsoluteVirtualAddress(outref)
if ih.address in f.apirefs:
yield Characteristic("calls from"), ih.address
# this is a feature that's most relevant at the function or basic block scope,
# however, its most efficient to extract at the instruction scope.
def extract_function_indirect_call_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract indirect function call characteristic (e.g., call eax or call dword ptr [edx+4])
does not include calls like => call ds:dword_ABD4974
"""
insn: smda.Insn = ih.inner
if insn.mnemonic != "call":
return
if insn.operands.startswith("0x"):
return False
if "qword ptr" in insn.operands and "rip" in insn.operands:
return False
if insn.operands.startswith("dword ptr [0x"):
return False
# call edx
# call dword ptr [eax+50h]
# call qword ptr [rsp+78h]
yield Characteristic("indirect call"), ih.address
def extract_features(f, bb, insn):
"""
extract features from the given insn.
args:
f: the function to process.
bb: the basic block to process.
insn: the instruction to process.
yields:
Tuple[Feature, Address]: the features and their location found in this insn.
"""
for insn_handler in INSTRUCTION_HANDLERS:
for feature, addr in insn_handler(f, bb, insn):
yield feature, addr
INSTRUCTION_HANDLERS = (
extract_insn_api_features,
extract_insn_number_features,
extract_insn_string_features,
extract_insn_bytes_features,
extract_insn_offset_features,
extract_insn_nzxor_characteristic_features,
extract_insn_mnemonic_features,
extract_insn_obfs_call_plus_5_characteristic_features,
extract_insn_peb_access_characteristic_features,
extract_insn_cross_section_cflow,
extract_insn_segment_access_features,
extract_function_calls_from,
extract_function_indirect_call_characteristic_features,
)

View File

@@ -31,7 +31,7 @@ def interface_extract_basic_block_XXX(f: FunctionHandle, bb: BBHandle) -> Iterat
yields:
(Feature, Address): the feature and the address at which its found.
"""
...
raise NotImplementedError
def _bb_has_tight_loop(f, bb):

View File

@@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
class VivisectFeatureExtractor(FeatureExtractor):
def __init__(self, vw, path):
super(VivisectFeatureExtractor, self).__init__()
super().__init__()
self.vw = vw
self.path = path
with open(self.path, "rb") as f:
@@ -34,6 +34,7 @@ class VivisectFeatureExtractor(FeatureExtractor):
# pre-compute these because we'll yield them at *every* scope.
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.viv.file.extract_file_format(self.buf))
self.global_features.extend(capa.features.extractors.common.extract_os(self.buf))
self.global_features.extend(capa.features.extractors.viv.global_.extract_arch(self.vw))

View File

@@ -27,7 +27,7 @@ def interface_extract_function_XXX(fh: FunctionHandle) -> Iterator[Tuple[Feature
yields:
(Feature, Address): the feature and the address at which its found.
"""
...
raise NotImplementedError
def extract_function_calls_to(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
@@ -47,6 +47,10 @@ def extract_function_loop(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Ad
for bb in f.basic_blocks:
if len(bb.instructions) > 0:
for bva, bflags in bb.instructions[-1].getBranches():
if bva is None:
# vivisect may be unable to recover the call target, e.g. on dynamic calls like `call esi`
# for this bva is None, and we don't want to add it for loop detection, ref: vivisect#574
continue
# vivisect does not set branch flags for non-conditional jmp so add explicit check
if (
bflags & envi.BR_COND

View File

@@ -42,7 +42,7 @@ def get_previous_instructions(vw: VivWorkspace, va: int) -> List[int]:
ret = []
# find the immediate prior instruction.
# ensure that it fallsthrough to this one.
# ensure that it falls through to this one.
loc = vw.getPrevLocation(va, adjacent=True)
if loc is not None:
ploc = vw.getPrevLocation(va, adjacent=True)
@@ -59,7 +59,7 @@ def get_previous_instructions(vw: VivWorkspace, va: int) -> List[int]:
#
# from vivisect.const:
# xref: (XR_FROM, XR_TO, XR_RTYPE, XR_RFLAG)
for (xfrom, _, _, xflag) in vw.getXrefsTo(va, REF_CODE):
for xfrom, _, _, xflag in vw.getXrefsTo(va, REF_CODE):
if (xflag & FAR_BRANCH_MASK) != 0:
continue
ret.append(xfrom)

View File

@@ -44,7 +44,7 @@ def interface_extract_instruction_XXX(
yields:
(Feature, Address): the feature and the address at which its found.
"""
...
raise NotImplementedError
def get_imports(vw):
@@ -271,6 +271,10 @@ def extract_insn_bytes_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera
if capa.features.extractors.helpers.all_zeros(buf):
continue
if f.vw.isProbablyString(v):
# don't extract byte features for obvious strings
continue
yield Bytes(buf), ih.address
@@ -281,7 +285,12 @@ def read_string(vw, offset: int) -> str:
pass
else:
if alen > 0:
return read_memory(vw, offset, alen).decode("utf-8")
buf = read_memory(vw, offset, alen)
if b"\x00" in buf:
# account for bug #1271.
# remove when vivisect is fixed.
buf = buf.partition(b"\x00")[0]
return buf.decode("utf-8")
try:
ulen = vw.detectUnicode(offset)
@@ -300,7 +309,9 @@ def read_string(vw, offset: int) -> str:
# vivisect seems to mis-detect the end unicode strings
# off by two, too short
ulen += 2
return read_memory(vw, offset, ulen).decode("utf-16")
# partition to account for bug #1271.
# remove when vivisect is fixed.
return read_memory(vw, offset, ulen).decode("utf-16").partition("\x00")[0]
raise ValueError("not a string", offset)
@@ -493,7 +504,8 @@ def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper):
oper = insn.opers[0]
target = oper.getOperAddr(insn)
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
if target >= 0:
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
# call via thunk on x86,
# see 9324d1a8ae37a36ae560c37448c9705a at 0x407985
@@ -509,7 +521,8 @@ def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera
elif isinstance(insn.opers[0], envi.archs.amd64.disasm.Amd64RipRelOper):
op = insn.opers[0]
target = op.getOperAddr(insn)
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
if target >= 0:
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
if target and target == f.va:
# if we found a jump target and it's the function address
@@ -663,11 +676,12 @@ def extract_op_string_features(
for v in derefs(f.vw, v):
try:
s = read_string(f.vw, v)
s = read_string(f.vw, v).rstrip("\x00")
except ValueError:
continue
else:
yield String(s.rstrip("\x00")), ih.address
if len(s) >= 4:
yield String(s), ih.address
def extract_operand_features(f: FunctionHandle, bb, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:

View File

@@ -12,19 +12,19 @@ from capa.features.common import Feature
class Export(Feature):
def __init__(self, value: str, description=None):
# value is export name
super(Export, self).__init__(value, description=description)
super().__init__(value, description=description)
class Import(Feature):
def __init__(self, value: str, description=None):
# value is import name
super(Import, self).__init__(value, description=description)
super().__init__(value, description=description)
class Section(Feature):
def __init__(self, value: str, description=None):
# value is section name
super(Section, self).__init__(value, description=description)
super().__init__(value, description=description)
class FunctionName(Feature):
@@ -32,7 +32,7 @@ class FunctionName(Feature):
def __init__(self, name: str, description=None):
# value is function name
super(FunctionName, self).__init__(name, description=description)
super().__init__(name, description=description)
# override the name property set by `capa.features.Feature`
# that would be `functionname` (note missing dash)
self.name = "function-name"

View File

@@ -12,9 +12,8 @@ See the License for the specific language governing permissions and limitations
import zlib
import logging
from enum import Enum
from typing import Any, List, Tuple
from typing import Any, List, Tuple, Union
import dncil.clr.token
from pydantic import Field, BaseModel
import capa.helpers
@@ -47,7 +46,7 @@ class AddressType(str, Enum):
class Address(HashableModel):
type: AddressType
value: Any
value: Union[int, Tuple[int, int], None]
@classmethod
def from_capa(cls, a: capa.features.address.Address) -> "Address":
@@ -61,10 +60,10 @@ class Address(HashableModel):
return cls(type=AddressType.FILE, value=int(a))
elif isinstance(a, capa.features.address.DNTokenAddress):
return cls(type=AddressType.DN_TOKEN, value=a.token.value)
return cls(type=AddressType.DN_TOKEN, value=int(a))
elif isinstance(a, capa.features.address.DNTokenOffsetAddress):
return cls(type=AddressType.DN_TOKEN_OFFSET, value=(a.token.value, a.offset))
return cls(type=AddressType.DN_TOKEN_OFFSET, value=(a.token, a.offset))
elif a == capa.features.address.NO_ADDRESS or isinstance(a, capa.features.address._NoAddress):
return cls(type=AddressType.NO_ADDRESS, value=None)
@@ -80,20 +79,27 @@ class Address(HashableModel):
def to_capa(self) -> capa.features.address.Address:
if self.type is AddressType.ABSOLUTE:
assert isinstance(self.value, int)
return capa.features.address.AbsoluteVirtualAddress(self.value)
elif self.type is AddressType.RELATIVE:
assert isinstance(self.value, int)
return capa.features.address.RelativeVirtualAddress(self.value)
elif self.type is AddressType.FILE:
assert isinstance(self.value, int)
return capa.features.address.FileOffsetAddress(self.value)
elif self.type is AddressType.DN_TOKEN:
return capa.features.address.DNTokenAddress(dncil.clr.token.Token(self.value))
assert isinstance(self.value, int)
return capa.features.address.DNTokenAddress(self.value)
elif self.type is AddressType.DN_TOKEN_OFFSET:
assert isinstance(self.value, tuple)
token, offset = self.value
return capa.features.address.DNTokenOffsetAddress(dncil.clr.token.Token(token), offset)
assert isinstance(token, int)
assert isinstance(offset, int)
return capa.features.address.DNTokenOffsetAddress(token, offset)
elif self.type is AddressType.NO_ADDRESS:
return capa.features.address.NO_ADDRESS
@@ -109,7 +115,11 @@ class Address(HashableModel):
return True
else:
return self.value < other.value
assert self.type == other.type
# mypy doesn't realize we've proven that either
# both are ints, or both are tuples of ints.
# and both of these are comparable.
return self.value < other.value # type: ignore
class GlobalFeature(HashableModel):
@@ -146,10 +156,13 @@ class BasicBlockFeature(HashableModel):
versus right at its starting address.
"""
basic_block: Address
basic_block: Address = Field(alias="basic block")
address: Address
feature: Feature
class Config:
allow_population_by_field_name = True
class InstructionFeature(HashableModel):
"""
@@ -180,7 +193,7 @@ class BasicBlockFeatures(BaseModel):
class FunctionFeatures(BaseModel):
address: Address
features: Tuple[FunctionFeature, ...]
basic_blocks: Tuple[BasicBlockFeatures, ...] = Field(alias="basic block")
basic_blocks: Tuple[BasicBlockFeatures, ...] = Field(alias="basic blocks")
class Config:
allow_population_by_field_name = True

View File

@@ -66,6 +66,9 @@ class FeatureModel(BaseModel):
elif isinstance(self, APIFeature):
return capa.features.insn.API(self.api, description=self.description)
elif isinstance(self, PropertyFeature):
return capa.features.insn.Property(self.property, access=self.access, description=self.description)
elif isinstance(self, NumberFeature):
return capa.features.insn.Number(self.number, description=self.description)
@@ -147,6 +150,9 @@ def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
elif isinstance(f, capa.features.insn.API):
return APIFeature(api=f.value, description=f.description)
elif isinstance(f, capa.features.insn.Property):
return PropertyFeature(property=f.value, access=f.access, description=f.description)
elif isinstance(f, capa.features.insn.Number):
return NumberFeature(number=f.value, description=f.description)
@@ -266,6 +272,13 @@ class APIFeature(FeatureModel):
description: Optional[str]
class PropertyFeature(FeatureModel):
type: str = "property"
access: Optional[str]
property: str
description: Optional[str]
class NumberFeature(FeatureModel):
type: str = "number"
number: Union[int, float]
@@ -320,13 +333,13 @@ Feature = Union[
ClassFeature,
NamespaceFeature,
APIFeature,
PropertyFeature,
NumberFeature,
BytesFeature,
OffsetFeature,
MnemonicFeature,
OperandNumberFeature,
OperandOffsetFeature,
# this has to go last because...? pydantic fails to serialize correctly otherwise.
# possibly because this feature has no associated value?
# Note! this must be last, see #1161
BasicBlockFeature,
]

View File

@@ -6,9 +6,10 @@
# 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 abc
from typing import Union
from typing import Union, Optional
from capa.features.common import Feature
import capa.helpers
from capa.features.common import VALID_FEATURE_ACCESS, Feature
def hex(n: int) -> str:
@@ -21,16 +22,42 @@ def hex(n: int) -> str:
class API(Feature):
def __init__(self, name: str, description=None):
super(API, self).__init__(name, description=description)
super().__init__(name, description=description)
class _AccessFeature(Feature, abc.ABC):
# superclass: don't use directly
def __init__(self, value: str, access: Optional[str] = None, description: Optional[str] = None):
super().__init__(value, description=description)
if access is not None:
if access not in VALID_FEATURE_ACCESS:
raise ValueError("%s access type %s not valid" % (self.name, access))
self.access = access
def __hash__(self):
return hash((self.name, self.value, self.access))
def __eq__(self, other):
return super().__eq__(other) and self.access == other.access
def get_name_str(self) -> str:
if self.access is not None:
return f"{self.name}/{self.access}"
return self.name
class Property(_AccessFeature):
def __init__(self, value: str, access: Optional[str] = None, description=None):
super().__init__(value, access=access, description=description)
class Number(Feature):
def __init__(self, value: Union[int, float], description=None):
super(Number, self).__init__(value, description=description)
super().__init__(value, description=description)
def get_value_str(self):
if isinstance(self.value, int):
return hex(self.value)
return capa.helpers.hex(self.value)
elif isinstance(self.value, float):
return str(self.value)
else:
@@ -43,18 +70,19 @@ MAX_STRUCTURE_SIZE = 0x10000
class Offset(Feature):
def __init__(self, value: int, description=None):
super(Offset, self).__init__(value, description=description)
super().__init__(value, description=description)
def get_value_str(self):
assert isinstance(self.value, int)
return hex(self.value)
class Mnemonic(Feature):
def __init__(self, value: str, description=None):
super(Mnemonic, self).__init__(value, description=description)
super().__init__(value, description=description)
# max number of operands to consider for a given instrucion.
# max number of operands to consider for a given instruction.
# since we only support Intel and .NET, we can assume this is 3
# which covers cases up to e.g. "vinserti128 ymm0,ymm0,ymm5,1"
MAX_OPERAND_COUNT = 4
@@ -65,7 +93,7 @@ class _Operand(Feature, abc.ABC):
# superclass: don't use directly
# subclasses should set self.name and provide the value string formatter
def __init__(self, index: int, value: int, description=None):
super(_Operand, self).__init__(value, description=description)
super().__init__(value, description=description)
self.index = index
def __hash__(self):
@@ -81,7 +109,7 @@ class OperandNumber(_Operand):
# operand[i].number: 0x12
def __init__(self, index: int, value: int, description=None):
super(OperandNumber, self).__init__(index, value, description=description)
super().__init__(index, value, description=description)
self.name = self.NAMES[index]
def get_value_str(self) -> str:
@@ -95,7 +123,7 @@ class OperandOffset(_Operand):
# operand[i].offset: 0x12
def __init__(self, index: int, value: int, description=None):
super(OperandOffset, self).__init__(index, value, description=description)
super().__init__(index, value, description=description)
self.name = self.NAMES[index]
def get_value_str(self) -> str:

View File

@@ -10,7 +10,7 @@ import logging
from typing import NoReturn
from capa.exceptions import UnsupportedFormatError
from capa.features.common import FORMAT_SC32, FORMAT_SC64, FORMAT_UNKNOWN
from capa.features.common import FORMAT_PE, FORMAT_SC32, FORMAT_SC64, FORMAT_DOTNET, FORMAT_UNKNOWN, Format
EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32")
EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64")
@@ -18,11 +18,13 @@ EXTENSIONS_ELF = "elf_"
logger = logging.getLogger("capa")
_hex = hex
def hex(i):
return _hex(int(i))
def hex(n: int) -> str:
"""render the given number using upper case hex, like: 0x123ABC"""
if n < 0:
return "-0x%X" % (-n)
else:
return "0x%X" % n
def get_file_taste(sample_path: str) -> bytes:
@@ -66,11 +68,17 @@ def get_auto_format(path: str) -> str:
def get_format(sample: str) -> str:
# imported locally to avoid import cycle
from capa.features.extractors.common import extract_format
from capa.features.extractors.dnfile_ import DnfileFeatureExtractor
with open(sample, "rb") as f:
buf = f.read()
for feature, _ in extract_format(buf):
if feature == Format(FORMAT_PE):
dnfile_extractor = DnfileFeatureExtractor(sample)
if dnfile_extractor.is_dotnet_file():
feature = Format(FORMAT_DOTNET)
assert isinstance(feature.value, str)
return feature.value

View File

@@ -5,20 +5,25 @@
# 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 json
import logging
import datetime
import contextlib
from typing import Optional
import idc
import idaapi
import idautils
import ida_bytes
import ida_loader
from netnode import netnode
import capa
import capa.version
import capa.render.utils as rutils
import capa.features.common
import capa.render.result_document
from capa.features.address import AbsoluteVirtualAddress
logger = logging.getLogger("capa")
@@ -27,12 +32,17 @@ SUPPORTED_FILE_TYPES = (
idaapi.f_PE,
idaapi.f_ELF,
idaapi.f_BIN,
idaapi.f_COFF,
# idaapi.f_MACHO,
)
# arch type as returned by idainfo.procname
SUPPORTED_ARCH_TYPES = ("metapc",)
CAPA_NETNODE = f"$ com.mandiant.capa.v{capa.version.__version__}"
NETNODE_RESULTS = "results"
NETNODE_RULES_CACHE_ID = "rules-cache-id"
def inform_user_ida_ui(message):
idaapi.info("%s. Please refer to IDA Output window for more information." % message)
@@ -170,7 +180,7 @@ class IDAIO:
"""
def __init__(self):
super(IDAIO, self).__init__()
super().__init__()
self.offset = 0
def seek(self, offset, whence=0):
@@ -180,11 +190,63 @@ class IDAIO:
def read(self, size):
ea = ida_loader.get_fileregion_ea(self.offset)
if ea == idc.BADADDR:
# best guess, such as if file is mapped at address 0x0.
ea = self.offset
logger.debug("cannot read 0x%x bytes at 0x%x (ea: BADADDR)", size, self.offset)
return b""
logger.debug("reading 0x%x bytes at 0x%x (ea: 0x%x)", size, self.offset, ea)
return ida_bytes.get_bytes(ea, size)
# get_bytes returns None on error, for consistency with read always return bytes
return ida_bytes.get_bytes(ea, size) or b""
def close(self):
return
def save_cached_results(resdoc):
logger.debug("saving cached capa results to netnode '%s'", CAPA_NETNODE)
n = netnode.Netnode(CAPA_NETNODE)
n[NETNODE_RESULTS] = resdoc.json()
def idb_contains_cached_results() -> bool:
try:
n = netnode.Netnode(CAPA_NETNODE)
return bool(n.get(NETNODE_RESULTS))
except netnode.NetnodeCorruptError as e:
logger.error("%s", e, exc_info=True)
return False
def load_and_verify_cached_results() -> Optional[capa.render.result_document.ResultDocument]:
"""verifies that cached results have valid (mapped) addresses for the current database"""
logger.debug("loading cached capa results from netnode '%s'", CAPA_NETNODE)
n = netnode.Netnode(CAPA_NETNODE)
doc = capa.render.result_document.ResultDocument.parse_obj(json.loads(n[NETNODE_RESULTS]))
for rule in rutils.capability_rules(doc):
for location_, _ in rule.matches:
location = location_.to_capa()
if isinstance(location, AbsoluteVirtualAddress):
ea = int(location)
if not idaapi.is_mapped(ea):
logger.error("cached address %s is not a valid location in this database", hex(ea))
return None
return doc
def save_rules_cache_id(ruleset_id):
logger.debug("saving ruleset ID to netnode '%s'", CAPA_NETNODE)
n = netnode.Netnode(CAPA_NETNODE)
n[NETNODE_RULES_CACHE_ID] = ruleset_id
def load_rules_cache_id():
n = netnode.Netnode(CAPA_NETNODE)
return n[NETNODE_RULES_CACHE_ID]
def delete_cached_results():
logger.debug("deleting cached capa data")
n = netnode.Netnode(CAPA_NETNODE)
del n[NETNODE_RESULTS]

View File

@@ -1,8 +1,8 @@
![capa explorer](../../../.github/capa-explorer-logo.png)
capa explorer is an IDAPython plugin that integrates the FLARE team's open-source framework, capa, with IDA Pro. 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 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. capa explorer runs capa directly against your IDA Pro database (IDB) without requiring access
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. capa explorer runs capa analysis on your IDA Pro database (IDB) without needing access
to the original binary file. Once a database has been analyzed, capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted from your IDB.
We love using capa explorer during malware analysis because it teaches us what parts of a program suggest a behavior. As we click on rows, capa explorer jumps directly
@@ -21,10 +21,10 @@ We can use capa explorer to navigate our Disassembly view directly to the suspec
Using the `Rule Information` and `Details` columns capa explorer shows us that the suspect function matched `self delete via COMSPEC environment variable` because it contains capa rule matches for `create process`, `get COMSPEC environment variable`,
and `query environment variable`, references to the strings `COMSPEC`, ` > nul`, and `/c del `, and calls to the Windows API functions `GetEnvironmentVariableA` and `ShellExecuteEx`.
capa explorer also helps you build new capa rules. To start select the `Rule Generator` tab, navigate to a function in your Disassembly view,
capa explorer also helps you build and test new capa rules. To start, select the `Rule Generator` tab, navigate to a function in your Disassembly view,
and click `Analyze`. capa explorer will extract features from the function and display them in the `Features` pane. You can add features listed in this pane to the `Editor` pane
by either double-clicking a feature or using multi-select + right-click to add multiple features at once. The `Preview` and `Editor` panes help edit your rule. Use the `Preview` pane
to modify the rule text directly and the `Editor` pane to construct and rearrange your hierarchy of statements and features. When you finish a rule you can save it directly to a file by clicking `Save`.
to modify rule text directly and the `Editor` pane to construct and rearrange your hierarchy of statements and features. When you finish a rule you can save it directly to a file by clicking `Save`.
![](../../../doc/img/rulegen_expanded.png)
@@ -32,62 +32,32 @@ For more information on the FLARE team's open-source framework, capa, check out
## Getting Started
### Requirements
### Installation
capa explorer supports Python versions >= 3.7.x and the following IDA Pro versions:
* IDA 7.4
* IDA 7.5
* IDA 7.6 (caveat below)
* IDA 7.7
capa explorer is however limited to the Python versions supported by your IDA installation (which may not include all Python versions >= 3.7.x). Based on our testing the following matrix shows the Python versions supported
by each supported IDA version:
| | IDA 7.4 | IDA 7.5 | IDA 7.6 |
| --- | --- | --- | --- |
| Python 3.7.x | Yes | Yes | Yes |
| Python 3.8.x | Partial (see below) | Yes | Yes |
| Python 3.9.x | No | Partial (see below) | Yes |
To use capa explorer with IDA 7.4 and Python 3.8.x you must follow the instructions provided by hex-rays [here](https://hex-rays.com/blog/ida-7-4-and-python-3-8/).
To use capa explorer with IDA 7.5 and Python 3.9.x you must follow the instructions provided by hex-rays [here](https://hex-rays.com/blog/python-3-9-support-for-ida-7-5/).
If you encounter issues with your specific setup, please open a new [Issue](https://github.com/mandiant/capa/issues).
#### IDA 7.6 caveat: IDA 7.6sp1 or patch required
As described [here](https://www.hex-rays.com/blog/ida-7-6-empty-qtreeview-qtreewidget/):
> A rather nasty issue evaded our testing and found its way into IDA 7.6: using the PyQt5 modules that are shipped with IDA, QTreeView (or QTreeWidget) instances will always fail to display contents.
Therefore, in order to use capa under IDA 7.6 you need the [Service Pack 1 for IDA 7.6](https://www.hex-rays.com/products/ida/news/7_6sp1). Alternatively, you can download and install the fix corresponding to your IDA installation, replacing the original QtWidgets DLL with the one contained in the .zip file (links to Hex-Rays):
- Windows: [pyqt5_qtwidgets_win](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_win.zip)
- Linux: [pyqt5_qtwidgets_linux](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_linux.zip)
- MacOS (Intel): [pyqt5_qtwidgets_mac_x64](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_mac_x64.zip)
- MacOS (AppleSilicon): [pyqt5_qtwidgets_mac_arm](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_mac_arm.zip)
You can install capa explorer using the following steps:
1. Install capa and its dependencies from PyPI using the Python interpreter configured for your IDA installation:
```
$ pip install flare-capa
```
2. Download and extract the [official capa rules](https://github.com/mandiant/capa-rules/releases) that match the version of capa you have installed
1. Use the following command to view the version of capa you have installed:
```commandline
$ pip show flare-capa
OR
$ capa --version
```
3. Copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory
- find your plugin directories via `idaapi.get_ida_subdirs("plugins")` or see this [Hex-Rays blog](https://hex-rays.com/blog/igors-tip-of-the-week-103-sharing-plugins-between-ida-installs/)
- common paths are `%APPDATA%\Hex-Rays\IDA Pro\plugins` (Windows) or `$HOME/.idapro/plugins` on Linux/Mac
### Supported File Types
capa explorer is limited to the file types supported by capa, which include:
* Windows x86 (32- and 64-bit) PE and ELF files
* Windows x86 (32- and 64-bit) PE files
* Windows x86 (32- and 64-bit) shellcode
### Installation
You can install capa explorer using the following steps:
1. Install capa and its dependencies from PyPI for the Python interpreter used by your IDA installation:
```
$ pip install flare-capa
```
3. Download the [standard collection of capa rules](https://github.com/mandiant/capa-rules) (capa explorer needs capa rules to analyze a database)
4. Copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory
* ELF files on various operating systems
### Usage
@@ -97,19 +67,20 @@ You can install capa explorer using the following steps:
3. Select the `Program Analysis` tab
4. Click the `Analyze` button
When running capa explorer for the first time you are prompted to select a file directory containing capa rules. The plugin conveniently
remembers your selection for future runs; you can change this selection and other default settings by clicking `Settings`. We recommend
downloading and using the [standard collection of capa rules](https://github.com/mandiant/capa-rules) when getting started with the plugin.
The first time you run capa explorer you will be asked to specify a local directory containing capa rules to use for analysis. We recommend downloading and extracting the [official capa rules](https://github.com/mandiant/capa-rules/releases) that match
the version of capa you have installed (see installation instructions above for more details). capa explorer remembers your selection for future analysis which you
can update using the `Settings` button.
#### Tips for Program Analysis
* Start analysis by clicking the `Analyze` button
* capa explorer caches results to the database and reuses them across IDA sessions
* Reset the plugin user interface and remove highlighting from your Disassembly view by clicking the `Reset` button
* Change your capa rules directory and other default settings by clicking `Settings`
* Change your local capa rules directory, auto analysis settings, and other default settings by clicking the `Settings` button
* Hover your cursor over a rule match to view the source content of the rule
* 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 Dissasembly view
* Select a checkbox in the `Rule Information` column to highlight the address of the associated feature in your Disassembly view
#### Tips for Rule Generator
@@ -122,6 +93,22 @@ downloading and using the [standard collection of capa rules](https://github.com
* Directly edit rule text and metadata fields using the `Preview` pane
* Change the default rule author and default rule scope displayed in the `Preview` pane by clicking `Settings`
### Requirements
capa explorer supports Python versions >= 3.7.x and IDA Pro versions >= 7.4. The following IDA Pro versions have been tested:
* IDA 7.4
* IDA 7.5
* IDA 7.6 Service Pack 1
* IDA 7.7
* IDA 8.0
* IDA 8.1
* IDA 8.2
capa explorer is however limited to the Python versions supported by your IDA installation (which may not include all Python versions >= 3.7.x).
If you encounter issues with your specific setup, please open a new [Issue](https://github.com/mandiant/capa/issues).
## Development
capa explorer is packaged with capa so you will need to install capa locally for development. You can install capa locally by following the steps outlined in `Method 3: Inspecting the capa source code` of the [capa

View File

@@ -17,7 +17,6 @@ logger = logging.getLogger(__name__)
class CapaExplorerPlugin(idaapi.plugin_t):
# Mandatory definitions
PLUGIN_NAME = "FLARE capa explorer"
PLUGIN_VERSION = "1.0.0"
@@ -85,7 +84,7 @@ class CapaExplorerPlugin(idaapi.plugin_t):
# so we need to register a callback that's invoked from the main thread after the plugin is registered.
#
# after a lot of guess-and-check, we can use `UI_Hooks.updated_actions` to
# receive notications after IDA has created an action for each plugin.
# receive notifications after IDA has created an action for each plugin.
# so, create this hook, wait for capa plugin to load, set the icon, and unhook.
@@ -93,7 +92,7 @@ class OnUpdatedActionsHook(ida_kernwin.UI_Hooks):
"""register a callback to be invoked each time the UI actions are updated"""
def __init__(self, cb):
super(OnUpdatedActionsHook, self).__init__()
super().__init__()
self.cb = cb
def updated_actions(self):

220
capa/ida/plugin/cache.py Normal file
View File

@@ -0,0 +1,220 @@
# 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 __future__ import annotations
import itertools
import collections
from typing import Set, Dict, List, Tuple, Union, Optional
import capa.engine
from capa.rules import Scope, RuleSet
from capa.engine import FeatureSet, MatchResults
from capa.features.address import NO_ADDRESS, Address
from capa.ida.plugin.extractor import CapaExplorerFeatureExtractor
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
class CapaRuleGenFeatureCacheNode:
def __init__(
self,
inner: Optional[Union[FunctionHandle, BBHandle, InsnHandle]],
parent: Optional[CapaRuleGenFeatureCacheNode],
):
self.inner: Optional[Union[FunctionHandle, BBHandle, InsnHandle]] = inner
self.address = NO_ADDRESS if self.inner is None else self.inner.address
self.parent: Optional[CapaRuleGenFeatureCacheNode] = parent
if self.parent is not None:
self.parent.children.add(self)
self.features: FeatureSet = collections.defaultdict(set)
self.children: Set[CapaRuleGenFeatureCacheNode] = set()
def __hash__(self):
# TODO: unique enough?
return hash((self.address,))
def __eq__(self, other):
if not isinstance(other, type(self)):
return NotImplemented
# TODO: unique enough?
return self.address == other.address
class CapaRuleGenFeatureCache:
def __init__(self, fh_list: List[FunctionHandle], extractor: CapaExplorerFeatureExtractor):
self.global_features: FeatureSet = collections.defaultdict(set)
self.file_node: CapaRuleGenFeatureCacheNode = CapaRuleGenFeatureCacheNode(None, None)
self.func_nodes: Dict[Address, CapaRuleGenFeatureCacheNode] = {}
self.bb_nodes: Dict[Address, CapaRuleGenFeatureCacheNode] = {}
self.insn_nodes: Dict[Address, CapaRuleGenFeatureCacheNode] = {}
self._find_global_features(extractor)
self._find_file_features(extractor)
self._find_function_and_below_features(fh_list, extractor)
def _find_global_features(self, extractor: CapaExplorerFeatureExtractor):
for feature, addr in extractor.extract_global_features():
# not all global features may have virtual addresses.
# if not, then at least ensure the feature shows up in the index.
# the set of addresses will still be empty.
if addr is not None:
self.global_features[feature].add(addr)
else:
if feature not in self.global_features:
self.global_features[feature] = set()
def _find_file_features(self, extractor: CapaExplorerFeatureExtractor):
# not all file features may have virtual addresses.
# if not, then at least ensure the feature shows up in the index.
# the set of addresses will still be empty.
for feature, addr in extractor.extract_file_features():
if addr is not None:
self.file_node.features[feature].add(addr)
else:
if feature not in self.file_node.features:
self.file_node.features[feature] = set()
def _find_function_and_below_features(self, fh_list: List[FunctionHandle], extractor: CapaExplorerFeatureExtractor):
for fh in fh_list:
f_node: CapaRuleGenFeatureCacheNode = CapaRuleGenFeatureCacheNode(fh, self.file_node)
# extract basic block and below features
for bbh in extractor.get_basic_blocks(fh):
bb_node: CapaRuleGenFeatureCacheNode = CapaRuleGenFeatureCacheNode(bbh, f_node)
# extract instruction features
for ih in extractor.get_instructions(fh, bbh):
inode: CapaRuleGenFeatureCacheNode = CapaRuleGenFeatureCacheNode(ih, bb_node)
for feature, addr in extractor.extract_insn_features(fh, bbh, ih):
inode.features[feature].add(addr)
self.insn_nodes[inode.address] = inode
# extract basic block features
for feature, addr in extractor.extract_basic_block_features(fh, bbh):
bb_node.features[feature].add(addr)
# store basic block features in cache and function parent
self.bb_nodes[bb_node.address] = bb_node
# extract function features
for feature, addr in extractor.extract_function_features(fh):
f_node.features[feature].add(addr)
self.func_nodes[f_node.address] = f_node
def _find_instruction_capabilities(
self, ruleset: RuleSet, insn: CapaRuleGenFeatureCacheNode
) -> Tuple[FeatureSet, MatchResults]:
features: FeatureSet = collections.defaultdict(set)
for feature, locs in itertools.chain(insn.features.items(), self.global_features.items()):
features[feature].update(locs)
_, matches = ruleset.match(Scope.INSTRUCTION, features, insn.address)
for name, result in matches.items():
rule = ruleset[name]
for addr, _ in result:
capa.engine.index_rule_matches(features, rule, [addr])
return features, matches
def _find_basic_block_capabilities(
self, ruleset: RuleSet, bb: CapaRuleGenFeatureCacheNode
) -> Tuple[FeatureSet, MatchResults, MatchResults]:
features: FeatureSet = collections.defaultdict(set)
insn_matches: MatchResults = collections.defaultdict(list)
for insn in bb.children:
ifeatures, imatches = self._find_instruction_capabilities(ruleset, insn)
for feature, locs in ifeatures.items():
features[feature].update(locs)
for name, result in imatches.items():
insn_matches[name].extend(result)
for feature, locs in itertools.chain(bb.features.items(), self.global_features.items()):
features[feature].update(locs)
_, matches = ruleset.match(Scope.BASIC_BLOCK, features, bb.address)
for name, result in matches.items():
rule = ruleset[name]
for loc, _ in result:
capa.engine.index_rule_matches(features, rule, [loc])
return features, matches, insn_matches
def find_code_capabilities(
self, ruleset: RuleSet, fh: FunctionHandle
) -> Tuple[FeatureSet, MatchResults, MatchResults, MatchResults]:
f_node: Optional[CapaRuleGenFeatureCacheNode] = self.func_nodes.get(fh.address, None)
if f_node is None:
return {}, {}, {}, {}
insn_matches: MatchResults = collections.defaultdict(list)
bb_matches: MatchResults = collections.defaultdict(list)
function_features: FeatureSet = collections.defaultdict(set)
for bb in f_node.children:
features, bmatches, imatches = self._find_basic_block_capabilities(ruleset, bb)
for feature, locs in features.items():
function_features[feature].update(locs)
for name, result in bmatches.items():
bb_matches[name].extend(result)
for name, result in imatches.items():
insn_matches[name].extend(result)
for feature, locs in itertools.chain(f_node.features.items(), self.global_features.items()):
function_features[feature].update(locs)
_, function_matches = ruleset.match(Scope.FUNCTION, function_features, f_node.address)
return function_features, function_matches, bb_matches, insn_matches
def find_file_capabilities(self, ruleset: RuleSet) -> Tuple[FeatureSet, MatchResults]:
features: FeatureSet = collections.defaultdict(set)
for func_node in self.file_node.children:
assert func_node.inner is not None
assert isinstance(func_node.inner, FunctionHandle)
func_features, _, _, _ = self.find_code_capabilities(ruleset, func_node.inner)
for feature, locs in func_features.items():
features[feature].update(locs)
for feature, locs in itertools.chain(self.file_node.features.items(), self.global_features.items()):
features[feature].update(locs)
_, matches = ruleset.match(Scope.FILE, features, NO_ADDRESS)
return features, matches
def get_all_function_features(self, fh: FunctionHandle) -> FeatureSet:
f_node: Optional[CapaRuleGenFeatureCacheNode] = self.func_nodes.get(fh.address, None)
if f_node is None:
return {}
all_function_features: FeatureSet = collections.defaultdict(set)
all_function_features.update(f_node.features)
for bb_node in f_node.children:
for i_node in bb_node.children:
for feature, locs in i_node.features.items():
all_function_features[feature].update(locs)
for feature, locs in bb_node.features.items():
all_function_features[feature].update(locs)
# include global features just once
for feature, locs in self.global_features.items():
all_function_features[feature].update(locs)
return all_function_features
def get_all_file_features(self):
yield from itertools.chain(self.file_node.features.items(), self.global_features.items())

13
capa/ida/plugin/error.py Normal file
View File

@@ -0,0 +1,13 @@
# 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.
class UserCancelledError(Exception):
"""throw exception when user cancels action"""
pass

View File

@@ -0,0 +1,44 @@
# 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.
import ida_kernwin
from PyQt5 import QtCore
from capa.ida.plugin.error import UserCancelledError
from capa.features.extractors.ida.extractor import IdaFeatureExtractor
from capa.features.extractors.base_extractor import FunctionHandle
class CapaExplorerProgressIndicator(QtCore.QObject):
"""implement progress signal, used during feature extraction"""
progress = QtCore.pyqtSignal(str)
def update(self, text):
"""emit progress update
check if user cancelled action, raise exception for parent function to catch
"""
if ida_kernwin.user_cancelled():
raise UserCancelledError("user cancelled")
self.progress.emit("extracting features from %s" % text)
class CapaExplorerFeatureExtractor(IdaFeatureExtractor):
"""subclass the IdaFeatureExtractor
track progress during feature extraction, also allow user to cancel feature extraction
"""
def __init__(self):
super().__init__()
self.indicator = CapaExplorerProgressIndicator()
def extract_function_features(self, fh: FunctionHandle):
self.indicator.update("function at 0x%X" % fh.inner.start_ea)
return super().extract_function_features(fh)

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ class CapaExplorerIdaHooks(idaapi.UI_Hooks):
@param screen_ea_changed_hook: function hook for IDA screen ea changed
@param action_hooks: dict of IDA action handles
"""
super(CapaExplorerIdaHooks, self).__init__()
super().__init__()
self.screen_ea_changed_hook = screen_ea_changed_hook
self.process_action_hooks = action_hooks

View File

@@ -36,7 +36,7 @@ def ea_to_hex(ea):
class CapaExplorerDataItem:
"""store data for CapaExplorerDataModel"""
def __init__(self, parent: "CapaExplorerDataItem", data: List[str], can_check=True):
def __init__(self, parent: Optional["CapaExplorerDataItem"], data: List[str], can_check=True):
"""initialize item"""
self.pred = parent
self._data = data
@@ -110,7 +110,7 @@ class CapaExplorerDataItem:
except IndexError:
return None
def parent(self) -> "CapaExplorerDataItem":
def parent(self) -> Optional["CapaExplorerDataItem"]:
"""get parent"""
return self.pred
@@ -181,7 +181,7 @@ class CapaExplorerRuleItem(CapaExplorerDataItem):
@param source: rule source (tooltip)
"""
display = self.fmt % (name, count) if count > 1 else name
super(CapaExplorerRuleItem, self).__init__(parent, [display, "", namespace], can_check)
super().__init__(parent, [display, "", namespace], can_check)
self._source = source
@property
@@ -200,7 +200,7 @@ class CapaExplorerRuleMatchItem(CapaExplorerDataItem):
@param display: text to display in UI
@param source: rule match source to display (tooltip)
"""
super(CapaExplorerRuleMatchItem, self).__init__(parent, [display, "", ""])
super().__init__(parent, [display, "", ""])
self._source = source
@property
@@ -222,14 +222,12 @@ class CapaExplorerFunctionItem(CapaExplorerDataItem):
"""
assert isinstance(location, AbsoluteVirtualAddress)
ea = int(location)
super(CapaExplorerFunctionItem, self).__init__(
parent, [self.fmt % idaapi.get_name(ea), ea_to_hex(ea), ""], can_check
)
super().__init__(parent, [self.fmt % idaapi.get_name(ea), ea_to_hex(ea), ""], can_check)
@property
def info(self):
"""return function name"""
info = super(CapaExplorerFunctionItem, self).info
info = super().info
display = info_to_name(info)
return display if display else info
@@ -255,7 +253,7 @@ class CapaExplorerSubscopeItem(CapaExplorerDataItem):
@param parent: parent node
@param scope: subscope name
"""
super(CapaExplorerSubscopeItem, self).__init__(parent, [self.fmt % scope, "", ""])
super().__init__(parent, [self.fmt % scope, "", ""])
class CapaExplorerBlockItem(CapaExplorerDataItem):
@@ -271,7 +269,13 @@ class CapaExplorerBlockItem(CapaExplorerDataItem):
"""
assert isinstance(location, AbsoluteVirtualAddress)
ea = int(location)
super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % ea, ea_to_hex(ea), ""])
super().__init__(parent, [self.fmt % ea, ea_to_hex(ea), ""])
class CapaExplorerInstructionItem(CapaExplorerBlockItem):
"""store data for instruction match"""
fmt = "instruction(loc_%08X)"
class CapaExplorerDefaultItem(CapaExplorerDataItem):
@@ -292,9 +296,7 @@ class CapaExplorerDefaultItem(CapaExplorerDataItem):
assert isinstance(location, AbsoluteVirtualAddress)
ea = int(location)
super(CapaExplorerDefaultItem, self).__init__(
parent, [display, ea_to_hex(ea) if ea is not None else "", details]
)
super().__init__(parent, [display, ea_to_hex(ea) if ea is not None else "", details])
class CapaExplorerFeatureItem(CapaExplorerDataItem):
@@ -313,9 +315,9 @@ class CapaExplorerFeatureItem(CapaExplorerDataItem):
if location:
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
ea = int(location)
super(CapaExplorerFeatureItem, self).__init__(parent, [display, ea_to_hex(ea), details])
super().__init__(parent, [display, ea_to_hex(ea), details])
else:
super(CapaExplorerFeatureItem, self).__init__(parent, [display, "", details])
super().__init__(parent, [display, "", details])
class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
@@ -333,7 +335,7 @@ class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
assert isinstance(location, AbsoluteVirtualAddress)
ea = int(location)
details = capa.ida.helpers.get_disasm_line(ea)
super(CapaExplorerInstructionViewItem, self).__init__(parent, display, location=location, details=details)
super().__init__(parent, display, location=location, details=details)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
@@ -359,7 +361,7 @@ class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
byte_snap = codecs.encode(byte_snap, "hex").upper()
details = " ".join([byte_snap[i : i + 2].decode() for i in range(0, len(byte_snap), 2)])
super(CapaExplorerByteViewItem, self).__init__(parent, display, location=location, details=details)
super().__init__(parent, display, location=location, details=details)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
@@ -376,5 +378,5 @@ class CapaExplorerStringViewItem(CapaExplorerFeatureItem):
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
ea = int(location)
super(CapaExplorerStringViewItem, self).__init__(parent, display, location=location, details=value)
super().__init__(parent, display, location=location, details=value)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)

View File

@@ -6,7 +6,7 @@
# 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 Set, Dict, List, Tuple
from typing import Set, Dict, List, Tuple, Optional
from collections import deque
import idc
@@ -31,6 +31,7 @@ from capa.ida.plugin.item import (
CapaExplorerSubscopeItem,
CapaExplorerRuleMatchItem,
CapaExplorerStringViewItem,
CapaExplorerInstructionItem,
CapaExplorerInstructionViewItem,
)
from capa.features.address import Address, AbsoluteVirtualAddress
@@ -50,7 +51,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
def __init__(self, parent=None):
"""initialize model"""
super(CapaExplorerDataModel, self).__init__(parent)
super().__init__(parent)
# root node does not have parent, contains header columns
self.root_node = CapaExplorerDataItem(None, ["Rule Information", "Address", "Details"])
@@ -142,6 +143,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
CapaExplorerFunctionItem,
CapaExplorerFeatureItem,
CapaExplorerSubscopeItem,
CapaExplorerInstructionItem,
),
)
and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION
@@ -363,12 +365,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
@param doc: result doc
"""
if isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement)):
display = statement.type
if statement.description:
display += " (%s)" % statement.description
return CapaExplorerDefaultItem(parent, display)
elif isinstance(statement, rd.NotStatement):
if isinstance(statement, rd.CompoundStatement):
if statement.type != rd.CompoundStatementType.NOT:
display = statement.type
if statement.description:
display += " (%s)" % statement.description
return CapaExplorerDefaultItem(parent, display)
elif isinstance(statement, rd.CompoundStatement) and statement.type == rd.CompoundStatementType.NOT:
# TODO: do we display 'not'
pass
elif isinstance(statement, rd.SomeStatement):
@@ -422,7 +425,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
return
# optional statement with no successful children is empty
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL:
if not any(map(lambda m: m.success, match.children)):
return
@@ -441,37 +444,45 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
self.render_capa_doc_match(parent2, child, doc)
def render_capa_doc_by_function(self, doc: rd.ResultDocument):
""" """
matches_by_function: Dict[int, Tuple[CapaExplorerFunctionItem, Set[str]]] = {}
"""render rule matches by function meaning each rule match is nested under function where it was found"""
matches_by_function: Dict[AbsoluteVirtualAddress, Tuple[CapaExplorerFunctionItem, Set[str]]] = {}
for rule in rutils.capability_rules(doc):
for location_, _ in rule.matches:
location = location_.to_capa()
match_eas: List[int] = []
if not isinstance(location, AbsoluteVirtualAddress):
# only handle matches with a VA
continue
ea = int(location)
# initial pass of rule matches
for addr_, _ in rule.matches:
addr: Address = addr_.to_capa()
if isinstance(addr, AbsoluteVirtualAddress):
match_eas.append(int(addr))
ea = capa.ida.helpers.get_func_start_ea(ea)
if ea is None:
# file scope, skip rendering in this mode
for ea in match_eas:
func_ea: Optional[int] = capa.ida.helpers.get_func_start_ea(ea)
if func_ea is None:
# rule match address is not located in a defined function
continue
if not matches_by_function.get(ea, ()):
# new function root
matches_by_function[ea] = (
CapaExplorerFunctionItem(self.root_node, location, can_check=False),
func_address: AbsoluteVirtualAddress = AbsoluteVirtualAddress(func_ea)
if not matches_by_function.get(func_address, ()):
# create a new function root to nest its rule matches; Note: we must use the address of the
# function here so everything is displayed properly
matches_by_function[func_address] = (
CapaExplorerFunctionItem(self.root_node, func_address, can_check=False),
set(),
)
function_root, match_cache = matches_by_function[ea]
if rule.meta.name in match_cache:
# rule match already rendered for this function root, skip it
func_root, func_match_cache = matches_by_function[func_address]
if rule.meta.name in func_match_cache:
# only nest each rule once, so if found, skip
continue
match_cache.add(rule.meta.name)
# add matched rule to its function cache; create a new rule node whose parent is the matched
# function node
func_match_cache.add(rule.meta.name)
CapaExplorerRuleItem(
function_root,
func_root,
rule.meta.name,
rule.meta.namespace or "",
len(rule.matches),
len([ea for ea in match_eas if capa.ida.helpers.get_func_start_ea(ea) == func_ea]),
rule.source,
can_check=False,
)
@@ -483,7 +494,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
rule_namespace = rule.meta.namespace or ""
parent = CapaExplorerRuleItem(self.root_node, rule_name, rule_namespace, len(rule.matches), rule.source)
for (location_, match) in rule.matches:
for location_, match in rule.matches:
location = location_.to_capa()
parent2: CapaExplorerDataItem
@@ -493,6 +504,8 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
parent2 = CapaExplorerFunctionItem(parent, location)
elif rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE:
parent2 = CapaExplorerBlockItem(parent, location)
elif rule.meta.scope == capa.rules.INSTRUCTION_SCOPE:
parent2 = CapaExplorerInstructionItem(parent, location)
else:
raise RuntimeError("unexpected rule scope: " + str(rule.meta.scope))
@@ -520,11 +533,19 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
@param feature: capa feature read from doc
"""
key = feature.type
value = getattr(feature, feature.type)
value = feature.dict(by_alias=True).get(feature.type)
if value:
if isinstance(feature, frzf.StringFeature):
value = '"%s"' % capa.features.common.escape_string(value)
if isinstance(feature, frzf.PropertyFeature) and feature.access is not None:
key = f"property/{feature.access}"
elif isinstance(feature, frzf.OperandNumberFeature):
key = f"operand[{feature.index}].number"
elif isinstance(feature, frzf.OperandOffsetFeature):
key = f"operand[{feature.index}].offset"
if feature.description:
return "%s(%s = %s)" % (key, value, feature.description)
else:
@@ -634,6 +655,8 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
frzf.MnemonicFeature,
frzf.NumberFeature,
frzf.OffsetFeature,
frzf.OperandNumberFeature,
frzf.OperandOffsetFeature,
),
):
# display instruction preview

View File

@@ -22,7 +22,7 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
"""initialize proxy filter"""
super(CapaExplorerRangeProxyModel, self).__init__(parent)
super().__init__(parent)
self.min_ea = None
self.max_ea = None
@@ -92,7 +92,7 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
@param parent: QModelIndex of parent
"""
# filter not set
if self.min_ea is None and self.max_ea is None:
if self.min_ea is None or self.max_ea is None:
return True
index = self.sourceModel().index(row, 0, parent)
@@ -145,7 +145,7 @@ class CapaExplorerSearchProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
""" """
super(CapaExplorerSearchProxyModel, self).__init__(parent)
super().__init__(parent)
self.query = ""
self.setFilterKeyColumn(-1) # all columns

View File

@@ -18,7 +18,7 @@ import capa.ida.helpers
import capa.features.common
import capa.features.basicblock
from capa.ida.plugin.item import CapaExplorerFunctionItem
from capa.features.address import Address, _NoAddress
from capa.features.address import AbsoluteVirtualAddress, _NoAddress
from capa.ida.plugin.model import CapaExplorerDataModel
MAX_SECTION_SIZE = 750
@@ -74,7 +74,7 @@ def parse_node_for_feature(feature, description, comment, depth):
if feature.startswith("#"):
display += "%s%s\n" % (" " * depth, feature)
elif description:
if feature.startswith(("- and", "- or", "- optional", "- basic block", "- not")):
if feature.startswith(("- and", "- or", "- optional", "- basic block", "- not", "- instruction:")):
display += "%s%s" % (" " * depth, feature)
if comment:
display += " # %s" % comment
@@ -174,16 +174,16 @@ def resize_columns_to_content(header):
class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
INDENT = " " * 2
def __init__(self, parent=None):
""" """
super(CapaExplorerRulegenPreview, self).__init__(parent)
super().__init__(parent)
self.setFont(QtGui.QFont("Courier", weight=QtGui.QFont.Bold))
self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.setAcceptRichText(False)
def reset_view(self):
""" """
@@ -200,7 +200,8 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
" authors:",
" - %s" % author,
" scope: %s" % scope,
" references: <insert_references>",
" references:",
" - <insert_references>",
" examples:",
" - %s:0x%X" % (capa.ida.helpers.get_file_md5().upper(), ea)
if ea
@@ -253,7 +254,7 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
lines_modified = 0
first_modified = False
change = []
for (lineno, line) in enumerate(plain[start_lineno : end_lineno + 1]):
for lineno, line in enumerate(plain[start_lineno : end_lineno + 1]):
if line.startswith(self.INDENT):
if lineno == 0:
# keep track if first line is modified, so we can properly display
@@ -284,7 +285,7 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
self.set_selection(select_start_ppos, select_end_ppos, len(self.toPlainText()))
self.verticalScrollBar().setSliderPosition(scroll_ppos)
else:
super(CapaExplorerRulegenPreview, self).keyPressEvent(e)
super().keyPressEvent(e)
def count_previous_lines_from_block(self, block):
"""calculate number of lines preceding block"""
@@ -305,12 +306,11 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
updated = QtCore.pyqtSignal()
def __init__(self, preview, parent=None):
""" """
super(CapaExplorerRulegenEditor, self).__init__(parent)
super().__init__(parent)
self.preview = preview
@@ -374,18 +374,18 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
def dragMoveEvent(self, e):
""" """
super(CapaExplorerRulegenEditor, self).dragMoveEvent(e)
super().dragMoveEvent(e)
def dragEventEnter(self, e):
""" """
super(CapaExplorerRulegenEditor, self).dragEventEnter(e)
super().dragEventEnter(e)
def dropEvent(self, e):
""" """
if not self.indexAt(e.pos()).isValid():
return
super(CapaExplorerRulegenEditor, self).dropEvent(e)
super().dropEvent(e)
self.update_preview()
expand_tree(self.invisibleRootItem())
@@ -427,6 +427,10 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
# add default child expression when nesting under basic block
new_parent.setExpanded(True)
new_parent = self.new_expression_node(new_parent, ("- or:", ""))
elif "instruction" in action.data()[0]:
# add default child expression when nesting under instruction
new_parent.setExpanded(True)
new_parent = self.new_expression_node(new_parent, ("- or:", ""))
for o in self.get_features(selected=True):
# take child from its parent by index, add to new parent
@@ -447,6 +451,16 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
for child in children:
new_parent.addChild(child)
new_parent.setExpanded(True)
elif "instruction" in expression and "instruction" not in o.text(
CapaExplorerRulegenEditor.get_column_feature_index()
):
# current expression is "instruction", and not changing to "instruction" expression
children = o.takeChildren()
new_parent = self.new_expression_node(o, ("- or:", ""))
for child in children:
new_parent.addChild(child)
new_parent.setExpanded(True)
o.setText(CapaExplorerRulegenEditor.get_column_feature_index(), expression)
def slot_clear_all(self, action):
@@ -520,6 +534,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
("not", ("- not:",), self.slot_nest_features),
("optional", ("- optional:",), self.slot_nest_features),
("basic block", ("- basic block:",), self.slot_nest_features),
("instruction", ("- instruction:",), self.slot_nest_features),
)
# build submenu with modify actions
@@ -541,6 +556,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
("not", ("- not:", self.itemAt(pos)), self.slot_edit_expression),
("optional", ("- optional:", self.itemAt(pos)), self.slot_edit_expression),
("basic block", ("- basic block:", self.itemAt(pos)), self.slot_edit_expression),
("instruction", ("- instruction:", self.itemAt(pos)), self.slot_edit_expression),
)
# build submenu with modify actions
@@ -601,7 +617,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
""" """
o = QtWidgets.QTreeWidgetItem(parent)
self.set_expression_node(o)
for (i, v) in enumerate(values):
for i, v in enumerate(values):
o.setText(i, v)
return o
@@ -609,7 +625,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
""" """
o = QtWidgets.QTreeWidgetItem(parent)
self.set_feature_node(o)
for (i, v) in enumerate(values):
for i, v in enumerate(values):
o.setText(i, v)
return o
@@ -617,7 +633,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
""" """
o = QtWidgets.QTreeWidgetItem(parent)
self.set_comment_node(o)
for (i, v) in enumerate(values):
for i, v in enumerate(values):
o.setText(i, v)
return o
@@ -636,7 +652,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
counted = list(zip(Counter(features).keys(), Counter(features).values()))
# single features
for (k, v) in filter(lambda t: t[1] == 1, counted):
for k, v in filter(lambda t: t[1] == 1, counted):
if isinstance(k, (capa.features.common.String,)):
value = '"%s"' % capa.features.common.escape_string(k.get_value_str())
else:
@@ -644,7 +660,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
self.new_feature_node(top_node, ("- %s: %s" % (k.name.lower(), value), ""))
# n > 1 features
for (k, v) in filter(lambda t: t[1] > 1, counted):
for k, v in filter(lambda t: t[1] > 1, counted):
if k.value:
if isinstance(k, (capa.features.common.String,)):
value = '"%s"' % capa.features.common.escape_string(k.get_value_str())
@@ -689,11 +705,11 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
node = QtWidgets.QTreeWidgetItem(parent)
# set node text to data parsed from feature
for (idx, text) in enumerate((feature, comment, description)):
for idx, text in enumerate((feature, comment, description)):
node.setText(idx, text)
# we need to set our own type so we can control the GUI accordingly
if feature.startswith(("- and:", "- or:", "- not:", "- basic block:", "- optional:")):
if feature.startswith(("- and:", "- or:", "- not:", "- basic block:", "- instruction:", "- optional:")):
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_expression())
elif feature.startswith("#"):
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_comment())
@@ -784,7 +800,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
def __init__(self, editor, parent=None):
""" """
super(CapaExplorerRulegenFeatures, self).__init__(parent)
super().__init__(parent)
self.parent_items = {}
self.editor = editor
@@ -981,7 +997,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
o = QtWidgets.QTreeWidgetItem(parent)
self.set_parent_node(o)
for (i, v) in enumerate(data):
for i, v in enumerate(data):
o.setText(i, v)
if feature:
o.setData(0, 0x100, feature)
@@ -993,7 +1009,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
o = QtWidgets.QTreeWidgetItem(parent)
self.set_leaf_node(o)
for (i, v) in enumerate(data):
for i, v in enumerate(data):
o.setText(i, v)
if feature:
o.setData(0, 0x100, feature)
@@ -1012,8 +1028,10 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
self.parent_items = {}
def format_address(e):
assert isinstance(e, Address)
return "%X" % e if not isinstance(e, _NoAddress) else ""
if isinstance(e, AbsoluteVirtualAddress):
return "%X" % int(e)
else:
return ""
def format_feature(feature):
""" """
@@ -1023,7 +1041,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
value = '"%s"' % capa.features.common.escape_string(value)
return "%s(%s)" % (name, value)
for (feature, addrs) in sorted(features.items(), key=lambda k: sorted(k[1])):
for feature, addrs in sorted(features.items(), key=lambda k: sorted(k[1])):
if isinstance(feature, capa.features.basicblock.BasicBlock):
# filter basic blocks for now, we may want to add these back in some time
# in the future
@@ -1056,7 +1074,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
else:
# some features may not have an address e.g. "format"
addr = _NoAddress()
for (i, v) in enumerate((format_feature(feature), format_address(addr))):
for i, v in enumerate((format_feature(feature), format_address(addr))):
self.parent_items[feature].setText(i, v)
self.parent_items[feature].setData(0, 0x100, feature)
@@ -1072,7 +1090,7 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
def __init__(self, model, parent=None):
"""initialize view"""
super(CapaExplorerQtreeView, self).__init__(parent)
super().__init__(parent)
self.setModel(model)

View File

@@ -17,11 +17,10 @@ import os.path
import argparse
import datetime
import textwrap
import warnings
import itertools
import contextlib
import collections
from typing import Any, Dict, List, Tuple
from typing import Any, Dict, List, Tuple, Callable
import halo
import tqdm
@@ -34,6 +33,7 @@ import capa.rules
import capa.engine
import capa.version
import capa.render.json
import capa.rules.cache
import capa.render.default
import capa.render.verbose
import capa.features.common
@@ -66,25 +66,24 @@ from capa.features.common import (
FORMAT_DOTNET,
FORMAT_FREEZE,
)
from capa.features.address import NO_ADDRESS
from capa.features.address import NO_ADDRESS, Address
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
RULES_PATH_DEFAULT_STRING = "(embedded rules)"
SIGNATURES_PATH_DEFAULT_STRING = "(embedded signatures)"
BACKEND_VIV = "vivisect"
BACKEND_SMDA = "smda"
BACKEND_DOTNET = "dotnet"
E_MISSING_RULES = -10
E_MISSING_FILE = -11
E_INVALID_RULE = -12
E_CORRUPT_FILE = -13
E_FILE_LIMITATION = -14
E_INVALID_SIG = -15
E_INVALID_FILE_TYPE = -16
E_INVALID_FILE_ARCH = -17
E_INVALID_FILE_OS = -18
E_UNSUPPORTED_IDA_VERSION = -19
E_MISSING_RULES = 10
E_MISSING_FILE = 11
E_INVALID_RULE = 12
E_CORRUPT_FILE = 13
E_FILE_LIMITATION = 14
E_INVALID_SIG = 15
E_INVALID_FILE_TYPE = 16
E_INVALID_FILE_ARCH = 17
E_INVALID_FILE_OS = 18
E_UNSUPPORTED_IDA_VERSION = 19
logger = logging.getLogger("capa")
@@ -332,7 +331,7 @@ def has_file_limitation(rules: RuleSet, capabilities: MatchResults, is_standalon
logger.warning("-" * 80)
for line in file_limitation_rule.meta.get("description", "").split("\n"):
logger.warning(" " + line)
logger.warning(" %s", line)
logger.warning(" Identified via rule: %s", file_limitation_rule.name)
if is_standalone:
logger.warning(" ")
@@ -433,7 +432,7 @@ def get_default_signatures() -> List[str]:
logger.debug("signatures path: %s", sigs_path)
ret = []
for root, dirs, files in os.walk(sigs_path):
for root, _, files in os.walk(sigs_path):
for file in files:
if not (file.endswith(".pat") or file.endswith(".pat.gz") or file.endswith(".sig")):
continue
@@ -461,6 +460,7 @@ def get_workspace(path, format_, sigpaths):
# lazy import enables us to not require viv if user wants SMDA, for example.
import viv_utils
import viv_utils.flirt
logger.debug("generating vivisect workspace for: %s", path)
# TODO should not be auto at this point, anymore
@@ -513,22 +513,7 @@ def get_extractor(
return capa.features.extractors.dnfile.extractor.DnfileFeatureExtractor(path)
if backend == "smda":
from smda.SmdaConfig import SmdaConfig
from smda.Disassembler import Disassembler
import capa.features.extractors.smda.extractor
logger.warning("Deprecation warning: v4.0 will be the last capa version to support the SMDA backend.")
warnings.warn("v4.0 will be the last capa version to support the SMDA backend.", DeprecationWarning)
smda_report = None
with halo.Halo(text="analyzing program", spinner="simpleDots", stream=sys.stderr, enabled=not disable_progress):
config = SmdaConfig()
config.STORE_BUFFER = True
smda_disasm = Disassembler(config)
smda_report = smda_disasm.disassembleFile(path)
return capa.features.extractors.smda.extractor.SmdaFeatureExtractor(smda_report, path)
# default to use vivisect backend
else:
import capa.features.extractors.viv.extractor
@@ -551,12 +536,12 @@ def get_extractor(
def get_file_extractors(sample: str, format_: str) -> List[FeatureExtractor]:
file_extractors: List[FeatureExtractor] = list()
if format_ == capa.features.extractors.common.FORMAT_PE:
if format_ == FORMAT_PE:
file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(sample))
dnfile_extractor = capa.features.extractors.dnfile_.DnfileFeatureExtractor(sample)
if dnfile_extractor.is_dotnet_file():
file_extractors.append(dnfile_extractor)
elif format_ == FORMAT_DOTNET:
file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(sample))
file_extractors.append(capa.features.extractors.dnfile_.DnfileFeatureExtractor(sample))
elif format_ == capa.features.extractors.common.FORMAT_ELF:
file_extractors.append(capa.features.extractors.elffile.ElfFeatureExtractor(sample))
@@ -577,7 +562,10 @@ def is_nursery_rule_path(path: str) -> bool:
return "nursery" in path
def get_rules(rule_paths: List[str], disable_progress=False) -> List[Rule]:
def collect_rule_file_paths(rule_paths: List[str]) -> List[str]:
"""
collect all rule file paths, including those in subdirectories.
"""
rule_file_paths = []
for rule_path in rule_paths:
if not os.path.exists(rule_path):
@@ -587,7 +575,7 @@ def get_rules(rule_paths: List[str], disable_progress=False) -> List[Rule]:
rule_file_paths.append(rule_path)
elif os.path.isdir(rule_path):
logger.debug("reading rules from directory %s", rule_path)
for root, dirs, files in os.walk(rule_path):
for root, _, files in os.walk(rule_path):
if ".git" in root:
# the .github directory contains CI config in capa-rules
# this includes some .yml files
@@ -605,28 +593,69 @@ def get_rules(rule_paths: List[str], disable_progress=False) -> List[Rule]:
rule_path = os.path.join(root, file)
rule_file_paths.append(rule_path)
return rule_file_paths
# TypeAlias. note: using `foo: TypeAlias = bar` is Python 3.10+
RulePath = str
def on_load_rule_default(_path: RulePath, i: int, _total: int) -> None:
return
def get_rules(
rule_paths: List[RulePath],
cache_dir=None,
on_load_rule: Callable[[RulePath, int, int], None] = on_load_rule_default,
) -> RuleSet:
"""
args:
rule_paths: list of paths to rules files or directories containing rules files
cache_dir: directory to use for caching rules, or will use the default detected cache directory if None
on_load_rule: callback to invoke before a rule is loaded, use for progress or cancellation
"""
if cache_dir is None:
cache_dir = capa.rules.cache.get_default_cache_directory()
# rule_paths may contain directory paths,
# so search for file paths recursively.
rule_file_paths = collect_rule_file_paths(rule_paths)
# this list is parallel to `rule_file_paths`:
# rule_file_paths[i] corresponds to rule_contents[i].
rule_contents = []
for file_path in rule_file_paths:
with open(file_path, "rb") as f:
rule_contents.append(f.read())
ruleset = capa.rules.cache.load_cached_ruleset(cache_dir, rule_contents)
if ruleset is not None:
return ruleset
rules = [] # type: List[Rule]
pbar = tqdm.tqdm
if disable_progress:
# do not use tqdm to avoid unnecessary side effects when caller intends
# to disable progress completely
pbar = lambda s, *args, **kwargs: s
total_rule_count = len(rule_file_paths)
for i, (path, content) in enumerate(zip(rule_file_paths, rule_contents)):
on_load_rule(path, i, total_rule_count)
for rule_file_path in pbar(list(rule_file_paths), desc="loading ", unit=" rules"):
try:
rule = capa.rules.Rule.from_yaml_file(rule_file_path)
rule = capa.rules.Rule.from_yaml(content.decode("utf-8"))
except capa.rules.InvalidRule:
raise
else:
rule.meta["capa/path"] = rule_file_path
if is_nursery_rule_path(rule_file_path):
rule.meta["capa/path"] = path
if is_nursery_rule_path(path):
rule.meta["capa/nursery"] = True
rules.append(rule)
logger.debug("loaded rule: '%s' with scope: %s", rule.name, rule.scope)
return rules
ruleset = capa.rules.RuleSet(rules)
capa.rules.cache.cache_ruleset(cache_dir, ruleset)
return ruleset
def get_signatures(sigs_path):
@@ -638,7 +667,7 @@ def get_signatures(sigs_path):
paths.append(sigs_path)
elif os.path.isdir(sigs_path):
logger.debug("reading signatures from directory %s", os.path.abspath(os.path.normpath(sigs_path)))
for root, dirs, files in os.walk(sigs_path):
for root, _, files in os.walk(sigs_path):
for file in files:
if file.endswith((".pat", ".pat.gz", ".sig")):
sig_path = os.path.join(root, file)
@@ -717,8 +746,8 @@ def compute_layout(rules, extractor, capabilities):
otherwise, we may pollute the json document with
a large amount of un-referenced data.
"""
functions_by_bb = {}
bbs_by_function = {}
functions_by_bb: Dict[Address, Address] = {}
bbs_by_function: Dict[Address, List[Address]] = {}
for f in extractor.get_functions():
bbs_by_function[f.address] = []
for bb in extractor.get_basic_blocks(f):
@@ -729,7 +758,7 @@ def compute_layout(rules, extractor, capabilities):
for rule_name, matches in capabilities.items():
rule = rules[rule_name]
if rule.meta.get("scope") == capa.rules.BASIC_BLOCK_SCOPE:
for (addr, match) in matches:
for addr, _ in matches:
assert addr in functions_by_bb
matched_bbs.add(addr)
@@ -830,7 +859,7 @@ def install_common_args(parser, wanted=None):
"--backend",
type=str,
help="select the backend to use",
choices=(BACKEND_VIV, BACKEND_SMDA),
choices=(BACKEND_VIV,),
default=BACKEND_VIV,
)
@@ -865,6 +894,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:
- is_default_rules: if the default rules were used.
args:
args (argparse.Namespace): parsed arguments that included at least `install_common_args` args.
"""
@@ -924,6 +956,7 @@ def handle_common_args(args):
return E_MISSING_RULES
rules_paths.append(default_rule_path)
args.is_default_rules = True
else:
rules_paths = args.rules
@@ -933,6 +966,8 @@ def handle_common_args(args):
for rule_path in rules_paths:
logger.debug("using rules path: %s", rule_path)
args.is_default_rules = False
args.rules = rules_paths
if hasattr(args, "signatures"):
@@ -1010,20 +1045,27 @@ def main(argv=None):
if format_ == FORMAT_AUTO:
try:
format_ = get_auto_format(args.sample)
except PEFormatError as e:
logger.error("Input file '%s' is not a valid PE file: %s", args.sample, str(e))
return E_CORRUPT_FILE
except UnsupportedFormatError:
log_unsupported_format_error()
return E_INVALID_FILE_TYPE
try:
rules = get_rules(args.rules, disable_progress=args.quiet)
rules = capa.rules.RuleSet(rules)
if is_running_standalone() and args.is_default_rules:
cache_dir = os.path.join(get_default_root(), "cache")
else:
cache_dir = capa.rules.cache.get_default_cache_directory()
rules = get_rules(args.rules, cache_dir=cache_dir)
logger.debug(
"successfully loaded %s rules",
# during the load of the RuleSet, we extract subscope statements into their own rules
# that are subsequently `match`ed upon. this inflates the total rule count.
# so, filter out the subscope rules when reporting total number of loaded rules.
len([i for i in filter(lambda r: not r.is_subscope_rule(), rules.rules.values())]),
len(list(filter(lambda r: not r.is_subscope_rule(), rules.rules.values()))),
)
if args.tag:
rules = rules.filter_rules_by_meta(args.tag)
@@ -1034,12 +1076,12 @@ def main(argv=None):
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
logger.error("%s", str(e))
logger.error(
"Please ensure you're using the rules that correspond to your major version of capa (%s)",
capa.version.get_major_version(),
"Make sure your file directory contains properly formatted capa rules. You can download the standard "
"collection of capa rules from https://github.com/mandiant/capa-rules/releases."
)
logger.error(
"You can check out these rules with the following command:\n %s",
capa.version.get_rules_checkout_command(),
"Please ensure you're using the rules that correspond to your major version of capa (%s)",
capa.version.get_major_version(),
)
logger.error(
"Or, for more details, see the rule set documentation here: %s",
@@ -1073,9 +1115,6 @@ def main(argv=None):
logger.error("Input file '%s' is not a valid ELF file: %s", args.sample, str(e))
return E_CORRUPT_FILE
if isinstance(file_extractor, capa.features.extractors.dnfile_.DnfileFeatureExtractor):
format_ = FORMAT_DOTNET
# file limitations that rely on non-file scope won't be detected here.
# nor on FunctionName features, because pefile doesn't support this.
if has_file_limitation(rules, pure_file_capabilities):
@@ -1166,8 +1205,7 @@ def ida_main():
rules_path = os.path.join(get_default_root(), "rules")
logger.debug("rule path: %s", rules_path)
rules = get_rules(rules_path)
rules = capa.rules.RuleSet(rules)
rules = get_rules([rules_path])
meta = capa.ida.helpers.collect_metadata([rules_path])

View File

@@ -47,7 +47,7 @@ def optimize_statement(statement):
if isinstance(statement, (ceng.And, ceng.Or, ceng.Some)):
# has .children
statement.children = sorted(statement.children, key=lambda n: get_node_cost(n))
statement.children = sorted(statement.children, key=get_node_cost)
return
elif isinstance(statement, (ceng.Not, ceng.Range)):
# has .child

View File

@@ -1,8 +1,8 @@
import typing
import collections
from typing import Dict
# this structure is unstable and may change before the next major release.
counters: Dict[str, int] = collections.Counter()
counters: typing.Counter[str] = collections.Counter()
def reset():

View File

@@ -133,7 +133,7 @@ def render_attack(doc: rd.ResultDocument, ostream: StringIO):
rows = []
for tactic, techniques in sorted(tactics.items()):
inner_rows = []
for (technique, subtechnique, id) in sorted(techniques):
for technique, subtechnique, id in sorted(techniques):
if not subtechnique:
inner_rows.append("%s %s" % (rutils.bold(technique), id))
else:
@@ -176,7 +176,7 @@ def render_mbc(doc: rd.ResultDocument, ostream: StringIO):
rows = []
for objective, behaviors in sorted(objectives.items()):
inner_rows = []
for (behavior, method, id) in sorted(behaviors):
for behavior, method, id in sorted(behaviors):
if not method:
inner_rows.append("%s [%s]" % (rutils.bold(behavior), id))
else:

View File

@@ -15,6 +15,7 @@ import capa.engine
import capa.features.common
import capa.features.freeze as frz
import capa.features.address
import capa.features.freeze.features as frzf
from capa.rules import RuleSet
from capa.engine import MatchResults
from capa.helpers import assert_never
@@ -75,7 +76,7 @@ class Analysis(FrozenModel):
class Metadata(FrozenModel):
timestamp: datetime.datetime
version: str
argv: Tuple[str, ...]
argv: Optional[Tuple[str, ...]]
sample: Sample
analysis: Analysis
@@ -99,64 +100,56 @@ class Metadata(FrozenModel):
rules=meta["analysis"]["rules"],
base_address=frz.Address.from_capa(meta["analysis"]["base_address"]),
layout=Layout(
functions=[
functions=tuple(
FunctionLayout(
address=frz.Address.from_capa(address),
matched_basic_blocks=[
matched_basic_blocks=tuple(
BasicBlockLayout(address=frz.Address.from_capa(bb)) for bb in f["matched_basic_blocks"]
],
),
)
for address, f in meta["analysis"]["layout"]["functions"].items()
]
)
),
feature_counts=FeatureCounts(
file=meta["analysis"]["feature_counts"]["file"],
functions=[
functions=tuple(
FunctionFeatureCount(address=frz.Address.from_capa(address), count=count)
for address, count in meta["analysis"]["feature_counts"]["functions"].items()
],
),
),
library_functions=[
library_functions=tuple(
LibraryFunction(address=frz.Address.from_capa(address), name=name)
for address, name in meta["analysis"]["library_functions"].items()
],
),
),
)
class CompoundStatementType:
AND = "and"
OR = "or"
NOT = "not"
OPTIONAL = "optional"
class StatementModel(FrozenModel):
...
class AndStatement(StatementModel):
type = "and"
description: Optional[str]
class OrStatement(StatementModel):
type = "or"
description: Optional[str]
class NotStatement(StatementModel):
type = "not"
description: Optional[str]
class CompoundStatement(StatementModel):
type: str
description: Optional[str] = None
class SomeStatement(StatementModel):
type = "some"
description: Optional[str]
description: Optional[str] = None
count: int
class OptionalStatement(StatementModel):
type = "optional"
description: Optional[str]
class RangeStatement(StatementModel):
type = "range"
description: Optional[str]
description: Optional[str] = None
min: int
max: int
child: frz.Feature
@@ -164,18 +157,16 @@ class RangeStatement(StatementModel):
class SubscopeStatement(StatementModel):
type = "subscope"
description: Optional[str]
scope = capa.rules.Scope
description: Optional[str] = None
scope: capa.rules.Scope
Statement = Union[
OptionalStatement,
AndStatement,
OrStatement,
NotStatement,
SomeStatement,
# Note! order matters, see #1161
RangeStatement,
SomeStatement,
SubscopeStatement,
CompoundStatement,
]
@@ -185,18 +176,12 @@ class StatementNode(FrozenModel):
def statement_from_capa(node: capa.engine.Statement) -> Statement:
if isinstance(node, capa.engine.And):
return AndStatement(description=node.description)
elif isinstance(node, capa.engine.Or):
return OrStatement(description=node.description)
elif isinstance(node, capa.engine.Not):
return NotStatement(description=node.description)
if isinstance(node, (capa.engine.And, capa.engine.Or, capa.engine.Not)):
return CompoundStatement(type=node.__class__.__name__.lower(), description=node.description)
elif isinstance(node, capa.engine.Some):
if node.count == 0:
return OptionalStatement(description=node.description)
return CompoundStatement(type=CompoundStatementType.OPTIONAL, description=node.description)
else:
return SomeStatement(
@@ -293,7 +278,7 @@ class Match(BaseModel):
# finally, splice that logic into this tree.
if (
isinstance(node, FeatureNode)
and isinstance(node.feature, frz.features.MatchFeature)
and isinstance(node.feature, frzf.MatchFeature)
# only add subtree on success,
# because there won't be results for the other rule on failure.
and success
@@ -375,14 +360,14 @@ class Match(BaseModel):
def parse_parts_id(s: str):
id = ""
id_ = ""
parts = s.split("::")
if len(parts) > 0:
last = parts.pop()
last, _, id = last.rpartition(" ")
id = id.lstrip("[").rstrip("]")
last, _, id_ = last.rpartition(" ")
id_ = id_.lstrip("[").rstrip("]")
parts.append(last)
return parts, id
return tuple(parts), id_
class AttackSpec(FrozenModel):
@@ -408,7 +393,7 @@ class AttackSpec(FrozenModel):
tactic = ""
technique = ""
subtechnique = ""
parts, id = parse_parts_id(s)
parts, id_ = parse_parts_id(s)
if len(parts) > 0:
tactic = parts[0]
if len(parts) > 1:
@@ -421,7 +406,7 @@ class AttackSpec(FrozenModel):
tactic=tactic,
technique=technique,
subtechnique=subtechnique,
id=id,
id=id_,
)
@@ -448,7 +433,7 @@ class MBCSpec(FrozenModel):
objective = ""
behavior = ""
method = ""
parts, id = parse_parts_id(s)
parts, id_ = parse_parts_id(s)
if len(parts) > 0:
objective = parts[0]
if len(parts) > 1:
@@ -461,7 +446,7 @@ class MBCSpec(FrozenModel):
objective=objective,
behavior=behavior,
method=method,
id=id,
id=id_,
)
@@ -548,10 +533,10 @@ class ResultDocument(BaseModel):
rule_matches[rule_name] = RuleMatches(
meta=RuleMetadata.from_capa(rule),
source=rule.definition,
matches=[
matches=tuple(
(frz.Address.from_capa(addr), Match.from_capa(rules, capabilities, match))
for addr, match in matches
],
),
)
return ResultDocument(meta=Metadata.from_capa(meta), rules=rule_matches)

View File

@@ -24,12 +24,8 @@ def bold2(s: str) -> str:
return termcolor.colored(s, "green")
def hex(n: int) -> str:
"""render the given number using upper case hex, like: 0x123ABC"""
if n < 0:
return "-0x%X" % (-n)
else:
return "0x%X" % n
def warn(s: str) -> str:
return termcolor.colored(s, "yellow")
def format_parts_id(data: Union[rd.AttackSpec, rd.MBCSpec]):
@@ -41,7 +37,7 @@ def format_parts_id(data: Union[rd.AttackSpec, rd.MBCSpec]):
def capability_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]:
"""enumerate the rules in (namespace, name) order that are 'capability' rules (not lib/subscope/disposition/etc)."""
for (_, _, rule) in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
for _, _, rule in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
if rule.meta.lib:
continue
if rule.meta.is_subscope_rule:

View File

@@ -22,14 +22,14 @@ Unless required by applicable law or agreed to in writing, software distributed
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 enum
import tabulate
import dnfile.mdtable
import dncil.clr.token
import capa.rules
import capa.helpers
import capa.render.utils as rutils
import capa.features.freeze as frz
import capa.render.result_document
import capa.render.result_document as rd
from capa.rules import RuleSet
from capa.engine import MatchResults
@@ -37,18 +37,23 @@ from capa.engine import MatchResults
def format_address(address: frz.Address) -> str:
if address.type == frz.AddressType.ABSOLUTE:
return rutils.hex(address.value)
assert isinstance(address.value, int)
return capa.helpers.hex(address.value)
elif address.type == frz.AddressType.RELATIVE:
return f"base address+{rutils.hex(address.value)}"
assert isinstance(address.value, int)
return f"base address+{capa.helpers.hex(address.value)}"
elif address.type == frz.AddressType.FILE:
return f"file+{rutils.hex(address.value)}"
assert isinstance(address.value, int)
return f"file+{capa.helpers.hex(address.value)}"
elif address.type == frz.AddressType.DN_TOKEN:
token = dncil.clr.token.Token(address.value)
return f"token({rutils.hex(token.value)})"
assert isinstance(address.value, int)
return f"token({capa.helpers.hex(address.value)})"
elif address.type == frz.AddressType.DN_TOKEN_OFFSET:
assert isinstance(address.value, tuple)
token, offset = address.value
token = dncil.clr.token.Token(token)
return f"token({rutils.hex(token.value)})+{rutils.hex(offset)}"
assert isinstance(token, int)
assert isinstance(offset, int)
return f"token({capa.helpers.hex(token)})+{capa.helpers.hex(offset)}"
elif address.type == frz.AddressType.NO_ADDRESS:
return "global"
else:
@@ -130,6 +135,9 @@ def render_rules(ostream, doc: rd.ResultDocument):
if isinstance(v, list) and len(v) == 1:
v = v[0]
if isinstance(v, enum.Enum):
v = v.value
rows.append((key, v))
if rule.meta.scope != capa.rules.FILE_SCOPE:

View File

@@ -6,11 +6,12 @@
# 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 Dict, List, Iterable
from typing import Dict, Iterable
import tabulate
import capa.rules
import capa.helpers
import capa.render.utils as rutils
import capa.render.verbose
import capa.features.common
@@ -64,7 +65,7 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
ostream.write(" = %s" % statement.description)
ostream.writeln("")
elif isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement, rd.NotStatement)):
elif isinstance(statement, (rd.CompoundStatement)):
# emit `and:` `or:` `optional:` `not:`
ostream.write(statement.type)
@@ -87,7 +88,7 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
# so, we have to inline some of the feature rendering here.
child = statement.child
value = getattr(child, child.type)
value = child.dict(by_alias=True).get(child.type)
if value:
if isinstance(child, frzf.StringFeature):
@@ -128,26 +129,44 @@ def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
ostream.write(" " * indent)
key = feature.type
if isinstance(feature, frzf.ImportFeature):
if isinstance(feature, frzf.BasicBlockFeature):
# i don't think it makes sense to have standalone basic block features.
# we don't parse them from rules, only things like: `count(basic block) > 1`
raise ValueError("cannot render basic block feature directly")
elif isinstance(feature, frzf.ImportFeature):
# fixup access to Python reserved name
value = feature.import_
if isinstance(feature, frzf.ClassFeature):
elif isinstance(feature, frzf.ClassFeature):
value = feature.class_
else:
value = getattr(feature, key)
# convert attributes to dictionary using aliased names, if applicable
value = feature.dict(by_alias=True).get(key, None)
if key not in ("regex", "substring"):
if value is None:
raise ValueError("%s contains None" % key)
if not isinstance(feature, (frzf.RegexFeature, frzf.SubstringFeature)):
# like:
# number: 10 = SOME_CONSTANT @ 0x401000
if key == "string":
if isinstance(feature, frzf.StringFeature):
value = render_string_value(value)
if key == "number":
elif isinstance(
feature, (frzf.NumberFeature, frzf.OffsetFeature, frzf.OperandNumberFeature, frzf.OperandOffsetFeature)
):
assert isinstance(value, int)
value = hex(value)
value = capa.helpers.hex(value)
ostream.write(key)
ostream.write(": ")
if isinstance(feature, frzf.PropertyFeature) and feature.access is not None:
key = f"property/{feature.access}"
elif isinstance(feature, frzf.OperandNumberFeature):
key = f"operand[{feature.index}].number"
elif isinstance(feature, frzf.OperandOffsetFeature):
key = f"operand[{feature.index}].offset"
ostream.write(f"{key}: ")
if value:
ostream.write(rutils.bold2(value))
@@ -156,7 +175,7 @@ def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
ostream.write(capa.rules.DESCRIPTION_SEPARATOR)
ostream.write(feature.description)
if key not in ("os", "arch"):
if not isinstance(feature, (frzf.OSFeature, frzf.ArchFeature, frzf.FormatFeature)):
render_locations(ostream, match.locations)
ostream.write("\n")
else:
@@ -202,12 +221,12 @@ def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
return
# optional statement with no successful children is empty
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL:
if not any(map(lambda m: m.success, match.children)):
return
# not statement, so invert the child mode to show failed evaluations
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.NOT:
child_mode = MODE_FAILURE
elif mode == MODE_FAILURE:
@@ -216,12 +235,12 @@ def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
return
# optional statement with successful children is not relevant
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL:
if any(map(lambda m: m.success, match.children)):
return
# not statement, so invert the child mode to show successful evaluations
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.NOT:
child_mode = MODE_SUCCESS
else:
raise RuntimeError("unexpected mode: " + mode)
@@ -258,7 +277,7 @@ def render_rules(ostream, doc: rd.ResultDocument):
had_match = False
for (_, _, rule) in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
for _, _, rule in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
# default scope hides things like lib rules, malware-category rules, etc.
# but in vverbose mode, we really want to show everything.
#
@@ -266,17 +285,24 @@ def render_rules(ostream, doc: rd.ResultDocument):
if rule.meta.is_subscope_rule:
continue
lib_info = ""
count = len(rule.matches)
if count == 1:
capability = rutils.bold(rule.meta.name)
if rule.meta.lib:
lib_info = " (library rule)"
capability = "%s%s" % (rutils.bold(rule.meta.name), lib_info)
else:
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
if rule.meta.lib:
lib_info = ", only showing first match of library rule"
capability = "%s (%d matches%s)" % (rutils.bold(rule.meta.name), count, lib_info)
ostream.writeln(capability)
had_match = True
rows = []
rows.append(("namespace", rule.meta.namespace))
if not rule.meta.lib:
# library rules should not have a namespace
rows.append(("namespace", rule.meta.namespace))
if rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov:
rows.append(
@@ -289,7 +315,7 @@ def render_rules(ostream, doc: rd.ResultDocument):
if rule.meta.maec.malware_family:
rows.append(("maec/malware-family", rule.meta.maec.malware_family))
if rule.meta.maec.malware_category or rule.meta.maec.malware_category:
if rule.meta.maec.malware_category or rule.meta.maec.malware_category_ov:
rows.append(
("maec/malware-category", rule.meta.maec.malware_category or rule.meta.maec.malware_category_ov)
)
@@ -336,6 +362,10 @@ def render_rules(ostream, doc: rd.ResultDocument):
ostream.write("\n")
render_match(ostream, match, indent=1)
if rule.meta.lib:
# only show first match
break
ostream.write("\n")
if not had_match:

View File

@@ -27,7 +27,9 @@ except ImportError:
from typing import Any, Set, Dict, List, Tuple, Union, Iterator
import yaml
import pydantic
import ruamel.yaml
import yaml.parser
import capa.perf
import capa.engine as ceng
@@ -122,6 +124,7 @@ SUPPORTED_FEATURES: Dict[str, Set] = {
INSTRUCTION_SCOPE: {
capa.features.common.MatchedRule,
capa.features.insn.API,
capa.features.insn.Property,
capa.features.insn.Number,
capa.features.common.String,
capa.features.common.Bytes,
@@ -156,7 +159,7 @@ SUPPORTED_FEATURES[FUNCTION_SCOPE].update(SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE])
class InvalidRule(ValueError):
def __init__(self, msg):
super(InvalidRule, self).__init__()
super().__init__()
self.msg = msg
def __str__(self):
@@ -168,7 +171,7 @@ class InvalidRule(ValueError):
class InvalidRuleWithPath(InvalidRule):
def __init__(self, path, msg):
super(InvalidRuleWithPath, self).__init__(msg)
super().__init__(msg)
self.path = path
self.msg = msg
self.__cause__ = None
@@ -179,7 +182,7 @@ class InvalidRuleWithPath(InvalidRule):
class InvalidRuleSet(ValueError):
def __init__(self, msg):
super(InvalidRuleSet, self).__init__()
super().__init__()
self.msg = msg
def __str__(self):
@@ -231,23 +234,23 @@ def parse_range(s: str):
min_spec = min_spec.strip()
max_spec = max_spec.strip()
min = None
min_ = None
if min_spec:
min = parse_int(min_spec)
if min < 0:
min_ = parse_int(min_spec)
if min_ < 0:
raise InvalidRule("range min less than zero")
max = None
max_ = None
if max_spec:
max = parse_int(max_spec)
if max < 0:
max_ = parse_int(max_spec)
if max_ < 0:
raise InvalidRule("range max less than zero")
if min is not None and max is not None:
if max < min:
if min_ is not None and max_ is not None:
if max_ < min_:
raise InvalidRule("range max less than min")
return min, max
return min_, max_
def parse_feature(key: str):
@@ -290,6 +293,8 @@ def parse_feature(key: str):
return capa.features.common.Class
elif key == "namespace":
return capa.features.common.Namespace
elif key == "property":
return capa.features.insn.Property
else:
raise InvalidRule("unexpected statement: %s" % key)
@@ -535,14 +540,15 @@ def build_statements(d, scope: str):
index = key[len("operand[") : -len("].number")]
try:
index = int(index)
except ValueError:
raise InvalidRule("operand index must be an integer")
except ValueError as e:
raise InvalidRule("operand index must be an integer") from e
value, description = parse_description(d[key], key, d.get("description"))
assert isinstance(value, int)
try:
feature = capa.features.insn.OperandNumber(index, value, description=description)
except ValueError as e:
raise InvalidRule(str(e))
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
return feature
@@ -550,14 +556,15 @@ def build_statements(d, scope: str):
index = key[len("operand[") : -len("].offset")]
try:
index = int(index)
except ValueError:
raise InvalidRule("operand index must be an integer")
except ValueError as e:
raise InvalidRule("operand index must be an integer") from e
value, description = parse_description(d[key], key, d.get("description"))
assert isinstance(value, int)
try:
feature = capa.features.insn.OperandOffset(index, value, description=description)
except ValueError as e:
raise InvalidRule(str(e))
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
return feature
@@ -567,13 +574,27 @@ def build_statements(d, scope: str):
or (key == "arch" and d[key] not in capa.features.common.VALID_ARCH)
):
raise InvalidRule("unexpected %s value %s" % (key, d[key]))
elif key.startswith("property/"):
access = key[len("property/") :]
if access not in capa.features.common.VALID_FEATURE_ACCESS:
raise InvalidRule("unexpected %s access %s" % (key, access))
value, description = parse_description(d[key], key, d.get("description"))
try:
feature = capa.features.insn.Property(value, access=access, description=description)
except ValueError as e:
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
return feature
else:
Feature = parse_feature(key)
value, description = parse_description(d[key], key, d.get("description"))
try:
feature = Feature(value, description=description)
except ValueError as e:
raise InvalidRule(str(e))
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
return feature
@@ -588,7 +609,7 @@ def second(s: List[Any]) -> Any:
class Rule:
def __init__(self, name: str, scope: str, statement: Statement, meta, definition=""):
super(Rule, self).__init__()
super().__init__()
self.name = name
self.scope = scope
self.statement = statement
@@ -614,7 +635,7 @@ class Rule:
Returns:
List[str]: names of rules upon which this rule depends.
"""
deps = set([])
deps: Set[str] = set([])
def rec(statement):
if isinstance(statement, capa.features.common.MatchedRule):
@@ -631,6 +652,7 @@ class Rule:
deps.update(map(lambda r: r.name, namespaces[statement.value]))
else:
# not a namespace, assume its a rule name.
assert isinstance(statement.value, str)
deps.add(statement.value)
elif isinstance(statement, ceng.Statement):
@@ -646,7 +668,11 @@ class Rule:
def _extract_subscope_rules_rec(self, statement):
if isinstance(statement, ceng.Statement):
# for each child that is a subscope,
for subscope in filter(lambda statement: isinstance(statement, ceng.Subscope), statement.get_children()):
for child in statement.get_children():
if not isinstance(child, ceng.Subscope):
continue
subscope = child
# create a new rule from it.
# the name is a randomly generated, hopefully unique value.
@@ -717,7 +743,7 @@ class Rule:
return self.statement.evaluate(features, short_circuit=short_circuit)
@classmethod
def from_dict(cls, d, definition):
def from_dict(cls, d: Dict[str, Any], definition: str) -> "Rule":
meta = d["rule"]["meta"]
name = meta["name"]
# if scope is not specified, default to function scope.
@@ -751,14 +777,12 @@ class Rule:
# prefer to use CLoader to be fast, see #306
# on Linux, make sure you install libyaml-dev or similar
# on Windows, get WHLs from pyyaml.org/pypi
loader = yaml.CLoader
logger.debug("using libyaml CLoader.")
return yaml.CLoader
except:
loader = yaml.Loader
logger.debug("unable to import libyaml CLoader, falling back to Python yaml parser.")
logger.debug("this will be slower to load rules.")
return loader
return yaml.Loader
@staticmethod
def _get_ruamel_yaml_parser():
@@ -770,8 +794,9 @@ class Rule:
# use block mode, not inline json-like mode
y.default_flow_style = False
# leave quotes unchanged
y.preserve_quotes = True
# leave quotes unchanged.
# manually verified this property exists, even if mypy complains.
y.preserve_quotes = True # type: ignore
# indent lists by two spaces below their parent
#
@@ -782,12 +807,13 @@ class Rule:
y.indent(sequence=2, offset=2)
# avoid word wrapping
y.width = 4096
# manually verified this property exists, even if mypy complains.
y.width = 4096 # type: ignore
return y
@classmethod
def from_yaml(cls, s, use_ruamel=False):
def from_yaml(cls, s: str, use_ruamel=False) -> "Rule":
if use_ruamel:
# ruamel enables nice formatting and doc roundtripping with comments
doc = cls._get_ruamel_yaml_parser().load(s)
@@ -797,14 +823,24 @@ class Rule:
return cls.from_dict(doc, s)
@classmethod
def from_yaml_file(cls, path, use_ruamel=False):
def from_yaml_file(cls, path, use_ruamel=False) -> "Rule":
with open(path, "rb") as f:
try:
return cls.from_yaml(f.read().decode("utf-8"), use_ruamel=use_ruamel)
except InvalidRule as e:
raise InvalidRuleWithPath(path, str(e))
rule = cls.from_yaml(f.read().decode("utf-8"), use_ruamel=use_ruamel)
# import here to avoid circular dependency
from capa.render.result_document import RuleMetadata
def to_yaml(self):
# validate meta data fields
_ = RuleMetadata.from_capa(rule)
return rule
except InvalidRule as e:
raise InvalidRuleWithPath(path, str(e)) from e
except pydantic.ValidationError as e:
raise InvalidRuleWithPath(path, str(e)) from e
except yaml.parser.ParserError as e:
raise InvalidRuleWithPath(path, str(e)) from e
def to_yaml(self) -> str:
# reformat the yaml document with a common style.
# this includes:
# - ordering the meta elements
@@ -1041,10 +1077,18 @@ class RuleSet:
"""
def __init__(self, rules: List[Rule]):
super(RuleSet, self).__init__()
super().__init__()
ensure_rules_are_unique(rules)
# in the next step we extract subscope rules,
# which may inflate the number of rules tracked in this ruleset.
# so record number of rules initially provided to this ruleset.
#
# this number is really only meaningful to the user,
# who may compare it against the number of files on their file system.
self.source_rule_count = len(rules)
rules = self._extract_subscope_rules(rules)
ensure_rule_dependencies_are_met(rules)
@@ -1233,7 +1277,7 @@ class RuleSet:
return (easy_rules_by_feature, hard_rules)
@staticmethod
def _get_rules_for_scope(rules, scope):
def _get_rules_for_scope(rules, scope) -> List[Rule]:
"""
given a collection of rules, collect the rules that are needed at the given scope.
these rules are ordered topologically.
@@ -1241,7 +1285,7 @@ class RuleSet:
don't include auto-generated "subscope" rules.
we want to include general "lib" rules here - even if they are not dependencies of other rules, see #398
"""
scope_rules = set([])
scope_rules: Set[Rule] = set([])
# we need to process all rules, not just rules with the given scope.
# this is because rules with a higher scope, e.g. file scope, may have subscope rules
@@ -1255,7 +1299,7 @@ class RuleSet:
return get_rules_with_scope(topologically_order_rules(list(scope_rules)), scope)
@staticmethod
def _extract_subscope_rules(rules):
def _extract_subscope_rules(rules) -> List[Rule]:
"""
process the given sequence of rules.
for each one, extract any embedded subscope rules into their own rule.
@@ -1291,13 +1335,13 @@ class RuleSet:
for k, v in rule.meta.items():
if isinstance(v, str) and tag in v:
logger.debug('using rule "%s" and dependencies, found tag in meta.%s: %s', rule.name, k, v)
rules_filtered.update(set(capa.rules.get_rules_and_dependencies(rules, rule.name)))
rules_filtered.update(set(get_rules_and_dependencies(rules, rule.name)))
break
if isinstance(v, list):
for vv in v:
if tag in vv:
logger.debug('using rule "%s" and dependencies, found tag in meta.%s: %s', rule.name, k, vv)
rules_filtered.update(set(capa.rules.get_rules_and_dependencies(rules, rule.name)))
rules_filtered.update(set(get_rules_and_dependencies(rules, rule.name)))
break
return RuleSet(list(rules_filtered))

155
capa/rules/cache.py Normal file
View File

@@ -0,0 +1,155 @@
import sys
import zlib
import pickle
import hashlib
import logging
import os.path
from typing import List, Optional
from dataclasses import dataclass
import capa.rules
import capa.helpers
import capa.version
logger = logging.getLogger(__name__)
# TypeAlias. note: using `foo: TypeAlias = bar` is Python 3.10+
CacheIdentifier = str
def compute_cache_identifier(rule_content: List[bytes]) -> CacheIdentifier:
hash = hashlib.sha256()
# note that this changes with each release,
# so cache identifiers will never collide across releases.
version = capa.version.__version__
hash.update(version.encode("utf-8"))
hash.update(b"\x00")
rule_hashes = list(sorted([hashlib.sha256(buf).hexdigest() for buf in rule_content]))
for rule_hash in rule_hashes:
hash.update(rule_hash.encode("ascii"))
hash.update(b"\x00")
return hash.hexdigest()
def get_default_cache_directory() -> str:
# ref: https://github.com/mandiant/capa/issues/1212#issuecomment-1361259813
#
# Linux: $XDG_CACHE_HOME/capa/
# Windows: %LOCALAPPDATA%\flare\capa\cache
# MacOS: ~/Library/Caches/capa
# ref: https://stackoverflow.com/a/8220141/87207
if sys.platform == "linux" or sys.platform == "linux2":
directory = os.environ.get("XDG_CACHE_HOME", os.path.join(os.environ["HOME"], ".cache", "capa"))
elif sys.platform == "darwin":
directory = os.path.join(os.environ["HOME"], "Library", "Caches", "capa")
elif sys.platform == "win32":
directory = os.path.join(os.environ["LOCALAPPDATA"], "flare", "capa", "cache")
else:
raise NotImplementedError(f"unsupported platform: {sys.platform}")
os.makedirs(directory, exist_ok=True)
return directory
def get_cache_path(cache_dir: str, id: CacheIdentifier) -> str:
filename = "capa-" + id[:8] + ".cache"
return os.path.join(cache_dir, filename)
MAGIC = b"capa"
VERSION = b"\x00\x00\x00\x01"
@dataclass
class RuleCache:
id: CacheIdentifier
ruleset: capa.rules.RuleSet
def dump(self):
return MAGIC + VERSION + self.id.encode("ascii") + zlib.compress(pickle.dumps(self))
@staticmethod
def load(data):
assert data.startswith(MAGIC + VERSION)
id = data[0x8:0x48].decode("ascii")
cache = pickle.loads(zlib.decompress(data[0x48:]))
assert isinstance(cache, RuleCache)
assert cache.id == id
return cache
def get_ruleset_content(ruleset: capa.rules.RuleSet) -> List[bytes]:
rule_contents = []
for rule in ruleset.rules.values():
if rule.is_subscope_rule():
continue
rule_contents.append(rule.definition.encode("utf-8"))
return rule_contents
def compute_ruleset_cache_identifier(ruleset: capa.rules.RuleSet) -> CacheIdentifier:
rule_contents = get_ruleset_content(ruleset)
return compute_cache_identifier(rule_contents)
def cache_ruleset(cache_dir: str, ruleset: capa.rules.RuleSet):
"""
cache the given ruleset to disk, using the given cache directory.
this can subsequently be reloaded via `load_cached_ruleset`,
assuming the capa version and rule content does not change.
callers should use this function to avoid the performance overhead
of validating rules on each run.
"""
id = compute_ruleset_cache_identifier(ruleset)
path = get_cache_path(cache_dir, id)
if os.path.exists(path):
logger.debug("rule set already cached to %s", path)
return
cache = RuleCache(id, ruleset)
with open(path, "wb") as f:
f.write(cache.dump())
logger.debug("rule set cached to %s", path)
return
def load_cached_ruleset(cache_dir: str, rule_contents: List[bytes]) -> Optional[capa.rules.RuleSet]:
"""
load a cached ruleset from disk, using the given cache directory.
the raw rule contents are required here to prove that the rules haven't changed
and to avoid stale cache entries.
callers should use this function to avoid the performance overhead
of validating rules on each run.
"""
id = compute_cache_identifier(rule_contents)
path = get_cache_path(cache_dir, id)
if not os.path.exists(path):
logger.debug("rule set cache does not exist: %s", path)
return None
logger.debug("loading rule set from cache: %s", path)
with open(path, "rb") as f:
buf = f.read()
try:
cache = RuleCache.load(buf)
except AssertionError:
logger.debug("rule set cache is invalid: %s", path)
# delete the cache that seems to be invalid.
os.remove(path)
return None
else:
return cache.ruleset

View File

@@ -1,13 +1,5 @@
__version__ = "4.0.1"
__version__ = "5.0.0"
def get_major_version():
return int(__version__.partition(".")[0])
def get_rules_branch():
return f"v{get_major_version()}"
def get_rules_checkout_command():
return f"$ git clone https://github.com/mandiant/capa-rules.git -b {get_rules_branch()} /local/path/to/rules"

View File

@@ -6,13 +6,11 @@ If you simply want to use capa, use the standalone binaries we host on GitHub: h
We use PyInstaller to create these packages.
The capa [README](../README.md#download) also links to nightly builds of standalone binaries from the latest development branch.
### Linux Standalone installation
The Linux Standalone binary has been built using GLIB 2.26.
Consequently it works when using GLIB >= 2.26.
This requirement is satisfied by default in most newer distribution such as Ubuntu >= 18, Debian >= 10, openSUSE >= 15.1 and CentOS >= 8.
Consequently, it works when using GLIB >= 2.26.
This requirement is satisfied by default in newer distribution such as Ubuntu >= 18, Debian >= 10, openSUSE >= 15.1 and CentOS >= 8.
But the binary may not work in older distributions.
### MacOS Standalone installation
@@ -24,24 +22,27 @@ By default, on MacOS Catalina or greater, Gatekeeper will block execution of the
## Method 2: Using capa as a Python library
To install capa as a Python library use `pip` to fetch the `flare-capa` module.
#### *Note*:
### 1. Install capa module
Use `pip` to install the capa module to your local Python environment. This fetches the library code to your computer but does not keep editable source files around for you to hack on. If you'd like to edit the source files, see below. `$ pip install flare-capa`
#### *Note on capa rules and library identification signatures*
This method is appropriate for integrating capa in an existing project.
This technique doesn't pull the default rule set, so you should check it out separately from [capa-rules](https://github.com/mandiant/capa-rules/) and pass the directory to the entrypoint using `-r` or set the rules path in the IDA Pro plugin:
This technique doesn't pull the default rule set. You can obtain rule releases from [capa-rules](https://github.com/mandiant/capa-rules/releases) and pass the directory to the entrypoint using `-r`. In the IDA Pro plugin you need to configure the rules directory path once.
```console
$ git clone https://github.com/mandiant/capa-rules.git -b v3 /local/path/to/rules
$ capa -r /local/path/to/rules suspicious.exe
$ wget https://github.com/mandiant/capa-rules/archive/refs/tags/v4.0.0.zip
$ unzip v4.0.0.zip
$ capa -r /path/to/capa-rules suspicious.exe
```
This technique also doesn't set up the default library identification [signatures](https://github.com/mandiant/capa/tree/master/sigs). You can pass the signature directory using the `-s` argument.
For example, to run capa with both a rule path and a signature path:
```console
$ capa -s /path/to/capa-sigs suspicious.exe
```
capa -r /path/to/capa-rules -s /path/to/capa-sigs suspicious.exe
Alternatively, see Method 3 below.
### 1. Install capa module
Use `pip` to install the capa module to your local Python environment. This fetches the library code to your computer but does not keep editable source files around for you to hack on. If you'd like to edit the source files, see below. `$ pip install flare-capa`
### 2. Use capa
You can now import the `capa` module from a Python script or use the IDA Pro plugins from the `capa/ida` directory. For more information please see the [usage](usage.md) documentation.
@@ -49,18 +50,20 @@ You can now import the `capa` module from a Python script or use the IDA Pro plu
If you'd like to review and modify the capa source code, you'll need to check it out from GitHub and install it locally. By following these instructions, you'll maintain a local directory of source code that you can modify and run easily.
### 1. Check out source code
Next, clone the capa git repository.
Clone the capa git repository.
We use submodules to separate [code](https://github.com/mandiant/capa), [rules](https://github.com/mandiant/capa-rules), and [test data](https://github.com/mandiant/capa-testfiles).
To clone everything use the `--recurse-submodules` option:
- CAUTION: The capa testfiles repository contains many malware samples. If you pull down everything using this method, you may want to install to a directory that won't trigger your anti-virus software.
- CAUTION: The capa testfiles repository contains many malware samples. If you pull down everything using this method, you may want to install to a directory that is ignored by your anti-virus software.
- `$ git clone --recurse-submodules https://github.com/mandiant/capa.git /local/path/to/src` (HTTPS)
- `$ git clone --recurse-submodules git@github.com:mandiant/capa.git /local/path/to/src` (SSH)
To only get the source code and our provided rules (common), follow these steps:
To only get the source code and our provided rules (a more common use-case), follow these steps:
- clone repository
- `$ git clone https://github.com/mandiant/capa.git /local/path/to/src` (HTTPS)
- `$ git clone git@github.com:mandiant/capa.git /local/path/to/src` (SSH)
- `$ cd /local/path/to/src`
- initialize the rules submodule and pull rules
- `$ git submodule update --init rules`
### 2. Install the local source code
@@ -76,8 +79,7 @@ You'll find that the `capa.exe` (Windows) or `capa` (Linux/MacOS) executables in
For development, we recommend to use [venv](https://docs.python.org/3/tutorial/venv.html). It allows you to create a virtual environment: a self-contained directory tree that contains a Python installation for a particular version of Python, plus a number of additional packages. This approach avoids conflicts between the requirements of different applications on your computer. It also ensures that you don't overlook to add a new requirement to `setup.up` using a library already installed on your system.
To create an environment (in the parent directory, to avoid commiting it by accident or messing with the linters), run:
`$ python3 -m venv ../capa-env`
To create an environment (in the parent directory, to avoid committing it by accident or messing with the linters), run: `$ python3 -m venv ../capa-env`
To activate `capa-env` in Linux or MacOS, run:
`$ source ../capa-env/bin/activate`
@@ -90,8 +92,8 @@ For more details about creating and using virtual environments, check out the [v
##### Install development dependencies
We use the following tools to ensure consistent code style and formatting:
- [black](https://github.com/psf/black) code formatter, with `-l 120`
- [isort 5](https://pypi.org/project/isort/) code formatter, with `--profile black --length-sort --line-width 120`
- [black](https://github.com/psf/black) code formatter
- [isort 5](https://pypi.org/project/isort/) code formatter
- [dos2unix](https://linux.die.net/man/1/dos2unix) for UNIX-style LF newlines
- [capafmt](https://github.com/mandiant/capa/blob/master/scripts/capafmt.py) rule formatter
@@ -104,7 +106,7 @@ You can run it with the argument `no_tests` to skip the tests and only run the c
##### Setup hooks [optional]
If you plan to contribute to capa, you may want to setup the hooks.
If you plan to contribute to capa, you may want to setup the provided hooks.
Run `scripts/setup-hooks.sh` to set the following hooks up:
- The `pre-commit` hook runs checks before every `git commit`.
It runs `scripts/ci.sh no_tests` aborting the commit if there are code style or rule linter offenses you need to fix.
@@ -112,13 +114,17 @@ Run `scripts/setup-hooks.sh` to set the following hooks up:
It runs `scripts/ci.sh` aborting the push if there are code style or rule linter offenses or if the tests fail.
This way you can ensure everything is alright before sending a pull request.
You can skip the checks by using the `--no-verify` git option.
You can skip the checks by using the `-n`/`--no-verify` git option.
### 3. Compile binary using PyInstaller
We compile capa standalone binaries using PyInstaller. To reproduce the build process check out the source code as described above and follow these steps.
We compile capa standalone binaries using PyInstaller. To reproduce the build process check out the source code as described above and follow the following steps.
#### Install PyInstaller:
`$ pip install pyinstaller` (Python 3)
`$ pip install pyinstaller`
Or install capa with build dependencies:
`$ pip install -e /local/path/to/src[build]`
#### Run Pyinstaller
`$ pyinstaller .github/pyinstaller/pyinstaller.spec`

View File

@@ -3,7 +3,7 @@
- [ ] Ensure all [milestoned issues/PRs](https://github.com/mandiant/capa/milestones) are addressed, or reassign to a new milestone.
- [ ] Add the `dont merge` label to all PRs that are close to be ready to merge (or merge them if they are ready) in [capa](https://github.com/mandiant/capa/pulls) and [capa-rules](https://github.com/mandiant/capa-rules/pulls).
- [ ] Ensure the [CI workflow succeeds in master](https://github.com/mandiant/capa/actions/workflows/tests.yml?query=branch%3Amaster).
- [ ] Ensure that `python scripts/lint.py rules/ --thorough` succeeds (only `missing examples` offenses are allowed in the nursery).
- [ ] Ensure that `python scripts/lint.py rules/ --thorough` succeeds (only `missing examples` offenses are allowed in the nursery). You can [manually trigger a thorough lint](https://github.com/mandiant/capa-rules/actions/workflows/tests.yml) in CI via the "Run workflow" option.
- [ ] Review changes
- capa https://github.com/mandiant/capa/compare/\<last-release\>...master
- capa-rules https://github.com/mandiant/capa-rules/compare/\<last-release>\...master
@@ -37,13 +37,10 @@
- [ ] Update [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py)
- [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py). Copy this checklist in the PR description.
- [ ] After PR review, merge the PR and [create the release in GH](https://github.com/mandiant/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md).
- [ ] Verify GH actions [upload artifacts](https://github.com/mandiant/capa/releases), [publish to PyPI](https://pypi.org/project/flare-capa) and [create a tag in capa rules](https://github.com/mandiant/capa-rules/tags) upon completion.
- [ ] Manually update capa rules major version rule branch
```commandline
[capa/rules] $ git pull master
[capa/rules] $ git checkout v3 # create if new major version: git checkout -b vX
[capa/rules] $ git merge master
[capa/rules] $ git push origin v3
```
- Verify GH actions
- [ ] [upload artifacts](https://github.com/mandiant/capa/releases)
- [ ] [publish to PyPI](https://pypi.org/project/flare-capa)
- [ ] [create tag in capa rules](https://github.com/mandiant/capa-rules/tags)
- [ ] [create release in capa rules](https://github.com/mandiant/capa-rules/releases)
- [ ] [Spread the word](https://twitter.com)
- [ ] Update internal service

View File

@@ -1,6 +1,5 @@
### rules
capa uses a collection of rules to identify capabilities within a program.
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
@@ -12,8 +11,8 @@ $ capa suspicious.exe
However, you may want to modify the rules for a variety of reasons:
- develop new rules to find behaviors, and/or
- tweak existing rules to reduce false positives, and/or
- develop new rules to find behaviors,
- tweak existing rules to reduce false positives,
- collect a private selection of rules not shared publicly.
Or, you may want to use capa as a Python library within another application.
@@ -21,22 +20,18 @@ Or, you may want to use capa as a Python library within another application.
In these scenarios, you must provide the rule set to capa as a directory on your file system. Do this using the `-r`/`--rules` parameter:
```console
$ capa --rules /local/path/to/rules suspicious.exe
$ capa --rules /local/path/to/rules suspicious.exe
```
You can collect the standard set of rules in two ways:
You can download the standard set of rules as ZIP or TGZ archives from the [capa-rules release page](https://github.com/mandiant/capa-rules/releases).
- [download from the Github releases page](#download-release-archive), or
- [clone from Github](#clone-with-git).
Note that you must use match the rules major version with the capa major version,
i.e., use `v1` rules with `v1` of capa.
Note that you must use match the rules major version with the capa major version, i.e., use `v1` rules with `v1` of capa.
This is so that new versions of capa can update rule syntax, such as by adding new fields and logic.
Otherwise, using rules with a mismatched version of capa may lead to errors like:
```
$ capa --rules /path/to/mismatched/rules suspicious.exe
$ capa --rules /path/to/mismatched/rules suspicious.exe
ERROR:lint:invalid rule: injection.yml: invalid rule: unexpected statement: instruction
```
@@ -46,27 +41,3 @@ You can check the version of capa you're currently using like this:
$ capa --version
capa 3.0.3
```
#### download release archive
The releases page is [here](https://github.com/mandiant/capa-rules/tags/).
Find the most recent release corresponding to your major version of capa and download the ZIP archive.
Here are some quick links:
- v1: [v1](https://github.com/mandiant/capa-rules/releases/tag/v1)
- v2: [v2](https://github.com/mandiant/capa-rules/releases/tag/v2)
- v3: [v3](https://github.com/mandiant/capa-rules/releases/tag/v3)
#### clone with git
To fetch with git, clone the appropriate branch like this:
```console
$ git clone https://github.com/mandiant/capa-rules.git -b v3 /local/path/to/rules
```
Note that the branch name (`v3` in the example above) must match the major version of capa you're using.
- [v1](https://github.com/mandiant/capa-rules/tree/v1): `v1`
- [v2](https://github.com/mandiant/capa-rules/tree/v2): `v2`
- [v3](https://github.com/mandiant/capa-rules/tree/v3): `v3`

2
rules

Submodule rules updated: 506e3132bc...ca0d7ad855

View File

@@ -153,7 +153,6 @@ def main(argv=None):
try:
rules = capa.main.get_rules(args.rules)
rules = capa.rules.RuleSet(rules)
logger.info("successfully loaded %s rules", len(rules))
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
logger.error("%s", str(e))
@@ -161,12 +160,12 @@ def main(argv=None):
try:
sig_paths = capa.main.get_signatures(args.signatures)
except (IOError) as e:
except IOError as e:
logger.error("%s", str(e))
return -1
samples = []
for (base, directories, files) in os.walk(args.input):
for base, directories, files in os.walk(args.input):
for file in files:
samples.append(os.path.join(base, file))

67
scripts/cache-ruleset.py Normal file
View File

@@ -0,0 +1,67 @@
"""
Create a cache of the given rules.
This is only really intended to be used by CI to pre-cache rulesets
that will be distributed within PyInstaller binaries.
Usage:
$ python scripts/cache-ruleset.py rules/ /path/to/cache/directory
Copyright (C) 2023 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 os
import sys
import time
import logging
import argparse
import capa.main
import capa.rules
import capa.engine
import capa.helpers
import capa.rules.cache
import capa.features.insn
logger = logging.getLogger("cache-ruleset")
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
parser = argparse.ArgumentParser(description="Cache ruleset.")
capa.main.install_common_args(parser)
parser.add_argument("rules", type=str, action="append", help="Path to rules")
parser.add_argument("cache", type=str, help="Path to cache directory")
args = parser.parse_args(args=argv)
capa.main.handle_common_args(args)
if args.debug:
logging.getLogger("capa").setLevel(logging.DEBUG)
else:
logging.getLogger("capa").setLevel(logging.ERROR)
try:
os.makedirs(args.cache, exist_ok=True)
rules = capa.main.get_rules(args.rules, cache_dir=args.cache)
logger.info("successfully loaded %s rules", len(rules))
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
logger.error("%s", str(e))
return -1
content = capa.rules.cache.get_ruleset_content(rules)
id = capa.rules.cache.compute_cache_identifier(content)
path = capa.rules.cache.get_cache_path(args.cache, id)
assert os.path.exists(path)
logger.info("cached to: %s", path)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -64,7 +64,6 @@ unsupported = ["characteristic", "mnemonic", "offset", "subscope", "Range"]
# collect all converted rules to be able to check if we have needed sub rules for match:
converted_rules = []
count_incomplete = 0
default_tags = "CAPA "
@@ -129,8 +128,7 @@ def convert_capa_number_to_yara_bytes(number):
def convert_rule_name(rule_name):
# yara rule names: "Identifiers must follow the same lexical conventions of the C programming language, they can contain any alphanumeric character and the underscore character, but the first character cannot be a digit. Rule identifiers are case sensitive and cannot exceed 128 characters." so we replace any non-alpanum with _
# yara rule names: "Identifiers must follow the same lexical conventions of the C programming language, they can contain any alphanumeric character and the underscore character, but the first character cannot be a digit. Rule identifiers are case sensitive and cannot exceed 128 characters." so we replace any non-alphanum with _
rule_name = re.sub(r"\W", "_", rule_name)
rule_name = "capa_" + rule_name
@@ -152,7 +150,6 @@ def convert_description(statement):
def convert_rule(rule, rulename, cround, depth):
depth += 1
logger.info("recursion depth: " + str(depth))
@@ -284,12 +281,12 @@ def convert_rule(rule, rulename, cround, depth):
# change capas /xxx/i to yaras /xxx/ nocase, count will be used later to decide appending 'nocase'
regex, count = re.subn(r"/i$", "/", regex)
# remove / in the begining and end
# remove / in the beginning and end
regex = regex[1:-1]
# all .* in the regexes of capa look like they should be maximum 100 chars so take 1000 to speed up rules and prevent yara warnings on poor performance
regex = regex.replace(".*", ".{,1000}")
# strange: capa accepts regexes with unsescaped / like - string: /com/exe4j/runtime/exe4jcontroller/i in capa-rules/compiler/exe4j/compiled-with-exe4j.yml, needs a fix for yara:
# strange: capa accepts regexes with unescaped / like - string: /com/exe4j/runtime/exe4jcontroller/i in capa-rules/compiler/exe4j/compiled-with-exe4j.yml, needs a fix for yara:
# would assume that get_value_str() gives the raw string
regex = re.sub(r"(?<!\\)/", r"\/", regex)
@@ -297,7 +294,7 @@ def convert_rule(rule, rulename, cround, depth):
# /reg(|.exe)/ => /reg(.exe)?/
regex = re.sub(r"\(\|([^\)]+)\)", r"(\1)?", regex)
# change begining of line to null byte, e.g. /^open => /\x00open (not word boundary because we're not looking for the begining of a word in a text but usually a function name if there's ^ in a capa rule)
# change beginning of line to null byte, e.g. /^open => /\x00open (not word boundary because we're not looking for the beginning of a word in a text but usually a function name if there's ^ in a capa rule)
regex = re.sub(r"^\^", r"\\x00", regex)
# regex = re.sub(r"^\^", r"\\b", regex)
@@ -378,7 +375,7 @@ def convert_rule(rule, rulename, cround, depth):
if s_type == "Some":
cmin = kid.count
logger.info("Some type with mininum: " + str(cmin))
logger.info("Some type with minimum: " + str(cmin))
if not cmin:
logger.info("this is optional: which means, we can just ignore it")
@@ -483,7 +480,7 @@ def convert_rule(rule, rulename, cround, depth):
elif statement == "Some":
cmin = rule.count
logger.info("Some type with mininum at2: " + str(cmin))
logger.info("Some type with minimum at2: " + str(cmin))
if not cmin:
logger.info("this is optional: which means, we can just ignore it")
@@ -516,7 +513,6 @@ def output_yar(yara):
def output_unsupported_capa_rules(yaml, capa_rulename, url, reason):
if reason != "NOLOG":
if capa_rulename not in unsupported_capa_rules_list:
logger.info("unsupported: " + capa_rulename + " - reason: " + reason + " - url: " + url)
@@ -537,9 +533,9 @@ def output_unsupported_capa_rules(yaml, capa_rulename, url, reason):
unsupported_capa_rules_names.write(url.encode("utf-8") + b"\n")
def convert_rules(rules, namespaces, cround):
def convert_rules(rules, namespaces, cround, make_priv):
count_incomplete = 0
for rule in rules.rules.values():
rule_name = convert_rule_name(rule.name)
if rule.is_subscope_rule():
@@ -579,7 +575,6 @@ def convert_rules(rules, namespaces, cround):
output_unsupported_capa_rules(rule.to_yaml(), rule.name, url, yara_condition)
logger.info("Unknown feature at5: " + rule.name)
else:
yara_meta = ""
metas = rule.meta
rule_tags = ""
@@ -623,7 +618,7 @@ def convert_rules(rules, namespaces, cround):
value = re.sub(r"^([0-9a-f]{20,64}):0x[0-9a-f]{1,10}$", r"\1", value, flags=re.IGNORECASE)
# examples in capa can contain the same hash several times with different offset, so check if it's already there:
# (keeping the offset might be interessting for some but breaks yara-ci for checking of the final rules
# (keeping the offset might be interesting for some but breaks yara-ci for checking of the final rules
if value not in seen_hashes:
yara_meta += "\t" + meta_name + ' = "' + value + '"\n'
seen_hashes.append(value)
@@ -652,7 +647,6 @@ def convert_rules(rules, namespaces, cround):
if meta_name and meta_value:
yara_meta += "\t" + meta_name + ' = "' + meta_value + '"\n'
rule_name_bonus = ""
if rule_comment:
yara_meta += '\tcomment = "' + rule_comment + '"\n'
yara_meta += '\tdate = "' + today + '"\n'
@@ -662,7 +656,6 @@ def convert_rules(rules, namespaces, cround):
# check if there's some beef in condition:
tmp_yc = re.sub(r"(and|or|not)", "", yara_condition)
if re.search(r"\w", tmp_yc):
yara = ""
if make_priv:
yara = "private "
@@ -679,12 +672,13 @@ def convert_rules(rules, namespaces, cround):
# TODO: now the rule is finished and could be automatically checked with the capa-testfile(s) named in meta (doing it for all of them using yara-ci upload at the moment)
output_yar(yara)
converted_rules.append(rule_name)
global count_incomplete
count_incomplete += incomplete
else:
output_unsupported_capa_rules(rule.to_yaml(), rule.name, url, yara_condition)
pass
return count_incomplete
def main(argv=None):
if argv is None:
@@ -696,7 +690,6 @@ def main(argv=None):
capa.main.install_common_args(parser, wanted={"tag"})
args = parser.parse_args(args=argv)
global make_priv
make_priv = args.private
if args.verbose:
@@ -710,9 +703,8 @@ def main(argv=None):
logging.getLogger("capa2yara").setLevel(level)
try:
rules = capa.main.get_rules([args.rules], disable_progress=True)
namespaces = capa.rules.index_rules_by_namespace(list(rules))
rules = capa.rules.RuleSet(rules)
rules = capa.main.get_rules([args.rules])
namespaces = capa.rules.index_rules_by_namespace(list(rules.rules.values()))
logger.info("successfully loaded %s rules (including subscope rules which will be ignored)", len(rules))
if args.tag:
rules = rules.filter_rules_by_meta(args.tag)
@@ -745,14 +737,15 @@ def main(argv=None):
# do several rounds of converting rules because some rules for match: might not be converted in the 1st run
num_rules = 9999999
cround = 0
count_incomplete = 0
while num_rules != len(converted_rules) or cround < min_rounds:
cround += 1
logger.info("doing convert_rules(), round: " + str(cround))
num_rules = len(converted_rules)
convert_rules(rules, namespaces, cround)
count_incomplete += convert_rules(rules, namespaces, cround, make_priv)
# one last round to collect all unconverted rules
convert_rules(rules, namespaces, 9000)
count_incomplete += convert_rules(rules, namespaces, 9000, make_priv)
stats = "\n// converted rules : " + str(len(converted_rules))
stats += "\n// among those are incomplete : " + str(count_incomplete)

View File

@@ -16,7 +16,7 @@ import capa.features.freeze.features as frzf
from capa.engine import *
# == Render ddictionary helpers
# == Render dictionary helpers
def render_meta(doc: rd.ResultDocument, result):
result["md5"] = doc.meta.sample.md5
result["sha1"] = doc.meta.sample.sha1
@@ -106,7 +106,7 @@ def render_attack(doc, result):
for tactic, techniques in sorted(tactics.items()):
inner_rows = []
for (technique, subtechnique, id) in sorted(techniques):
for technique, subtechnique, id in sorted(techniques):
if subtechnique is None:
inner_rows.append("%s %s" % (technique, id))
else:
@@ -140,7 +140,7 @@ def render_mbc(doc, result):
for objective, behaviors in sorted(objectives.items()):
inner_rows = []
for (behavior, method, id) in sorted(behaviors):
for behavior, method, id in sorted(behaviors):
if method is None:
inner_rows.append("%s [%s]" % (behavior, id))
else:
@@ -161,7 +161,7 @@ def render_dictionary(doc: rd.ResultDocument) -> Dict[str, Any]:
# ==== render dictionary helpers
def capa_details(rules_path, file_path, output_format="dictionary"):
# load rules from disk
rules = capa.rules.RuleSet(capa.main.get_rules([rules_path], disable_progress=True))
rules = capa.main.get_rules([rules_path])
# extract features and find capabilities
extractor = capa.main.get_extractor(file_path, "auto", capa.main.BACKEND_VIV, [], False, disable_progress=True)
@@ -172,7 +172,7 @@ def capa_details(rules_path, file_path, output_format="dictionary"):
meta["analysis"].update(counts)
meta["analysis"]["layout"] = capa.main.compute_layout(rules, extractor, capabilities)
capa_output = False
capa_output: Any = False
if output_format == "dictionary":
# ...as python dictionary, simplified as textable but in dictionary
doc = rd.ResultDocument.from_capa(meta, rules, capabilities)

View File

@@ -11,12 +11,12 @@
# Use a console with emojis support for a better experience
# Use venv to ensure that `python` calls the correct python version
# Stash uncommited changes
# Stash uncommitted changes
MSG="pre-push-$(date +%s)";
git stash push -kum "$MSG" &>/dev/null ;
STASH_LIST=$(git stash list);
if [[ "$STASH_LIST" == *"$MSG"* ]]; then
echo "Uncommited changes stashed with message '$MSG', if you abort before they are restored run \`git stash pop\`";
echo "Uncommitted changes stashed with message '$MSG', if you abort before they are restored run \`git stash pop\`";
fi
restore_stashed() {

View File

@@ -28,7 +28,7 @@ def main(argv=None):
if capa.helpers.is_runtime_ida():
from capa.ida.helpers import IDAIO
f: BinaryIO = IDAIO()
f: BinaryIO = IDAIO() # type: ignore
else:
if argv is None:

View File

@@ -31,7 +31,7 @@ See the License for the specific language governing permissions and limitations
import json
import logging
import idautils
import ida_nalt
import ida_funcs
import ida_kernwin
@@ -51,7 +51,11 @@ def append_func_cmt(va, cmt, repeatable=False):
if cmt in existing:
return
new = existing + "\n" + cmt
if len(existing) > 0:
new = existing + "\n" + cmt
else:
new = cmt
ida_funcs.set_func_cmt(func, new, repeatable)
@@ -73,7 +77,7 @@ def main():
#
# see: https://github.com/idapython/bin/issues/11
a = doc["meta"]["sample"]["md5"].lower()
b = idautils.GetInputFileMD5().decode("ascii").lower().rstrip("\x00")
b = ida_nalt.retrieve_input_file_md5().lower()
if not a.startswith(b):
logger.error("sample mismatch")
return -2

View File

@@ -248,7 +248,7 @@ class InvalidAttckOrMbcTechnique(Lint):
"""
def __init__(self):
super(InvalidAttckOrMbcTechnique, self).__init__()
super().__init__()
try:
with open(f"{os.path.dirname(__file__)}/linter-data.json", "rb") as fd:
@@ -307,11 +307,7 @@ def get_sample_capabilities(ctx: Context, path: Path) -> Set[str]:
elif nice_path.endswith(capa.helpers.EXTENSIONS_SHELLCODE_64):
format_ = "sc64"
else:
format_ = "auto"
if not nice_path.endswith(capa.helpers.EXTENSIONS_ELF):
dnfile_extractor = capa.features.extractors.dnfile_.DnfileFeatureExtractor(nice_path)
if dnfile_extractor.is_dotnet_file():
format_ = FORMAT_DOTNET
format_ = capa.main.get_auto_format(nice_path)
logger.debug("analyzing sample: %s", nice_path)
extractor = capa.main.get_extractor(nice_path, format_, "", DEFAULT_SIGNATURES, False, disable_progress=True)
@@ -894,7 +890,6 @@ def redirecting_print_to_tqdm():
old_print = print
def new_print(*args, **kwargs):
# If tqdm.tqdm.write raises error, use builtin print
try:
tqdm.tqdm.write(*args, **kwargs)
@@ -902,11 +897,15 @@ def redirecting_print_to_tqdm():
old_print(*args, **kwargs)
try:
# Globaly replace print with new_print
inspect.builtins.print = new_print
# Globally replace print with new_print.
# Verified this works manually on Python 3.11:
# >>> import inspect
# >>> inspect.builtins
# <module 'builtins' (built-in)>
inspect.builtins.print = new_print # type: ignore
yield
finally:
inspect.builtins.print = old_print
inspect.builtins.print = old_print # type: ignore
def lint(ctx: Context):
@@ -917,12 +916,11 @@ def lint(ctx: Context):
"""
ret = {}
with tqdm.contrib.logging.tqdm_logging_redirect(ctx.rules.rules.items(), unit="rule") as pbar:
source_rules = [rule for rule in ctx.rules.rules.values() if not rule.is_subscope_rule()]
with tqdm.contrib.logging.tqdm_logging_redirect(source_rules, unit="rule") as pbar:
with redirecting_print_to_tqdm():
for name, rule in pbar:
if rule.is_subscope_rule():
continue
for rule in pbar:
name = rule.name
pbar.set_description(width("linting rule: %s" % (name), 48))
ret[name] = lint_rule(ctx, rule)
@@ -998,10 +996,8 @@ def main(argv=None):
time0 = time.time()
try:
rules = capa.main.get_rules(args.rules, disable_progress=True)
rule_count = len(rules)
rules = capa.rules.RuleSet(rules)
logger.info("successfully loaded %s rules", rule_count)
rules = capa.main.get_rules(args.rules)
logger.info("successfully loaded %s rules", rules.source_rule_count)
if args.tag:
rules = rules.filter_rules_by_meta(args.tag)
logger.debug("selected %s rules", len(rules))

File diff suppressed because it is too large Load Diff

View File

@@ -88,14 +88,14 @@ def main(argv=None):
try:
with capa.main.timing("load rules"):
rules = capa.rules.RuleSet(capa.main.get_rules(args.rules, disable_progress=True))
except (IOError) as e:
rules = capa.main.get_rules(args.rules)
except IOError as e:
logger.error("%s", str(e))
return -1
try:
sig_paths = capa.main.get_signatures(args.signatures)
except (IOError) as e:
except IOError as e:
logger.error("%s", str(e))
return -1
@@ -120,7 +120,7 @@ def main(argv=None):
logger.debug("perf: find capabilities: avg: %0.2fs" % (sum(samples) / float(args.repeat) / float(args.number)))
logger.debug("perf: find capabilities: max: %0.2fs" % (max(samples) / float(args.number)))
for (counter, count) in capa.perf.counters.most_common():
for counter, count in capa.perf.counters.most_common():
logger.debug("perf: counter: {:}: {:,}".format(counter, count))
print(

View File

@@ -70,7 +70,7 @@ class MitreExtractor:
self._memory_store = MemoryStore(stix_data=stix_json["objects"])
@staticmethod
def _remove_deprecated_objetcs(stix_objects) -> List[AttackPattern]:
def _remove_deprecated_objects(stix_objects) -> List[AttackPattern]:
"""Remove any revoked or deprecated objects from queries made to the data source."""
return list(
filter(
@@ -82,7 +82,7 @@ class MitreExtractor:
def _get_tactics(self) -> List[Dict]:
"""Get tactics IDs from Mitre matrix."""
# Only one matrix for enterprise att&ck framework
matrix = self._remove_deprecated_objetcs(
matrix = self._remove_deprecated_objects(
self._memory_store.query(
[
Filter("type", "=", "x-mitre-matrix"),
@@ -93,7 +93,7 @@ class MitreExtractor:
def _get_techniques_from_tactic(self, tactic: str) -> List[AttackPattern]:
"""Get techniques and sub techniques from a Mitre tactic (kill_chain_phases->phase_name)"""
techniques = self._remove_deprecated_objetcs(
techniques = self._remove_deprecated_objects(
self._memory_store.query(
[
Filter("type", "=", "attack-pattern"),
@@ -107,7 +107,7 @@ class MitreExtractor:
def _get_parent_technique_from_subtechnique(self, technique: AttackPattern) -> AttackPattern:
"""Get parent technique of a sub technique using the technique ID TXXXX.YYY"""
sub_id = technique["external_references"][0]["external_id"].split(".")[0]
parent_technique = self._remove_deprecated_objetcs(
parent_technique = self._remove_deprecated_objects(
self._memory_store.query(
[
Filter("type", "=", "attack-pattern"),
@@ -125,7 +125,10 @@ class MitreExtractor:
data: Dict[str, Dict[str, str]] = {}
for tactic in self._get_tactics():
data[tactic["name"]] = {}
for technique in self._get_techniques_from_tactic(tactic["x_mitre_shortname"]):
for technique in sorted(
self._get_techniques_from_tactic(tactic["x_mitre_shortname"]),
key=lambda x: x["external_references"][0]["external_id"],
):
tid = technique["external_references"][0]["external_id"]
technique_name = technique["name"].split("::")[0]
if technique["x_mitre_is_subtechnique"]:
@@ -151,7 +154,7 @@ class MbcExtractor(MitreExtractor):
def _get_tactics(self) -> List[Dict]:
"""Override _get_tactics to edit the tactic name for Micro-objective"""
tactics = super(MbcExtractor, self)._get_tactics()
tactics = super()._get_tactics()
# We don't want the Micro-objective string inside objective names
for tactic in tactics:
tactic["name"] = tactic["name"].replace(" Micro-objective", "")

View File

@@ -142,7 +142,6 @@ def main(argv=None):
try:
rules = capa.main.get_rules(args.rules)
rules = capa.rules.RuleSet(rules)
logger.info("successfully loaded %s rules", len(rules))
if args.tag:
rules = rules.filter_rules_by_meta(args.tag)
@@ -153,16 +152,16 @@ def main(argv=None):
try:
sig_paths = capa.main.get_signatures(args.signatures)
except (IOError) as e:
except IOError as e:
logger.error("%s", str(e))
return -1
if (args.format == "freeze") or (args.format == "auto" and capa.features.freeze.is_freeze(taste)):
format = "freeze"
format_ = "freeze"
with open(args.sample, "rb") as f:
extractor = capa.features.freeze.load(f.read())
else:
format = args.format
format_ = args.format
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
try:

View File

@@ -79,6 +79,7 @@ import capa.exceptions
import capa.render.verbose as v
import capa.features.common
import capa.features.freeze
import capa.features.address
import capa.features.extractors.base_extractor
from capa.helpers import log_unsupported_runtime_error
@@ -108,7 +109,7 @@ def main(argv=None):
try:
sig_paths = capa.main.get_signatures(args.signatures)
except (IOError) as e:
except IOError as e:
logger.error("%s", str(e))
return -1
@@ -135,7 +136,7 @@ def main(argv=None):
for feature, addr in extractor.extract_file_features():
print("file: %s: %s" % (format_address(addr), feature))
function_handles = extractor.get_functions()
function_handles = tuple(extractor.get_functions())
if args.function:
if args.format == "freeze":
@@ -172,7 +173,7 @@ def ida_main():
print("file: %s: %s" % (format_address(addr), feature))
return
function_handles = extractor.get_functions()
function_handles = tuple(extractor.get_functions())
if function:
function_handles = tuple(filter(lambda fh: fh.inner.start_ea == function, function_handles))

View File

@@ -19,3 +19,10 @@ test = pytest
ignore = E203, E302, E402, E501, E712, E722, E731, W291, W503
max-line-length = 180
statistics = True
[pylint.FORMAT]
max-line-length = 180
[pylint]
disable = missing-docstring,invalid-name,import-outside-toplevel,redefined-outer-name,consider-using-f-string

View File

@@ -11,24 +11,23 @@ import os
import setuptools
requirements = [
"tqdm==4.64.0",
"tqdm==4.64.1",
"pyyaml==6.0",
"tabulate==0.8.9",
"tabulate==0.9.0",
"colorama==0.4.5",
"termcolor==1.1.0",
"wcwidth==0.2.5",
"termcolor==2.2.0",
"wcwidth==0.2.6",
"ida-settings==2.1.0",
"viv-utils[flirt]==0.7.5",
"viv-utils[flirt]==0.7.7",
"halo==0.0.31",
"networkx==2.5.1",
"networkx==2.5.1", # newer versions no longer support py3.7.
"ruamel.yaml==0.17.21",
"vivisect==1.0.8",
"smda==1.8.4",
"pefile==2022.5.30",
"pyelftools==0.28",
"dnfile==0.11.0",
"dncil==1.0.1",
"pydantic==1.9.1",
"pyelftools==0.29",
"dnfile==0.13.0",
"dncil==1.0.2",
"pydantic==1.10.4",
]
# this sets __version__
@@ -69,28 +68,28 @@ setuptools.setup(
install_requires=requirements,
extras_require={
"dev": [
"pytest==7.1.2",
"pytest==7.1.3",
"pytest-sugar==0.9.4",
"pytest-instafail==0.4.2",
"pytest-cov==3.0.0",
"pycodestyle==2.9.1",
"black==22.6.0",
"isort==5.10.1",
"mypy==0.971",
"psutil==5.9.1",
"pytest-cov==4.0.0",
"pycodestyle==2.10.0",
"black==23.1.0",
"isort==5.11.4",
"mypy==0.991",
"psutil==5.9.2",
"stix2==3.0.1",
"requests==2.28.0",
# type stubs for mypy
"types-backports==0.1.3",
"types-colorama==0.4.15",
"types-PyYAML==6.0.8",
"types-tabulate==0.8.9",
"types-tabulate==0.9.0.0",
"types-termcolor==1.1.4",
"types-psutil==5.8.23",
"types_requests==2.28.1",
],
"build": [
"pyinstaller==5.3",
"pyinstaller==5.7.0",
],
},
zip_safe=False,

View File

@@ -36,6 +36,7 @@ from capa.features.common import (
Arch,
Format,
Feature,
FeatureAccess,
)
from capa.features.address import Address
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
@@ -117,21 +118,9 @@ def fixup_viv(path, extractor):
if "3b13b" in path:
# vivisect only recognizes calling thunk function at 0x10001573
extractor.vw.makeFunction(0x10006860)
@lru_cache()
def get_smda_extractor(path):
from smda.SmdaConfig import SmdaConfig
from smda.Disassembler import Disassembler
import capa.features.extractors.smda.extractor
config = SmdaConfig()
config.STORE_BUFFER = True
disasm = Disassembler(config)
report = disasm.disassembleFile(path)
return capa.features.extractors.smda.extractor.SmdaFeatureExtractor(report, path)
if "294b8d" in path:
# see vivisect/#561
extractor.vw.makeFunction(0x404970)
@lru_cache(maxsize=1)
@@ -277,8 +266,22 @@ def get_data_path_by_name(name):
return os.path.join(DNFILE_TESTFILES, "hello-world", "hello-world.exe")
elif name.startswith("_1c444"):
return os.path.join(CD, "data", "dotnet", "1c444ebeba24dcba8628b7dfe5fec7c6.exe_")
elif name.startswith("_387f15"):
return os.path.join(
CD, "data", "dotnet", "387f15043f0198fd3a637b0758c2b6dde9ead795c3ed70803426fc355731b173.dll_"
)
elif name.startswith("_692f"):
return os.path.join(CD, "data", "dotnet", "692f7fd6d198e804d6af98eb9e390d61.exe_")
elif name.startswith("_0953c"):
return os.path.join(CD, "data", "0953cc3b77ed2974b09e3a00708f88de931d681e2d0cb64afbaf714610beabe6.exe_")
elif name.startswith("_039a6"):
return os.path.join(CD, "data", "039a6336d0802a2255669e6867a5679c7eb83313dbc61fb1c7232147379bd304.exe_")
elif name.startswith("b5f052"):
return os.path.join(CD, "data", "b5f0524e69b3a3cf636c7ac366ca57bf5e3a8fdc8a9f01caf196c611a7918a87.elf_")
elif name.startswith("bf7a9c"):
return os.path.join(CD, "data", "bf7a9c8bdfa6d47e01ad2b056264acc3fd90cf43fe0ed8deec93ab46b47d76cb.elf_")
elif name.startswith("294b8d"):
return os.path.join(CD, "data", "294b8db1f2702b60fb2e42fdc50c2cee6a5046112da9a5703a548a4fa50477bc.elf_")
else:
raise ValueError("unexpected sample fixture: %s" % name)
@@ -360,7 +363,7 @@ def get_function(extractor, fva: int) -> FunctionHandle:
def get_function_by_token(extractor, token: int) -> FunctionHandle:
for fh in extractor.get_functions():
if fh.address.token.value == token:
if fh.address == token:
return fh
raise ValueError("function not found by token")
@@ -368,7 +371,7 @@ def get_function_by_token(extractor, token: int) -> FunctionHandle:
def get_basic_block(extractor, fh: FunctionHandle, va: int) -> BBHandle:
for bbh in extractor.get_basic_blocks(fh):
if isinstance(extractor, DnfileFeatureExtractor):
addr = bbh.inner.offset
addr = bbh.inner.instructions[0].offset
else:
addr = bbh.address
if addr == va:
@@ -629,6 +632,8 @@ FEATURE_PRESENCE_TESTS = sorted(
("mimikatz", "function=0x40105D", capa.features.common.String("ACR > "), True),
("mimikatz", "function=0x40105D", capa.features.common.String("nope"), False),
("773290...", "function=0x140001140", capa.features.common.String(r"%s:\\OfficePackagesForWDAG"), True),
# overlapping string, see #1271
("294b8d...", "function=0x404970,bb=0x404970,insn=0x40499F", capa.features.common.String("\r\n\x00:ht"), False),
# insn/regex
("pma16-01", "function=0x4021B0", capa.features.common.Regex("HTTP/1.0"), True),
("pma16-01", "function=0x402F40", capa.features.common.Regex("www.practicalmalwareanalysis.com"), True),
@@ -684,14 +689,22 @@ FEATURE_PRESENCE_TESTS = sorted(
# os & format & arch
("pma16-01", "file", OS(OS_WINDOWS), True),
("pma16-01", "file", OS(OS_LINUX), False),
("mimikatz", "file", OS(OS_WINDOWS), True),
("pma16-01", "function=0x404356", OS(OS_WINDOWS), True),
("pma16-01", "function=0x404356,bb=0x4043B9", OS(OS_WINDOWS), True),
("mimikatz", "function=0x40105D", OS(OS_WINDOWS), True),
("pma16-01", "file", Arch(ARCH_I386), True),
("pma16-01", "file", Arch(ARCH_AMD64), False),
("mimikatz", "file", Arch(ARCH_I386), True),
("pma16-01", "function=0x404356", Arch(ARCH_I386), True),
("pma16-01", "function=0x404356,bb=0x4043B9", Arch(ARCH_I386), True),
("mimikatz", "function=0x40105D", Arch(ARCH_I386), True),
("pma16-01", "file", Format(FORMAT_PE), True),
("pma16-01", "file", Format(FORMAT_ELF), False),
("mimikatz", "file", Format(FORMAT_PE), True),
# format is also a global feature
("pma16-01", "function=0x404356", Format(FORMAT_PE), True),
("mimikatz", "function=0x456BB9", Format(FORMAT_PE), True),
# elf support
("7351f.elf", "file", OS(OS_LINUX), True),
("7351f.elf", "file", OS(OS_WINDOWS), False),
@@ -718,18 +731,19 @@ FEATURE_PRESENCE_TESTS_DOTNET = sorted(
("mixed-mode-64", "file", capa.features.common.Characteristic("mixed mode"), True),
("hello-world", "file", capa.features.common.Characteristic("mixed mode"), False),
("b9f5b", "file", OS(OS_ANY), True),
("b9f5b", "file", Format(FORMAT_PE), True),
("b9f5b", "file", Format(FORMAT_DOTNET), True),
("hello-world", "file", capa.features.file.FunctionName("HelloWorld::Main"), True),
("hello-world", "file", capa.features.file.FunctionName("HelloWorld::.ctor"), True),
("hello-world", "file", capa.features.file.FunctionName("HelloWorld::.cctor"), False),
("hello-world", "file", capa.features.file.FunctionName("HelloWorld::ctor"), True),
("hello-world", "file", capa.features.file.FunctionName("HelloWorld::cctor"), False),
("hello-world", "file", capa.features.common.String("Hello World!"), True),
("hello-world", "file", capa.features.common.Class("HelloWorld"), True),
("hello-world", "file", capa.features.common.Class("System.Console"), True),
("hello-world", "file", capa.features.common.Namespace("System.Diagnostics"), True),
("hello-world", "function=0x250", capa.features.common.String("Hello World!"), True),
("hello-world", "function=0x250, bb=0x250, insn=0x252", capa.features.common.String("Hello World!"), True),
("hello-world", "function=0x250, bb=0x250, insn=0x257", capa.features.common.Class("System.Console"), True),
("hello-world", "function=0x250, bb=0x250, insn=0x257", capa.features.common.Namespace("System"), True),
("hello-world", "function=0x250, bb=0x251, insn=0x252", capa.features.common.String("Hello World!"), True),
("hello-world", "function=0x250, bb=0x251, insn=0x257", capa.features.common.Class("System.Console"), True),
("hello-world", "function=0x250, bb=0x251, insn=0x257", capa.features.common.Namespace("System"), True),
("hello-world", "function=0x250", capa.features.insn.API("System.Console::WriteLine"), True),
("hello-world", "file", capa.features.file.Import("System.Console::WriteLine"), True),
("_1c444", "file", capa.features.common.String(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), True),
@@ -740,11 +754,28 @@ FEATURE_PRESENCE_TESTS_DOTNET = sorted(
("_1c444", "function=0x1F68", capa.features.insn.API("GetWindowDC"), True),
("_1c444", "function=0x1F68", capa.features.insn.API("user32.GetWindowDC"), True),
("_1c444", "function=0x1F68", capa.features.insn.Number(0xCC0020), True),
("_1c444", "token=0x600001D", capa.features.common.Characteristic("calls to"), True),
("_1c444", "token=0x6000018", capa.features.common.Characteristic("calls to"), False),
("_1c444", "token=0x600001D", capa.features.common.Characteristic("calls from"), True),
("_1c444", "token=0x600000F", capa.features.common.Characteristic("calls from"), False),
("_1c444", "token=0x600001D", capa.features.common.Characteristic("loop"), True),
("_1c444", "token=0x0600008C", capa.features.common.Characteristic("loop"), False),
("_1c444", "function=0x1F68", capa.features.insn.Number(0x0), True),
("_1c444", "function=0x1F68", capa.features.insn.Number(0x1), False),
("_692f", "token=0x6000004", capa.features.insn.API("System.Linq.Enumerable::First"), True), # generic method
(
"_692f",
"token=0x6000004",
capa.features.insn.Property("System.Linq.Enumerable::First"),
False,
), # generic method
("_692f", "token=0x6000004", capa.features.common.Namespace("System.Linq"), True), # generic method
("_692f", "token=0x6000004", capa.features.common.Class("System.Linq.Enumerable"), True), # generic method
("_1c444", "token=0x6000020", capa.features.common.Namespace("Reqss"), True), # ldftn
("_1c444", "token=0x6000020", capa.features.common.Class("Reqss.Reqss"), True), # ldftn
(
"_1c444",
"function=0x1F59, bb=0x1F59, insn=0x1F5B",
"function=0x1F59, bb=0x1F5A, insn=0x1F5B",
capa.features.common.Characteristic("unmanaged call"),
True,
),
@@ -753,11 +784,166 @@ FEATURE_PRESENCE_TESTS_DOTNET = sorted(
("_1c444", "token=0x6000088", capa.features.common.Characteristic("unmanaged call"), False),
(
"_1c444",
"function=0x1F68, bb=0x1F68, insn=0x1FF9",
"function=0x1F68, bb=0x1F74, insn=0x1FF9",
capa.features.insn.API("System.Drawing.Image::FromHbitmap"),
True,
),
("_1c444", "function=0x1F68, bb=0x1F68, insn=0x1FF9", capa.features.insn.API("FromHbitmap"), False),
("_1c444", "function=0x1F68, bb=0x1F74, insn=0x1FF9", capa.features.insn.API("FromHbitmap"), False),
(
"_1c444",
"token=0x600002B",
capa.features.insn.Property("System.IO.FileInfo::Length", access=FeatureAccess.READ),
True,
), # MemberRef property access
(
"_1c444",
"token=0x600002B",
capa.features.insn.Property("System.IO.FileInfo::Length"),
True,
), # MemberRef property access
(
"_1c444",
"token=0x6000081",
capa.features.insn.API("System.Diagnostics.Process::Start"),
True,
), # MemberRef property access
(
"_1c444",
"token=0x6000081",
capa.features.insn.Property(
"System.Diagnostics.ProcessStartInfo::UseShellExecute", access=FeatureAccess.WRITE
), # MemberRef property access
True,
),
(
"_1c444",
"token=0x6000081",
capa.features.insn.Property(
"System.Diagnostics.ProcessStartInfo::WorkingDirectory", access=FeatureAccess.WRITE
), # MemberRef property access
True,
),
(
"_1c444",
"token=0x6000081",
capa.features.insn.Property(
"System.Diagnostics.ProcessStartInfo::FileName", access=FeatureAccess.WRITE
), # MemberRef property access
True,
),
(
"_1c444",
"token=0x6000087",
capa.features.insn.Property(
"Sockets.MySocket::reConnectionDelay", access=FeatureAccess.WRITE
), # Field property access
True,
),
(
"_1c444",
"token=0x600008A",
capa.features.insn.Property(
"Sockets.MySocket::isConnected", access=FeatureAccess.WRITE
), # Field property access
True,
),
(
"_1c444",
"token=0x600008A",
capa.features.common.Class("Sockets.MySocket"), # Field property access
True,
),
(
"_1c444",
"token=0x600008A",
capa.features.common.Namespace("Sockets"), # Field property access
True,
),
(
"_1c444",
"token=0x600008A",
capa.features.insn.Property(
"Sockets.MySocket::onConnected", access=FeatureAccess.READ
), # Field property access
True,
),
(
"_0953c",
"token=0x6000004",
capa.features.insn.Property(
"System.Diagnostics.Debugger::IsAttached", access=FeatureAccess.READ
), # MemberRef property access
True,
),
(
"_0953c",
"token=0x6000004",
capa.features.common.Class("System.Diagnostics.Debugger"), # MemberRef property access
True,
),
(
"_0953c",
"token=0x6000004",
capa.features.common.Namespace("System.Diagnostics"), # MemberRef property access
True,
),
(
"_692f",
"token=0x6000006",
capa.features.insn.Property(
"System.Management.Automation.PowerShell::Streams", access=FeatureAccess.READ
), # MemberRef property access
False,
),
(
"_387f15",
"token=0x600009E",
capa.features.insn.Property(
"Modulo.IqQzcRDvSTulAhyLtZHqyeYGgaXGbuLwhxUKXYmhtnOmgpnPJDTSIPhYPpnE::geoplugin_countryCode",
access=FeatureAccess.READ,
), # MethodDef property access
True,
),
(
"_387f15",
"token=0x600009E",
capa.features.common.Class(
"Modulo.IqQzcRDvSTulAhyLtZHqyeYGgaXGbuLwhxUKXYmhtnOmgpnPJDTSIPhYPpnE"
), # MethodDef property access
True,
),
(
"_387f15",
"token=0x600009E",
capa.features.common.Namespace("Modulo"), # MethodDef property access
True,
),
(
"_039a6",
"token=0x6000007",
capa.features.insn.API("System.Reflection.Assembly::Load"),
True,
),
(
"_039a6",
"token=0x600001D",
capa.features.insn.Property("StagelessHollow.Arac::Marka", access=FeatureAccess.READ), # MethodDef method
True,
),
(
"_039a6",
"token=0x600001C",
capa.features.insn.Property("StagelessHollow.Arac::Marka", access=FeatureAccess.READ), # MethodDef method
False,
),
(
"_039a6",
"token=0x6000023",
capa.features.insn.Property(
"System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Task", access=FeatureAccess.READ
), # MemberRef method
False,
),
],
# order tests by (file, item)
# so that our LRU cache is most effective.
@@ -770,6 +956,7 @@ FEATURE_PRESENCE_TESTS_IDA = [
("mimikatz", "file", capa.features.file.Import("cabinet.FCIAddFile"), True),
]
FEATURE_COUNT_TESTS = [
("mimikatz", "function=0x40E5C2", capa.features.basicblock.BasicBlock(), 7),
("mimikatz", "function=0x4702FD", capa.features.common.Characteristic("calls from"), 0),
@@ -778,8 +965,12 @@ FEATURE_COUNT_TESTS = [
("mimikatz", "function=0x40B1F1", capa.features.common.Characteristic("calls to"), 3),
]
FEATURE_COUNT_TESTS_DOTNET = [] # type: ignore
FEATURE_COUNT_TESTS_DOTNET = [
("_1c444", "token=0x06000072", capa.features.basicblock.BasicBlock(), 1),
("_1c444", "token=0x0600008C", capa.features.basicblock.BasicBlock(), 10),
("_1c444", "token=0x600001D", capa.features.common.Characteristic("calls to"), 1),
("_1c444", "token=0x600001D", capa.features.common.Characteristic("calls from"), 9),
]
def do_test_feature_presence(get_extractor, sample, scope, feature, expected):
@@ -904,3 +1095,13 @@ def _1c444_dotnetfile_extractor():
@pytest.fixture
def _692f_dotnetfile_extractor():
return get_dnfile_extractor(get_data_path_by_name("_692f"))
@pytest.fixture
def _0953c_dotnetfile_extractor():
return get_dnfile_extractor(get_data_path_by_name("_0953c"))
@pytest.fixture
def _039a6_dotnetfile_extractor():
return get_dnfile_extractor(get_data_path_by_name("_039a6"))

Some files were not shown because too many files have changed in this diff Show More