Compare commits

...

779 Commits

Author SHA1 Message Date
Moritz
81cb4b31e1 Release v4.0.0 (#1105)
* release: v4 prep

* add SMDA deprecation warning

* doc: update v4 changes

* Update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>

* doc: add DeprecationWarning

* fix: add __index__ method

* ci: test build run on more OSs

* explorer: update supported versions to include IDA 8.0

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2022-08-10 15:32:52 +02:00
Capa Bot
e564466ac8 Sync capa rules submodule 2022-08-09 10:50:59 +00:00
Capa Bot
63e0d903c7 Sync capa rules submodule 2022-08-09 10:35:39 +00:00
Moritz
dbc1ddcd7b Merge pull request #1133 from mandiant/dependabot/pip/smda-1.8.4
build(deps): bump smda from 1.7.4 to 1.8.4
2022-08-09 12:34:32 +02:00
Moritz
a00d0d5222 Merge pull request #1128 from mandiant/dependabot/pip/pyinstaller-5.3
build(deps-dev): bump pyinstaller from 5.2 to 5.3
2022-08-09 12:33:59 +02:00
Moritz
428d125340 Merge pull request #1132 from mandiant/dependabot/pip/pycodestyle-2.9.1
build(deps-dev): bump pycodestyle from 2.8.0 to 2.9.1
2022-08-09 12:32:19 +02:00
dependabot[bot]
f94314d8ec build(deps): bump smda from 1.7.4 to 1.8.4
Bumps [smda](https://github.com/danielplohmann/smda) from 1.7.4 to 1.8.4.
- [Release notes](https://github.com/danielplohmann/smda/releases)
- [Commits](https://github.com/danielplohmann/smda/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-08 14:11:12 +00:00
dependabot[bot]
bb94ca3b18 build(deps-dev): bump pycodestyle from 2.8.0 to 2.9.1
Bumps [pycodestyle](https://github.com/PyCQA/pycodestyle) from 2.8.0 to 2.9.1.
- [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.8.0...2.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-08 14:11:11 +00:00
Capa Bot
5823d421fd Sync capa-testfiles submodule 2022-08-01 20:50:09 +00:00
Capa Bot
045a64496e Sync capa-testfiles submodule 2022-08-01 20:36:11 +00:00
Capa Bot
b8905e3e48 Sync capa-testfiles submodule 2022-08-01 20:35:55 +00:00
Capa Bot
7c6f27c6d7 Sync capa-testfiles submodule 2022-08-01 20:35:14 +00:00
Capa Bot
995b144f0b Sync capa-testfiles submodule 2022-08-01 20:34:46 +00:00
Capa Bot
ba93803d3f Sync capa-testfiles submodule 2022-08-01 20:30:55 +00:00
dependabot[bot]
96b13907e2 build(deps-dev): bump pyinstaller from 5.2 to 5.3
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.2 to 5.3.
- [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.2...v5.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 14:14:42 +00:00
Willi Ballenthin
2f7aa14f61 Merge pull request #1118 from mandiant/dependabot/pip/dncil-1.0.1
build(deps): bump dncil from 1.0.0 to 1.0.1
2022-07-25 10:57:12 -06:00
Willi Ballenthin
f93b94f073 Merge pull request #1119 from mandiant/dependabot/pip/smda-1.7.4
build(deps): bump smda from 1.7.2 to 1.7.4
2022-07-25 10:56:54 -06:00
Willi Ballenthin
30835b5ce4 Merge pull request #1120 from mandiant/dependabot/pip/mypy-0.971
build(deps-dev): bump mypy from 0.961 to 0.971
2022-07-25 10:56:19 -06:00
dependabot[bot]
98db89e45a build(deps-dev): bump mypy from 0.961 to 0.971
Bumps [mypy](https://github.com/python/mypy) from 0.961 to 0.971.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.961...v0.971)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-25 14:09:17 +00:00
dependabot[bot]
84c4b3ca8f build(deps): bump smda from 1.7.2 to 1.7.4
Bumps [smda](https://github.com/danielplohmann/smda) from 1.7.2 to 1.7.4.
- [Release notes](https://github.com/danielplohmann/smda/releases)
- [Commits](https://github.com/danielplohmann/smda/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-25 14:09:11 +00:00
dependabot[bot]
cd32abc405 build(deps): bump dncil from 1.0.0 to 1.0.1
Bumps [dncil](https://github.com/mandiant/dncil) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/mandiant/dncil/releases)
- [Commits](https://github.com/mandiant/dncil/compare/v1.0.0...v1.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-25 14:09:10 +00:00
dependabot[bot]
bae1b29505 build(deps-dev): bump types-colorama from 0.4.14 to 0.4.15 (#1113)
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.14 to 0.4.15.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-19 20:46:20 +02:00
dependabot[bot]
5061a0c717 build(deps-dev): bump types-requests from 2.28.0 to 2.28.1 (#1112)
Bumps [types-requests](https://github.com/python/typeshed) from 2.28.0 to 2.28.1.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-19 20:45:53 +02:00
Capa Bot
404de45103 Sync capa rules submodule 2022-07-12 16:10:32 +00:00
Willi Ballenthin
39c8674da5 Merge pull request #1103 from mandiant/dependabot/pip/pyinstaller-5.2
build(deps-dev): bump pyinstaller from 5.1 to 5.2
2022-07-11 08:57:24 -06:00
Willi Ballenthin
954b90befb Merge pull request #1101 from mandiant/dependabot/pip/smda-1.7.2
build(deps): bump smda from 1.7.1 to 1.7.2
2022-07-11 08:56:18 -06:00
dependabot[bot]
62422ae4d9 build(deps-dev): bump pyinstaller from 5.1 to 5.2
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.1 to 5.2.
- [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.1...v5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-11 14:26:33 +00:00
dependabot[bot]
6594d9d911 build(deps): bump smda from 1.7.1 to 1.7.2
Bumps [smda](https://github.com/danielplohmann/smda) from 1.7.1 to 1.7.2.
- [Release notes](https://github.com/danielplohmann/smda/releases)
- [Commits](https://github.com/danielplohmann/smda/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-11 14:26:26 +00:00
Anushka Virgaonkar
6e9676e0be fix #1095 (#1098) 2022-07-08 15:41:42 -06:00
Capa Bot
6764830f2d Sync capa-testfiles submodule 2022-07-08 18:59:42 +00:00
Mike Hunhoff
747eed4db7 render: display number feature as hex vverbose (#1097) 2022-07-08 10:37:41 -06:00
Capa Bot
28f32eebfc Sync capa rules submodule 2022-07-07 21:24:45 +00:00
Capa Bot
3dbd57ffe4 Sync capa rules submodule 2022-07-07 21:23:20 +00:00
Capa Bot
e63a9c801b Sync capa rules submodule 2022-07-07 19:40:55 +00:00
Capa Bot
0fbea75513 Sync capa rules submodule 2022-07-07 17:38:22 +00:00
Mike Hunhoff
4b3129e30a ida: bug fixes including #1090 for Address abstraction (#1091) 2022-07-07 09:54:20 -06:00
Anushka Virgaonkar
10c16e8a71 fix #1089: add feature format to global scope (#1093) 2022-07-07 08:15:34 +02:00
Capa Bot
21efdd2e0e Sync capa rules submodule 2022-07-06 19:36:29 +00:00
dependabot[bot]
ac1add3fcb build(deps-dev): bump black from 22.3.0 to 22.6.0 (#1085)
Bumps [black](https://github.com/psf/black) from 22.3.0 to 22.6.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.3.0...22.6.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2022-07-06 10:11:31 +02:00
Moritz
b4d2fecf4b fix: string extraction and rendering (#1083) 2022-07-05 13:17:24 -06:00
Capa Bot
ec81768fb5 Sync capa rules submodule 2022-07-05 15:37:24 +00:00
Capa Bot
0f60165135 Sync capa rules submodule 2022-07-05 07:53:28 +00:00
Capa Bot
7c54502dc8 Sync capa rules submodule 2022-07-05 07:17:51 +00:00
Moritz
38668b2c4a fix: use DnfileFeatureExtractor (#1088)
closes #1087
2022-07-05 07:53:47 +02:00
Moritz
d210645aee fix: api handling workaround (#1079)
* fix: api handling workaround

* fix: workaround .NET api handling
2022-07-01 17:37:56 +02:00
Capa Bot
444c30d720 Sync capa rules submodule 2022-07-01 14:50:27 +00:00
Willi Ballenthin
22bc26905f Merge pull request #1082 from mandiant/fix/addr-check
check address value
2022-07-01 08:49:17 -06:00
Moritz Raabe
9f4479582a check address value
closes #1081
2022-07-01 15:40:23 +02:00
Capa Bot
7bd49b56c4 Sync capa rules submodule 2022-06-30 15:17:33 +00:00
Capa Bot
9015761d4d Sync capa-testfiles submodule 2022-06-30 15:16:42 +00:00
Capa Bot
36eabc1c39 Sync capa rules submodule 2022-06-29 23:22:05 +00:00
Capa Bot
2f792427f9 Sync capa rules submodule 2022-06-29 17:55:13 +00:00
Capa Bot
cc06101cdc Sync capa rules submodule 2022-06-29 17:54:42 +00:00
Capa Bot
7387c56af9 Sync capa-testfiles submodule 2022-06-29 17:47:36 +00:00
Mike Hunhoff
998364d500 update documentation for .NET (#1074) 2022-06-29 08:22:48 -06:00
Willi Ballenthin
e7cf69a82e pep8 2022-06-28 15:58:02 -06:00
Willi Ballenthin
8dbb5a097c Merge branch 'master' of github.com:mandiant/capa 2022-06-28 15:54:19 -06:00
Willi Ballenthin
91818a116d scripts/capa_as_library: use new ResultDocument
closes #1071
2022-06-28 15:53:37 -06:00
Capa Bot
82e8f8f090 Sync capa rules submodule 2022-06-28 21:34:02 +00:00
Willi Ballenthin
2a0ada9848 Merge pull request #1072 from mandiant/revert-1014-fix/temp-accept-x32-x64-but-ignore
Revert "fix: temporarily accept x32/x64 flavors but ignore"
2022-06-28 14:28:08 -06:00
Willi Ballenthin
b87b03300a Revert "fix: temporarily accept x32/x64 flavors but ignore (#1014)"
This reverts commit daf483309e.
2022-06-28 14:27:48 -06:00
Capa Bot
ecd88680dd Sync capa rules submodule 2022-06-28 20:26:53 +00:00
Capa Bot
45c39cfd7a Sync capa rules submodule 2022-06-28 20:11:57 +00:00
Capa Bot
46ad23fb30 Sync capa rules submodule 2022-06-28 20:04:59 +00:00
Capa Bot
0e6a050921 Sync capa rules submodule 2022-06-28 20:04:14 +00:00
Moritz
f72f8b054a ci: use macos-11 (#1066)
* ci: use macos-11

* ci: use macos-10.15 for build
2022-06-28 21:47:06 +02:00
Willi Ballenthin
1d61b24eb0 Merge pull request #1055 from mandiant/dependabot/pip/types-requests-2.28.0
build(deps-dev): bump types-requests from 2.27.30 to 2.28.0
2022-06-28 11:35:09 -06:00
Willi Ballenthin
5a73a8d7bb Merge pull request #1069 from mandiant/fix-1053
render: verbose: render metadata tokens correctly
2022-06-28 11:30:48 -06:00
Willi Ballenthin
b2507d14c0 Merge pull request #1070 from mandiant/williballenthin-patch-1
changelog: document upcoming deprecation of SMDA backend
2022-06-28 11:30:10 -06:00
Willi Ballenthin
b6f932ea15 changelog: document upcoming deprecation of SMDA backend
closes #1061
2022-06-28 11:24:31 -06:00
Willi Ballenthin
bb1afb3356 render: verbose: render metadata tokens correctly
closes #1053
2022-06-28 11:18:24 -06:00
Capa Bot
d35ac32f0a Sync capa rules submodule 2022-06-28 17:13:42 +00:00
Willi Ballenthin
cb6781a143 verbose: remove extraneous import 2022-06-28 11:11:00 -06:00
Willi Ballenthin
e7fa1ae52c Merge pull request #1068 from mandiant/feat-lint-dotnet-filename
linter: expect file path "dotnet" for ".NET" rules
2022-06-28 11:05:37 -06:00
Willi Ballenthin
8b7ddc5679 linter: expect file path "dotnet" for ".NET" rules
ref: https://github.com/mandiant/capa-rules/pull/568#discussion_r908718249
2022-06-28 10:48:03 -06:00
Willi Ballenthin
3323d85067 Merge pull request #1067 from mandiant/fix-966
main: suppress low level ELF parsing log messages
2022-06-28 10:46:32 -06:00
Willi Ballenthin
9019e6b0f5 main: suppress low level ELF parsing log messages
closes #966
2022-06-28 10:45:38 -06:00
Capa Bot
c6c2fc9f2a Sync capa rules submodule 2022-06-28 16:44:02 +00:00
Willi Ballenthin
6ea15901d6 Merge pull request #1064 from mandiant/mr-tz-patch-1
Update README.md
2022-06-28 10:42:44 -06:00
Willi Ballenthin
400e28c3f7 document rule branches (#1006)
* doc: add rules documentation

* version: add accessor for capa major version and git checkout command

* doc: rules: document version output

* doc: rules: show example error due to mismatched rule version

* doc: rules: update links

* ci: tag major rule version test

* ci: tag major rule version

* doc: update changelog

* ci: force to replace tag if exists

* ci: simplify tagging step

* Update doc/rules.md

* feat: disable tag, show error branch cmd

* doc: update major version rules branch

* Apply suggestions from code review

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>

* Update capa/main.py

* black

Co-authored-by: Moritz Raabe <moritz.raabe@mandiant.com>
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2022-06-28 18:38:51 +02:00
Moritz
f2281b8e6e Update README.md 2022-06-28 18:33:42 +02:00
Willi Ballenthin
ad88e51228 Merge pull request #1060 from mandiant/feat/frz-extractor-info
Feat/frz extractor info
2022-06-28 10:32:37 -06:00
Capa Bot
2b17b22d33 Sync capa rules submodule 2022-06-28 16:30:17 +00:00
Willi Ballenthin
da6f6dd94f Merge pull request #1063 from mandiant/fix-blog-links
readme: fix blog links
2022-06-28 10:30:08 -06:00
Willi Ballenthin
09d444222a readme: fix blog links
closes #800
2022-06-28 10:27:31 -06:00
Capa Bot
a5c9993b61 Sync capa rules submodule 2022-06-28 14:06:49 +00:00
Moritz
f03eb87892 Merge pull request #1059 from mandiant/fix/linter-refs-authors
feat: lint check value types
2022-06-28 15:57:39 +02:00
Moritz Raabe
a7c4761fef isort, black 2022-06-28 15:53:10 +02:00
Moritz Raabe
e2156c3854 refactor: parametrize test 2022-06-28 15:49:21 +02:00
Moritz Raabe
bf53958887 fix!: global feat frz address 2022-06-28 15:47:12 +02:00
Moritz Raabe
e4d532e212 feat!: add extractor info to frz format 2022-06-28 15:43:59 +02:00
Moritz Raabe
9bf582a89a feat: lint check value types
closes #1027
2022-06-28 12:53:31 +02:00
dependabot[bot]
470995a541 build(deps-dev): bump types-requests from 2.27.30 to 2.28.0
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.30 to 2.28.0.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-27 14:10:51 +00:00
Capa Bot
79ce903817 Sync capa rules submodule 2022-06-23 13:27:54 +00:00
Willi Ballenthin
6fa8f9e401 Merge pull request #1052 from mandiant/dependabot/pip/colorama-0.4.5
build(deps): bump colorama from 0.4.4 to 0.4.5
2022-06-21 09:45:21 -06:00
Willi Ballenthin
fb99ef56e3 Merge pull request #986 from mandiant/feature-981
add Address abstraction
2022-06-21 09:44:57 -06:00
Willi Ballenthin
be2dffe863 bulk-process: use new ResultDocument json 2022-06-20 14:43:30 -06:00
Willi Ballenthin
e3804a0596 main: add types for collect_metadata 2022-06-20 14:43:18 -06:00
Willi Ballenthin
9ebea05933 show-capabilities-by-function: use new ResultDocument 2022-06-20 14:32:10 -06:00
Willi Ballenthin
a453258a51 tests: fix render test for MBC 2022-06-20 14:25:18 -06:00
Willi Ballenthin
246ef58e7b tests: fix render test for ATT&CK metadata 2022-06-20 14:24:01 -06:00
dependabot[bot]
d55d1facd5 build(deps): bump colorama from 0.4.4 to 0.4.5
Bumps [colorama](https://github.com/tartley/colorama) from 0.4.4 to 0.4.5.
- [Release notes](https://github.com/tartley/colorama/releases)
- [Changelog](https://github.com/tartley/colorama/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tartley/colorama/compare/0.4.4...0.4.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-20 14:20:36 +00:00
William Ballenthin
a5979d3b4d Merge branch 'feature-981' of github.com:fireeye/capa into feature-981 2022-06-14 17:43:24 -06:00
William Ballenthin
af9049da6e dnfile: return NO_ADDRESS for base_address 2022-06-14 17:43:04 -06:00
William Ballenthin
6b5e125592 extractors: mypy 2022-06-14 17:42:04 -06:00
William Ballenthin
ee5c86913d extractor: clarify base address handling 2022-06-14 17:40:04 -06:00
Willi Ballenthin
0ff3bf1e5e Update .github/workflows/tests.yml 2022-06-14 17:07:25 -06:00
Willi Ballenthin
f5b79c0285 Update .github/workflows/tests.yml 2022-06-14 17:06:48 -06:00
William Ballenthin
c417b5dd79 merge master 2022-06-14 17:05:46 -06:00
William Ballenthin
bb74c73f6f sync rules 2022-06-14 17:02:34 -06:00
Willi Ballenthin
df101e5a60 Update capa/features/extractors/dnfile/extractor.py
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2022-06-14 17:01:20 -06:00
William Ballenthin
aff6191b11 ida: meta: provide [] as argv 2022-06-14 16:59:58 -06:00
William Ballenthin
269f056e52 ida: use new ResultDocument structures 2022-06-14 16:58:32 -06:00
William Ballenthin
9c77488937 ida: meta: extract os/format/platform 2022-06-14 16:57:19 -06:00
Moritz
2ceed78924 Merge pull request #1050 from mandiant/dependabot/pip/mypy-0.961
build(deps-dev): bump mypy from 0.960 to 0.961
2022-06-13 19:24:37 +02:00
Moritz
df99b1d394 Merge pull request #1049 from mandiant/dependabot/pip/requests-2.28.0
build(deps-dev): bump requests from 2.27.1 to 2.28.0
2022-06-13 19:24:29 +02:00
dependabot[bot]
57633ceeb2 build(deps-dev): bump mypy from 0.960 to 0.961
Bumps [mypy](https://github.com/python/mypy) from 0.960 to 0.961.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.960...v0.961)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-13 14:16:18 +00:00
dependabot[bot]
7aa041c4d1 build(deps-dev): bump requests from 2.27.1 to 2.28.0
Bumps [requests](https://github.com/psf/requests) from 2.27.1 to 2.28.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.27.1...v2.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-13 14:16:11 +00:00
Willi Ballenthin
8031be75ab render: fix computation of subrule matches 2022-06-10 15:06:22 -06:00
Willi Ballenthin
3103307601 tests: fix reference error 2022-06-10 14:58:26 -06:00
Willi Ballenthin
6568189839 freeze: fix sorting of addresses 2022-06-10 14:58:17 -06:00
Capa Bot
c653dd7e72 Sync capa-testfiles submodule 2022-06-10 20:48:49 +00:00
Willi Ballenthin
1c771da848 pep8 2022-06-10 14:47:23 -06:00
Willi Ballenthin
5b5ac16830 render: fix rendering of .NET tokens 2022-06-10 14:47:06 -06:00
Willi Ballenthin
67221e5907 freeze: fix (de)serialization of tokens 2022-06-10 14:46:56 -06:00
Willi Ballenthin
6a5271c16f remove old file 2022-06-10 14:38:14 -06:00
William Ballenthin
c3418fddb5 tests: json: fix address representation 2022-06-08 13:29:04 -06:00
William Ballenthin
faf414e3d8 tests: add more dotnet tests 2022-06-08 13:28:53 -06:00
William Ballenthin
c6144a1dfa freeze: address: fix .NET address sorting 2022-06-08 13:07:10 -06:00
William Ballenthin
ad153499a3 address: implement __eq__ 2022-06-08 12:46:50 -06:00
William Ballenthin
2767660722 features: substring: correctly record no captures 2022-06-08 11:09:53 -06:00
Willi Ballenthin
9433d41588 Update capa/features/address.py
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2022-06-08 10:10:20 -06:00
Willi Ballenthin
96b522cf6c Update capa/features/address.py
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2022-06-08 10:10:12 -06:00
Willi Ballenthin
f35a82562b Update capa/features/extractors/dnfile/insn.py
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2022-06-08 10:00:21 -06:00
Willi Ballenthin
bfda997fdf freeze: support Class and Namespace features, too 2022-06-06 15:55:12 -06:00
Willi Ballenthin
9c09923b86 main: fix .NET format detection 2022-06-06 15:46:40 -06:00
Willi Ballenthin
3ef126fbd7 show-features: fix rendering addresses 2022-06-06 15:27:06 -06:00
Willi Ballenthin
9fdaa91fa9 render: vverbose: fixup rendering of imports 2022-06-06 15:22:06 -06:00
Willi Ballenthin
0987141970 tests: add tests demonstrating rending of .NET samples 2022-06-06 15:13:20 -06:00
Willi Ballenthin
c73db051c1 fixtures: add path to extractors 2022-06-06 15:13:11 -06:00
Willi Ballenthin
9a8d28d107 viv: remove old handle implementation 2022-06-06 15:05:24 -06:00
Willi Ballenthin
0b11a35358 Merge pull request #1045 from mandiant/dependabot/pip/types-requests-2.27.30
build(deps-dev): bump types-requests from 2.27.29 to 2.27.30
2022-06-06 15:00:48 -06:00
dependabot[bot]
524ab86d24 build(deps-dev): bump types-requests from 2.27.29 to 2.27.30
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.29 to 2.27.30.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-06 21:00:10 +00:00
Willi Ballenthin
0060daf2e8 Merge pull request #1046 from mandiant/dependabot/pip/types-psutil-5.8.23
build(deps-dev): bump types-psutil from 5.8.22 to 5.8.23
2022-06-06 14:59:18 -06:00
Willi Ballenthin
f5eb52f7c9 Merge pull request #1048 from mandiant/dependabot/pip/types-pyyaml-6.0.8
build(deps-dev): bump types-pyyaml from 6.0.7 to 6.0.8
2022-06-06 14:59:01 -06:00
Willi Ballenthin
59944d6aa6 Merge pull request #1039 from mandiant/dependabot/pip/pefile-2022.5.30
build(deps): bump pefile from 2021.9.3 to 2022.5.30
2022-06-06 14:58:52 -06:00
Willi Ballenthin
a6a48dc7a3 Merge pull request #1047 from mandiant/dependabot/pip/viv-utils-flirt--0.7.5
build(deps): bump viv-utils[flirt] from 0.7.4 to 0.7.5
2022-06-06 14:58:43 -06:00
Willi Ballenthin
1b951aa2d5 *: remove unused imports 2022-06-06 14:33:34 -06:00
Willi Ballenthin
a66c6c9d23 setup: fix pydantic dep version 2022-06-06 14:29:22 -06:00
Willi Ballenthin
dddcec4be3 setup: fix dep spec 2022-06-06 14:27:34 -06:00
Willi Ballenthin
1a290a38c4 Merge branch 'master' into feature-981 2022-06-06 14:07:51 -06:00
Willi Ballenthin
dcdc70de49 Merge branch 'feature-981' of github.com:mandiant/capa into feature-981 2022-06-06 13:49:28 -06:00
Willi Ballenthin
f8b10a2c0a render: verbose: update to use new result document 2022-06-06 13:48:41 -06:00
Willi Ballenthin
5960f51f13 result document: fix type of statement node 2022-06-06 13:47:39 -06:00
Willi Ballenthin
59e0518e6d pep8/mypy 2022-06-06 13:47:17 -06:00
Willi Ballenthin
afc2953538 frz: address: make sortable 2022-06-06 13:45:13 -06:00
Willi Ballenthin
f58966acf8 address: implement repr, not str 2022-06-06 13:44:59 -06:00
Willi Ballenthin
cb44704d38 features: bb: add description to BasicBlock feature 2022-06-06 13:44:40 -06:00
Willi Ballenthin
ab4177fae1 render: default: fix rendering of mbc/att&ck 2022-06-06 13:44:08 -06:00
Willi Ballenthin
867662ba5a rules: remove unused rule-category meta 2022-06-06 13:43:44 -06:00
Capa Bot
6cb4493b8e Sync capa rules submodule 2022-06-06 19:18:31 +00:00
dependabot[bot]
0444ab0bc5 build(deps-dev): bump types-pyyaml from 6.0.7 to 6.0.8
Bumps [types-pyyaml](https://github.com/python/typeshed) from 6.0.7 to 6.0.8.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-06 14:11:09 +00:00
dependabot[bot]
51a2da7e05 build(deps): bump viv-utils[flirt] from 0.7.4 to 0.7.5
Bumps [viv-utils[flirt]](https://github.com/williballenthin/viv-utils) from 0.7.4 to 0.7.5.
- [Release notes](https://github.com/williballenthin/viv-utils/releases)
- [Commits](https://github.com/williballenthin/viv-utils/compare/v0.7.4...v0.7.5)

---
updated-dependencies:
- dependency-name: viv-utils[flirt]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-06 14:11:08 +00:00
dependabot[bot]
d625e99dd0 build(deps-dev): bump types-psutil from 5.8.22 to 5.8.23
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.22 to 5.8.23.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-06 14:11:03 +00:00
Capa Bot
43dca13f26 Sync capa rules submodule 2022-06-03 17:16:28 +00:00
Capa Bot
bc8c4a0323 Sync capa rules submodule 2022-06-01 12:13:22 +00:00
Moritz
d8e68255a0 Merge pull request #1044 from mandiant/fix/rules-meta-authors
fix!: authors instead of author
2022-06-01 14:12:31 +02:00
Capa Bot
781ec74310 Sync capa-testfiles submodule 2022-06-01 12:12:01 +00:00
Moritz Raabe
1df60186f0 fix!: authors instead of author 2022-05-31 23:05:13 +02:00
Moritz
b8e297c5ba Merge pull request #1043 from mandiant/mr-tz-patch-1
Update build.yml
2022-05-31 22:40:45 +02:00
Zander Work
486ffed4bd ignore .git/ when loading capa rules (#1038)
* ignore .git/ when loading capa rules

* moved check outside of for loop
2022-05-31 22:35:28 +02:00
Moritz
cb703aea18 Update build.yml 2022-05-31 22:33:53 +02:00
Willi Ballenthin
5084cb0887 Merge branch 'feature-981' of github.com:mandiant/capa into feature-981 2022-05-31 07:45:38 -06:00
Willi Ballenthin
5d6c12d900 sync rules 2022-05-31 07:45:13 -06:00
Capa Bot
2f47fddda9 Sync capa rules submodule 2022-05-31 13:42:34 +00:00
Willi Ballenthin
42e2c53e5e wip: pydantic result document 2022-05-31 07:40:50 -06:00
Willi Ballenthin
8080752815 freeze: pass descriptions around 2022-05-31 07:40:30 -06:00
Willi Ballenthin
2dec484676 typing fixes 2022-05-31 07:40:20 -06:00
Moritz
3d0a59cf74 Update pyinstaller.spec (#1042)
* Update pyinstaller.spec
2022-05-31 13:29:04 +02:00
Moritz
5169568c3b Merge pull request #1034 from mandiant/dependabot/pip/pyinstaller-5.1
build(deps-dev): bump pyinstaller from 5.0.1 to 5.1
2022-05-31 12:33:38 +02:00
dependabot[bot]
44a5dc0cd0 build(deps-dev): bump pyinstaller from 5.0.1 to 5.1
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.0.1 to 5.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.0.1...v5.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-05-31 09:18:38 +00:00
Moritz
1f38004114 Merge pull request #1040 from mandiant/dependabot/pip/mypy-0.960
build(deps-dev): bump mypy from 0.950 to 0.960
2022-05-31 11:18:04 +02:00
Moritz
8e7143556b Merge pull request #1041 from mandiant/dependabot/pip/types-requests-2.27.29
build(deps-dev): bump types-requests from 2.27.27 to 2.27.29
2022-05-31 11:17:52 +02:00
dependabot[bot]
2f519cba30 build(deps-dev): bump types-requests from 2.27.27 to 2.27.29
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.27 to 2.27.29.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-30 14:14:17 +00:00
dependabot[bot]
02444d801e build(deps-dev): bump mypy from 0.950 to 0.960
Bumps [mypy](https://github.com/python/mypy) from 0.950 to 0.960.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.950...v0.960)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-30 14:14:14 +00:00
dependabot[bot]
85d4991cb3 build(deps): bump pefile from 2021.9.3 to 2022.5.30
Bumps [pefile](https://github.com/erocarrera/pefile) from 2021.9.3 to 2022.5.30.
- [Release notes](https://github.com/erocarrera/pefile/releases)
- [Commits](https://github.com/erocarrera/pefile/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-30 14:14:11 +00:00
Willi Ballenthin
4ae4bab254 lint: use meta.authors 2022-05-26 12:02:47 -06:00
Mike Hunhoff
3514d5c05c dotnet: support file/function scope class and namespace features (#1030) 2022-05-26 11:19:31 -06:00
Willi Ballenthin
9236a36ef4 rule: factor out is subscope check 2022-05-26 10:24:31 -06:00
Willi Ballenthin
b2318ce957 features: remove freeze_(de)serialize with preference to freeze module 2022-05-25 17:19:56 -06:00
Willi Ballenthin
3879e33cce freeze: model each features separately 2022-05-25 17:12:02 -06:00
Willi Ballenthin
eb6de90059 changelog 2022-05-25 15:30:46 -06:00
Willi Ballenthin
6b633efdba freeze: fix schema to support overlapping functions 2022-05-25 15:28:02 -06:00
Willi Ballenthin
02cef8297c pep8 2022-05-25 15:27:56 -06:00
Willi Ballenthin
adb425aeb3 freeze: use pydantic for (de)serialization
also, move null extractor to its own namespace
2022-05-25 15:09:31 -06:00
Willi Ballenthin
b1fa5be7b1 show-features: render features in a tree to better group scopes 2022-05-25 15:08:44 -06:00
Willi Ballenthin
d7cfa4ee96 features: make features implement __lt__ 2022-05-25 15:08:26 -06:00
Willi Ballenthin
46a79f43bb Merge pull request #1037 from mandiant/gh-codespaces
add Github Codespaces config
2022-05-25 11:38:15 -06:00
Willi Ballenthin
5a71caf09c add Github Codespaces config 2022-05-25 14:44:10 +00:00
Willi Ballenthin
a4003d7d91 tests: fix scripts using json document 2022-05-24 14:50:17 -06:00
Willi Ballenthin
b35fe6cdb2 json, render: work with and serialize addresses 2022-05-24 13:52:56 -06:00
Willi Ballenthin
d728869690 freeze: mypy and pep8 2022-05-24 13:52:40 -06:00
Willi Ballenthin
6b6dd70110 freeze: use address abstraction 2022-05-24 12:30:06 -06:00
Willi Ballenthin
fc9681f6d5 helpers: fix import loop 2022-05-24 12:29:56 -06:00
Willi Ballenthin
e4caa1d729 base extractor: use handles 2022-05-24 12:29:38 -06:00
Willi Ballenthin
4a577fabfc Merge pull request #1031 from mandiant/fix/ida-plugin
fix: rule generator handles
2022-05-23 11:29:48 -06:00
Willi Ballenthin
314ad4ea4d Merge pull request #1028 from mandiant/fix-988
elf: better detect Linux OS
2022-05-23 11:29:13 -06:00
Willi Ballenthin
2b446c75dd Merge pull request #1032 from mandiant/dependabot/pip/types-requests-2.27.27
build(deps-dev): bump types-requests from 2.27.25 to 2.27.27
2022-05-23 11:28:51 -06:00
Willi Ballenthin
ecf22c2c50 Merge pull request #1033 from mandiant/dependabot/pip/psutil-5.9.1
build(deps-dev): bump psutil from 5.9.0 to 5.9.1
2022-05-23 11:28:43 -06:00
dependabot[bot]
6f234b57fc build(deps-dev): bump psutil from 5.9.0 to 5.9.1
Bumps [psutil](https://github.com/giampaolo/psutil) from 5.9.0 to 5.9.1.
- [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.0...release-5.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-23 14:10:27 +00:00
dependabot[bot]
ddb6c810eb build(deps-dev): bump types-requests from 2.27.25 to 2.27.27
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.25 to 2.27.27.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-23 14:10:21 +00:00
Capa Bot
8f2c9cbd11 Sync capa rules submodule 2022-05-20 14:36:37 +00:00
Moritz Raabe
a4f0c1c04c fix: rule generator handles 2022-05-19 20:43:02 +02:00
Willi Ballenthin
7642db332a Merge pull request #1029 from mandiant/feat/981-add-ida-handles
feat: add IDA handles and type annotations
2022-05-17 09:33:46 -06:00
Moritz Raabe
8e1f710312 fix: add __str__ 2022-05-12 18:49:53 +02:00
Moritz Raabe
83cae29dbe ci: temporarily test on PR 2022-05-12 18:40:45 +02:00
Moritz Raabe
b2853cc56b feat: update dnfile tests and extractor 2022-05-12 18:37:02 +02:00
Moritz Raabe
d8c9941f6b fix: filter address 2022-05-12 16:39:36 +02:00
Moritz Raabe
716a73dfb4 feat: add handles and type annotations 2022-05-12 15:42:25 +02:00
Capa Bot
cded1d3125 Sync capa-testfiles submodule 2022-05-12 06:35:04 +00:00
Willi Ballenthin
7b05fc4180 pep8 + mypy 2022-05-11 13:25:25 -06:00
Willi Ballenthin
78e9280a93 Merge branch 'master' into feature-981 2022-05-11 13:20:48 -06:00
Willi Ballenthin
ca2adb85b0 Merge pull request #1026 from mandiant/dependabot/pip/types-colorama-0.4.14
build(deps-dev): bump types-colorama from 0.4.13 to 0.4.14
2022-05-11 13:12:39 -06:00
Willi Ballenthin
fca612e873 Merge pull request #1025 from mandiant/dependabot/pip/types-tabulate-0.8.9
build(deps-dev): bump types-tabulate from 0.8.8 to 0.8.9
2022-05-11 13:12:30 -06:00
Willi Ballenthin
07e35780d3 Merge branch 'master' into fix-988 2022-05-11 13:10:45 -06:00
Willi Ballenthin
521cbf9104 pep8 2022-05-11 13:10:08 -06:00
Willi Ballenthin
a6427364e0 tests: add test demonstrating elf OS detection 2022-05-11 13:09:12 -06:00
Willi Ballenthin
c30ce6e73a changelog 2022-05-11 12:54:30 -06:00
Willi Ballenthin
e4abe46d16 elf: better detect Linux OS
closes #988
2022-05-11 12:53:17 -06:00
Willi Ballenthin
71cf19b850 render: handle dn tokens 2022-05-11 12:51:42 -06:00
Willi Ballenthin
a734a045ae dnfile: address: use rva 2022-05-11 12:51:29 -06:00
Capa Bot
141da27715 Sync capa rules submodule 2022-05-11 16:34:46 +00:00
dependabot[bot]
7971b94001 build(deps-dev): bump types-colorama from 0.4.13 to 0.4.14
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.13 to 0.4.14.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-09 14:13:43 +00:00
dependabot[bot]
95b3c6a594 build(deps-dev): bump types-tabulate from 0.8.8 to 0.8.9
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.8.8 to 0.8.9.
- [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-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-09 14:13:40 +00:00
Mike Hunhoff
0d849142ba dotnet: emit mixed mode characteristic (#1024) 2022-05-06 14:32:06 -06:00
Moritz
f96c7379e0 Merge pull request #1021 from mandiant/ci/fix-build-workflow
ci: build on PR
2022-05-06 22:07:53 +02:00
Mike Hunhoff
6fb9dd961a dotnet: emit unmanaged call characteristic (#1023) 2022-05-06 13:05:48 -06:00
Mike Hunhoff
a9c9b3cea8 dotnet: extract file function names (#1015) 2022-05-06 08:34:50 -06:00
Moritz
ff2810654e Merge pull request #1018 from mandiant/dependabot/pip/vivisect-1.0.8
build(deps): bump vivisect from 1.0.7 to 1.0.8
2022-05-06 16:30:38 +02:00
Moritz Raabe
80e4161b40 ci: build on PR 2022-05-06 16:29:54 +02:00
Moritz
0473ce3259 Merge pull request #1017 from mandiant/dependabot/pip/mypy-0.950
build(deps-dev): bump mypy from 0.942 to 0.950
2022-05-06 15:50:15 +02:00
Moritz
0a211c1461 Merge pull request #1019 from mandiant/dependabot/pip/types-termcolor-1.1.4
build(deps-dev): bump types-termcolor from 1.1.3 to 1.1.4
2022-05-06 15:50:00 +02:00
Moritz Raabe
5573794a1f dep: bump viv-utils 2022-05-06 15:49:04 +02:00
dependabot[bot]
d0a1313f33 build(deps-dev): bump types-termcolor from 1.1.3 to 1.1.4
Bumps [types-termcolor](https://github.com/python/typeshed) from 1.1.3 to 1.1.4.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-05 20:21:15 +00:00
dependabot[bot]
aca4f27ee8 build(deps-dev): bump mypy from 0.942 to 0.950
Bumps [mypy](https://github.com/python/mypy) from 0.942 to 0.950.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.942...v0.950)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-05 20:20:18 +00:00
dependabot[bot]
bcd00004b8 build(deps-dev): bump types-tabulate from 0.8.7 to 0.8.8 (#1009) 2022-05-05 14:20:16 -06:00
dependabot[bot]
eefc0a9632 build(deps-dev): bump pyinstaller from 5.0 to 5.0.1 (#1008) 2022-05-05 14:19:56 -06:00
dependabot[bot]
dcf43b6fee build(deps): bump vivisect from 1.0.7 to 1.0.8
Bumps [vivisect](https://github.com/vivisect/vivisect) from 1.0.7 to 1.0.8.
- [Release notes](https://github.com/vivisect/vivisect/releases)
- [Changelog](https://github.com/vivisect/vivisect/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/vivisect/vivisect/compare/v1.0.7...v1.0.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-05 20:19:53 +00:00
dependabot[bot]
6d218aaf0d build(deps-dev): bump types-requests from 2.27.20 to 2.27.25 (#1007) 2022-05-05 14:19:35 -06:00
dependabot[bot]
20d80c1a2e build(deps-dev): bump types-colorama from 0.4.12 to 0.4.13 (#1010) 2022-05-05 14:19:21 -06:00
Mike Hunhoff
24c4215820 dotnet: add file string parsing (#1012) 2022-05-05 13:39:29 -06:00
dependabot[bot]
0066b3f33a build(deps): bump dnfile from 0.10.0 to 0.11.0 (#1004) 2022-05-05 13:28:48 -06:00
Moritz
daf483309e fix: temporarily accept x32/x64 flavors but ignore (#1014) 2022-05-05 20:19:35 +02:00
Capa Bot
49b1296d6e Sync capa rules submodule 2022-04-27 17:18:08 +00:00
Moritz
9f12f069ee ci: fix build (#980)
* ci: fix build

* fix: newest PyInstaller version

* fix: logo path

* fix: logo path 2

* fix: logo path 3

* fix: icon another way

* fix: remove icon for now

* ci: only build after tests succeed

* ci: add workflow_run check
2022-04-27 15:09:58 +02:00
Capa Bot
10852a5d96 Sync capa rules submodule 2022-04-27 11:36:08 +00:00
Moritz
3347245c2e Merge pull request #1003 from mandiant/dependabot/pip/viv-utils-flirt--0.7.1
build(deps): bump viv-utils[flirt] from 0.6.11 to 0.7.1
2022-04-26 21:09:13 +02:00
Moritz
3e8e88c363 Merge pull request #1002 from mandiant/dependabot/pip/types-requests-2.27.20
build(deps-dev): bump types-requests from 2.27.19 to 2.27.20
2022-04-26 21:08:54 +02:00
Moritz
e4dfa45057 Merge pull request #1001 from mandiant/dependabot/pip/types-pyyaml-6.0.7
build(deps-dev): bump types-pyyaml from 6.0.6 to 6.0.7
2022-04-26 21:08:44 +02:00
Moritz
b58e90e8dd Merge pull request #1000 from mandiant/dependabot/pip/pytest-7.1.2
build(deps-dev): bump pytest from 7.1.1 to 7.1.2
2022-04-26 21:08:31 +02:00
dependabot[bot]
0e18cea11a build(deps): bump viv-utils[flirt] from 0.6.11 to 0.7.1
Bumps [viv-utils[flirt]](https://github.com/williballenthin/viv-utils) from 0.6.11 to 0.7.1.
- [Release notes](https://github.com/williballenthin/viv-utils/releases)
- [Commits](https://github.com/williballenthin/viv-utils/compare/v0.6.11...v0.7.1)

---
updated-dependencies:
- dependency-name: viv-utils[flirt]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 14:13:34 +00:00
dependabot[bot]
e950932e43 build(deps-dev): bump types-requests from 2.27.19 to 2.27.20
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.19 to 2.27.20.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 14:13:31 +00:00
dependabot[bot]
45738773ca build(deps-dev): bump types-pyyaml from 6.0.6 to 6.0.7
Bumps [types-pyyaml](https://github.com/python/typeshed) from 6.0.6 to 6.0.7.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 14:13:28 +00:00
dependabot[bot]
054bcc9cb8 build(deps-dev): bump pytest from 7.1.1 to 7.1.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.1 to 7.1.2.
- [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.1...7.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 14:13:26 +00:00
Moritz
4d49b749c5 Merge pull request #998 from mandiant/dependabot/pip/types-tabulate-0.8.7
build(deps-dev): bump types-tabulate from 0.8.6 to 0.8.7
2022-04-20 17:53:08 +02:00
Moritz
4d86774266 Merge pull request #997 from mandiant/dependabot/pip/types-colorama-0.4.12
build(deps-dev): bump types-colorama from 0.4.10 to 0.4.12
2022-04-20 17:52:57 +02:00
Moritz
20171fe4f2 Merge pull request #995 from mandiant/dependabot/pip/types-psutil-5.8.22
build(deps-dev): bump types-psutil from 5.8.20 to 5.8.22
2022-04-20 17:52:40 +02:00
dependabot[bot]
308a47a784 build(deps-dev): bump types-tabulate from 0.8.6 to 0.8.7
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.8.6 to 0.8.7.
- [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-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-20 15:48:18 +00:00
dependabot[bot]
2226bf0faa build(deps-dev): bump types-psutil from 5.8.20 to 5.8.22
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.20 to 5.8.22.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-20 15:48:17 +00:00
dependabot[bot]
65cf8509f9 build(deps-dev): bump types-colorama from 0.4.10 to 0.4.12
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.10 to 0.4.12.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-20 15:48:15 +00:00
Moritz
523ec7f453 Merge pull request #996 from mandiant/dependabot/pip/types-pyyaml-6.0.6
build(deps-dev): bump types-pyyaml from 6.0.5 to 6.0.6
2022-04-20 17:47:32 +02:00
Moritz
8a1bc39eb2 Merge pull request #994 from mandiant/dependabot/pip/types-requests-2.27.19
build(deps-dev): bump types-requests from 2.27.16 to 2.27.19
2022-04-20 17:45:36 +02:00
dependabot[bot]
fd1785fe65 build(deps-dev): bump types-pyyaml from 6.0.5 to 6.0.6
Bumps [types-pyyaml](https://github.com/python/typeshed) from 6.0.5 to 6.0.6.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-18 14:12:43 +00:00
dependabot[bot]
45c22a24a6 build(deps-dev): bump types-requests from 2.27.16 to 2.27.19
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.16 to 2.27.19.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-18 14:12:38 +00:00
Willi Ballenthin
c236293185 features: insn: number: allow floats, too 2022-04-08 18:41:19 -06:00
Willi Ballenthin
bfb6d4d142 dn: fix access to ctx 2022-04-08 18:41:09 -06:00
Willi Ballenthin
723efe1755 address: better implement .NET token 2022-04-08 18:40:58 -06:00
Willi Ballenthin
e029547035 show-features: learn to use Addresses 2022-04-08 18:33:49 -06:00
Willi Ballenthin
d9ede95cf7 dnfile: use Address 2022-04-08 18:33:39 -06:00
Willi Ballenthin
70c3487bc7 address: better implement .NET token 2022-04-08 18:33:23 -06:00
Willi Ballenthin
808b7fb4dc dnfile: fix types 2022-04-08 18:33:12 -06:00
Willi Ballenthin
ed1009096d Merge branch 'master' of github.com:mandiant/capa into feature-981 2022-04-08 16:01:59 -06:00
Mike Hunhoff
580a2d7e45 dotnet: basic detection and feature extraction (#987) 2022-04-08 14:55:00 -06:00
Willi Ballenthin
87d3d6c577 smda: use Addresses 2022-04-08 13:15:04 -06:00
Willi Ballenthin
ae87fa1785 elf: use addresses 2022-04-08 12:49:48 -06:00
Willi Ballenthin
2b00bc0fdb pep8 2022-04-08 12:46:43 -06:00
Willi Ballenthin
43b8ad80c7 pefile: extract Addresses 2022-04-08 12:45:46 -06:00
Willi Ballenthin
65b462f62c render: format various address types differently 2022-04-08 12:39:10 -06:00
Willi Ballenthin
7e7740cf77 viv: insn: use handles for code merged from master 2022-04-08 12:38:46 -06:00
Willi Ballenthin
a3d1b1403c address: fix min value for unsigned addresses 2022-04-08 12:38:21 -06:00
Willi Ballenthin
31977e6523 changelog 2022-04-08 12:19:50 -06:00
Willi Ballenthin
9164713dd9 Merge branch 'dotnet-main' of github.com:mandiant/capa into feature-981 2022-04-08 12:17:16 -06:00
Willi Ballenthin
bfb01e3729 extractor: viv: use handles throughout 2022-04-08 11:54:27 -06:00
Willi Ballenthin
fc1709ba6c extractor: add types throughout 2022-04-08 11:53:42 -06:00
Willi Ballenthin
1b79aae836 extractor: introduce standardized handles for function, bb, insn 2022-04-08 11:46:07 -06:00
Willi Ballenthin
6355fb3f3e add Address abstraction to handle various ways of identifing things in files 2022-04-08 11:44:24 -06:00
Moritz
c8a772d19a test: update dotnet dirs and sync master (#984) 2022-04-08 09:34:22 -06:00
Capa Bot
5bc44aef0f Sync capa-testfiles submodule 2022-04-08 10:34:02 +00:00
Willi Ballenthin
b455b67da3 Merge pull request #977 from mandiant/feature-320
extract extra offset/number features
2022-04-07 14:20:10 -06:00
Willi Ballenthin
351d70aafe smda: implement additional offset and number features 2022-04-07 12:56:24 -06:00
Willi Ballenthin
8a2276f398 smda: implement operand number/offset features
cause its not too hard
2022-04-07 12:48:25 -06:00
Moritz
65552575f8 Update dotnet-main (#979)
* Sync capa rules submodule

* Sync capa-testfiles submodule

* Sync capa rules submodule

* changelog

* *: remove /x32 and /x64 flavors from number and offset features

* *: remove more references to /x32 and /x64

* linter: accept instruction scope

* rules: fix max operand index (4)

* API: better support A/W functions

* vverbose: show lib rule matches

* main: accept multiple paths to rules

* main: fix removal of default rules path

* lint: fix rules path

* changelog

* capa_as_library: fix rules path is list now

* main: better handle multiple rules paths

* main: bail if python 3.6 or below

closes #964

* ida: readme: remove python 3.6 support

* capa2yara: fix rules paths

* render: meta: display rule paths on separate lines

closes #971

* render: verbose: add doc

* verbose: make rule path multiline more concise

* vverbose: don't show examples in output

closes #970

* vverbose: render subscope name, like "basic block:"

closes #963

* build(deps-dev): bump pytest from 7.0.1 to 7.1.1

Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.1 to 7.1.1.
- [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.0.1...7.1.1)

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

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

* ci: build: update pip and setuptools

* ci: build: bump pyinstall to v4.10

* Sync capa rules submodule

* Dotnet mixed mode detect (#969)

* feat: start dotnet detection (#955)

* feat: start dotnet detection

* Apply suggestions from code review

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>

* refactor: dn instead of dotnet

* refactor: format branches, extractor reorg

* refactor: format selection and dotnet detect

* feat: get format, arch, os

* refactor: log errors and exceptions

* ci: also test and build for dotnet-main dev

* fix: import path

* fix: circular dep

* fix: remove buf argument
feat: get runtime meta data

* fix: log unsupported runtime error

* fix: type ignore

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>

* fix: imports and add tests

* feat: detect mixed mode and tests

* feat: start dotnet detection (#955)

* feat: start dotnet detection

* Apply suggestions from code review

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>

* refactor: dn instead of dotnet

* refactor: format branches, extractor reorg

* refactor: format selection and dotnet detect

* feat: get format, arch, os

* refactor: log errors and exceptions

* ci: also test and build for dotnet-main dev

* fix: import path

* fix: circular dep

* fix: remove buf argument
feat: get runtime meta data

* fix: log unsupported runtime error

* fix: type ignore

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>

* fix: imports and add tests

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>

* test: checkout submodules recursively

Co-authored-by: Capa Bot <capa-dev@mandiant.com>
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-07 17:45:29 +02:00
Capa Bot
4c84a77053 Sync capa rules submodule 2022-04-07 07:50:51 +00:00
Willi Ballenthin
6b810a1f72 ida: insn: look for numbers in displ, not phrase 2022-04-06 15:41:17 -06:00
Willi Ballenthin
c36bde0f2d ida: insn: ignore numbers when SIB present 2022-04-06 15:38:04 -06:00
Willi Ballenthin
1a44dd8a2b insn: better detect offset/numbers 2022-04-06 15:12:59 -06:00
Willi Ballenthin
1c7b6bcf7d fixtures: use function that IDA doesn't recognize as lib func 2022-04-06 15:07:35 -06:00
Willi Ballenthin
e2c6f5e393 ida: insn: use .ea not .va 2022-04-06 15:03:24 -06:00
Willi Ballenthin
85d5043992 changelog 2022-04-06 14:59:24 -06:00
Willi Ballenthin
47dfeafdc8 ida, viv: implement extra offset/number extraction 2022-04-06 14:57:51 -06:00
Willi Ballenthin
b843cef986 tests: add tests for #320 2022-04-06 14:38:56 -06:00
Willi Ballenthin
0e95691cde tests: fixtures: enable assertions against instruction scope 2022-04-06 14:38:33 -06:00
Willi Ballenthin
54aa14c4f5 Merge pull request #975 from mandiant/fix-936
ci: build: bump pyinstall to v4.10
2022-04-06 14:20:21 -06:00
Willi Ballenthin
dfcb3cc2ea ci: build: bump pyinstall to v4.10 2022-04-06 14:17:59 -06:00
Willi Ballenthin
587202ce43 ci: build: update pip and setuptools 2022-04-06 14:03:44 -06:00
Willi Ballenthin
6b2529bc80 Merge pull request #916 from mandiant/dependabot/pip/pytest-7.1.1
build(deps-dev): bump pytest from 7.0.1 to 7.1.1
2022-04-06 13:44:40 -06:00
Willi Ballenthin
52137f310a Merge pull request #974 from mandiant/feature-vverbose-subscope
in vverbose mode, show subscope name
2022-04-06 13:44:15 -06:00
Willi Ballenthin
ad90145aa7 Merge pull request #973 from mandiant/feature-remove-example-vverbose
vverbose: don't show examples in output
2022-04-06 13:42:12 -06:00
Willi Ballenthin
05f7ac0802 Merge pull request #972 from mandiant/feature-many-rule-paths-meta
render: meta: display rule paths on separate lines
2022-04-06 13:41:48 -06:00
Willi Ballenthin
fccca823c5 verbose: make rule path multiline more concise 2022-04-06 13:41:05 -06:00
Willi Ballenthin
441373ea13 vverbose: render subscope name, like "basic block:"
closes #963
2022-04-06 13:33:56 -06:00
Capa Bot
57d2df4922 Sync capa rules submodule 2022-04-06 19:28:26 +00:00
Willi Ballenthin
632e778376 vverbose: don't show examples in output
closes #970
2022-04-06 13:24:36 -06:00
Willi Ballenthin
d47b1503b2 render: verbose: add doc 2022-04-06 13:21:11 -06:00
Willi Ballenthin
938c75737b render: meta: display rule paths on separate lines
closes #971
2022-04-06 13:18:06 -06:00
Willi Ballenthin
55a5d10859 Merge pull request #961 from mandiant/feature-remove-flavors
remove /x32 and /x64 flavors of number and offset features
2022-04-06 12:57:18 -06:00
Willi Ballenthin
0c354cf268 capa2yara: fix rules paths 2022-04-06 12:36:28 -06:00
Willi Ballenthin
485600801c ida: readme: remove python 3.6 support 2022-04-06 12:16:06 -06:00
Willi Ballenthin
4916933139 main: bail if python 3.6 or below
closes #964
2022-04-06 12:14:53 -06:00
Capa Bot
73f1eb9c30 Sync capa rules submodule 2022-04-06 18:08:02 +00:00
Willi Ballenthin
e788384d42 main: better handle multiple rules paths 2022-04-06 12:05:01 -06:00
Capa Bot
633d8df1a4 Sync capa-testfiles submodule 2022-04-06 17:21:09 +00:00
Willi Ballenthin
aff72ad983 capa_as_library: fix rules path is list now 2022-04-06 11:07:34 -06:00
Willi Ballenthin
c9763c4d70 Merge branch 'master' into feature-remove-flavors 2022-04-06 11:05:05 -06:00
Capa Bot
931a13e505 Sync capa rules submodule 2022-04-06 17:04:16 +00:00
Moritz Raabe
97e76a88e3 fix: imports and add tests 2022-04-06 17:30:51 +02:00
Moritz
b5be876e61 feat: start dotnet detection (#955)
* feat: start dotnet detection

* Apply suggestions from code review

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>

* refactor: dn instead of dotnet

* refactor: format branches, extractor reorg

* refactor: format selection and dotnet detect

* feat: get format, arch, os

* refactor: log errors and exceptions

* ci: also test and build for dotnet-main dev

* fix: import path

* fix: circular dep

* fix: remove buf argument
feat: get runtime meta data

* fix: log unsupported runtime error

* fix: type ignore

Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2022-04-06 11:33:14 +02:00
Willi Ballenthin
7370a8f296 changelog 2022-04-05 17:21:04 -06:00
Willi Ballenthin
11b773573e lint: fix rules path 2022-04-05 17:17:44 -06:00
Willi Ballenthin
67dc2cb0fa main: fix removal of default rules path 2022-04-05 17:17:35 -06:00
Willi Ballenthin
bad9ecf3b1 main: accept multiple paths to rules 2022-04-05 17:14:53 -06:00
Willi Ballenthin
ef835649fd vverbose: show lib rule matches 2022-04-05 16:57:36 -06:00
Willi Ballenthin
e9bb56f3cf API: better support A/W functions 2022-04-05 14:54:15 -06:00
Willi Ballenthin
58acc9c2b7 rules: fix max operand index (4) 2022-04-05 14:53:58 -06:00
Willi Ballenthin
f923a4ea9b linter: accept instruction scope 2022-04-05 12:24:41 -06:00
Willi Ballenthin
5957dfecf0 Merge branch 'feature-remove-flavors' of github.com:mandiant/capa into feature-remove-flavors 2022-04-05 10:41:41 -06:00
Willi Ballenthin
aee61b35e4 *: remove more references to /x32 and /x64 2022-04-05 10:41:03 -06:00
Willi Ballenthin
169d5ab826 Merge branch 'master' into feature-remove-flavors 2022-04-05 10:37:18 -06:00
Willi Ballenthin
de312d87dc Merge pull request #960 from mandiant/feature-py37
upgrade min python version to 3.7
2022-04-05 10:36:33 -06:00
Willi Ballenthin
ecabd557a7 *: remove /x32 and /x64 flavors from number and offset features 2022-04-05 10:35:41 -06:00
Willi Ballenthin
f246a01484 changelog 2022-04-05 10:24:55 -06:00
Willi Ballenthin
0617b87f36 ci: no longer test against py3.6 2022-04-05 10:19:09 -06:00
Willi Ballenthin
715ac64ae6 changelog 2022-04-05 10:19:04 -06:00
Willi Ballenthin
78c0afe006 setup: min python version is now 3.7 2022-04-05 10:18:55 -06:00
Willi Ballenthin
df03932f89 gitignore 2022-04-04 16:54:51 -06:00
dependabot[bot]
15196c847a build(deps-dev): bump pytest from 7.0.1 to 7.1.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.1 to 7.1.1.
- [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.0.1...7.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-04 22:43:41 +00:00
Willi Ballenthin
b2b4471851 Merge pull request #930 from mandiant/feature-insn-scope
feature: instruction scope
2022-04-04 16:42:56 -06:00
Willi Ballenthin
5ffb73c5f5 ida: insn: extract operand number and offset features 2022-04-04 15:13:43 -06:00
Willi Ballenthin
ef93fcc89e tests: smda: xfail operand number/offset features 2022-04-04 12:05:15 -06:00
Willi Ballenthin
0af60d9a7e freeze: fix mypy 2022-04-04 12:01:13 -06:00
Willi Ballenthin
750803c3cc freeze: register operand features 2022-04-04 11:57:02 -06:00
Willi Ballenthin
b318b0a288 freeze: fix freeze_deserialize for features with multiple args 2022-04-04 11:56:47 -06:00
Willi Ballenthin
2989af0a3f features: use ABC to denote abstract classes 2022-04-04 11:49:51 -06:00
Moritz
3f168772aa Merge pull request #934 from mandiant/dependabot/pip/types-colorama-0.4.10
build(deps-dev): bump types-colorama from 0.4.9 to 0.4.10
2022-04-04 17:42:16 +02:00
Moritz
2ba25f096d Merge pull request #935 from mandiant/dependabot/pip/types-requests-2.27.16
build(deps-dev): bump types-requests from 2.27.15 to 2.27.16
2022-04-04 17:42:02 +02:00
Moritz
6d35e19571 Merge pull request #933 from mandiant/dependabot/pip/tqdm-4.64.0
build(deps): bump tqdm from 4.63.1 to 4.64.0
2022-04-04 17:41:56 +02:00
dependabot[bot]
0d9583f7e7 build(deps-dev): bump types-requests from 2.27.15 to 2.27.16
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.15 to 2.27.16.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-04 14:12:27 +00:00
dependabot[bot]
fe6b18135c build(deps-dev): bump types-colorama from 0.4.9 to 0.4.10
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.9 to 0.4.10.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-04 14:12:24 +00:00
dependabot[bot]
e89fe57def build(deps): bump tqdm from 4.63.1 to 4.64.0
Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.63.1 to 4.64.0.
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.63.1...v4.64.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-04 14:12:21 +00:00
Willi Ballenthin
85b1d50945 isort 2022-03-31 10:40:48 -06:00
Willi Ballenthin
856443319c viv: insn: fix OperandNumber reference 2022-03-31 10:39:18 -06:00
Willi Ballenthin
9da4ff10da *: rename OperandImmediate to OperandNumber 2022-03-31 10:37:06 -06:00
Willi Ballenthin
76831e9b9d changelog 2022-03-30 13:20:51 -06:00
Willi Ballenthin
997daf537e viv: insn: extract OperandOffset and OperandImmediate 2022-03-30 13:14:08 -06:00
Willi Ballenthin
c7aadca25c tests: demonstrate OperandOffset and OperandImmediate 2022-03-30 13:13:50 -06:00
Willi Ballenthin
6cbbd4d97f rules: parse OperandOffset and OperandImmediate features 2022-03-30 13:13:30 -06:00
Willi Ballenthin
e4c5ec278d features: insn: define OperandImmediate and OperandOffset 2022-03-30 13:13:07 -06:00
Willi Ballenthin
cce1e41519 formatting 2022-03-30 13:12:49 -06:00
Willi Ballenthin
b942050c4e features: viv: factor out operand feature extraction 2022-03-30 09:58:08 -06:00
Willi Ballenthin
d8d671e36f rules: add global scope features to file scope 2022-03-30 09:40:43 -06:00
Willi Ballenthin
49adb8de0c pep8 2022-03-29 13:00:28 -06:00
Willi Ballenthin
fb6b60bee3 tests: add tests demonstrating instruction (sub)scope matching 2022-03-29 12:58:38 -06:00
Willi Ballenthin
e0fca277f2 rules: update valid features per scope 2022-03-29 12:58:27 -06:00
Willi Ballenthin
0effb5f8b0 changelog 2022-03-29 12:33:55 -06:00
Willi Ballenthin
1839746bf8 main: factor out matching at instruction scope 2022-03-29 12:29:54 -06:00
Willi Ballenthin
1a28c324f1 rules: doc 2022-03-29 12:26:39 -06:00
Willi Ballenthin
c1b28f58d0 rules: don't use global features to downselect rules
closes #931
2022-03-29 12:25:27 -06:00
Willi Ballenthin
565e4e0a2f Merge branch 'feature-insn-scope' of github.com:mandiant/capa into feature-insn-scope 2022-03-29 11:52:45 -06:00
Willi Ballenthin
7487da89a1 Merge branch 'master' into feature-insn-scope 2022-03-29 11:51:14 -06:00
Willi Ballenthin
fe5d88585c setup: bump black to 22.3.0 to fix CI 2022-03-29 11:40:34 -06:00
Willi Ballenthin
bd6e62e9bf Update scripts/lint.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2022-03-29 11:26:21 -06:00
Willi Ballenthin
b76930d2a3 main: split out basic block feature, match extraction 2022-03-28 13:47:53 -06:00
Willi Ballenthin
00d439f681 main: rename find_code_capabilities 2022-03-28 13:29:06 -06:00
Willi Ballenthin
963cfbf380 pep8 2022-03-28 13:17:35 -06:00
Willi Ballenthin
031ea167e8 add pycodestyle config 2022-03-28 13:17:18 -06:00
Willi Ballenthin
dde52f2bc8 pep8 2022-03-28 13:04:44 -06:00
Willi Ballenthin
46cc681eba tests: demonstrate instruct subscope rule extraction 2022-03-28 13:04:13 -06:00
Willi Ballenthin
b0619f4f01 rules: index instruction rules in ruleset 2022-03-28 13:03:58 -06:00
Willi Ballenthin
2baf05acdb rules: parse instruction subscope with implied AND 2022-03-28 12:55:09 -06:00
Willi Ballenthin
890870bf45 rules: let subscope blocks have descriptions 2022-03-28 12:54:54 -06:00
Willi Ballenthin
9da9c3aceb rules: add valid features for insn scope 2022-03-28 12:40:10 -06:00
Willi Ballenthin
c8fedb0f70 gitignore 2022-03-28 12:39:58 -06:00
Willi Ballenthin
a203f56bdb rules: add new scope "instruction" 2022-03-28 12:14:07 -06:00
Willi Ballenthin
18880c40d5 Merge pull request #927 from mandiant/dependabot/pip/mypy-0.942
build(deps-dev): bump mypy from 0.941 to 0.942
2022-03-28 11:26:06 -06:00
Willi Ballenthin
bd62661ef3 Merge pull request #928 from mandiant/dependabot/pip/types-requests-2.27.15
build(deps-dev): bump types-requests from 2.27.12 to 2.27.15
2022-03-28 11:25:57 -06:00
Willi Ballenthin
8d285c03ad Merge pull request #929 from mandiant/dependabot/pip/tqdm-4.63.1
build(deps): bump tqdm from 4.63.0 to 4.63.1
2022-03-28 11:25:25 -06:00
dependabot[bot]
7a4ee78805 build(deps): bump tqdm from 4.63.0 to 4.63.1
Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.63.0 to 4.63.1.
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.63.0...v4.63.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-03-28 14:13:45 +00:00
dependabot[bot]
6105d2a36c build(deps-dev): bump types-requests from 2.27.12 to 2.27.15
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.12 to 2.27.15.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 14:13:39 +00:00
dependabot[bot]
7db90ba35e build(deps-dev): bump mypy from 0.941 to 0.942
Bumps [mypy](https://github.com/python/mypy) from 0.941 to 0.942.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.941...v0.942)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 14:13:37 +00:00
Mike Hunhoff
fb34b1674b improve handling _ prefix added to library functions as compile/link artifact (#924) 2022-03-25 13:34:39 -06:00
Capa Bot
eaf978da0a Sync capa rules submodule 2022-03-24 09:43:38 +00:00
Capa Bot
ecea572192 Sync capa-testfiles submodule 2022-03-24 09:30:26 +00:00
Capa Bot
5552baa5e2 Sync capa rules submodule 2022-03-24 08:16:48 +00:00
Capa Bot
3b86ccc1a4 Sync capa rules submodule 2022-03-23 17:57:09 +00:00
Mike Hunhoff
8fd81d1098 Merge pull request #922 from mandiant/pin-smda-version-171
update pinned smda version
2022-03-22 12:46:35 -06:00
Mike Hunhoff
b7badede86 update pinned smda version 2022-03-22 12:25:41 -06:00
Mike Hunhoff
4c4e633395 Merge pull request #919 from mandiant/fix/917
fixes #917
2022-03-22 07:15:40 -06:00
Capa Bot
1cd5e89f85 Sync capa-testfiles submodule 2022-03-22 07:22:11 +00:00
Michael Hunhoff
768050f36c update CHANGELOG 2022-03-21 15:37:21 -06:00
Michael Hunhoff
f7f286db6c merge upstream 2022-03-21 15:35:49 -06:00
Mike Hunhoff
6d2ec59653 Merge pull request #918 from mandiant/fix/911
fixes #911
2022-03-21 15:33:45 -06:00
Michael Hunhoff
924d0111fd fixes #917 2022-03-21 15:28:39 -06:00
Michael Hunhoff
fe87838dbe fixes #911 2022-03-21 13:48:41 -06:00
Willi Ballenthin
1b2f0fc85d Merge pull request #913 from mandiant/dependabot/pip/types-tabulate-0.8.6
build(deps-dev): bump types-tabulate from 0.8.5 to 0.8.6
2022-03-21 10:36:55 -06:00
dependabot[bot]
e3bec5f186 build(deps-dev): bump types-tabulate from 0.8.5 to 0.8.6
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.8.5 to 0.8.6.
- [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-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-21 16:36:24 +00:00
Willi Ballenthin
729b459701 Merge pull request #914 from mandiant/dependabot/pip/types-pyyaml-6.0.5
build(deps-dev): bump types-pyyaml from 6.0.4 to 6.0.5
2022-03-21 10:35:53 -06:00
Willi Ballenthin
1609bd5d07 Merge pull request #915 from mandiant/dependabot/pip/mypy-0.941
build(deps-dev): bump mypy from 0.940 to 0.941
2022-03-21 10:35:07 -06:00
dependabot[bot]
78222a530c build(deps-dev): bump types-pyyaml from 6.0.4 to 6.0.5
Bumps [types-pyyaml](https://github.com/python/typeshed) from 6.0.4 to 6.0.5.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-21 16:34:23 +00:00
dependabot[bot]
6613ee3c87 build(deps-dev): bump mypy from 0.940 to 0.941
Bumps [mypy](https://github.com/python/mypy) from 0.940 to 0.941.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.940...v0.941)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-21 16:34:23 +00:00
Willi Ballenthin
356b2f5ffb Merge pull request #912 from mandiant/dependabot/pip/types-colorama-0.4.9
build(deps-dev): bump types-colorama from 0.4.8 to 0.4.9
2022-03-21 10:33:37 -06:00
dependabot[bot]
a52cc7280f build(deps-dev): bump types-colorama from 0.4.8 to 0.4.9
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.8 to 0.4.9.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-21 14:13:40 +00:00
Capa Bot
0d38e3065c Sync capa rules submodule 2022-03-16 09:43:58 +00:00
Willi Ballenthin
3d13d501e7 Merge pull request #907 from mandiant/dependabot/pip/mypy-0.940
build(deps-dev): bump mypy from 0.931 to 0.940
2022-03-14 10:49:05 -06:00
Willi Ballenthin
ccf1f6205c Merge pull request #908 from mandiant/dependabot/pip/types-requests-2.27.12
build(deps-dev): bump types-requests from 2.27.11 to 2.27.12
2022-03-14 10:48:40 -06:00
dependabot[bot]
8d2b6df385 build(deps-dev): bump types-requests from 2.27.11 to 2.27.12
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.11 to 2.27.12.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-14 14:14:24 +00:00
dependabot[bot]
62fd13c892 build(deps-dev): bump mypy from 0.931 to 0.940
Bumps [mypy](https://github.com/python/mypy) from 0.931 to 0.940.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.931...v0.940)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-14 14:14:21 +00:00
Capa Bot
cbf9f321c6 Sync capa-testfiles submodule 2022-03-14 10:18:05 +00:00
Moritz
c975305e95 Merge pull request #906 from mandiant/ci/remove-windows-2016
remove windows-2016
2022-03-14 10:18:11 +01:00
Willi Ballenthin
8afd12103d ci: build: try to use windows-2022 2022-03-11 17:40:35 -07:00
Moritz
5d106afca6 remove windows-2016 2022-03-11 08:38:09 +01:00
Capa Bot
8e43a23766 Sync capa rules submodule 2022-03-10 18:21:36 +00:00
Moritz
d9d72ad8df Merge pull request #905 from mandiant/v320
release v3.2.0
2022-03-03 19:59:17 +01:00
Moritz Raabe
1c5af81a4e release v3.2.0 2022-03-03 10:45:43 +01:00
Capa Bot
014fc4cda9 Sync capa rules submodule 2022-03-03 09:26:55 +00:00
Moritz
f29992741d Merge pull request #904 from mandiant/bump-viv-utils-vivisect
bump vivisect 1.0.7 and viv-utils 0.6.11
2022-03-02 08:59:21 +01:00
Moritz Raabe
5fa5f08607 bump vivisect 1.0.7 and viv-utils 0.6.11 2022-03-02 07:51:29 +01:00
Moritz
d4921c4a2f Merge pull request #902 from mandiant/feature/call5-ida
Feature/call5 ida
2022-03-01 09:05:33 +01:00
Moritz
64238062ca Merge pull request #901 from uckelman-sf/use_stdlib_typing
Don't require typing package; it's in the stdlib now
2022-03-01 09:00:25 +01:00
Moritz Raabe
00f977fff9 add call $+5 characteristic for IDA extractor 2022-03-01 08:50:06 +01:00
Moritz
c7ae2cd540 Merge pull request #899 from kn0wl3dge/feature/366-shellcode_obfs_call
Add characteristic "call $+5" feature with support for vivisect and smda
2022-03-01 08:48:50 +01:00
Moritz
293d88b1b9 Merge pull request #900 from mandiant/dependabot/pip/tqdm-4.63.0
build(deps): bump tqdm from 4.62.3 to 4.63.0
2022-02-28 22:28:52 +01:00
Joel Uckelman
fa2d19a5ca Update change log. 2022-02-28 16:43:18 +00:00
Joel Uckelman
f0f22041ca Remove requirement for separate typing package; typing is in the Python
stdlib from 3.5, and we require >= 3.6. From 3.7, installing the typing
package causes import failures.
2022-02-28 14:55:18 +00:00
dependabot[bot]
321316f99f build(deps): bump tqdm from 4.62.3 to 4.63.0
Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.62.3 to 4.63.0.
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.62.3...v4.63.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-28 14:09:59 +00:00
Baptistin Boilot
4d915020a8 extractor: add characteristic(call $+5) feature extraction for vivisect and smda 2022-02-27 18:15:25 +01:00
Moritz
350eff27b7 Merge pull request #898 from mandiant/dependabot/pip/types-requests-2.27.11
build(deps-dev): bump types-requests from 2.27.10 to 2.27.11
2022-02-23 08:54:56 +01:00
dependabot[bot]
f9732db799 build(deps-dev): bump types-requests from 2.27.10 to 2.27.11
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.10 to 2.27.11.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-22 18:29:53 +00:00
Moritz
73a7842a85 Merge pull request #897 from mandiant/dependabot/pip/types-requests-2.27.10
build(deps-dev): bump types-requests from 2.27.9 to 2.27.10
2022-02-22 19:26:01 +01:00
dependabot[bot]
b13a402675 build(deps-dev): bump types-requests from 2.27.9 to 2.27.10
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.9 to 2.27.10.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-21 14:11:58 +00:00
Moritz
915cd5e4bc Merge pull request #896 from mandiant/dependabot/pip/pytest-7.0.1
build(deps-dev): bump pytest from 7.0.0 to 7.0.1
2022-02-15 10:23:25 +01:00
Moritz
151adfd5ed Merge pull request #894 from mandiant/dependabot/pip/ruamel-yaml-0.17.21
build(deps): bump ruamel-yaml from 0.17.20 to 0.17.21
2022-02-15 10:23:14 +01:00
Moritz
37519a038b Merge pull request #895 from mandiant/dependabot/pip/types-requests-2.27.9
build(deps-dev): bump types-requests from 2.27.8 to 2.27.9
2022-02-15 10:22:57 +01:00
dependabot[bot]
d0cc1b0b1d build(deps-dev): bump pytest from 7.0.0 to 7.0.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.0 to 7.0.1.
- [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.0.0...7.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-14 14:14:42 +00:00
dependabot[bot]
869ad9d561 build(deps-dev): bump types-requests from 2.27.8 to 2.27.9
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.8 to 2.27.9.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-14 14:14:38 +00:00
dependabot[bot]
b31a4d6242 build(deps): bump ruamel-yaml from 0.17.20 to 0.17.21
Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.20 to 0.17.21.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-14 14:14:34 +00:00
Moritz
439a855383 Merge pull request #893 from re-fox/master
Update capa_as_library.py
2022-02-14 10:36:42 +01:00
re-fox
37f51690d0 Update capa_as_library.py 2022-02-13 13:09:58 -05:00
Moritz
1bd807a1a0 Merge pull request #890 from mandiant/dependabot/pip/pyelftools-0.28
build(deps): bump pyelftools from 0.27 to 0.28
2022-02-07 21:25:23 +01:00
Moritz
ac6fef2e29 Merge pull request #889 from mandiant/dependabot/pip/pytest-7.0.0
build(deps-dev): bump pytest from 6.2.5 to 7.0.0
2022-02-07 21:24:52 +01:00
dependabot[bot]
e873086ddf build(deps): bump pyelftools from 0.27 to 0.28
Bumps [pyelftools](https://github.com/eliben/pyelftools) from 0.27 to 0.28.
- [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.27...v0.28)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-07 14:11:51 +00:00
dependabot[bot]
dd6159b062 build(deps-dev): bump pytest from 6.2.5 to 7.0.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.0.0.
- [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/6.2.5...7.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-07 14:11:46 +00:00
Willi Ballenthin
7511563865 Merge pull request #888 from kn0wl3dge/fix/879_mbc_names
Add MBC names and IDs to the linting script
2022-02-06 11:49:58 -07:00
Capa Bot
9923216558 Sync capa rules submodule 2022-02-06 18:49:26 +00:00
Baptistin Boilot
d026d21073 linter: add MBC names and IDs to the linting script 2022-02-06 11:47:49 +01:00
Capa Bot
5bfe706b56 Sync capa rules submodule 2022-02-04 19:27:03 +00:00
Willi Ballenthin
2407015620 Merge pull request #887 from mandiant/dependabot/pip/types-colorama-0.4.8
build(deps-dev): bump types-colorama from 0.4.7 to 0.4.8
2022-01-31 12:55:33 -07:00
Willi Ballenthin
a8dd9d4bfd Merge branch 'master' into dependabot/pip/types-colorama-0.4.8 2022-01-31 12:55:27 -07:00
Willi Ballenthin
8d247bd1b6 Merge pull request #886 from mandiant/dependabot/pip/types-psutil-5.8.20
build(deps-dev): bump types-psutil from 5.8.19 to 5.8.20
2022-01-31 11:56:56 -07:00
Willi Ballenthin
533666d40c Merge branch 'master' into dependabot/pip/types-psutil-5.8.20 2022-01-31 11:56:50 -07:00
Willi Ballenthin
b85ee0b7a0 Merge pull request #885 from mandiant/dependabot/pip/black-22.1.0
build(deps-dev): bump black from 21.12b0 to 22.1.0
2022-01-31 11:56:25 -07:00
dependabot[bot]
9466038e62 build(deps-dev): bump types-colorama from 0.4.7 to 0.4.8
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.7 to 0.4.8.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-31 18:53:47 +00:00
dependabot[bot]
e5eb9bf4f2 build(deps-dev): bump types-psutil from 5.8.19 to 5.8.20
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.19 to 5.8.20.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-31 18:53:45 +00:00
Willi Ballenthin
a3615ad0d3 Merge pull request #884 from mandiant/dependabot/pip/types-requests-2.27.8
build(deps-dev): bump types-requests from 2.27.7 to 2.27.8
2022-01-31 11:53:32 -07:00
Willi Ballenthin
2f6b5566d8 Merge pull request #883 from mandiant/dependabot/pip/types-pyyaml-6.0.4
build(deps-dev): bump types-pyyaml from 6.0.3 to 6.0.4
2022-01-31 11:53:20 -07:00
dependabot[bot]
79b40cab14 build(deps-dev): bump black from 21.12b0 to 22.1.0
Bumps [black](https://github.com/psf/black) from 21.12b0 to 22.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/commits/22.1.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-31 18:53:12 +00:00
Willi Ballenthin
6276b5d79e Merge pull request #882 from mandiant/dependabot/pip/smda-1.7.0
build(deps): bump smda from 1.6.2 to 1.7.0
2022-01-31 11:52:52 -07:00
dependabot[bot]
fac7ec1e00 build(deps-dev): bump types-requests from 2.27.7 to 2.27.8
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.7 to 2.27.8.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-31 14:15:22 +00:00
dependabot[bot]
356e5babd0 build(deps-dev): bump types-pyyaml from 6.0.3 to 6.0.4
Bumps [types-pyyaml](https://github.com/python/typeshed) from 6.0.3 to 6.0.4.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-31 14:15:20 +00:00
dependabot[bot]
b2de090581 build(deps): bump smda from 1.6.2 to 1.7.0
Bumps [smda](https://github.com/danielplohmann/smda) from 1.6.2 to 1.7.0.
- [Release notes](https://github.com/danielplohmann/smda/releases)
- [Commits](https://github.com/danielplohmann/smda/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-31 14:15:18 +00:00
Willi Ballenthin
364ec1fa2c Merge pull request #880 from mandiant/dependabot/pip/types-requests-2.27.7
build(deps-dev): bump types-requests from 2.27.3 to 2.27.7
2022-01-26 08:50:16 -07:00
Willi Ballenthin
afc64b8287 Merge branch 'master' into dependabot/pip/types-requests-2.27.7 2022-01-26 08:50:08 -07:00
dependabot[bot]
5953f86c7e build(deps-dev): bump types-requests from 2.27.3 to 2.27.7
Bumps [types-requests](https://github.com/python/typeshed) from 2.27.3 to 2.27.7.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-26 09:33:05 +00:00
Moritz
cfad012f92 Merge pull request #875 from kn0wl3dge/feature/103-validate_attck_mbc
Validate ATT&CK/MBC categories and IDs
2022-01-26 10:32:44 +01:00
Baptistin Boilot
2e8c2f40d6 linter: update linter-data.json with mitre att&ck references only 2022-01-26 00:11:01 +01:00
Baptistin Boilot
377c805fe7 linter: improve linter-data.json opening and add documentation
- Open linter-data.json in byte mode
- Add a comment explaining how to invoke the script
2022-01-24 22:48:59 +01:00
Capa Bot
bbb97da3fc Sync capa rules submodule 2022-01-24 17:10:29 +00:00
Capa Bot
78fde6f812 Sync capa rules submodule 2022-01-24 16:57:32 +00:00
Capa Bot
09081c0d2d Sync capa rules submodule 2022-01-24 16:51:22 +00:00
Willi Ballenthin
abeb507ea0 Merge pull request #876 from mandiant/dependabot/pip/types-colorama-0.4.7
build(deps-dev): bump types-colorama from 0.4.6 to 0.4.7
2022-01-24 09:49:41 -07:00
dependabot[bot]
d8c2759a72 build(deps-dev): bump types-colorama from 0.4.6 to 0.4.7
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.6 to 0.4.7.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-24 14:12:06 +00:00
Capa Bot
f0fc39e1d0 Sync capa-testfiles submodule 2022-01-24 13:37:25 +00:00
Capa Bot
81d604d85a Sync capa-testfiles submodule 2022-01-24 11:00:44 +00:00
Baptistin Boilot
0c978a8def scripts: fix typing issue in setup-linter-dependencies 2022-01-22 17:18:02 +01:00
Baptistin Boilot
c6ac239c5a linter: fix imports and codingstyle 2022-01-22 16:45:50 +01:00
Baptistin Boilot
370ad6cdd7 docs: add code documentation and update changelog 2022-01-22 16:45:49 +01:00
Baptistin Boilot
2bcd725e04 linter: add the possibility to enable or disable mbc and att&ck linting 2022-01-22 16:45:47 +01:00
Baptistin Boilot
0b487546bb linter: add mbc data extractor and linter 2022-01-22 16:45:46 +01:00
Baptistin Boilot
67d8d832c9 linter: refactor att&ck linter and add attck json data 2022-01-22 16:45:35 +01:00
Baptistin Boilot
fa99782f02 linter: add a linter rule that checks for invalid att&ck technique 2022-01-22 16:44:07 +01:00
Baptistin Boilot
60a30518bc linter: add mitre att&ck ttps extraction script 2022-01-22 16:43:42 +01:00
dependabot[bot]
122fb5f9f1 build(deps-dev): bump types-termcolor from 1.1.2 to 1.1.3
Bumps [types-termcolor](https://github.com/python/typeshed) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-22 16:43:37 +01:00
Willi Ballenthin
5daf444c8a Merge pull request #871 from mandiant/dependabot/pip/types-termcolor-1.1.3
build(deps-dev): bump types-termcolor from 1.1.2 to 1.1.3
2022-01-17 14:02:51 -07:00
Willi Ballenthin
41fbb8cdc4 Merge pull request #872 from mandiant/dependabot/pip/types-colorama-0.4.6
build(deps-dev): bump types-colorama from 0.4.5 to 0.4.6
2022-01-17 14:02:37 -07:00
dependabot[bot]
edfb69f8e9 build(deps-dev): bump types-colorama from 0.4.5 to 0.4.6
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.5 to 0.4.6.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-17 14:18:10 +00:00
dependabot[bot]
14b0d8e7a6 build(deps-dev): bump types-termcolor from 1.1.2 to 1.1.3
Bumps [types-termcolor](https://github.com/python/typeshed) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-17 14:18:07 +00:00
Willi Ballenthin
a085ff855d Merge pull request #868 from mandiant/fix/867
elf: parse section headers looking for Linux notes, too
2022-01-14 11:41:22 -07:00
William Ballenthin
b392b48b28 black 2022-01-13 15:24:58 -07:00
William Ballenthin
93355a6884 changelog 2022-01-13 15:23:17 -07:00
William Ballenthin
b28b30eb0f elf: parse section headers looking for Linux notes, too
closes #867
2022-01-13 15:21:23 -07:00
Willi Ballenthin
c0851fc643 Merge pull request #863 from mandiant/v3.1.0
version: v3.1.0
2022-01-12 14:18:22 -07:00
Willi Ballenthin
de7592b351 changelog: add additional contributor 2022-01-11 14:29:15 -07:00
Willi Ballenthin
5530bbad53 Update CHANGELOG.md
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2022-01-11 14:28:17 -07:00
Willi Ballenthin
4f0067e408 Update CHANGELOG.md
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2022-01-11 14:27:59 -07:00
Willi Ballenthin
b444c28a19 changelog: fix format 2022-01-11 10:05:40 -07:00
Willi Ballenthin
a4cc409c95 Update capa/version.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2022-01-10 12:39:07 -07:00
Moritz
fcb08501c0 Merge pull request #865 from mandiant/mr-tz-patch-1
Update global_.py
2022-01-10 19:21:24 +01:00
Moritz
cb2d00cefc Update global_.py 2022-01-10 19:04:52 +01:00
Willi Ballenthin
1cb9fc8a40 Merge pull request #864 from doomedraven/patch-1
Fix deprication warning from IDA
2022-01-10 10:52:10 -07:00
doomedraven
85cfc04bdb Fix deprication warning from IDA
```
    if info.procName == "metapc" and info.is_64bit():
```
Please use "procname" instead of "procName" ("procName" is kept for backward-compatibility, and will be removed soon.)
2022-01-10 18:37:59 +01:00
Willi Ballenthin
6555a3604f changelog: intro section 2022-01-10 09:49:00 -07:00
Willi Ballenthin
a97262d022 changelog: v3.1.0 2022-01-10 09:39:46 -07:00
Willi Ballenthin
8ad54271e9 version: v3.1.0 2022-01-10 09:33:39 -07:00
Willi Ballenthin
e5b9a20d09 changelog: add rule changes and contributors 2022-01-10 09:32:49 -07:00
Willi Ballenthin
0d37d182ea changelog: add some additional entries 2022-01-10 09:26:14 -07:00
Willi Ballenthin
6690634a3f Merge pull request #858 from mandiant/dependabot/pip/types-pyyaml-6.0.3
build(deps-dev): bump types-pyyaml from 6.0.1 to 6.0.3
2022-01-10 08:26:25 -07:00
dependabot[bot]
8f3730bae3 build(deps-dev): bump types-pyyaml from 6.0.1 to 6.0.3
Bumps [types-pyyaml](https://github.com/python/typeshed) from 6.0.1 to 6.0.3.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-10 15:25:42 +00:00
Willi Ballenthin
8f4e726774 Merge pull request #859 from mandiant/dependabot/pip/types-tabulate-0.8.5
build(deps-dev): bump types-tabulate from 0.8.4 to 0.8.5
2022-01-10 08:25:12 -07:00
Willi Ballenthin
5b8eda0f08 Merge pull request #861 from mandiant/dependabot/pip/mypy-0.931
build(deps-dev): bump mypy from 0.930 to 0.931
2022-01-10 08:24:59 -07:00
Willi Ballenthin
f5f62bbd71 Merge pull request #862 from mandiant/dependabot/pip/types-psutil-5.8.19
build(deps-dev): bump types-psutil from 5.8.17 to 5.8.19
2022-01-10 08:24:41 -07:00
dependabot[bot]
24c3edc7ec build(deps-dev): bump types-psutil from 5.8.17 to 5.8.19
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.17 to 5.8.19.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-10 14:18:21 +00:00
dependabot[bot]
0e3d46ef5e build(deps-dev): bump mypy from 0.930 to 0.931
Bumps [mypy](https://github.com/python/mypy) from 0.930 to 0.931.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.930...v0.931)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-10 14:18:19 +00:00
dependabot[bot]
a3546b65f7 build(deps-dev): bump types-tabulate from 0.8.4 to 0.8.5
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.8.4 to 0.8.5.
- [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-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-10 14:18:09 +00:00
Willi Ballenthin
01b694b6ab Merge pull request #851 from kn0wl3dge/fix/430
smda: fix negative number extraction
2022-01-03 12:08:41 -07:00
Moritz
3598f83091 Merge pull request #856 from mandiant/dependabot/pip/psutil-5.9.0
build(deps-dev): bump psutil from 5.8.0 to 5.9.0
2022-01-03 17:33:56 +01:00
Moritz
2085dd7b02 Merge pull request #853 from mandiant/dependabot/pip/ruamel-yaml-0.17.20
build(deps): bump ruamel-yaml from 0.17.19 to 0.17.20
2022-01-03 17:33:40 +01:00
Moritz
65d916332d Merge pull request #855 from mandiant/dependabot/pip/types-psutil-5.8.17
build(deps-dev): bump types-psutil from 5.8.16 to 5.8.17
2022-01-03 17:33:26 +01:00
Moritz
1937efce88 Merge pull request #852 from mandiant/dependabot/pip/types-tabulate-0.8.4
build(deps-dev): bump types-tabulate from 0.8.3 to 0.8.4
2022-01-03 17:33:19 +01:00
Moritz
501d607b3a Merge pull request #854 from mandiant/dependabot/pip/types-colorama-0.4.5
build(deps-dev): bump types-colorama from 0.4.4 to 0.4.5
2022-01-03 17:33:07 +01:00
dependabot[bot]
7d6670c59e build(deps-dev): bump psutil from 5.8.0 to 5.9.0
Bumps [psutil](https://github.com/giampaolo/psutil) from 5.8.0 to 5.9.0.
- [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.8.0...release-5.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-03 14:11:03 +00:00
dependabot[bot]
fe608db16a build(deps-dev): bump types-psutil from 5.8.16 to 5.8.17
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.16 to 5.8.17.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-03 14:10:58 +00:00
dependabot[bot]
be1f313d57 build(deps-dev): bump types-colorama from 0.4.4 to 0.4.5
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.4 to 0.4.5.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-03 14:10:55 +00:00
dependabot[bot]
cb77c55d2c build(deps): bump ruamel-yaml from 0.17.19 to 0.17.20
Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.19 to 0.17.20.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-03 14:10:53 +00:00
dependabot[bot]
417aa35c60 build(deps-dev): bump types-tabulate from 0.8.3 to 0.8.4
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.8.3 to 0.8.4.
- [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-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-03 14:10:46 +00:00
Baptistin Boilot
18877eb676 changelog: add fixed issue 2021-12-31 21:14:56 +01:00
Baptistin Boilot
a9670c9510 smda: fix number extractor to return only unsigned values
SmdaInstruction operands are python `str` objects. SMDA number operands are signed integers.
This commit adds a converter to the SMDA number extractor.
The goal is to convert any signed number to the two’s complement representation with the correct bitness.
2021-12-31 20:10:36 +01:00
Baptistin Boilot
8474369575 tests: add fixtures for two's complement numbers
Add fixtures to validate the following number features:
- number(0x0): to check feature extraction for null number
- number(0xFFFFFFFF): to check feature extraction for -1 number
- number(0xFFFFFFF0): to check feature extraction for negative number (-0x10 in this case)
2021-12-31 20:08:56 +01:00
Baptistin Boilot
4739d121a2 scripts: add backend parameter (-b) to show-features.py 2021-12-31 20:07:34 +01:00
Mike Hunhoff
e47f5a2548 Merge pull request #849 from mandiant/fix/845
capa explorer: updating supported IDA versions
2021-12-31 10:48:53 -07:00
Willi Ballenthin
51f5628383 Merge pull request #847 from mandiant/dependabot/pip/ruamel-yaml-0.17.19
build(deps): bump ruamel-yaml from 0.17.17 to 0.17.19
2021-12-29 09:44:24 -07:00
Willi Ballenthin
aa67a1b285 Merge pull request #846 from mandiant/dependabot/pip/types-psutil-5.8.16
build(deps-dev): bump types-psutil from 5.8.15 to 5.8.16
2021-12-29 09:44:15 -07:00
Willi Ballenthin
d22e51fd84 Merge pull request #848 from mandiant/dependabot/pip/mypy-0.930
build(deps-dev): bump mypy from 0.920 to 0.930
2021-12-29 09:42:21 -07:00
Michael Hunhoff
cde4af40fe capa explorer: updating supported IDA versions 2021-12-28 10:51:53 -07:00
dependabot[bot]
a147755d13 build(deps-dev): bump mypy from 0.920 to 0.930
Bumps [mypy](https://github.com/python/mypy) from 0.920 to 0.930.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.920...v0.930)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-27 14:12:16 +00:00
dependabot[bot]
7b6c293069 build(deps): bump ruamel-yaml from 0.17.17 to 0.17.19
Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.17 to 0.17.19.

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-27 14:12:12 +00:00
dependabot[bot]
b3f1244641 build(deps-dev): bump types-psutil from 5.8.15 to 5.8.16
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.15 to 5.8.16.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-27 14:12:06 +00:00
Capa Bot
e6423700b9 Sync capa rules submodule 2021-12-23 16:34:46 +00:00
Moritz
9462a26a05 Merge pull request #844 from mandiant/dependabot/pip/mypy-0.920
build(deps-dev): bump mypy from 0.910 to 0.920
2021-12-20 16:31:41 +01:00
dependabot[bot]
c059a52d0e build(deps-dev): bump mypy from 0.910 to 0.920
Bumps [mypy](https://github.com/python/mypy) from 0.910 to 0.920.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.910...v0.920)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-20 14:09:06 +00:00
Capa Bot
a221db8a59 Sync capa rules submodule 2021-12-20 12:48:22 +00:00
Moritz
df43ed0219 Merge pull request #842 from mandiant/fix/maec-mal-fam
support maec/malware-family meta
2021-12-20 13:15:50 +01:00
Capa Bot
90430f52c6 Sync capa-testfiles submodule 2021-12-15 15:33:39 +00:00
Moritz Raabe
4e7f0b4591 support maec/malware-family meta 2021-12-15 10:40:34 +01:00
Capa Bot
bda76c22ec Sync capa rules submodule 2021-12-14 21:52:49 +00:00
Capa Bot
d67223c321 Sync capa rules submodule 2021-12-14 21:46:38 +00:00
Capa Bot
21278ff595 Sync capa rules submodule 2021-12-14 21:45:58 +00:00
Capa Bot
21fd6b27e2 Sync capa rules submodule 2021-12-13 18:48:16 +00:00
Capa Bot
cc8d57b242 Sync capa-testfiles submodule 2021-12-13 17:24:52 +00:00
Capa Bot
6081f4573c Sync capa-testfiles submodule 2021-12-13 17:24:32 +00:00
Capa Bot
ea2cafa715 Sync capa-testfiles submodule 2021-12-13 17:24:02 +00:00
Capa Bot
a34c993e31 Sync capa rules submodule 2021-12-07 04:32:49 +00:00
Willi Ballenthin
1a5fc3a21a Merge pull request #839 from cl3o/master
types: Add assert_never for exhaustivenes checking with mypy
2021-12-06 13:55:41 -07:00
cl3o
c15a9a72f5 Add local variable for easy_rules_by_feature at the beginning of match 2021-12-06 20:55:15 +01:00
cl3o
5b35058338 Forgot to add the second fix to the first commit. 2021-12-06 20:32:44 +01:00
cl3o
a0ca6e18c8 Made proposed changes to fix mypy errors 2021-12-06 20:30:07 +01:00
Capa Bot
1917004292 Sync capa rules submodule 2021-12-06 19:22:59 +00:00
Capa Bot
8ee3bb08bc Sync capa rules submodule 2021-12-06 18:24:54 +00:00
Capa Bot
7e96059fb5 Sync capa rules submodule 2021-12-06 17:58:59 +00:00
Capa Bot
4f7f06d316 Sync capa rules submodule 2021-12-06 17:57:11 +00:00
Capa Bot
448b5392be Sync capa rules submodule 2021-12-06 17:56:26 +00:00
Willi Ballenthin
6f5f3e091a Merge pull request #840 from mandiant/dependabot/pip/black-21.12b0
build(deps-dev): bump black from 21.11b1 to 21.12b0
2021-12-06 10:45:51 -07:00
dependabot[bot]
fa6a2069ce build(deps-dev): bump black from 21.11b1 to 21.12b0
Bumps [black](https://github.com/psf/black) from 21.11b1 to 21.12b0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/commits)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-06 14:12:23 +00:00
Capa Bot
09fd371b9d Sync capa-testfiles submodule 2021-12-06 10:13:41 +00:00
Capa Bot
a598745938 Sync capa-testfiles submodule 2021-12-06 10:06:57 +00:00
Capa Bot
7751f693c8 Sync capa-testfiles submodule 2021-12-06 10:02:45 +00:00
Capa Bot
7ade9ca43e Sync capa-testfiles submodule 2021-12-06 10:01:17 +00:00
cl3o
061a66e437 create function assert_never 2021-12-04 19:02:54 +01:00
Capa Bot
39536e2727 Sync capa rules submodule 2021-12-03 15:29:51 +00:00
Capa Bot
38038626d4 Sync capa rules submodule 2021-12-03 15:29:28 +00:00
Capa Bot
c3d34abe89 Sync capa-testfiles submodule 2021-12-03 12:12:30 +00:00
Capa Bot
baf5005998 Sync capa-testfiles submodule 2021-12-03 12:12:20 +00:00
Capa Bot
107c3c0cf9 Sync capa rules submodule 2021-11-30 22:06:21 +00:00
Capa Bot
2d1bd37816 Sync capa rules submodule 2021-11-30 15:24:28 +00:00
Capa Bot
de017b15d0 Sync capa-testfiles submodule 2021-11-30 15:24:09 +00:00
Capa Bot
3b0974ae3e Sync capa rules submodule 2021-11-29 23:46:52 +00:00
Willi Ballenthin
cf6cbc16df Merge pull request #838 from mandiant/dependabot/pip/types-psutil-5.8.15
build(deps-dev): bump types-psutil from 5.8.14 to 5.8.15
2021-11-29 08:47:44 -07:00
dependabot[bot]
bd60a8d9cd build(deps-dev): bump types-psutil from 5.8.14 to 5.8.15
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.14 to 5.8.15.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-29 14:09:09 +00:00
Capa Bot
c77240c6b4 Sync capa rules submodule 2021-11-26 16:21:34 +00:00
Moritz
14d803c604 Merge pull request #837 from mandiant/dependabot/pip/black-21.11b1
build(deps-dev): bump black from 21.10b0 to 21.11b1
2021-11-22 18:45:02 +01:00
dependabot[bot]
f764829ca9 build(deps-dev): bump black from 21.10b0 to 21.11b1
Bumps [black](https://github.com/psf/black) from 21.10b0 to 21.11b1.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/commits)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-22 14:10:19 +00:00
Willi Ballenthin
418eedd7bd freeze: fix doc describing format 2021-11-17 12:06:56 -07:00
Willi Ballenthin
b9f1fe56c8 Merge pull request #834 from mandiant/williballenthin-patch-1
setup: bump viv-utils to v0.6.9
2021-11-16 11:21:30 -07:00
Willi Ballenthin
7e50a957ff ci: tests: python versions are strings not floats 2021-11-16 10:12:34 -07:00
Willi Ballenthin
137cff6127 ci: tests: test under py3.10 too 2021-11-16 10:06:32 -07:00
Willi Ballenthin
807b99e5e5 changelog 2021-11-15 14:12:07 -07:00
Willi Ballenthin
e21c69f4e3 setup: bump viv-utils to v0.6.9
closes #816 
closes #683
2021-11-15 14:10:48 -07:00
Moritz
9f7daca86e Merge pull request #833 from mandiant/dependabot/pip/types-pyyaml-6.0.1
build(deps-dev): bump types-pyyaml from 6.0.0 to 6.0.1
2021-11-15 16:54:11 +01:00
Moritz
1b89e274c9 Merge pull request #832 from mandiant/dependabot/pip/isort-5.10.1
build(deps-dev): bump isort from 5.10.0 to 5.10.1
2021-11-15 16:54:02 +01:00
Moritz
dd768dc080 Merge pull request #831 from mandiant/dependabot/pip/viv-utils-flirt--0.6.8
build(deps): bump viv-utils[flirt] from 0.6.7 to 0.6.8
2021-11-15 16:53:53 +01:00
dependabot[bot]
4aea481967 build(deps-dev): bump types-pyyaml from 6.0.0 to 6.0.1
Bumps [types-pyyaml](https://github.com/python/typeshed) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-15 14:12:07 +00:00
dependabot[bot]
265629d127 build(deps-dev): bump isort from 5.10.0 to 5.10.1
Bumps [isort](https://github.com/pycqa/isort) from 5.10.0 to 5.10.1.
- [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.0...5.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-15 14:12:04 +00:00
dependabot[bot]
cef0cb809f build(deps): bump viv-utils[flirt] from 0.6.7 to 0.6.8
Bumps [viv-utils[flirt]](https://github.com/williballenthin/viv-utils) from 0.6.7 to 0.6.8.
- [Release notes](https://github.com/williballenthin/viv-utils/releases)
- [Commits](https://github.com/williballenthin/viv-utils/compare/v0.6.7...v0.6.8)

---
updated-dependencies:
- dependency-name: viv-utils[flirt]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-15 14:11:59 +00:00
Willi Ballenthin
57fe1e27b6 Merge pull request #830 from mandiant/perf/rule-selection
perf: don't try to match rules that will never match
2021-11-12 11:54:29 -07:00
Willi Ballenthin
83253eb7d0 rules: better variable name 2021-11-12 11:53:03 -07:00
Willi Ballenthin
9b5e8ff45d Update capa/rules.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2021-11-12 11:51:39 -07:00
William Ballenthin
cdfacc6247 Merge branch 'master' of github.com:fireeye/capa into perf/rule-selection 2021-11-10 14:30:08 -07:00
Capa Bot
10d747cc8c Sync capa rules submodule 2021-11-10 21:29:25 +00:00
William Ballenthin
a6b366602c mypy 2021-11-10 14:21:28 -07:00
William Ballenthin
80fb9dec3c pep8 2021-11-10 14:15:52 -07:00
William Ballenthin
68c86cf620 rules: easy/hard: better detect edge cases in optional, some, and range 2021-11-10 14:13:57 -07:00
William Ballenthin
e550d48bcd linter: optional maps to some, not range 2021-11-10 14:13:37 -07:00
William Ballenthin
1aaaa8919c rules: easy/hard: simplify indexing by considering not: hard 2021-11-10 13:55:34 -07:00
William Ballenthin
72c2ffc40b linter: add checks for not and optional not under and 2021-11-10 13:47:30 -07:00
William Ballenthin
f7ab2fb13a rules: easy/hard rules: detect not/optional at the root 2021-11-10 13:36:10 -07:00
William Ballenthin
3a1272246f rules: code consistency 2021-11-10 13:36:00 -07:00
William Ballenthin
6039a33bf8 engine: remove old import 2021-11-10 12:56:40 -07:00
William Ballenthin
2d68fb2536 pep8 2021-11-10 12:51:27 -07:00
William Ballenthin
845df282ef tests: split out match tests and validate alternative algorithms 2021-11-10 12:44:58 -07:00
William Ballenthin
1406dc28d9 rules: ruleset: fix collection of features under not statements 2021-11-10 12:44:19 -07:00
William Ballenthin
67884dd255 rules: match: more documentation 2021-11-09 16:42:32 -07:00
William Ballenthin
2bf05ac631 rules: index easy/hard: better handle not: statements 2021-11-09 16:37:30 -07:00
William Ballenthin
8cb04e4737 Merge branch 'master' into perf/rule-selection 2021-11-09 16:28:03 -07:00
William Ballenthin
733126591e Merge branch 'perf/query-optimizer' 2021-11-09 16:27:09 -07:00
William Ballenthin
d4d801c246 optimizer: tweak costs slightly 2021-11-09 16:26:26 -07:00
Willi Ballenthin
84ba32a8fe Merge pull request #829 from mandiant/perf/query-optimizer
perf: add query optimizer
2021-11-09 16:25:22 -07:00
William Ballenthin
ea386d02b6 tests: add test demonstrating optimizer 2021-11-09 16:24:26 -07:00
William Ballenthin
77cac63443 Merge branch 'master' into perf/query-optimizer 2021-11-09 16:12:30 -07:00
Willi Ballenthin
9350ee9479 Merge pull request #827 from mandiant/perf/short-circuit
perf: short circuit logic nodes when appropriate
2021-11-09 16:10:20 -07:00
Willi Ballenthin
025d156068 Merge pull request #828 from mandiant/profiling
profile infrastructure
2021-11-09 16:09:34 -07:00
William Ballenthin
7a4aee592b profile-time: add doc 2021-11-09 16:08:39 -07:00
Willi Ballenthin
f427c5e961 Update capa/engine.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2021-11-09 10:49:10 -07:00
Willi Ballenthin
51af2d4a56 Update capa/engine.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2021-11-09 10:49:01 -07:00
Willi Ballenthin
a68812b223 Update capa/engine.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2021-11-09 10:48:54 -07:00
William Ballenthin
e05f8c7034 changelog 2021-11-09 10:27:33 -07:00
William Ballenthin
182377581a main: use ruleset.match instead of engine.mathc 2021-11-09 09:52:45 -07:00
William Ballenthin
e647ae2ac4 rules: ruleset: add optimized match routine 2021-11-09 09:52:32 -07:00
William Ballenthin
1311da99ff rules: make Scope an enum 2021-11-09 09:51:50 -07:00
William Ballenthin
8badf226a2 engine: document match routine 2021-11-09 09:51:18 -07:00
William Ballenthin
6909d6a541 changelog 2021-11-08 16:04:15 -07:00
William Ballenthin
e287dc9a32 optimizer: fix sort order 2021-11-08 15:54:14 -07:00
William Ballenthin
152d0f3244 ruleset: add query optimizer 2021-11-08 15:34:59 -07:00
William Ballenthin
a6e2cfc90a Merge branch 'profiling' into perf/short-circuit 2021-11-08 15:24:50 -07:00
William Ballenthin
18c30e4f12 main: remove perf debug msgs 2021-11-08 15:24:43 -07:00
William Ballenthin
3c4f4d302c Merge branch 'profiling' into perf/short-circuit 2021-11-08 15:23:23 -07:00
William Ballenthin
2abebfbce7 main: remove perf messages 2021-11-08 15:22:58 -07:00
William Ballenthin
0b517c51d8 main: remove perf messages 2021-11-08 15:22:01 -07:00
William Ballenthin
9fbbda11b8 Merge branch 'profiling' into perf/short-circuit 2021-11-08 15:20:22 -07:00
William Ballenthin
6f6831f812 perf: document that counters is unstable 2021-11-08 15:20:11 -07:00
William Ballenthin
d425bb31c4 Merge branch 'profiling' into perf/short-circuit 2021-11-08 15:16:22 -07:00
William Ballenthin
334425a08f changelog 2021-11-08 15:16:08 -07:00
William Ballenthin
3e74da96a6 engine: make short circuiting configurable 2021-11-08 14:55:11 -07:00
William Ballenthin
ad119d789b Merge branch 'profiling' into perf/short-circuit 2021-11-08 14:35:26 -07:00
William Ballenthin
6c8d246af9 fix bad merge 2021-11-08 14:31:43 -07:00
William Ballenthin
26b7a0b91d Merge branch 'master' into profiling 2021-11-08 14:29:40 -07:00
Willi Ballenthin
0b6c6227b9 Merge pull request #825 from mandiant/fix/circular-import-freeze
fix circular import freeze
2021-11-08 14:28:01 -07:00
William Ballenthin
94fd7673fd common: mypy 2021-11-08 14:27:44 -07:00
William Ballenthin
f598acb8fc scripts: remove old profiling scripts 2021-11-08 14:24:48 -07:00
William Ballenthin
b621205a06 mypy 2021-11-08 14:24:13 -07:00
William Ballenthin
9fa9c6a5d0 tests: add test demonstrating short circuiting 2021-11-08 14:07:44 -07:00
William Ballenthin
1a84051679 changelog 2021-11-08 14:07:31 -07:00
William Ballenthin
d987719889 engine: some: correctly count satisfied children 2021-11-08 13:53:37 -07:00
William Ballenthin
96813c37b7 remove old improt 2021-11-08 13:48:33 -07:00
William Ballenthin
70f007525d pep8 2021-11-08 12:11:01 -07:00
William Ballenthin
e3496b0660 engine: move optimizer into its own module 2021-11-08 12:10:22 -07:00
William Ballenthin
24b4c99635 changelog 2021-11-08 11:58:02 -07:00
William Ballenthin
27b4a8ba73 common: remove old import 2021-11-08 11:55:58 -07:00
William Ballenthin
51b3f38f55 common: move Result to capa.common from capa.engine
fixes circular import error in capa.features.freeze
2021-11-08 11:54:36 -07:00
William Ballenthin
a35be4a666 scripts: add py script for profiling time 2021-11-08 11:52:34 -07:00
William Ballenthin
5770d0c12d perf: add reset routine 2021-11-08 11:52:25 -07:00
William Ballenthin
0629c584e1 common: move Result to capa.common from capa.engine
fixes circular import error in capa.features.freeze
2021-11-08 11:52:13 -07:00
William Ballenthin
480df323e5 scripts: add py script for profiling time 2021-11-08 11:51:09 -07:00
William Ballenthin
a995b53c38 perf: add reset routine 2021-11-08 11:50:49 -07:00
William Ballenthin
35fa50dbee pep8 2021-11-08 11:50:37 -07:00
William Ballenthin
d86c3f4d48 common: move Result to capa.common from capa.engine
fixes circular import error in capa.features.freeze
2021-11-08 11:50:16 -07:00
Moritz
4696c0ebb6 Merge pull request #822 from mandiant/dependabot/pip/types-psutil-5.8.14
build(deps-dev): bump types-psutil from 5.8.13 to 5.8.14
2021-11-08 17:02:58 +01:00
Moritz
09724e9787 Merge pull request #823 from mandiant/dependabot/pip/isort-5.10.0
build(deps-dev): bump isort from 5.9.3 to 5.10.0
2021-11-08 17:02:33 +01:00
dependabot[bot]
636548cdec build(deps-dev): bump isort from 5.9.3 to 5.10.0
Bumps [isort](https://github.com/pycqa/isort) from 5.9.3 to 5.10.0.
- [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.9.3...5.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-08 14:16:28 +00:00
dependabot[bot]
b3970808df build(deps-dev): bump types-psutil from 5.8.13 to 5.8.14
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.13 to 5.8.14.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-08 14:16:15 +00:00
William Ballenthin
d573b83c94 rule: optimization: add some documentation 2021-11-05 16:49:38 -06:00
William Ballenthin
e63f072e40 rules: optimizer: use recursive cost of statements 2021-11-05 16:39:00 -06:00
William Ballenthin
a329147d28 engine: some: short circuit 2021-11-05 16:32:23 -06:00
William Ballenthin
18ba986eba engine: or: short circuit 2021-11-05 16:32:12 -06:00
William Ballenthin
8d9f418b2b rules: optimize by cost 2021-11-05 16:20:22 -06:00
William Ballenthin
623bac1a40 engine: statement: document that the order of children is important 2021-11-05 16:19:16 -06:00
William Ballenthin
702d00da91 gitignore 2021-11-05 15:24:24 -06:00
William Ballenthin
3a12472be8 perf: render: show evaluate.feature counter 2021-11-05 15:23:34 -06:00
William Ballenthin
6524449ad1 main: perf: human format the numbers 2021-11-05 15:23:22 -06:00
William Ballenthin
86cab26a69 add perf counters in module capa.perf 2021-11-05 14:59:22 -06:00
William Ballenthin
3d068fe3cd scripts: add utilities for collecting profile traces 2021-11-04 13:17:38 -06:00
William Ballenthin
f98236046b main: add coarse timing measurements 2021-11-04 12:38:35 -06:00
William Ballenthin
ed3bd4ef75 main: add timing ctx manager 2021-11-04 12:20:05 -06:00
Capa Bot
7d3ae7a91b Sync capa rules submodule 2021-11-03 18:29:09 +00:00
Capa Bot
0409c431b8 Sync capa rules submodule 2021-11-02 18:47:47 +00:00
Capa Bot
ffbb841b03 Sync capa rules submodule 2021-11-02 18:47:18 +00:00
Willi Ballenthin
e9a7dbc2ff Merge pull request #820 from mandiant/fix/linter-file-format
auto recognize shellcode based on file extension
2021-11-02 11:31:33 -06:00
Capa Bot
10dc8950c1 Sync capa rules submodule 2021-11-02 17:29:30 +00:00
Capa Bot
fe0fb1ccd2 Sync capa rules submodule 2021-11-02 17:17:47 +00:00
Moritz Raabe
e9170a1d4b auto recognize shellcode based on file extension 2021-11-02 18:02:37 +01:00
Capa Bot
02bd8581d8 Sync capa-testfiles submodule 2021-11-02 16:42:40 +00:00
Moritz
ca574201a4 Merge pull request #818 from mandiant/dependabot/pip/ruamel-yaml-0.17.17
build(deps): bump ruamel-yaml from 0.17.16 to 0.17.17
2021-11-02 17:36:03 +01:00
Moritz
8e744d94e6 Merge pull request #817 from mandiant/dependabot/pip/black-21.10b0
build(deps-dev): bump black from 21.9b0 to 21.10b0
2021-11-02 17:35:52 +01:00
dependabot[bot]
6a28330dd1 build(deps): bump ruamel-yaml from 0.17.16 to 0.17.17
Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.16 to 0.17.17.

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 14:11:49 +00:00
dependabot[bot]
4537b52c18 build(deps-dev): bump black from 21.9b0 to 21.10b0
Bumps [black](https://github.com/psf/black) from 21.9b0 to 21.10b0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/commits)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 14:11:42 +00:00
Willi Ballenthin
29e61e24a6 Merge pull request #815 from mandiant/feature-3.0.3
v3.0.3
2021-10-27 10:14:35 -06:00
William Ballenthin
041c8a4c2d changelog 2021-10-27 09:43:35 -06:00
Capa Bot
433dfd8fa9 Sync capa rules submodule 2021-10-27 15:34:46 +00:00
William Ballenthin
2b46043419 v3.0.3 2021-10-27 09:32:45 -06:00
William Ballenthin
d31c8b0190 changelog 2021-10-27 09:29:54 -06:00
Willi Ballenthin
9003fdc1a2 Merge pull request #814 from mandiant/fix-802
bail with unique error codes
2021-10-27 09:25:55 -06:00
William Ballenthin
b1f4a2853e Merge branch 'master' of github.com:fireeye/capa into fix-802 2021-10-27 09:25:29 -06:00
William Ballenthin
07412f047d tests: fix check of status code E_MISSING_FILE 2021-10-27 09:24:22 -06:00
Willi Ballenthin
26ac21b908 Merge pull request #813 from mandiant/fix-130
Fix 130
2021-10-27 09:20:43 -06:00
William Ballenthin
4cc496a8e5 main: use constants to represent error codes 2021-10-26 16:57:33 -06:00
William Ballenthin
4f4e0881b5 changelog 2021-10-26 16:48:02 -06:00
William Ballenthin
9fe164665c main: exit with unique error codes when bailing
TODO: create an enum of all these things so they're easy for a human to
read.

closes #802
2021-10-26 16:46:43 -06:00
William Ballenthin
c74193b5d7 Merge branch 'master' of github.com:fireeye/capa into fix-130 2021-10-26 15:26:22 -06:00
William Ballenthin
31ef06ef2b sync testfiles 2021-10-26 15:26:18 -06:00
Capa Bot
83a95d66d1 Sync capa-testfiles submodule 2021-10-26 21:24:10 +00:00
William Ballenthin
4451b76f89 pep8 2021-10-26 15:21:28 -06:00
William Ballenthin
a1075b63ec tests: add demonstration of bb layout 2021-10-26 15:20:08 -06:00
William Ballenthin
97c41228e0 changelog 2021-10-26 15:10:50 -06:00
William Ballenthin
8903d2abcb show-capabilities-by-function: also include matches from BBs in fn 2021-10-26 15:05:53 -06:00
William Ballenthin
328e13fbfe main: compute function & bb layout
so bb can be associated with function in output.
only captures BBs that have a rule match,
otherwise, there might be too much data captured.
closes #130.
2021-10-26 15:04:50 -06:00
Capa Bot
b7cd5fec76 Sync capa rules submodule 2021-10-25 19:26:56 +00:00
Willi Ballenthin
6086dbcd84 Merge pull request #812 from mandiant/dependabot/pip/viv-utils-flirt--0.6.7
build(deps): bump viv-utils[flirt] from 0.6.6 to 0.6.7
2021-10-25 09:14:41 -06:00
Willi Ballenthin
5f88e02aa3 Merge pull request #811 from mandiant/dependabot/pip/types-pyyaml-6.0.0
build(deps-dev): bump types-pyyaml from 5.4.12 to 6.0.0
2021-10-25 09:04:56 -06:00
dependabot[bot]
96a4f585cd build(deps): bump viv-utils[flirt] from 0.6.6 to 0.6.7
Bumps [viv-utils[flirt]](https://github.com/williballenthin/viv-utils) from 0.6.6 to 0.6.7.
- [Release notes](https://github.com/williballenthin/viv-utils/releases)
- [Commits](https://github.com/williballenthin/viv-utils/compare/v0.6.6...v0.6.7)

---
updated-dependencies:
- dependency-name: viv-utils[flirt]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-25 14:09:33 +00:00
dependabot[bot]
73ec980e01 build(deps-dev): bump types-pyyaml from 5.4.12 to 6.0.0
Bumps [types-pyyaml](https://github.com/python/typeshed) from 5.4.12 to 6.0.0.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-25 14:09:26 +00:00
Capa Bot
e5ed7ce0d3 Sync capa rules submodule 2021-10-25 03:39:00 +00:00
Capa Bot
08a7b8afb7 Sync capa-testfiles submodule 2021-10-24 22:00:33 +00:00
Capa Bot
bb7a588f6b Sync capa rules submodule 2021-10-22 17:23:31 +00:00
Capa Bot
9faa0734c1 Sync capa-testfiles submodule 2021-10-22 17:11:32 +00:00
Capa Bot
cf55b34b4e Sync capa-testfiles submodule 2021-10-22 16:57:10 +00:00
Capa Bot
5881899cc2 Sync capa-testfiles submodule 2021-10-22 16:56:36 +00:00
William Ballenthin
4e64ef8ab3 gitignore 2021-10-22 10:20:14 -06:00
Willi Ballenthin
7e5532ac84 Merge pull request #807 from mandiant/dependabot/pip/types-pyyaml-5.4.12
build(deps-dev): bump types-pyyaml from 5.4.10 to 5.4.12
2021-10-18 13:49:55 -06:00
dependabot[bot]
3d638df08c build(deps-dev): bump types-pyyaml from 5.4.10 to 5.4.12
Bumps [types-pyyaml](https://github.com/python/typeshed) from 5.4.10 to 5.4.12.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-18 18:50:32 +00:00
Willi Ballenthin
bf984a38ed Merge pull request #808 from mandiant/dependabot/pip/types-tabulate-0.8.3
build(deps-dev): bump types-tabulate from 0.8.2 to 0.8.3
2021-10-18 12:49:47 -06:00
dependabot[bot]
e68f2ce141 build(deps-dev): bump types-tabulate from 0.8.2 to 0.8.3
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.8.2 to 0.8.3.
- [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-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-18 18:42:21 +00:00
Willi Ballenthin
d0a3244108 Merge pull request #809 from mandiant/dependabot/pip/types-termcolor-1.1.2
build(deps-dev): bump types-termcolor from 1.1.1 to 1.1.2
2021-10-18 12:41:37 -06:00
dependabot[bot]
d09901d512 build(deps-dev): bump types-termcolor from 1.1.1 to 1.1.2
Bumps [types-termcolor](https://github.com/python/typeshed) from 1.1.1 to 1.1.2.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-18 18:26:20 +00:00
Willi Ballenthin
2d46bac351 Merge pull request #810 from mandiant/dependabot/pip/types-psutil-5.8.13
build(deps-dev): bump types-psutil from 5.8.12 to 5.8.13
2021-10-18 12:25:22 -06:00
Willi Ballenthin
2285c76cbf Merge pull request #806 from mandiant/dependabot/pip/types-colorama-0.4.4
build(deps-dev): bump types-colorama from 0.4.3 to 0.4.4
2021-10-18 12:25:08 -06:00
dependabot[bot]
c003ab4e42 build(deps-dev): bump types-psutil from 5.8.12 to 5.8.13
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.12 to 5.8.13.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-18 18:24:52 +00:00
Willi Ballenthin
78e97a217a Merge pull request #805 from mandiant/dependabot/pip/pyyaml-6.0
build(deps): bump pyyaml from 5.4.1 to 6.0
2021-10-18 12:24:20 -06:00
dependabot[bot]
720585170c build(deps-dev): bump types-colorama from 0.4.3 to 0.4.4
Bumps [types-colorama](https://github.com/python/typeshed) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-18 14:15:08 +00:00
dependabot[bot]
19d54f3f4d build(deps): bump pyyaml from 5.4.1 to 6.0
Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.4.1 to 6.0.
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/5.4.1...6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-18 14:15:05 +00:00
Moritz
23a0aec1e6 Merge pull request #803 from mandiant/dependabot/pip/types-psutil-5.8.12
build(deps-dev): bump types-psutil from 5.8.8 to 5.8.12
2021-10-12 14:22:52 +02:00
Moritz
6b0db01c13 Merge pull request #804 from mandiant/dependabot/pip/pycodestyle-2.8.0
build(deps-dev): bump pycodestyle from 2.7.0 to 2.8.0
2021-10-12 14:22:44 +02:00
dependabot[bot]
93c14c3a1f build(deps-dev): bump pycodestyle from 2.7.0 to 2.8.0
Bumps [pycodestyle](https://github.com/PyCQA/pycodestyle) from 2.7.0 to 2.8.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.7.0...2.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-11 14:10:44 +00:00
dependabot[bot]
b66760fc5c build(deps-dev): bump types-psutil from 5.8.8 to 5.8.12
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.8 to 5.8.12.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-11 14:10:38 +00:00
Willi Ballenthin
64a801cc55 Merge pull request #801 from mandiant/dependabot/pip/pytest-cov-3.0.0
build(deps-dev): bump pytest-cov from 2.12.1 to 3.0.0
2021-10-04 14:13:43 -06:00
dependabot[bot]
35fc8ee3e8 build(deps-dev): bump pytest-cov from 2.12.1 to 3.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.12.1 to 3.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/v2.12.1...v3.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>
2021-10-04 14:09:30 +00:00
Capa Bot
887c566f7c Sync capa rules submodule 2021-09-30 19:28:13 +00:00
Capa Bot
2f59499087 Sync capa rules submodule 2021-09-30 14:01:54 +00:00
Capa Bot
b4a239569c Sync capa rules submodule 2021-09-30 13:29:23 +00:00
Moritz
e4073a844b Merge pull request #794 from mandiant/go-mandiant
s/fireeye/mandiant
2021-09-30 15:28:53 +02:00
Capa Bot
f313ad37b3 Sync capa-testfiles submodule 2021-09-29 14:54:48 +00:00
Moritz Raabe
8de69c639a s/fireeye/mandiant 2021-09-29 12:55:16 +02:00
Willi Ballenthin
0714dbee0d changelog: formatting 2021-09-28 10:26:28 -06:00
127 changed files with 9886 additions and 3620 deletions

21
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/python-3/.devcontainer/base.Dockerfile
# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
ARG VARIANT="3.10-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

View File

@@ -0,0 +1,51 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/python-3
{
"name": "Python 3",
"build": {
"dockerfile": "Dockerfile",
"context": "..",
"args": {
// Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon.
"VARIANT": "3.10",
// Options
"NODE_VERSION": "none"
}
},
// Set *default* container specific settings.json values on container create.
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "git submodule update --init && pip3 install --user -e .[dev]",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode",
"features": {
"git": "latest"
}
}

View File

@@ -2,7 +2,7 @@
First off, thanks for taking the time to contribute!
The following is a set of guidelines for contributing to capa and its packages, which are hosted in the [FireEye Organization](https://github.com/fireeye) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
The following is a set of guidelines for contributing to capa and its packages, which are hosted in the [Mandiant Organization](https://github.com/mandiant) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
#### Table Of Contents
@@ -32,9 +32,9 @@ 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:
- [capa](https://github.com/fireeye/capa)
- [capa-rules](https://github.com/fireeye/capa-rules)
- [capa-testfiles](https://github.com/fireeye/capa-testfiles)
- [capa](https://github.com/mandiant/capa)
- [capa-rules](https://github.com/mandiant/capa-rules)
- [capa-testfiles](https://github.com/mandiant/capa-testfiles)
The command line tools, logic engine, and other Python source code are found in the `capa` repository.
This is the repository to fork when you want to enhance the features, performance, or user interface of capa.
@@ -54,7 +54,7 @@ These are files you'll need in order to run the linter (in `--thorough` mode) an
### Design Decisions
When we make a significant decision in how we maintain the project and what we can or cannot support,
we will document it in the [capa issues tracker](https://github.com/fireeye/capa/issues).
we will document it in the [capa issues tracker](https://github.com/mandiant/capa/issues).
This is the best place review our discussions about what/how/why we do things in the project.
If you have a question, check to see if it is documented there.
If it is *not* documented there, or you can't find an answer, please open a issue.
@@ -78,7 +78,7 @@ Fill out [the required template](./ISSUE_TEMPLATE/bug_report.md),
#### Before Submitting A Bug Report
* **Determine [which repository the problem should be reported in](#capa-and-its-repositories)**.
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
* **Perform a [cursory search](https://github.com/mandiant/capa/issues?q=is%3Aissue)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
#### How Do I Submit A (Good) Bug Report?
@@ -101,7 +101,7 @@ Explain the problem and include additional details to help maintainers reproduce
Provide more context by answering these questions:
* **Did the problem start happening recently** (e.g. after updating to a new version of capa) or was this always a problem?
* If the problem started happening recently, **can you reproduce the problem in an older version of capa?** What's the most recent version in which the problem doesn't happen? You can download older versions of capa from [the releases page](https://github.com/fireeye/capa/releases).
* If the problem started happening recently, **can you reproduce the problem in an older version of capa?** What's the most recent version in which the problem doesn't happen? You can download older versions of capa from [the releases page](https://github.com/mandiant/capa/releases).
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
* If the problem is related to working with files (e.g. opening and editing files), **does the problem happen for all files and projects or only some?** Does the problem happen only when working with local or remote files (e.g. on network drives), with files of a specific type (e.g. only JavaScript or Python files), with large files or files with very long lines, or with files in a specific encoding? Is there anything else special about the files you are using?
@@ -119,7 +119,7 @@ Before creating enhancement suggestions, please check [this list](#before-submit
#### Before Submitting An Enhancement Suggestion
* **Determine [which repository the enhancement should be suggested in](#capa-and-its-repositories).**
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
* **Perform a [cursory search](https://github.com/mandiant/capa/issues?q=is%3Aissue)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
#### How Do I Submit A (Good) Enhancement Suggestion?
@@ -138,15 +138,15 @@ Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com
Unsure where to begin contributing to capa? You can start by looking through these `good-first-issue` and `rule-idea` issues:
* [good-first-issue](https://github.com/fireeye/capa/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two.
* [rule-idea](https://github.com/fireeye/capa-rules/issues?q=is%3Aissue+is%3Aopen+label%3A%22rule+idea%22) - issues that describe potential new rule ideas.
* [good-first-issue](https://github.com/mandiant/capa/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two.
* [rule-idea](https://github.com/mandiant/capa-rules/issues?q=is%3Aissue+is%3Aopen+label%3A%22rule+idea%22) - issues that describe potential new rule ideas.
Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have.
#### Local development
capa and all its resources can be developed locally.
For instructions on how to do this, see the "Method 3" section of the [installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md).
For instructions on how to do this, see the "Method 3" section of the [installation guide](https://github.com/mandiant/capa/blob/master/doc/installation.md).
### Pull Requests
@@ -190,8 +190,8 @@ Our CI pipeline will reformat and enforce the Python styleguide.
All (non-nursery) capa rules must:
1. pass the [linter](https://github.com/fireeye/capa/blob/master/scripts/lint.py), and
2. be formatted with [capafmt](https://github.com/fireeye/capa/blob/master/scripts/capafmt.py)
1. pass the [linter](https://github.com/mandiant/capa/blob/master/scripts/lint.py), and
2. be formatted with [capafmt](https://github.com/mandiant/capa/blob/master/scripts/capafmt.py)
This ensures that all rules meet the same minimum level of quality and are structured in a consistent way.
Our CI pipeline will reformat and enforce the capa rules styleguide.

View File

@@ -5,16 +5,16 @@ about: Create a report to help us improve
---
<!--
# Is your bug report related to capa rules (for example a false positive)?
We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/mandiant/capa-rules/issues.
# Have you checked that your issue isn't already filed?
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
Please search if there is a similar issue at https://github.com/mandiant/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
# Have you read capa's Code of Conduct?
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/mandiant/capa/blob/master/.github/CODE_OF_CONDUCT.md
# Have you read capa's CONTRIBUTING guide?
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#reporting-bugs
It contains helpful information about how to contribute to capa. Check https://github.com/mandiant/capa/blob/master/.github/CONTRIBUTING.md#reporting-bugs
-->
### Description

View File

@@ -5,16 +5,16 @@ about: Suggest an idea for capa
---
<!--
# Is your issue related to capa rules (for example an idea for a new rule)?
We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/mandiant/capa-rules/issues.
# Have you checked that your issue isn't already filed?
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
Please search if there is a similar issue at https://github.com/mandiant/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
# Have you read capa's Code of Conduct?
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/mandiant/capa/blob/master/.github/CODE_OF_CONDUCT.md
# Have you read capa's CONTRIBUTING guide?
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#suggesting-enhancements
It contains helpful information about how to contribute to capa. Check https://github.com/mandiant/capa/blob/master/.github/CONTRIBUTING.md#suggesting-enhancements
-->
### Summary

View File

@@ -74,3 +74,6 @@ ignore_missing_imports = True
[mypy-elftools.*]
ignore_missing_imports = True
[mypy-dncil.*]
ignore_missing_imports = True

View File

@@ -3,7 +3,7 @@
Thank you for contributing to capa! <3
Please read capa's CONTRIBUTING guide if you haven't done so already.
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md
It contains helpful information about how to contribute to capa. Check https://github.com/mandiant/capa/blob/master/.github/CONTRIBUTING.md
Please describe the changes in this pull request (PR). Include your motivation and context to help us review.

View File

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

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
from PyInstaller.utils.hooks import copy_metadata

View File

@@ -1,5 +1,5 @@
# -*- mode: python -*-
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
import os.path
import subprocess
@@ -109,4 +109,4 @@ exe = EXE(pyz,
# a.datas,
# strip=None,
# upx=True,
# name='capa-dat')
# name='capa-dat')

View File

@@ -1,8 +1,8 @@
name: build
on:
push:
branches: [master]
pull_request:
branches: [ master ]
release:
types: [edited, published]
@@ -11,16 +11,19 @@ jobs:
name: PyInstaller for ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
# set to false for debugging
fail-fast: true
matrix:
include:
- os: ubuntu-18.04
# use old linux so that the shared library versioning is more portable
artifact_name: capa
asset_name: linux
- os: windows-2019
- os: windows-2022
artifact_name: capa.exe
asset_name: windows
- os: macos-10.15
# use older macOS for assumed better portability
artifact_name: capa
asset_name: macos
steps:
@@ -35,12 +38,12 @@ jobs:
python-version: 3.8
- if: matrix.os == 'ubuntu-18.04'
run: sudo apt-get install -y libyaml-dev
- name: Install PyInstaller
run: pip install 'pyinstaller==4.2'
- name: Install capa
run: pip install -e .
- name: Upgrade pip, setuptools
run: python -m pip install --upgrade pip setuptools
- name: Install capa with build requirements
run: pip install -e .[build]
- name: Build standalone executable
run: pyinstaller .github/pyinstaller/pyinstaller.spec
run: pyinstaller --log-level DEBUG .github/pyinstaller/pyinstaller.spec
- name: Does it run (PE)?
run: dist/capa "tests/data/Practical Malware Analysis Lab 01-01.dll_"
- name: Does it run (Shellcode)?
@@ -53,8 +56,6 @@ jobs:
path: dist/${{ matrix.artifact_name }}
test_run:
# test that binaries run on push to master
if: github.event_name == 'push'
name: Test run on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: [build]
@@ -68,7 +69,7 @@ jobs:
- os: ubuntu-20.04
artifact_name: capa
asset_name: linux
- os: windows-2016
- os: windows-2022
artifact_name: capa.exe
asset_name: windows
steps:
@@ -77,7 +78,7 @@ jobs:
with:
name: ${{ matrix.asset_name }}
- name: Set executable flag
if: matrix.os != 'windows-2016'
if: matrix.os != 'windows-2022'
run: chmod +x ${{ matrix.artifact_name }}
- name: Run capa
run: ./${{ matrix.artifact_name }} -h

View File

@@ -15,7 +15,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.6'
python-version: '3.7'
- name: Install dependencies
run: |
python -m pip install --upgrade pip

View File

@@ -12,18 +12,19 @@ jobs:
- name: Checkout capa-rules
uses: actions/checkout@v2
with:
repository: fireeye/capa-rules
repository: mandiant/capa-rules
token: ${{ secrets.CAPA_TOKEN }}
- name: Tag capa-rules
run: |
# user information is needed to create annotated tags (with a message)
git config user.email 'capa-dev@fireeye.com'
git config user.email 'capa-dev@mandiant.com'
git config user.name 'Capa Bot'
name=${{ github.event.release.tag_name }}
git tag $name -m "https://github.com/fireeye/capa/releases/$name"
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
with:
repository: fireeye/capa-rules
repository: mandiant/capa-rules
github_token: ${{ secrets.CAPA_TOKEN }}
tags: true

View File

@@ -30,13 +30,15 @@ jobs:
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: "3.8"
- name: Install dependencies
run: pip install -e .[dev]
- name: Lint with isort
run: isort --profile black --length-sort --line-width 120 -c .
- name: Lint with black
run: black -l 120 --check .
- 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/
@@ -46,11 +48,11 @@ jobs:
- name: Checkout capa with submodules
uses: actions/checkout@v2
with:
submodules: true
submodules: recursive
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: "3.8"
- name: Install capa
run: pip install -e .
- name: Run rule linter
@@ -63,20 +65,20 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, windows-2019, macos-10.15]
os: [ubuntu-20.04, windows-2019, macos-11]
# across all operating systems
python-version: [3.6, 3.9]
python-version: ["3.7", "3.10"]
include:
# on Ubuntu run these as well
- os: ubuntu-20.04
python-version: 3.7
python-version: "3.8"
- os: ubuntu-20.04
python-version: 3.8
python-version: "3.9"
steps:
- name: Checkout capa with submodules
uses: actions/checkout@v2
with:
submodules: true
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:

8
.gitignore vendored
View File

@@ -114,3 +114,11 @@ venv.bak/
isort-output.log
black-output.log
rule-linter-output.log
.vscode
scripts/perf/*.txt
scripts/perf/*.svg
scripts/perf/*.zip
.direnv
.envrc
.DS_Store
*/.DS_Store

View File

@@ -1,6 +1,5 @@
# Change Log
## master (unreleased)
### New Features
@@ -9,7 +8,7 @@
### New Rules (0)
-
-
### Bug Fixes
@@ -18,10 +17,249 @@
### Development
### Raw diffs
- [capa <release>...master](https://github.com/fireeye/capa/compare/v3.0.2...master)
- [capa-rules <release>...master](https://github.com/fireeye/capa-rules/compare/v3.0.2...master)
- [capa v4.0.0...master](https://github.com/mandiant/capa/compare/v4.0.0...master)
- [capa-rules v4.0.0...master](https://github.com/mandiant/capa-rules/compare/v4.0.0...master)
## v4.0.0 (2022-07-XX)
Version 4 adds support for analyzing .NET executables. capa will autodetect .NET modules, or you can explicitly invoke the new feature extractor via `--format dotnet`. We've also extended the rule syntax for .NET features including `namespace` and `class`.
Additionally, new `instruction` scope and `operand` features enable users to create more explicit rules. These features are not backwards compatible. We removed the previously used `/x32` and `/x64` flavors of number and operand features.
We updated 49 existing rules and added 22 new rules leveraging these new features and characteristics to detect capabilities seen in .NET malware.
More breaking changes include updates to the JSON results document, freeze file format schema (now format version v2), and the internal handling of addresses.
Thanks for all the support, especially to @htnhan, @jtothej, @sara-rn, @anushkavirgaonkar, and @_re_fox!
*Deprecation warning: v4.0 will be the last capa version to support the SMDA backend.*
### New Features
- add new scope "instruction" for matching mnemonics and operands #767 @williballenthin
- add new feature "operand[{0, 1, 2}].number" for matching instruction operand immediate values #767 @williballenthin
- add new feature "operand[{0, 1, 2}].offset" for matching instruction operand offsets #767 @williballenthin
- extract additional offset/number features in certain circumstances #320 @williballenthin
- add detection and basic feature extraction for dotnet #987 @mr-tz, @mike-hunhoff, @williballenthin
- add file string extraction for dotnet files #1012 @mike-hunhoff
- add file function-name extraction for dotnet files #1015 @mike-hunhoff
- add unmanaged call characteristic for dotnet files #1023 @mike-hunhoff
- add mixed mode characteristic feature extraction for dotnet files #1024 @mike-hunhoff
- emit class and namespace features for dotnet files #1030 @mike-hunhoff
- render: support Addresses that aren't simple integers, like .NET token+offset #981 @williballenthin
- document rule tags and branches #1006 @williballenthin, @mr-tz
### Breaking Changes
- instruction scope and operand feature are new and are not backwards compatible with older versions of capa
- Python 3.7 is now the minimum supported Python version #866 @williballenthin
- remove /x32 and /x64 flavors of number and operand features #932 @williballenthin
- the tool now accepts multiple paths to rules, and JSON doc updated accordingly @williballenthin
- extractors must use handles to identify functions/basic blocks/instructions #981 @williballenthin
- the freeze file format schema was updated, including format version bump to v2 #986 @williballenthin
Deprecation notice: as described in [#937](https://github.com/mandiant/capa/issues/937), we plan to remove the SMDA backend for v5. If you rely on this backend, please reach out so we can discuss extending the support for SMDA or transitioning your workflow to use vivisect.
### New Rules (30)
- data-manipulation/encryption/aes/manually-build-aes-constants huynh.t.nhan@gmail.com
- nursery/get-process-image-filename michael.hunhoff@mandiant.com
- compiler/v/compiled-with-v jakub.jozwiak@mandiant.com
- compiler/zig/compiled-with-zig jakub.jozwiak@mandiant.com
- anti-analysis/packer/huan/packed-with-huan jakub.jozwiak@mandiant.com
- internal/limitation/file/internal-dotnet-file-limitation william.ballenthin@mandiant.com
- nursery/get-os-information-via-kuser_shared_data @mr-tz
- load-code/pe/resolve-function-by-parsing-PE-exports @sara-rn
- anti-analysis/packer/huan/packed-with-huan jakub.jozwiak@mandiant.com
- nursery/execute-dotnet-assembly anushka.virgaonkar@mandiant.com
- nursery/invoke-dotnet-assembly-method anushka.virgaonkar@mandiant.com
- collection/screenshot/capture-screenshot-via-keybd-event @_re_fox
- collection/browser/gather-chrome-based-browser-login-information @_re_fox
- nursery/power-down-monitor michael.hunhoff@mandiant.com
- nursery/hash-data-using-aphash @_re_fox
- nursery/hash-data-using-jshash @_re_fox
- host-interaction/file-system/files/list/enumerate-files-on-windows moritz.raabe@mandiant.com anushka.virgaonkar@mandiant.com
- nursery/check-clipboard-data anushka.virgaonkar@mandiant.com
- nursery/clear-clipboard-data anushka.virgaonkar@mandiant.com
- nursery/compile-dotnet-assembly anushka.virgaonkar@mandiant.com
- nursery/create-process-via-wmi anushka.virgaonkar@mandiant.com
- nursery/display-service-notification-message-box anushka.virgaonkar@mandiant.com
- nursery/find-process-by-name anushka.virgaonkar@mandiant.com
- nursery/generate-random-numbers-in-dotnet anushka.virgaonkar@mandiant.com
- nursery/send-keystrokes anushka.virgaonkar@mandiant.com
- nursery/send-request-in-dotnet anushka.virgaonakr@mandiant.com
- nursery/terminate-process-by-name-in-dotnet anushka.virgaonkar@mandiant.com
- nursery/hash-data-using-rshash @_re_fox
- persistence/authentication-process/act-as-credential-manager-dll jakub.jozwiak@mandiant.com
- persistence/authentication-process/act-as-password-filter-dll jakub.jozwiak@mandiant.com
### Bug Fixes
- improve handling _ prefix compile/link artifact #924 @mike-hunhoff
- better detect OS in ELF samples #988 @williballenthin
- display number feature zero in vverbose #1097 @mike-hunhoff
### capa explorer IDA Pro plugin
- improve file format extraction #918 @mike-hunhoff
- remove decorators added by IDA to ELF imports #919 @mike-hunhoff
- bug fixes for Address abstraction #1091 @mike-hunhoff
### Development
### Raw diffs
- [capa v3.2.0...v4.0.0](https://github.com/mandiant/capa/compare/v3.2.0...master)
- [capa-rules v3.2.0...v4.0.0](https://github.com/mandiant/capa-rules/compare/v3.2.0...master)
## v3.2.1 (2022-06-06)
This out-of-band release bumps the SMDA dependency version to enable installation on Python 3.10.
### Bug Fixes
- update SMDA dependency @mike-hunhoff #922
### Raw diffs
- [capa v3.2.0...v3.2.1](https://github.com/mandiant/capa/compare/v3.2.0...v3.2.1)
- [capa-rules v3.2.0...v3.2.1](https://github.com/mandiant/capa-rules/compare/v3.2.0...v3.2.1)
## v3.2.0 (2022-03-03)
This release adds a new characteristic `characteristic: call $+5` enabling users to create more explicit rules. The linter now also validates ATT&CK and MBC categories. Additionally, many dependencies, including the vivisect backend, have been updated.
One rule has been added and many more have been improved.
Thanks for all the support, especially to @kn0wl3dge and first time contributor @uckelman-sf!
### New Features
- linter: validate ATT&CK/MBC categories and IDs #103 @kn0wl3dge
- extractor: add characteristic "call $+5" feature #366 @kn0wl3dge
### New Rules (1)
- anti-analysis/obfuscation/obfuscated-with-advobfuscator jakub.jozwiak@mandiant.com
### Bug Fixes
- remove typing package as a requirement for Python 3.7+ compatibility #901 @uckelman-sf
- elf: fix OS detection for Linux kernel modules #867 @williballenthin
### Raw diffs
- [capa v3.1.0...v3.2.0](https://github.com/mandiant/capa/compare/v3.1.0...v3.2.0)
- [capa-rules v3.1.0...v3.2.0](https://github.com/mandiant/capa-rules/compare/v3.1.0...v3.2.0)
## v3.1.0 (2022-01-10)
This release improves the performance of capa while also adding 23 new rules and many code quality enhancements. We profiled capa's CPU usage and optimized the way that it matches rules, such as by short circuiting when appropriate. According to our testing, the matching phase is approximately 66% faster than v3.0.3! We also added support for Python 3.10, aarch64 builds, and additional MAEC metadata in the rule headers.
## v3.0.2
This release adds 23 new rules, including nine by Jakub Jozwiak of Mandiant. @ryantxu1 and @dzbeck updated the ATT&CK and MBC mappings for many rules. Thank you!
And as always, welcome first time contributors!
- @kn0wl3dge
- @jtothej
- @cl30
### New Features
- engine: short circuit logic nodes for better performance #824 @williballenthin
- engine: add optimizer the order faster nodes first #829 @williballenthin
- engine: optimize rule evaluation by skipping rules that can't match #830 @williballenthin
- support python 3.10 #816 @williballenthin
- support aarch64 #683 @williballenthin
- rules: support maec/malware-family meta #841 @mr-tz
- engine: better type annotations/exhaustiveness checking #839 @cl30
### Breaking Changes: None
### New Rules (23)
- nursery/delete-windows-backup-catalog michael.hunhoff@mandiant.com
- nursery/disable-automatic-windows-recovery-features michael.hunhoff@mandiant.com
- nursery/capture-webcam-video @johnk3r
- nursery/create-registry-key-via-stdregprov michael.hunhoff@mandiant.com
- nursery/delete-registry-key-via-stdregprov michael.hunhoff@mandiant.com
- nursery/delete-registry-value-via-stdregprov michael.hunhoff@mandiant.com
- nursery/query-or-enumerate-registry-key-via-stdregprov michael.hunhoff@mandiant.com
- nursery/query-or-enumerate-registry-value-via-stdregprov michael.hunhoff@mandiant.com
- nursery/set-registry-value-via-stdregprov michael.hunhoff@mandiant.com
- data-manipulation/compression/decompress-data-using-ucl jakub.jozwiak@mandiant.com
- linking/static/wolfcrypt/linked-against-wolfcrypt jakub.jozwiak@mandiant.com
- linking/static/wolfssl/linked-against-wolfssl jakub.jozwiak@mandiant.com
- anti-analysis/packer/pespin/packed-with-pespin jakub.jozwiak@mandiant.com
- load-code/shellcode/execute-shellcode-via-windows-fibers jakub.jozwiak@mandiant.com
- load-code/shellcode/execute-shellcode-via-enumuilanguages jakub.jozwiak@mandiant.com
- anti-analysis/packer/themida/packed-with-themida william.ballenthin@mandiant.com
- load-code/shellcode/execute-shellcode-via-createthreadpoolwait jakub.jozwiak@mandiant.com
- host-interaction/process/inject/inject-shellcode-using-a-file-mapping-object jakub.jozwiak@mandiant.com
- load-code/shellcode/execute-shellcode-via-copyfile2 jakub.jozwiak@mandiant.com
- malware-family/plugx/match-known-plugx-module still@teamt5.org
### Rule Changes
- update ATT&CK mappings by @ryantxu1
- update ATT&CK and MBC mappings by @dzbeck
- aplib detection by @cdong1012
- golang runtime detection by @stevemk14eber
### Bug Fixes
- fix circular import error #825 @williballenthin
- fix smda negative number extraction #430 @kn0wl3dge
### capa explorer IDA Pro plugin
- pin supported versions to >= 7.4 and < 8.0 #849 @mike-hunhoff
### Development
- add profiling infrastructure #828 @williballenthin
- linter: detect shellcode extension #820 @mr-tz
- show features script: add backend flag #430 @kn0wl3dge
### Raw diffs
- [capa v3.0.3...v3.1.0](https://github.com/mandiant/capa/compare/v3.0.3...v3.1.0)
- [capa-rules v3.0.3...v3.1.0](https://github.com/mandiant/capa-rules/compare/v3.0.3...v3.1.0)
## v3.0.3 (2021-10-27)
This is primarily a rule maintenance release:
- eight new rules, including all relevant techniques from [ATT&CK v10](https://medium.com/mitre-attack/introducing-attack-v10-7743870b37e3), and
- two rules removed, due to the prevalence of false positives
We've also tweaked the status codes returned by capa.exe to be more specific and added a bit more metadata to the JSON output format.
As always, welcome first time contributors!
- still@teamt5.org
- zander.work@mandiant.com
### New Features
- show in which function a BB match is #130 @williballenthin
- main: exit with unique error codes when bailing #802 @williballenthin
### New Rules (8)
- nursery/resolve-function-by-fnv-1a-hash still@teamt5.org
- data-manipulation/encryption/encrypt-data-using-memfrob-from-glibc zander.work@mandiant.com
- collection/group-policy/discover-group-policy-via-gpresult william.ballenthin@mandiant.com
- host-interaction/bootloader/manipulate-safe-mode-programs william.ballenthin@mandiant.com
- nursery/enable-safe-mode-boot william.ballenthin@mandiant.com
- persistence/iis/persist-via-iis-module william.ballenthin@mandiant.com
- persistence/iis/persist-via-isapi-extension william.ballenthin@mandiant.com
- targeting/language/identify-system-language-via-api william.ballenthin@mandiant.com
## Removed rules (2)
- load-code/pe/parse-pe-exports: too many false positives in unrelated structure accesses
- anti-analysis/anti-vm/vm-detection/execute-anti-vm-instructions: too many false positives in junk code
### Bug Fixes
- update references from FireEye to Mandiant
### Raw diffs
- [capa v3.0.2...v3.0.3](https://github.com/fireeye/capa/compare/v3.0.2...v3.0.3)
- [capa-rules v3.0.2...v3.0.3](https://github.com/fireeye/capa-rules/compare/v3.0.2...v3.0.3)
## v3.0.2 (2021-09-28)
This release fixes an issue with the standalone executables built with PyInstaller when running capa against ELF files.
@@ -30,8 +268,8 @@ This release fixes an issue with the standalone executables built with PyInstall
- fix bug in PyInstaller config preventing ELF analysis #795 @mr-tz
### Raw diffs
- [capa v3.0.1...master](https://github.com/fireeye/capa/compare/v3.0.1...v3.0.2)
- [capa-rules v3.0.1...master](https://github.com/fireeye/capa-rules/compare/v3.0.1...v3.0.2)
- [capa v3.0.1...v3.0.2](https://github.com/fireeye/capa/compare/v3.0.1...v3.0.2)
- [capa-rules v3.0.1...v3.0.2](https://github.com/fireeye/capa-rules/compare/v3.0.1...v3.0.2)
## v3.0.1 (2021-09-27)
@@ -83,29 +321,29 @@ Also, welcome first time contributors:
### New Rules (80)
- collection/webcam/capture-webcam-image @johnk3r
- nursery/list-drag-and-drop-files michael.hunhoff@fireeye.com
- nursery/monitor-clipboard-content michael.hunhoff@fireeye.com
- nursery/monitor-local-ipv4-address-changes michael.hunhoff@fireeye.com
- nursery/load-windows-common-language-runtime michael.hunhoff@fireeye.com
- nursery/resize-volume-shadow-copy-storage michael.hunhoff@fireeye.com
- nursery/add-user-account-group michael.hunhoff@fireeye.com
- nursery/add-user-account-to-group michael.hunhoff@fireeye.com
- nursery/add-user-account michael.hunhoff@fireeye.com
- nursery/change-user-account-password michael.hunhoff@fireeye.com
- nursery/delete-user-account-from-group michael.hunhoff@fireeye.com
- nursery/delete-user-account-group michael.hunhoff@fireeye.com
- nursery/delete-user-account michael.hunhoff@fireeye.com
- nursery/list-domain-servers michael.hunhoff@fireeye.com
- nursery/list-groups-for-user-account michael.hunhoff@fireeye.com
- nursery/list-user-account-groups michael.hunhoff@fireeye.com
- nursery/list-user-accounts-for-group michael.hunhoff@fireeye.com
- nursery/list-user-accounts michael.hunhoff@fireeye.com
- nursery/parse-url michael.hunhoff@fireeye.com
- nursery/register-raw-input-devices michael.hunhoff@fireeye.com
- anti-analysis/packer/gopacker/packed-with-gopacker jared.wilson@fireeye.com
- nursery/list-drag-and-drop-files michael.hunhoff@mandiant.com
- nursery/monitor-clipboard-content michael.hunhoff@mandiant.com
- nursery/monitor-local-ipv4-address-changes michael.hunhoff@mandiant.com
- nursery/load-windows-common-language-runtime michael.hunhoff@mandiant.com
- nursery/resize-volume-shadow-copy-storage michael.hunhoff@mandiant.com
- nursery/add-user-account-group michael.hunhoff@mandiant.com
- nursery/add-user-account-to-group michael.hunhoff@mandiant.com
- nursery/add-user-account michael.hunhoff@mandiant.com
- nursery/change-user-account-password michael.hunhoff@mandiant.com
- nursery/delete-user-account-from-group michael.hunhoff@mandiant.com
- nursery/delete-user-account-group michael.hunhoff@mandiant.com
- nursery/delete-user-account michael.hunhoff@mandiant.com
- nursery/list-domain-servers michael.hunhoff@mandiant.com
- nursery/list-groups-for-user-account michael.hunhoff@mandiant.com
- nursery/list-user-account-groups michael.hunhoff@mandiant.com
- nursery/list-user-accounts-for-group michael.hunhoff@mandiant.com
- nursery/list-user-accounts michael.hunhoff@mandiant.com
- nursery/parse-url michael.hunhoff@mandiant.com
- nursery/register-raw-input-devices michael.hunhoff@mandiant.com
- anti-analysis/packer/gopacker/packed-with-gopacker jared.wilson@mandiant.com
- host-interaction/driver/create-device-object @mr-tz
- host-interaction/process/create/execute-command @mr-tz
- data-manipulation/encryption/create-new-key-via-cryptacquirecontext chuong.dong@fireeye.com
- data-manipulation/encryption/create-new-key-via-cryptacquirecontext chuong.dong@mandiant.com
- host-interaction/log/clfs/append-data-to-clfs-log-container blaine.stancill@mandiant.com
- host-interaction/log/clfs/read-data-from-clfs-log-container blaine.stancill@mandiant.com
- data-manipulation/encryption/hc-128/encrypt-data-using-hc-128-via-wolfssl blaine.stancill@mandiant.com
@@ -127,15 +365,15 @@ Also, welcome first time contributors:
- persistence/persist-via-shell-profile-or-rc-file joakim@intezer.com
- persistence/service/persist-via-rc-script joakim@intezer.com
- collection/get-current-user-on-linux joakim@intezer.com
- collection/network/get-mac-address-on-windows moritz.raabe@fireeye.com
- host-interaction/file-system/read/read-file-on-linux moritz.raabe@fireeye.com joakim@intezer.com
- host-interaction/file-system/read/read-file-on-windows moritz.raabe@fireeye.com
- host-interaction/file-system/write/write-file-on-windows william.ballenthin@fireeye.com
- host-interaction/os/info/get-system-information-on-windows moritz.raabe@fireeye.com joakim@intezer.com
- host-interaction/process/create/create-process-on-windows moritz.raabe@fireeye.com
- linking/runtime-linking/link-function-at-runtime-on-windows moritz.raabe@fireeye.com
- collection/network/get-mac-address-on-windows moritz.raabe@mandiant.com
- host-interaction/file-system/read/read-file-on-linux moritz.raabe@mandiant.com joakim@intezer.com
- host-interaction/file-system/read/read-file-on-windows moritz.raabe@mandiant.com
- host-interaction/file-system/write/write-file-on-windows william.ballenthin@mandiant.com
- host-interaction/os/info/get-system-information-on-windows moritz.raabe@mandiant.com joakim@intezer.com
- host-interaction/process/create/create-process-on-windows moritz.raabe@mandiant.com
- linking/runtime-linking/link-function-at-runtime-on-windows moritz.raabe@mandiant.com
- nursery/create-process-on-linux joakim@intezer.com
- nursery/enumerate-files-on-linux william.ballenthin@fireeye.com
- nursery/enumerate-files-on-linux william.ballenthin@mandiant.com
- nursery/get-mac-address-on-linux joakim@intezer.com
- nursery/get-system-information-on-linux joakim@intezer.com
- nursery/link-function-at-runtime-on-linux joakim@intezer.com
@@ -167,8 +405,8 @@ Also, welcome first time contributors:
### Development
### Raw diffs
- [capa v2.0.0...v3.0.0](https://github.com/fireeye/capa/compare/v2.0.0...v3.0.0)
- [capa-rules v2.0.0...v3.0.0](https://github.com/fireeye/capa-rules/compare/v2.0.0...v3.0.0)
- [capa v2.0.0...v3.0.0](https://github.com/mandiant/capa/compare/v2.0.0...v3.0.0)
- [capa-rules v2.0.0...v3.0.0](https://github.com/mandiant/capa-rules/compare/v2.0.0...v3.0.0)
## v2.0.0 (2021-07-19)
@@ -187,7 +425,7 @@ A huge thanks to everyone who submitted issues, provided feedback, and contribut
### New Features
- rules: update ATT&CK and MBC mappings https://github.com/fireeye/capa-rules/pull/317 @williballenthin
- rules: update ATT&CK and MBC mappings https://github.com/mandiant/capa-rules/pull/317 @williballenthin
- main: use FLIRT signatures to identify and ignore library code #446 @williballenthin
- tests: update test cases and caching #545 @mr-tz
- scripts: capa2yara.py convert capa rules to YARA rules #561 @ruppde
@@ -275,34 +513,34 @@ A huge thanks to everyone who submitted issues, provided feedback, and contribut
- nursery/run-in-container @williballenthin
- persistence/registry/appinitdlls/disable-appinit_dlls-code-signature-enforcement @williballenthin
- collection/password-manager/steal-keepass-passwords-using-keefarce @Ana06
- host-interaction/network/connectivity/check-internet-connectivity-via-wininet matthew.williams@fireeye.com michael.hunhoff@fireeye.com
- host-interaction/network/connectivity/check-internet-connectivity-via-wininet matthew.williams@mandiant.com michael.hunhoff@mandiant.com
- nursery/create-bits-job @mr-tz
- nursery/execute-syscall-instruction @kulinacs @mr-tz
- nursery/connect-to-wmi-namespace-via-wbemlocator michael.hunhoff@fireeye.com
- nursery/connect-to-wmi-namespace-via-wbemlocator michael.hunhoff@mandiant.com
- anti-analysis/obfuscation/obfuscated-with-callobfuscator johnk3r
- executable/installer/inno-setup/packaged-as-an-inno-setup-installer awillia2@cisco.com
- data-manipulation/hashing/djb2/hash-data-using-djb2 awillia2@cisco.com
- data-manipulation/encoding/base64/decode-data-using-base64-via-dword-translation-table gilbert.elliot@fireeye.com
- nursery/list-tcp-connections-and-listeners michael.hunhoff@fireeye.com
- nursery/list-udp-connections-and-listeners michael.hunhoff@fireeye.com
- nursery/log-keystrokes-via-raw-input-data michael.hunhoff@fireeye.com
- nursery/register-http-server-url michael.hunhoff@fireeye.com
- internal/limitation/file/internal-autoit-file-limitation.yml william.ballenthin@fireeye.com
- internal/limitation/file/internal-dotnet-file-limitation.yml william.ballenthin@fireeye.com
- internal/limitation/file/internal-installer-file-limitation.yml william.ballenthin@fireeye.com
- internal/limitation/file/internal-packer-file-limitation.yml william.ballenthin@fireeye.com
- data-manipulation/encoding/base64/decode-data-using-base64-via-dword-translation-table gilbert.elliot@mandiant.com
- nursery/list-tcp-connections-and-listeners michael.hunhoff@mandiant.com
- nursery/list-udp-connections-and-listeners michael.hunhoff@mandiant.com
- nursery/log-keystrokes-via-raw-input-data michael.hunhoff@mandiant.com
- nursery/register-http-server-url michael.hunhoff@mandiant.com
- internal/limitation/file/internal-autoit-file-limitation.yml william.ballenthin@mandiant.com
- internal/limitation/file/internal-dotnet-file-limitation.yml william.ballenthin@mandiant.com
- internal/limitation/file/internal-installer-file-limitation.yml william.ballenthin@mandiant.com
- internal/limitation/file/internal-packer-file-limitation.yml william.ballenthin@mandiant.com
- host-interaction/network/domain/enumerate-domain-computers-via-ldap awillia2@cisco.com
- host-interaction/network/domain/get-domain-controller-name awillia2@cisco.com
- internal/limitation/file/internal-visual-basic-file-limitation @mr-tz
- data-manipulation/hashing/md5/hash-data-with-md5 moritz.raabe@fireeye.com
- data-manipulation/hashing/md5/hash-data-with-md5 moritz.raabe@mandiant.com
- compiler/autohotkey/compiled-with-autohotkey awillia2@cisco.com
- internal/limitation/file/internal-autohotkey-file-limitation @mr-tz
- host-interaction/process/dump/create-process-memory-minidump michael.hunhoff@fireeye.com
- nursery/get-storage-device-properties michael.hunhoff@fireeye.com
- nursery/execute-shell-command-via-windows-remote-management michael.hunhoff@fireeye.com
- nursery/get-token-privileges michael.hunhoff@fireeye.com
- nursery/prompt-user-for-credentials michael.hunhoff@fireeye.com
- nursery/spoof-parent-pid michael.hunhoff@fireeye.com
- host-interaction/process/dump/create-process-memory-minidump michael.hunhoff@mandiant.com
- nursery/get-storage-device-properties michael.hunhoff@mandiant.com
- nursery/execute-shell-command-via-windows-remote-management michael.hunhoff@mandiant.com
- nursery/get-token-privileges michael.hunhoff@mandiant.com
- nursery/prompt-user-for-credentials michael.hunhoff@mandiant.com
- nursery/spoof-parent-pid michael.hunhoff@mandiant.com
### Bug Fixes
@@ -322,8 +560,8 @@ A huge thanks to everyone who submitted issues, provided feedback, and contribut
### Development
- ci: add capa release link to capa-rules tag #517 @Ana06
- ci, changelog: update `New Rules` section in CHANGELOG automatically https://github.com/fireeye/capa-rules/pull/374 #549 #604 @Ana06
- ci, changelog: support multiple author in sync GH https://github.com/fireeye/capa-rules/pull/378 @Ana06
- ci, changelog: update `New Rules` section in CHANGELOG automatically https://github.com/mandiant/capa-rules/pull/374 #549 #604 @Ana06
- ci, changelog: support multiple author in sync GH https://github.com/mandiant/capa-rules/pull/378 @Ana06
- ci, lint: check statements for single child statements #563 @mr-tz
- ci: reject PRs without CHANGELOG update to ensure CHANGELOG is kept up-to-date #584 @Ana06
- ci: test that scripts run #660 @mr-tz
@@ -331,8 +569,8 @@ A huge thanks to everyone who submitted issues, provided feedback, and contribut
### Raw diffs
<!-- The diff uses v1.6.1 because master doesn't include v1.6.2 and v1.6.3 -->
- [capa v1.6.1...v2.0.0](https://github.com/fireeye/capa/compare/v1.6.1...v2.0.0)
- [capa-rules v1.6.1...v2.0.0](https://github.com/fireeye/capa-rules/compare/v1.6.1...v2.0.0)
- [capa v1.6.1...v2.0.0](https://github.com/mandiant/capa/compare/v1.6.1...v2.0.0)
- [capa-rules v1.6.1...v2.0.0](https://github.com/mandiant/capa-rules/compare/v1.6.1...v2.0.0)
## v1.6.3 (2021-04-29)
@@ -345,7 +583,7 @@ This release adds IDA 7.6 support to capa.
### Raw diffs
- [capa v1.6.2...v1.6.3](https://github.com/fireeye/capa/compare/v1.6.2...v1.6.3)
- [capa v1.6.2...v1.6.3](https://github.com/mandiant/capa/compare/v1.6.2...v1.6.3)
## v1.6.2 (2021-04-13)
@@ -358,7 +596,7 @@ This release backports a fix to capa 1.6: The Windows binary was built with Pyth
### Raw diffs
- [capa v1.6.1...v1.6.2](https://github.com/fireeye/capa/compare/v1.6.1...v1.6.2)
- [capa v1.6.1...v1.6.2](https://github.com/mandiant/capa/compare/v1.6.1...v1.6.2)
## v1.6.1 (2021-04-07)
@@ -426,8 +664,8 @@ This release includes several bug fixes, such as a vivisect issue that prevented
### Raw diffs
- [capa v1.6.0...v1.6.1](https://github.com/fireeye/capa/compare/v1.6.0...v1.6.1)
- [capa-rules v1.6.0...v1.6.1](https://github.com/fireeye/capa-rules/compare/v1.6.0...v1.6.1)
- [capa v1.6.0...v1.6.1](https://github.com/mandiant/capa/compare/v1.6.0...v1.6.1)
- [capa-rules v1.6.0...v1.6.1](https://github.com/mandiant/capa-rules/compare/v1.6.0...v1.6.1)
## v1.6.0 (2021-03-09)
@@ -436,7 +674,7 @@ This release adds the capa explorer rule generator plugin for IDA Pro, vivisect
### Rule Generator IDA Plugin
The capa explorer IDA plugin now helps you quickly build new capa rules using features extracted directly from your IDA database. Without leaving the plugin interface you can use the features extracted by capa explorer to develop and test new rules and save your work directly to your capa rules directory. To get started select the new `Rule Generator` tab, navigate to a function in the IDA `Disassembly` view, and click `Analyze`. For more information check out the capa explorer [readme](https://github.com/fireeye/capa/blob/master/capa/ida/plugin/README.md).
The capa explorer IDA plugin now helps you quickly build new capa rules using features extracted directly from your IDA database. Without leaving the plugin interface you can use the features extracted by capa explorer to develop and test new rules and save your work directly to your capa rules directory. To get started select the new `Rule Generator` tab, navigate to a function in the IDA `Disassembly` view, and click `Analyze`. For more information check out the capa explorer [readme](https://github.com/mandiant/capa/blob/master/capa/ida/plugin/README.md).
![](doc/img/rulegen_expanded.png)
@@ -498,8 +736,8 @@ If you have workflows that rely on the Python 2 version and need future maintena
### Raw diffs
- [capa v1.5.1...v1.6.0](https://github.com/fireeye/capa/compare/v1.5.1...v1.6.0)
- [capa-rules v1.5.1...v1.6.0](https://github.com/fireeye/capa-rules/compare/v1.5.1...v1.6.0)
- [capa v1.5.1...v1.6.0](https://github.com/mandiant/capa/compare/v1.5.1...v1.6.0)
- [capa-rules v1.5.1...v1.6.0](https://github.com/mandiant/capa-rules/compare/v1.5.1...v1.6.0)
## v1.5.1 (2021-02-09)
@@ -512,8 +750,8 @@ This release fixes the version number that we forgot to update for v1.5.0 (there
### Raw diffs
- [capa v1.5.0...v1.5.1](https://github.com/fireeye/capa/compare/v1.5.1...v1.6.0)
- [capa-rules v1.5.0...v1.5.1](https://github.com/fireeye/capa-rules/compare/v1.5.1...v1.6.0)
- [capa v1.5.0...v1.5.1](https://github.com/mandiant/capa/compare/v1.5.1...v1.6.0)
- [capa-rules v1.5.0...v1.5.1](https://github.com/mandiant/capa-rules/compare/v1.5.1...v1.6.0)
## v1.5.0 (2021-02-05)
@@ -528,7 +766,7 @@ This release brings support for running capa under Python 3 via [SMDA](https://g
@dzbeck also added [Malware Behavior Catalog](https://github.com/MBCProject/mbc-markdown) (MBC) and ATT&CK mappings for many rules.
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/fireeye/capa/). Report issues on our [issue tracker](https://github.com/fireeye/capa/issues) and contribute new rules at [capa-rules](https://github.com/fireeye/capa-rules/).
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/mandiant/capa/). Report issues on our [issue tracker](https://github.com/mandiant/capa/issues) and contribute new rules at [capa-rules](https://github.com/mandiant/capa-rules/).
### New Features
@@ -611,8 +849,8 @@ Download a standalone binary below and checkout the readme [here on GitHub](http
### Raw diffs
- [capa v1.4.1...v1.5.0](https://github.com/fireeye/capa/compare/v1.4.1...v1.5.0)
- [capa-rules v1.4.0...v1.5.0](https://github.com/fireeye/capa-rules/compare/v1.4.0...v1.5.0)
- [capa v1.4.1...v1.5.0](https://github.com/mandiant/capa/compare/v1.4.1...v1.5.0)
- [capa-rules v1.4.0...v1.5.0](https://github.com/mandiant/capa-rules/compare/v1.4.0...v1.5.0)
## v1.4.1 (2020-10-23)
@@ -624,8 +862,8 @@ This release fixes an issue building capa on our CI server, which prevented us f
### Raw diffs
- [capa v1.4.0...v1.4.1](https://github.com/fireeye/capa/compare/v1.4.0...v1.4.1)
- [capa-rules v1.4.0...v1.4.1](https://github.com/fireeye/capa-rules/compare/v1.4.0...v1.4.1)
- [capa v1.4.0...v1.4.1](https://github.com/mandiant/capa/compare/v1.4.0...v1.4.1)
- [capa-rules v1.4.0...v1.4.1](https://github.com/mandiant/capa-rules/compare/v1.4.0...v1.4.1)
## v1.4.0 (2020-10-23)
@@ -636,7 +874,7 @@ This capa release includes changes to the rule parsing, enhanced feature extract
@dzbeck added [Malware Behavior Catalog](https://github.com/MBCProject/mbc-markdown) (MBC) and ATT&CK mappings for 86 rules.
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/fireeye/capa/). Report issues on our [issue tracker](https://github.com/fireeye/capa/issues) and contribute new rules at [capa-rules](https://github.com/fireeye/capa-rules/).
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/mandiant/capa/). Report issues on our [issue tracker](https://github.com/mandiant/capa/issues) and contribute new rules at [capa-rules](https://github.com/mandiant/capa-rules/).
### New features
@@ -739,8 +977,8 @@ Download a standalone binary below and checkout the readme [here on GitHub](http
### Raw diffs
- [capa v1.3.0...v1.4.0](https://github.com/fireeye/capa/compare/v1.3.0...v1.4.0)
- [capa-rules v1.3.0...v1.4.0](https://github.com/fireeye/capa-rules/compare/v1.3.0...v1.4.0)
- [capa v1.3.0...v1.4.0](https://github.com/mandiant/capa/compare/v1.3.0...v1.4.0)
- [capa-rules v1.3.0...v1.4.0](https://github.com/mandiant/capa-rules/compare/v1.3.0...v1.4.0)
## v1.3.0 (2020-09-14)
@@ -754,7 +992,7 @@ This release brings newly updated mappings to the [Malware Behavior Catalog vers
- @weslambert
- @stevemk14ebr
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/fireeye/capa/). Report issues on our [issue tracker](https://github.com/fireeye/capa/issues) and contribute new rules at [capa-rules](https://github.com/fireeye/capa-rules/).
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/mandiant/capa/). Report issues on our [issue tracker](https://github.com/mandiant/capa/issues) and contribute new rules at [capa-rules](https://github.com/mandiant/capa-rules/).
### Key changes to IDA Plugin
@@ -764,9 +1002,9 @@ The IDA Pro integration is now distributed as a real plugin, instead of a script
- updates distributed PyPI/`pip install --upgrade` without touching your `%IDADIR%`
- generally doing thing the "right way"
How to get this new version? Its easy: download [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory and update your capa installation (incidentally, this is a good opportunity to migrate to `pip install flare-capa` instead of git checkouts). Now you should see the plugin listed in the `Edit > Plugins > FLARE capa explorer` menu in IDA.
How to get this new version? Its easy: download [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory and update your capa installation (incidentally, this is a good opportunity to migrate to `pip install flare-capa` instead of git checkouts). Now you should see the plugin listed in the `Edit > Plugins > FLARE capa explorer` menu in IDA.
Please refer to the plugin [readme](https://github.com/fireeye/capa/blob/master/capa/ida/plugin/README.md) for additional information on installing and using the IDA Pro plugin.
Please refer to the plugin [readme](https://github.com/mandiant/capa/blob/master/capa/ida/plugin/README.md) for additional information on installing and using the IDA Pro plugin.
Please open an issue in this repository if you notice anything weird.
@@ -811,8 +1049,8 @@ Please open an issue in this repository if you notice anything weird.
### Raw diffs
- [capa v1.2.0...v1.3.0](https://github.com/fireeye/capa/compare/v1.2.0...v1.3.0)
- [capa-rules v1.2.0...v1.3.0](https://github.com/fireeye/capa-rules/compare/v1.2.0...v1.3.0)
- [capa v1.2.0...v1.3.0](https://github.com/mandiant/capa/compare/v1.2.0...v1.3.0)
- [capa-rules v1.2.0...v1.3.0](https://github.com/mandiant/capa-rules/compare/v1.2.0...v1.3.0)
## v1.2.0 (2020-08-31)
@@ -828,9 +1066,9 @@ We received contributions from ten reverse engineers, including five new ones:
- @edeca
- @winniepe
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/fireeye/capa/).
Report issues on our [issue tracker](https://github.com/fireeye/capa/issues)
and contribute new rules at [capa-rules](https://github.com/fireeye/capa-rules/).
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/mandiant/capa/).
Report issues on our [issue tracker](https://github.com/mandiant/capa/issues)
and contribute new rules at [capa-rules](https://github.com/mandiant/capa-rules/).
### New features
@@ -909,8 +1147,8 @@ and contribute new rules at [capa-rules](https://github.com/fireeye/capa-rules/)
### Raw diffs
- [capa v1.1.0...v1.2.0](https://github.com/fireeye/capa/compare/v1.1.0...v1.2.0)
- [capa-rules v1.1.0...v1.2.0](https://github.com/fireeye/capa-rules/compare/v1.1.0...v1.2.0)
- [capa v1.1.0...v1.2.0](https://github.com/mandiant/capa/compare/v1.1.0...v1.2.0)
- [capa-rules v1.1.0...v1.2.0](https://github.com/mandiant/capa-rules/compare/v1.1.0...v1.2.0)
## v1.1.0 (2020-08-05)
@@ -923,7 +1161,7 @@ We received contributions from eight reverse engineers, including four new ones:
- @bitsofbinary
- @threathive
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/fireeye/capa/). Report issues on our [issue tracker](https://github.com/fireeye/capa/issues) and contribute new rules at [capa-rules](https://github.com/fireeye/capa-rules/).
Download a standalone binary below and checkout the readme [here on GitHub](https://github.com/mandiant/capa/). Report issues on our [issue tracker](https://github.com/mandiant/capa/issues) and contribute new rules at [capa-rules](https://github.com/mandiant/capa-rules/).
### New features
@@ -996,5 +1234,5 @@ Download a standalone binary below and checkout the readme [here on GitHub](http
### Raw diffs
- [capa v1.0.0...v1.1.0](https://github.com/fireeye/capa/compare/v1.0.0...v1.1.0)
- [capa-rules v1.0.0...v1.1.0](https://github.com/fireeye/capa-rules/compare/v1.0.0...v1.1.0)
- [capa v1.0.0...v1.1.0](https://github.com/mandiant/capa/compare/v1.0.0...v1.1.0)
- [capa-rules v1.0.0...v1.1.0](https://github.com/mandiant/capa-rules/compare/v1.0.0...v1.1.0)

View File

@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2020 FireEye, Inc.
Copyright (C) 2020 Mandiant, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,20 +1,21 @@
![capa](https://github.com/fireeye/capa/blob/master/.github/logo.png)
![capa](https://github.com/mandiant/capa/blob/master/.github/logo.png)
[![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/fireeye/capa)](https://github.com/fireeye/capa/releases)
[![Number of rules](https://img.shields.io/badge/rules-633-blue.svg)](https://github.com/fireeye/capa-rules)
[![CI status](https://github.com/fireeye/capa/workflows/CI/badge.svg)](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
[![Downloads](https://img.shields.io/github/downloads/fireeye/capa/total)](https://github.com/fireeye/capa/releases)
[![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-702-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)
capa detects capabilities in executable files.
You run it against a PE, ELF, or shellcode file and it tells you what it thinks the program can do.
You run it against a PE, ELF, .NET module, or shellcode file and it tells you what it thinks the program can do.
For example, it might suggest that the file is a backdoor, is capable of installing services, or relies on HTTP to communicate.
Check out:
- the overview in our first [capa blog post](https://www.fireeye.com/blog/threat-research/2020/07/capa-automatically-identify-malware-capabilities.html)
- the major version 2.0 updates described in our [second blog post](https://www.fireeye.com/blog/threat-research/2021/07/capa-2-better-stronger-faster.html)
- the major version 3.0 (ELF support) described in the [third blog post](https://www.fireeye.com/blog/threat-research/2021/09/elfant-in-the-room-capa-v3.html)
- the overview in our first [capa blog post](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities)
- the major version 2.0 updates described in our [second blog post](https://www.mandiant.com/resources/capa-2-better-stronger-faster)
- the major version 3.0 (ELF support) described in the [third blog post](https://www.mandiant.com/resources/elfant-in-the-room-capa-v3)
- the major version 4.0 (.NET support) described in the TODO
```
$ capa.exe suspicious.exe
@@ -66,11 +67,11 @@ $ capa.exe suspicious.exe
# download and usage
Download stable releases of the standalone capa binaries [here](https://github.com/fireeye/capa/releases). You can run the standalone binaries without installation. capa is a command line tool that should be run from the terminal.
Download stable releases of the standalone capa binaries [here](https://github.com/mandiant/capa/releases). You can run the standalone binaries without installation. capa is a command line tool that should be run from the terminal.
To use capa as a library or integrate with another tool, see [doc/installation.md](https://github.com/fireeye/capa/blob/master/doc/installation.md) for further setup instructions.
To use capa as a library or integrate with another tool, see [doc/installation.md](https://github.com/mandiant/capa/blob/master/doc/installation.md) for further setup instructions.
For more information about how to use capa, see [doc/usage.md](https://github.com/fireeye/capa/blob/master/doc/usage.md).
For more information about how to use capa, see [doc/usage.md](https://github.com/mandiant/capa/blob/master/doc/usage.md).
# example
@@ -91,27 +92,36 @@ $ capa.exe suspicious.exe -vv
...
execute shell command and capture output
namespace c2/shell
author matthew.williams@fireeye.com
author matthew.williams@mandiant.com
scope function
att&ck Execution::Command and Scripting Interpreter::Windows Command Shell [T1059.003]
references https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
examples Practical Malware Analysis Lab 14-02.exe_:0x4011C0
function @ 0x10003A13
function @ 0x4011C0
and:
match: create a process with modified I/O handles and window @ 0x10003A13
match: create a process with modified I/O handles and window @ 0x4011C0
and:
number: 257 = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW @ 0x4012B8
or:
api: kernel32.CreateProcess @ 0x10003D6D
number: 0x101 @ 0x10003B03
or:
number: 0x44 @ 0x10003ADC
optional:
api: kernel32.GetStartupInfo @ 0x10003AE4
match: create pipe @ 0x10003A13
number: 68 = StartupInfo.cb (size) @ 0x401282
or: = API functions that accept a pointer to a STARTUPINFO structure
api: kernel32.CreateProcess @ 0x401343
match: create pipe @ 0x4011C0
or:
api: kernel32.CreatePipe @ 0x10003ACB
api: kernel32.CreatePipe @ 0x40126F, 0x401280
optional:
match: create thread @ 0x40136A, 0x4013BA
or:
and:
os: windows
or:
api: kernel32.CreateThread @ 0x4013D7
or:
and:
os: windows
or:
api: kernel32.CreateThread @ 0x401395
or:
string: cmd.exe /c @ 0x10003AED
string: "cmd.exe" @ 0x4012FD
...
```
@@ -127,39 +137,49 @@ rule:
meta:
name: hash data with CRC32
namespace: data-manipulation/checksum/crc32
author: moritz.raabe@fireeye.com
authors:
- moritz.raabe@mandiant.com
scope: function
mbc:
- Data::Checksum::CRC32 [C0032.001]
examples:
- 2D3EDC218A90F03089CC01715A9F047F:0x403CBD
- 7D28CB106CB54876B2A5C111724A07CD:0x402350 # RtlComputeCrc32
- 7EFF498DE13CC734262F87E6B3EF38AB:0x100084A6
features:
- or:
- and:
- mnemonic: shr
- number: 0xEDB88320
- or:
- number: 0xEDB88320
- bytes: 00 00 00 00 96 30 07 77 2C 61 0E EE BA 51 09 99 19 C4 6D 07 8F F4 6A 70 35 A5 63 E9 A3 95 64 9E = crc32_tab
- number: 8
- characteristic: nzxor
- and:
- number: 0x8320
- number: 0xEDB8
- characteristic: nzxor
- api: RtlComputeCrc32
```
The [github.com/fireeye/capa-rules](https://github.com/fireeye/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
Please learn to write rules and contribute new entries as you find interesting techniques in malware.
If you use IDA Pro, then you can use the [capa explorer](https://github.com/fireeye/capa/tree/master/capa/ida/plugin) plugin.
If you use IDA Pro, then you can use the [capa explorer](https://github.com/mandiant/capa/tree/master/capa/ida/plugin) plugin.
capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted directly from your IDA Pro database.
![capa + IDA Pro integration](https://github.com/fireeye/capa/blob/master/doc/img/explorer_expanded.png)
![capa + IDA Pro integration](https://github.com/mandiant/capa/blob/master/doc/img/explorer_expanded.png)
# further information
## capa
- [Installation](https://github.com/fireeye/capa/blob/master/doc/installation.md)
- [Usage](https://github.com/fireeye/capa/blob/master/doc/usage.md)
- [Limitations](https://github.com/fireeye/capa/blob/master/doc/limitations.md)
- [Contributing Guide](https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md)
- [Installation](https://github.com/mandiant/capa/blob/master/doc/installation.md)
- [Usage](https://github.com/mandiant/capa/blob/master/doc/usage.md)
- [Limitations](https://github.com/mandiant/capa/blob/master/doc/limitations.md)
- [Contributing Guide](https://github.com/mandiant/capa/blob/master/.github/CONTRIBUTING.md)
## capa rules
- [capa-rules repository](https://github.com/fireeye/capa-rules)
- [capa-rules rule format](https://github.com/fireeye/capa-rules/blob/master/doc/format.md)
- [capa-rules repository](https://github.com/mandiant/capa-rules)
- [capa-rules rule format](https://github.com/mandiant/capa-rules/blob/master/doc/format.md)
## capa testfiles
The [capa-testfiles repository](https://github.com/fireeye/capa-testfiles) contains the data we use to test capa's code and rules
The [capa-testfiles repository](https://github.com/mandiant/capa-testfiles) contains the data we use to test capa's code and rules

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -8,11 +8,17 @@
import copy
import collections
from typing import Set, Dict, List, Tuple, Union, Mapping, Iterable
from typing import TYPE_CHECKING, Set, Dict, List, Tuple, Mapping, Iterable
import capa.rules
import capa.perf
import capa.features.common
from capa.features.common import Feature
from capa.features.common import Result, Feature
from capa.features.address import Address
if TYPE_CHECKING:
# circular import, otherwise
import capa.rules
# a collection of features and the locations at which they are found.
#
@@ -21,7 +27,7 @@ from capa.features.common import Feature
# to collect the locations of a feature, do: `features[Number(0x10)]`
#
# aliased here so that the type can be documented and xref'd.
FeatureSet = Dict[Feature, Set[int]]
FeatureSet = Dict[Feature, Set[Address]]
class Statement:
@@ -45,15 +51,12 @@ class Statement:
def __repr__(self):
return str(self)
def evaluate(self, features: FeatureSet) -> "Result":
def evaluate(self, features: FeatureSet, short_circuit=True) -> Result:
"""
classes that inherit `Statement` must implement `evaluate`
args:
ctx (defaultdict[Feature, set[VA]])
returns:
Result
short_circuit (bool): if true, then statements like and/or/some may short circuit.
"""
raise NotImplementedError()
@@ -77,70 +80,70 @@ class Statement:
children[i] = new
class Result:
"""
represents the results of an evaluation of statements against features.
instances of this class should behave like a bool,
e.g. `assert Result(True, ...) == True`
instances track additional metadata about evaluation results.
they contain references to the statement node (e.g. an And statement),
as well as the children Result instances.
we need this so that we can render the tree of expressions and their results.
"""
def __init__(self, success: bool, statement: Union[Statement, Feature], children: List["Result"], locations=None):
"""
args:
success (bool)
statement (capa.engine.Statement or capa.features.Feature)
children (list[Result])
locations (iterable[VA])
"""
super(Result, self).__init__()
self.success = success
self.statement = statement
self.children = children
self.locations = locations if locations is not None else ()
def __eq__(self, other):
if isinstance(other, bool):
return self.success == other
return False
def __bool__(self):
return self.success
def __nonzero__(self):
return self.success
class And(Statement):
"""match if all of the children evaluate to True."""
"""
match if all of the children evaluate to True.
the order of evaluation is dictated by the property
`And.children` (type: List[Statement|Feature]).
a query optimizer may safely manipulate the order of these children.
"""
def __init__(self, children, description=None):
super(And, self).__init__(description=description)
self.children = children
def evaluate(self, ctx):
results = [child.evaluate(ctx) for child in self.children]
success = all(results)
return Result(success, self, results)
def evaluate(self, ctx, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.and"] += 1
if short_circuit:
results = []
for child in self.children:
result = child.evaluate(ctx, short_circuit=short_circuit)
results.append(result)
if not result:
# short circuit
return Result(False, self, results)
return Result(True, self, results)
else:
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
success = all(results)
return Result(success, self, results)
class Or(Statement):
"""match if any of the children evaluate to True."""
"""
match if any of the children evaluate to True.
the order of evaluation is dictated by the property
`Or.children` (type: List[Statement|Feature]).
a query optimizer may safely manipulate the order of these children.
"""
def __init__(self, children, description=None):
super(Or, self).__init__(description=description)
self.children = children
def evaluate(self, ctx):
results = [child.evaluate(ctx) for child in self.children]
success = any(results)
return Result(success, self, results)
def evaluate(self, ctx, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.or"] += 1
if short_circuit:
results = []
for child in self.children:
result = child.evaluate(ctx, short_circuit=short_circuit)
results.append(result)
if result:
# short circuit as soon as we hit one match
return Result(True, self, results)
return Result(False, self, results)
else:
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
success = any(results)
return Result(success, self, results)
class Not(Statement):
@@ -150,28 +153,55 @@ class Not(Statement):
super(Not, self).__init__(description=description)
self.child = child
def evaluate(self, ctx):
results = [self.child.evaluate(ctx)]
def evaluate(self, ctx, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.not"] += 1
results = [self.child.evaluate(ctx, short_circuit=short_circuit)]
success = not results[0]
return Result(success, self, results)
class Some(Statement):
"""match if at least N of the children evaluate to True."""
"""
match if at least N of the children evaluate to True.
the order of evaluation is dictated by the property
`Some.children` (type: List[Statement|Feature]).
a query optimizer may safely manipulate the order of these children.
"""
def __init__(self, count, children, description=None):
super(Some, self).__init__(description=description)
self.count = count
self.children = children
def evaluate(self, ctx):
results = [child.evaluate(ctx) for child in self.children]
# note that here we cast the child result as a bool
# because we've overridden `__bool__` above.
#
# we can't use `if child is True` because the instance is not True.
success = sum([1 for child in results if bool(child) is True]) >= self.count
return Result(success, self, results)
def evaluate(self, ctx, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.some"] += 1
if short_circuit:
results = []
satisfied_children_count = 0
for child in self.children:
result = child.evaluate(ctx, short_circuit=short_circuit)
results.append(result)
if result:
satisfied_children_count += 1
if satisfied_children_count >= self.count:
# short circuit as soon as we hit the threshold
return Result(True, self, results)
return Result(False, self, results)
else:
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
# note that here we cast the child result as a bool
# because we've overridden `__bool__` above.
#
# we can't use `if child is True` because the instance is not True.
success = sum([1 for child in results if bool(child) is True]) >= self.count
return Result(success, self, results)
class Range(Statement):
@@ -183,7 +213,10 @@ class Range(Statement):
self.min = min if min is not None else 0
self.max = max if max is not None else (1 << 64 - 1)
def evaluate(self, ctx):
def evaluate(self, ctx, **kwargs):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.range"] += 1
count = len(ctx.get(self.child, []))
if self.min == 0 and count == 0:
return Result(True, self, [])
@@ -203,12 +236,12 @@ class Subscope(Statement):
the engine should preprocess rules to extract subscope statements into their own rules.
"""
def __init__(self, scope, child):
super(Subscope, self).__init__()
def __init__(self, scope, child, description=None):
super(Subscope, self).__init__(description=description)
self.scope = scope
self.child = child
def evaluate(self, ctx):
def evaluate(self, ctx, **kwargs):
raise ValueError("cannot evaluate a subscope directly!")
@@ -225,10 +258,10 @@ class Subscope(Statement):
# inspect(match_details)
#
# aliased here so that the type can be documented and xref'd.
MatchResults = Mapping[str, List[Tuple[int, Result]]]
MatchResults = Mapping[str, List[Tuple[Address, Result]]]
def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations: Iterable[int]):
def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations: Iterable[Address]):
"""
record into the given featureset that the given rule matched at the given locations.
@@ -245,17 +278,22 @@ def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations:
namespace, _, _ = namespace.rpartition("/")
def match(rules: List["capa.rules.Rule"], features: FeatureSet, va: int) -> Tuple[FeatureSet, MatchResults]:
def match(rules: List["capa.rules.Rule"], features: FeatureSet, addr: Address) -> Tuple[FeatureSet, MatchResults]:
"""
Args:
rules (List[capa.rules.Rule]): these must already be ordered topologically by dependency.
features (Mapping[capa.features.Feature, int]):
va (int): location of the features
match the given rules against the given features,
returning an updated set of features and the matches.
Returns:
Tuple[FeatureSet, MatchResults]: two-tuple with entries:
- set of features used for matching (which may be a superset of the given `features` argument, due to rule match features), and
- mapping from rule name to [(location of match, result object)]
the updated features are just like the input,
but extended to include the match features (e.g. names of rules that matched).
the given feature set is not modified; an updated copy is returned.
the given list of rules must be ordered topologically by dependency,
or else `match` statements will not be handled correctly.
this routine should be fairly optimized, but is not guaranteed to be the fastest matcher possible.
it has a particularly convenient signature: (rules, features) -> matches
other strategies can be imagined that match differently; implement these elsewhere.
specifically, this routine does "top down" matching of the given rules against the feature set.
"""
results = collections.defaultdict(list) # type: MatchResults
@@ -266,12 +304,22 @@ def match(rules: List["capa.rules.Rule"], features: FeatureSet, va: int) -> Tupl
features = collections.defaultdict(set, copy.copy(features))
for rule in rules:
res = rule.evaluate(features)
res = rule.evaluate(features, short_circuit=True)
if res:
results[rule.name].append((va, res))
# we first matched the rule with short circuiting enabled.
# this is much faster than without short circuiting.
# however, we want to collect all results thoroughly,
# so once we've found a match quickly,
# go back and capture results without short circuiting.
res = rule.evaluate(features, short_circuit=False)
# sanity check
assert bool(res) is True
results[rule.name].append((addr, res))
# we need to update the current `features`
# because subsequent iterations of this loop may use newly added features,
# such as rule or namespace matches.
index_rule_matches(features, rule, [va])
index_rule_matches(features, rule, [addr])
return (features, results)

14
capa/exceptions.py Normal file
View File

@@ -0,0 +1,14 @@
class UnsupportedRuntimeError(RuntimeError):
pass
class UnsupportedFormatError(ValueError):
pass
class UnsupportedArchError(ValueError):
pass
class UnsupportedOSError(ValueError):
pass

117
capa/features/address.py Normal file
View File

@@ -0,0 +1,117 @@
import abc
from dncil.clr.token import Token
class Address(abc.ABC):
@abc.abstractmethod
def __eq__(self, other):
...
@abc.abstractmethod
def __lt__(self, other):
# implement < so that addresses can be sorted from low to high
...
@abc.abstractmethod
def __hash__(self):
# implement hash so that addresses can be used in sets and dicts
...
@abc.abstractmethod
def __repr__(self):
# implement repr to help during debugging
...
class AbsoluteVirtualAddress(int, Address):
"""an absolute memory address"""
def __new__(cls, v):
assert v >= 0
return int.__new__(cls, v)
def __repr__(self):
return f"absolute(0x{self:x})"
class RelativeVirtualAddress(int, Address):
"""a memory address relative to a base address"""
def __repr__(self):
return f"relative(0x{self:x})"
class FileOffsetAddress(int, Address):
"""an address relative to the start of a file"""
def __new__(cls, v):
assert v >= 0
return int.__new__(cls, v)
def __repr__(self):
return f"file(0x{self:x})"
class DNTokenAddress(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 __repr__(self):
return f"token(0x{self.token.value:x})"
def __index__(self):
# returns the object converted to an integer
return self.token.value
class DNTokenOffsetAddress(Address):
"""an offset into an object specified by a .NET token"""
def __init__(self, token: Token, 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)
def __lt__(self, other):
return (self.token.value, self.offset) < (other.token.value, other.offset)
def __hash__(self):
return hash((self.token.value, self.offset))
def __repr__(self):
return f"token(0x{self.token.value:x})+(0x{self.offset:x})"
def __index__(self):
return self.token.value + self.offset
class _NoAddress(Address):
def __eq__(self, other):
return True
def __lt__(self, other):
return False
def __hash__(self):
return hash(0)
def __repr__(self):
return "no address"
NO_ADDRESS = _NoAddress()

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -10,18 +10,11 @@ from capa.features.common import Feature
class BasicBlock(Feature):
def __init__(self):
super(BasicBlock, self).__init__(None)
def __init__(self, description=None):
super(BasicBlock, self).__init__(None, description=description)
def __str__(self):
return "basic block"
def get_value_str(self):
return ""
def freeze_serialize(self):
return (self.__class__.__name__, [])
@classmethod
def freeze_deserialize(cls, args):
return cls()

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -7,14 +7,20 @@
# See the License for the specific language governing permissions and limitations under the License.
import re
import abc
import codecs
import logging
import collections
from typing import Set, Dict, Union
from typing import TYPE_CHECKING, Set, Dict, List, Union, Optional, Sequence
import capa.engine
if TYPE_CHECKING:
# circular import, otherwise
import capa.engine
import capa.perf
import capa.features
import capa.features.extractors.elf
from capa.features.address import Address
logger = logging.getLogger(__name__)
MAX_BYTES_FEATURE_SIZE = 0x100
@@ -46,34 +52,72 @@ def escape_string(s: str) -> str:
return s
class Feature:
def __init__(self, value: Union[str, int, bytes], bitness=None, description=None):
class Result:
"""
represents the results of an evaluation of statements against features.
instances of this class should behave like a bool,
e.g. `assert Result(True, ...) == True`
instances track additional metadata about evaluation results.
they contain references to the statement node (e.g. an And statement),
as well as the children Result instances.
we need this so that we can render the tree of expressions and their results.
"""
def __init__(
self,
success: bool,
statement: Union["capa.engine.Statement", "Feature"],
children: List["Result"],
locations: Optional[Set[Address]] = None,
):
super(Result, self).__init__()
self.success = success
self.statement = statement
self.children = children
self.locations = locations if locations is not None else set()
def __eq__(self, other):
if isinstance(other, bool):
return self.success == other
return False
def __bool__(self):
return self.success
def __nonzero__(self):
return self.success
class Feature(abc.ABC):
def __init__(self, value: Union[str, int, float, bytes], description=None):
"""
Args:
value (any): the value of the feature, such as the number or string.
bitness (str): one of the VALID_BITNESS values, or None.
When None, then the feature applies to any bitness.
Modifies the feature name from `feature` to `feature/bitness`, like `offset/x32`.
description (str): a human-readable description that explains the feature value.
"""
super(Feature, self).__init__()
if bitness is not None:
if bitness not in VALID_BITNESS:
raise ValueError("bitness '%s' must be one of %s" % (bitness, VALID_BITNESS))
self.name = self.__class__.__name__.lower() + "/" + bitness
else:
self.name = self.__class__.__name__.lower()
self.name = self.__class__.__name__.lower()
self.value = value
self.bitness = bitness
self.description = description
def __hash__(self):
return hash((self.name, self.value, self.bitness))
return hash((self.name, self.value))
def __eq__(self, other):
return self.name == other.name and self.value == other.value and self.bitness == other.bitness
return self.name == other.name and self.value == other.value
def __lt__(self, other):
# TODO: this is a huge hack!
import capa.features.freeze.features
return (
capa.features.freeze.features.feature_from_capa(self).json()
< capa.features.freeze.features.feature_from_capa(other).json()
)
def get_value_str(self) -> str:
"""
@@ -96,26 +140,10 @@ class Feature:
def __repr__(self):
return str(self)
def evaluate(self, ctx: Dict["Feature", Set[int]]) -> "capa.engine.Result":
return capa.engine.Result(self in ctx, self, [], locations=ctx.get(self, []))
def freeze_serialize(self):
if self.bitness is not None:
return (self.__class__.__name__, [self.value, {"bitness": self.bitness}])
else:
return (self.__class__.__name__, [self.value])
@classmethod
def freeze_deserialize(cls, args):
# as you can see below in code,
# if the last argument is a dictionary,
# consider it to be kwargs passed to the feature constructor.
if len(args) == 1:
return cls(*args)
elif isinstance(args[-1], dict):
kwargs = args[-1]
args = args[:-1]
return cls(*args, **kwargs)
def evaluate(self, ctx: Dict["Feature", Set[Address]], **kwargs) -> Result:
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature." + self.name] += 1
return Result(self in ctx, self, [], locations=ctx.get(self, set()))
class MatchedRule(Feature):
@@ -126,7 +154,6 @@ class MatchedRule(Feature):
class Characteristic(Feature):
def __init__(self, value: str, description=None):
super(Characteristic, self).__init__(value, description=description)
@@ -135,12 +162,25 @@ class String(Feature):
super(String, self).__init__(value, description=description)
class Class(Feature):
def __init__(self, value: str, description=None):
super(Class, self).__init__(value, description=description)
class Namespace(Feature):
def __init__(self, value: str, description=None):
super(Namespace, self).__init__(value, description=description)
class Substring(String):
def __init__(self, value: str, description=None):
super(Substring, self).__init__(value, description=description)
self.value = value
def evaluate(self, ctx):
def evaluate(self, ctx, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.substring"] += 1
# mapping from string value to list of locations.
# will unique the locations later on.
matches = collections.defaultdict(list)
@@ -155,6 +195,10 @@ class Substring(String):
if self.value in feature.value:
matches[feature.value].extend(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
@@ -170,9 +214,9 @@ class Substring(String):
# 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 capa.engine.Result(True, _MatchedSubstring(self, matches), [], locations=locations)
return Result(True, _MatchedSubstring(self, matches), [], locations=locations)
else:
return capa.engine.Result(False, _MatchedSubstring(self, None), [])
return Result(False, _MatchedSubstring(self, {}), [])
def __str__(self):
return "substring(%s)" % self.value
@@ -186,11 +230,11 @@ class _MatchedSubstring(Substring):
note: this type should only ever be constructed by `Substring.evaluate()`. it is not part of the public API.
"""
def __init__(self, substring: Substring, matches):
def __init__(self, substring: Substring, matches: Dict[str, Set[Address]]):
"""
args:
substring (Substring): the substring feature that matches.
match (Dict[string, List[int]]|None): mapping from matching string to its locations.
substring: the substring feature that matches.
match: mapping from matching string to its locations.
"""
super(_MatchedSubstring, self).__init__(str(substring.value), description=substring.description)
# we want this to collide with the name of `Substring` above,
@@ -225,7 +269,10 @@ class Regex(String):
"invalid regular expression: %s it should use Python syntax, try it at https://pythex.org" % value
)
def evaluate(self, ctx):
def evaluate(self, ctx, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.regex"] += 1
# mapping from string value to list of locations.
# will unique the locations later on.
matches = collections.defaultdict(list)
@@ -244,6 +291,10 @@ class Regex(String):
# so that they don't have to prefix/suffix their terms like: /.*foo.*/.
if self.re.search(feature.value):
matches[feature.value].extend(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
@@ -260,9 +311,9 @@ class Regex(String):
# 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 capa.engine.Result(True, _MatchedRegex(self, matches), [], locations=locations)
return Result(True, _MatchedRegex(self, matches), [], locations=locations)
else:
return capa.engine.Result(False, _MatchedRegex(self, None), [])
return Result(False, _MatchedRegex(self, {}), [])
def __str__(self):
return "regex(string =~ %s)" % self.value
@@ -276,11 +327,11 @@ class _MatchedRegex(Regex):
note: this type should only ever be constructed by `Regex.evaluate()`. it is not part of the public API.
"""
def __init__(self, regex: Regex, matches):
def __init__(self, regex: Regex, matches: Dict[str, Set[Address]]):
"""
args:
regex (Regex): the regex feature that matches.
match (Dict[string, List[int]]|None): mapping from matching string to its locations.
regex: the regex feature that matches.
matches: mapping from matching string to its locations.
"""
super(_MatchedRegex, self).__init__(str(regex.value), description=regex.description)
# we want this to collide with the name of `Regex` above,
@@ -308,38 +359,29 @@ class Bytes(Feature):
super(Bytes, self).__init__(value, description=description)
self.value = value
def evaluate(self, ctx):
def evaluate(self, ctx, **kwargs):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.bytes"] += 1
for feature, locations in ctx.items():
if not isinstance(feature, (Bytes,)):
continue
if feature.value.startswith(self.value):
return capa.engine.Result(True, self, [], locations=locations)
return Result(True, self, [], locations=locations)
return capa.engine.Result(False, self, [])
return Result(False, self, [])
def get_value_str(self):
return hex_string(bytes_to_str(self.value))
def freeze_serialize(self):
return (self.__class__.__name__, [bytes_to_str(self.value).upper()])
@classmethod
def freeze_deserialize(cls, args):
return cls(*[codecs.decode(x, "hex") for x in args])
# identifiers for supported bitness names that tweak a feature
# for example, offset/x32
BITNESS_X32 = "x32"
BITNESS_X64 = "x64"
VALID_BITNESS = (BITNESS_X32, BITNESS_X64)
# other candidates here: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
ARCH_I386 = "i386"
ARCH_AMD64 = "amd64"
VALID_ARCH = (ARCH_I386, ARCH_AMD64)
# dotnet
ARCH_ANY = "any"
VALID_ARCH = (ARCH_I386, ARCH_AMD64, ARCH_ANY)
class Arch(Feature):
@@ -351,8 +393,10 @@ class Arch(Feature):
OS_WINDOWS = "windows"
OS_LINUX = "linux"
OS_MACOS = "macos"
# dotnet
OS_ANY = "any"
VALID_OS = {os.value for os in capa.features.extractors.elf.OS}
VALID_OS.update({OS_WINDOWS, OS_LINUX, OS_MACOS})
VALID_OS.update({OS_WINDOWS, OS_LINUX, OS_MACOS, OS_ANY})
class OS(Feature):
@@ -363,7 +407,14 @@ class OS(Feature):
FORMAT_PE = "pe"
FORMAT_ELF = "elf"
VALID_FORMAT = (FORMAT_PE, FORMAT_ELF)
FORMAT_DOTNET = "dotnet"
VALID_FORMAT = (FORMAT_PE, FORMAT_ELF, FORMAT_DOTNET)
# internal only, not to be used in rules
FORMAT_AUTO = "auto"
FORMAT_SC32 = "sc32"
FORMAT_SC64 = "sc64"
FORMAT_FREEZE = "freeze"
FORMAT_UNKNOWN = "unknown"
class Format(Feature):

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -7,23 +7,60 @@
# See the License for the specific language governing permissions and limitations under the License.
import abc
from typing import Tuple, Iterator, SupportsInt
import dataclasses
from typing import Any, Dict, Tuple, Union, Iterator
from dataclasses import dataclass
import capa.features.address
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
# feature extractors may reference functions, BBs, insns by opaque handle values.
# the only requirement of these handles are that they support `__int__`,
# so that they can be rendered as addresses.
# you can use the `.address` property to get and render the address of the feature.
#
# these handles are only consumed by routines on
# the feature extractor from which they were created.
#
# int(FunctionHandle) -> function start address
# int(BBHandle) -> BasicBlock start address
# int(InsnHandle) -> instruction address
FunctionHandle = SupportsInt
BBHandle = SupportsInt
InsnHandle = SupportsInt
@dataclass
class FunctionHandle:
"""reference to a function recognized by a feature extractor.
Attributes:
address: the address of the function.
inner: extractor-specific data.
ctx: a context object for the extractor.
"""
address: Address
inner: Any
ctx: Dict[str, Any] = dataclasses.field(default_factory=dict)
@dataclass
class BBHandle:
"""reference to a basic block recognized by a feature extractor.
Attributes:
address: the address of the basic block start address.
inner: extractor-specific data.
"""
address: Address
inner: Any
@dataclass
class InsnHandle:
"""reference to a instruction recognized by a feature extractor.
Attributes:
address: the address of the instruction address.
inner: extractor-specific data.
"""
address: Address
inner: Any
class FeatureExtractor:
@@ -53,14 +90,18 @@ class FeatureExtractor:
super(FeatureExtractor, self).__init__()
@abc.abstractmethod
def get_base_address(self) -> int:
def get_base_address(self) -> Union[AbsoluteVirtualAddress, capa.features.address._NoAddress]:
"""
fetch the preferred load address at which the sample was analyzed.
when the base address is `NO_ADDRESS`, then the loader has no concept of a preferred load address.
such as: shellcode, .NET modules, etc.
in these scenarios, RelativeVirtualAddresses aren't used.
"""
raise NotImplementedError()
@abc.abstractmethod
def extract_global_features(self) -> Iterator[Tuple[Feature, int]]:
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
"""
extract features found at every scope ("global").
@@ -71,12 +112,12 @@ class FeatureExtractor:
print('0x%x: %s', va, feature)
yields:
Tuple[Feature, int]: feature and its location
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@abc.abstractmethod
def extract_file_features(self) -> Iterator[Tuple[Feature, int]]:
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
"""
extract file-scope features.
@@ -87,7 +128,7 @@ class FeatureExtractor:
print('0x%x: %s', va, feature)
yields:
Tuple[Feature, int]: feature and its location
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@@ -99,32 +140,33 @@ class FeatureExtractor:
"""
raise NotImplementedError()
def is_library_function(self, va: int) -> bool:
def is_library_function(self, addr: Address) -> bool:
"""
is the given address a library function?
the backend may implement its own function matching algorithm, or none at all.
we accept a VA here, rather than function object, to handle addresses identified in instructions.
we accept an address here, rather than function object,
to handle addresses identified in instructions.
this information is used to:
- filter out matches in library functions (by default), and
- recognize when to fetch symbol names for called (non-API) functions
args:
va (int): the virtual address of a function.
addr (Address): the address of a function.
returns:
bool: True if the given address is the start of a library function.
"""
return False
def get_function_name(self, va: int) -> str:
def get_function_name(self, addr: Address) -> str:
"""
fetch any recognized name for the given address.
this is only guaranteed to return a value when the given function is a recognized library function.
we accept a VA here, rather than function object, to handle addresses identified in instructions.
args:
va (int): the virtual address of a function.
addr (Address): the address of a function.
returns:
str: the function name
@@ -132,10 +174,10 @@ class FeatureExtractor:
raises:
KeyError: when the given function does not have a name.
"""
raise KeyError(va)
raise KeyError(addr)
@abc.abstractmethod
def extract_function_features(self, f: FunctionHandle) -> Iterator[Tuple[Feature, int]]:
def extract_function_features(self, f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract function-scope features.
the arguments are opaque values previously provided by `.get_functions()`, etc.
@@ -144,14 +186,14 @@ class FeatureExtractor:
extractor = VivisectFeatureExtractor(vw, path)
for function in extractor.get_functions():
for feature, va in extractor.extract_function_features(function):
print('0x%x: %s', va, feature)
for feature, address in extractor.extract_function_features(function):
print('0x%x: %s', address, feature)
args:
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
yields:
Tuple[Feature, int]: feature and its location
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@@ -164,7 +206,7 @@ class FeatureExtractor:
raise NotImplementedError()
@abc.abstractmethod
def extract_basic_block_features(self, f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, int]]:
def extract_basic_block_features(self, f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract basic block-scope features.
the arguments are opaque values previously provided by `.get_functions()`, etc.
@@ -174,15 +216,15 @@ class FeatureExtractor:
extractor = VivisectFeatureExtractor(vw, path)
for function in extractor.get_functions():
for bb in extractor.get_basic_blocks(function):
for feature, va in extractor.extract_basic_block_features(function, bb):
print('0x%x: %s', va, feature)
for feature, address in extractor.extract_basic_block_features(function, bb):
print('0x%x: %s', address, feature)
args:
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
bb [BBHandle]: an opaque value previously fetched from `.get_basic_blocks()`.
yields:
Tuple[Feature, int]: feature and its location
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@@ -195,7 +237,9 @@ class FeatureExtractor:
raise NotImplementedError()
@abc.abstractmethod
def extract_insn_features(self, f: FunctionHandle, bb: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, int]]:
def extract_insn_features(
self, f: FunctionHandle, bb: BBHandle, insn: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
extract instruction-scope features.
the arguments are opaque values previously provided by `.get_functions()`, etc.
@@ -206,8 +250,8 @@ class FeatureExtractor:
for function in extractor.get_functions():
for bb in extractor.get_basic_blocks(function):
for insn in extractor.get_instructions(function, bb):
for feature, va in extractor.extract_insn_features(function, bb, insn):
print('0x%x: %s', va, feature)
for feature, address in extractor.extract_insn_features(function, bb, insn):
print('0x%x: %s', address, feature)
args:
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
@@ -215,123 +259,6 @@ class FeatureExtractor:
insn [InsnHandle]: an opaque value previously fetched from `.get_instructions()`.
yields:
Tuple[Feature, int]: feature and its location
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
class NullFeatureExtractor(FeatureExtractor):
"""
An extractor that extracts some user-provided features.
The structure of the single parameter is demonstrated in the example below.
This is useful for testing, as we can provide expected values and see if matching works.
Also, this is how we represent features deserialized from a freeze file.
example::
extractor = NullFeatureExtractor({
'base address: 0x401000,
'global features': [
(0x0, capa.features.Arch('i386')),
(0x0, capa.features.OS('linux')),
],
'file features': [
(0x402345, capa.features.Characteristic('embedded pe')),
],
'functions': {
0x401000: {
'features': [
(0x401000, capa.features.Characteristic('nzxor')),
],
'basic blocks': {
0x401000: {
'features': [
(0x401000, capa.features.Characteristic('tight-loop')),
],
'instructions': {
0x401000: {
'features': [
(0x401000, capa.features.Characteristic('nzxor')),
],
},
0x401002: ...
}
},
0x401005: ...
}
},
0x40200: ...
}
)
"""
def __init__(self, features):
super(NullFeatureExtractor, self).__init__()
self.features = features
def get_base_address(self):
return self.features["base address"]
def extract_global_features(self):
for p in self.features.get("global features", []):
va, feature = p
yield feature, va
def extract_file_features(self):
for p in self.features.get("file features", []):
va, feature = p
yield feature, va
def get_functions(self):
for va in sorted(self.features["functions"].keys()):
yield va
def extract_function_features(self, f):
for p in self.features.get("functions", {}).get(f, {}).get("features", []): # noqa: E127 line over-indented
va, feature = p
yield feature, va
def get_basic_blocks(self, f):
for va in sorted(
self.features.get("functions", {}) # noqa: E127 line over-indented
.get(f, {})
.get("basic blocks", {})
.keys()
):
yield va
def extract_basic_block_features(self, f, bb):
for p in (
self.features.get("functions", {}) # noqa: E127 line over-indented
.get(f, {})
.get("basic blocks", {})
.get(bb, {})
.get("features", [])
):
va, feature = p
yield feature, va
def get_instructions(self, f, bb):
for va in sorted(
self.features.get("functions", {}) # noqa: E127 line over-indented
.get(f, {})
.get("basic blocks", {})
.get(bb, {})
.get("instructions", {})
.keys()
):
yield va
def extract_insn_features(self, f, bb, insn):
for p in (
self.features.get("functions", {}) # noqa: E127 line over-indented
.get(f, {})
.get("basic blocks", {})
.get(bb, {})
.get("instructions", {})
.get(insn, {})
.get("features", [])
):
va, feature = p
yield feature, va

View File

@@ -2,33 +2,38 @@ import io
import logging
import binascii
import contextlib
from typing import Tuple, Iterator
import pefile
import capa.features
import capa.features.extractors.elf
import capa.features.extractors.pefile
from capa.features.common import OS, FORMAT_PE, FORMAT_ELF, OS_WINDOWS, Arch, Format, String
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
logger = logging.getLogger(__name__)
def extract_file_strings(buf, **kwargs):
def extract_file_strings(buf, **kwargs) -> Iterator[Tuple[String, Address]]:
"""
extract ASCII and UTF-16 LE strings from file
"""
for s in capa.features.extractors.strings.extract_ascii_strings(buf):
yield String(s.s), s.offset
yield String(s.s), FileOffsetAddress(s.offset)
for s in capa.features.extractors.strings.extract_unicode_strings(buf):
yield String(s.s), s.offset
yield String(s.s), FileOffsetAddress(s.offset)
def extract_format(buf):
def extract_format(buf) -> Iterator[Tuple[Feature, Address]]:
if buf.startswith(b"MZ"):
yield Format(FORMAT_PE), 0x0
yield Format(FORMAT_PE), NO_ADDRESS
elif buf.startswith(b"\x7fELF"):
yield Format(FORMAT_ELF), 0x0
yield Format(FORMAT_ELF), NO_ADDRESS
elif is_freeze(buf):
yield Format(FORMAT_FREEZE), NO_ADDRESS
else:
# we likely end up here:
# 1. handling a file format (e.g. macho)
@@ -38,7 +43,7 @@ def extract_format(buf):
return
def extract_arch(buf):
def extract_arch(buf) -> Iterator[Tuple[Feature, Address]]:
if buf.startswith(b"MZ"):
yield from capa.features.extractors.pefile.extract_file_arch(pe=pefile.PE(data=buf))
@@ -50,7 +55,7 @@ def extract_arch(buf):
logger.debug("unsupported arch: %s", arch)
return
yield Arch(arch), 0x0
yield Arch(arch), NO_ADDRESS
else:
# we likely end up here:
@@ -67,9 +72,9 @@ def extract_arch(buf):
return
def extract_os(buf):
def extract_os(buf) -> Iterator[Tuple[Feature, Address]]:
if buf.startswith(b"MZ"):
yield OS(OS_WINDOWS), 0x0
yield OS(OS_WINDOWS), NO_ADDRESS
elif buf.startswith(b"\x7fELF"):
with contextlib.closing(io.BytesIO(buf)) as f:
os = capa.features.extractors.elf.detect_elf_os(f)
@@ -78,7 +83,7 @@ def extract_os(buf):
logger.debug("unsupported os: %s", os)
return
yield OS(os), 0x0
yield OS(os), NO_ADDRESS
else:
# we likely end up here:

View File

@@ -0,0 +1,71 @@
# 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
from typing import List, Tuple, Iterator
import dnfile
from dncil.clr.token import Token
import capa.features.extractors
import capa.features.extractors.dnfile.file
import capa.features.extractors.dnfile.insn
from capa.features.common import Feature
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
from capa.features.extractors.dnfile.helpers import get_dotnet_managed_method_bodies
class DnfileFeatureExtractor(FeatureExtractor):
def __init__(self, path: str):
super(DnfileFeatureExtractor, self).__init__()
self.pe: dnfile.dnPE = dnfile.dnPE(path)
# 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_os(pe=self.pe))
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe))
def get_base_address(self):
return NO_ADDRESS
def extract_global_features(self):
yield from self.global_features
def extract_file_features(self):
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})
def extract_function_features(self, f):
# TODO
yield from []
def get_basic_blocks(self, f) -> Iterator[BBHandle]:
# each dotnet method is considered 1 basic block
yield BBHandle(
address=f.address,
inner=f.inner,
)
def extract_basic_block_features(self, fh, bbh):
# we don't support basic block features
yield from []
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)),
inner=insn,
)
def extract_insn_features(self, fh, bbh, ih) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.dnfile.insn.extract_features(fh, bbh, ih)

View File

@@ -0,0 +1,63 @@
# 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
from typing import Tuple, Iterator
import dnfile
import capa.features.extractors.dotnetfile
from capa.features.file import Import, FunctionName
from capa.features.common import Class, Format, String, Feature, Namespace, Characteristic
from capa.features.address import Address
def extract_file_import_names(pe: dnfile.dnPE) -> Iterator[Tuple[Import, Address]]:
yield from capa.features.extractors.dotnetfile.extract_file_import_names(pe=pe)
def extract_file_format(pe: dnfile.dnPE) -> Iterator[Tuple[Format, Address]]:
yield from capa.features.extractors.dotnetfile.extract_file_format(pe=pe)
def extract_file_function_names(pe: dnfile.dnPE) -> Iterator[Tuple[FunctionName, Address]]:
yield from capa.features.extractors.dotnetfile.extract_file_function_names(pe=pe)
def extract_file_strings(pe: dnfile.dnPE) -> Iterator[Tuple[String, Address]]:
yield from capa.features.extractors.dotnetfile.extract_file_strings(pe=pe)
def extract_file_mixed_mode_characteristic_features(pe: dnfile.dnPE) -> Iterator[Tuple[Characteristic, Address]]:
yield from capa.features.extractors.dotnetfile.extract_file_mixed_mode_characteristic_features(pe=pe)
def extract_file_namespace_features(pe: dnfile.dnPE) -> Iterator[Tuple[Namespace, Address]]:
yield from capa.features.extractors.dotnetfile.extract_file_namespace_features(pe=pe)
def extract_file_class_features(pe: dnfile.dnPE) -> Iterator[Tuple[Class, Address]]:
yield from capa.features.extractors.dotnetfile.extract_file_class_features(pe=pe)
def extract_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
for file_handler in FILE_HANDLERS:
for (feature, address) in file_handler(pe):
yield feature, address
FILE_HANDLERS = (
extract_file_import_names,
extract_file_function_names,
extract_file_strings,
extract_file_format,
extract_file_mixed_mode_characteristic_features,
extract_file_namespace_features,
extract_file_class_features,
)

View File

@@ -0,0 +1,261 @@
# 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 Any, Tuple, Iterator, Optional
import dnfile
from dncil.cil.body import CilMethodBody
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__)
# key indexes to dotnet metadata tables
DOTNET_META_TABLES_BY_INDEX = {table.value: table.name for table in dnfile.enums.MetadataTables}
class DnfileMethodBodyReader(CilMethodBodyReaderBase):
def __init__(self, pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow):
self.pe: dnfile.dnPE = pe
self.offset: int = self.pe.get_offset_from_rva(row.Rva)
def read(self, n: int) -> bytes:
data: bytes = self.pe.get_data(self.pe.get_rva_from_offset(self.offset), n)
self.offset += n
return data
def tell(self) -> int:
return self.offset
def seek(self, offset: int) -> int:
self.offset = offset
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:
"""map generic token to string or table row"""
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)
if table is None:
# table index is valid but table is not present
return InvalidToken(token.value)
try:
return table.rows[token.rid - 1]
except IndexError:
# table index is valid but row index is not valid
return InvalidToken(token.value)
def read_dotnet_method_body(pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow) -> Optional[CilMethodBody]:
"""read dotnet method body"""
try:
return CilMethodBody(DnfileMethodBodyReader(pe, row))
except MethodBodyFormatError as e:
logger.warn("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"""
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))
return None
if user_string is None:
return None
return user_string.value
def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnMethod]:
"""get managed imports from MemberRef table
see https://www.ntcore.com/files/dotnetformat.htm
10 - MemberRef Table
Each row represents an imported method
Class (index into the TypeRef, ModuleRef, MethodDef, TypeSpec or TypeDef tables)
Name (index into String heap)
01 - TypeRef Table
Each row represents an imported class, its namespace and the assembly which contains it
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):
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)
def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnMethod]:
"""get managed method names 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)
MethodList (index into MethodDef table; it marks the first of a continguous 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)
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, row) in enumerate(pe.net.mdtables.MethodDef):
if not row.ImplFlags.miIL or any((row.Flags.mdAbstract, row.Flags.mdPinvokeImpl)):
# skip methods that do not have a method body
continue
body: Optional[CilMethodBody] = read_dotnet_method_body(pe, row)
if body is None:
continue
token: int = calculate_dotnet_token_value(dnfile.enums.MetadataTables.MethodDef.value, rid + 1)
yield token, body
def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod]:
"""get unmanaged imports from ImplMap table
see https://www.ntcore.com/files/dotnetformat.htm
28 - ImplMap Table
ImplMap table holds information about unmanaged methods that can be reached from managed code, using PInvoke dispatch
MemberForwarded (index into the Field or MethodDef table; more precisely, a MemberForwarded coded index)
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
# 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)
# like Kernel32.dll
if modulename and "." in modulename:
modulename = modulename.split(".")[0]
# like kernel32.CreateFileA
yield DnUnmanagedMethod(token, modulename, methodname)
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:
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

View File

@@ -0,0 +1,182 @@
# 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
from typing import Any, Dict, Tuple, Union, Iterator, Optional
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.address import Address
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,
)
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"]
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_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"]
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:
# 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)
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):
return
callee: Union[DnMethod, DnUnmanagedMethod, None] = get_callee(fh.ctx, insn.operand.value)
if callee is None:
return
if isinstance(callee, DnUnmanagedMethod):
# like kernel32.CreateFileA
for name in capa.features.extractors.helpers.generate_symbols(callee.modulename, callee.methodname):
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
row: Any = resolve_dotnet_token(fh.ctx["pe"], Token(ih.inner.operand.value))
if not isinstance(row, dnfile.mdtable.MemberRefRow):
return
if not isinstance(row.Class.row, (dnfile.mdtable.TypeRefRow, dnfile.mdtable.TypeDefRow)):
return
yield Class(DnClass.format_name(row.Class.row.TypeNamespace, row.Class.row.TypeName)), 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
row: Any = resolve_dotnet_token(fh.ctx["pe"], Token(ih.inner.operand.value))
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
yield Namespace(row.Class.row.TypeNamespace), 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
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():
return
if not isinstance(insn.operand, StringToken):
return
user_string: Optional[str] = read_dotnet_user_string(fh.ctx["pe"], insn.operand)
if user_string is None:
return
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):
return
token: Any = resolve_dotnet_token(fh.ctx["pe"], insn.operand)
if isinstance(token, InvalidToken):
return
if not isinstance(token, dnfile.mdtable.MethodDefRow):
return
if any((token.Flags.mdPinvokeImpl, token.ImplFlags.miUnmanaged, token.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):
assert isinstance(addr, Address)
yield feature, addr
INSTRUCTION_HANDLERS = (
extract_insn_api_features,
extract_insn_number_features,
extract_insn_string_features,
extract_insn_namespace_features,
extract_insn_class_features,
extract_unmanaged_call_characteristic_features,
)

View File

@@ -0,0 +1,116 @@
import logging
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.address import NO_ADDRESS, Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import FeatureExtractor
logger = logging.getLogger(__name__)
def extract_file_format(**kwargs) -> Iterator[Tuple[Feature, Address]]:
yield Format(FORMAT_DOTNET), NO_ADDRESS
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]]:
# to distinguish in more detail, see https://stackoverflow.com/a/23614024/10548020
# .NET 4.5 added option: any CPU, 32-bit preferred
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:
yield Arch(ARCH_AMD64), NO_ADDRESS
else:
yield Arch(ARCH_ANY), NO_ADDRESS
def extract_file_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
for file_handler in FILE_HANDLERS:
for feature, address in file_handler(pe=pe): # type: ignore
yield feature, address
FILE_HANDLERS = (
# extract_file_export_names,
# extract_file_import_names,
# extract_file_section_names,
# extract_file_strings,
# extract_file_function_names,
extract_file_format,
)
def extract_global_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
for handler in GLOBAL_HANDLERS:
for feature, addr in handler(pe=pe): # type: ignore
yield feature, addr
GLOBAL_HANDLERS = (
extract_file_os,
extract_file_arch,
)
class DnfileFeatureExtractor(FeatureExtractor):
def __init__(self, path: str):
super(DnfileFeatureExtractor, self).__init__()
self.path: str = path
self.pe: dnfile.dnPE = dnfile.dnPE(path)
def get_base_address(self) -> AbsoluteVirtualAddress:
return AbsoluteVirtualAddress(0x0)
def get_entry_point(self) -> int:
# self.pe.net.Flags.CLT_NATIVE_ENTRYPOINT
# True: native EP: Token
# False: managed EP: RVA
return self.pe.net.struct.EntryPointTokenOrRva
def extract_global_features(self):
yield from extract_global_features(self.pe)
def extract_file_features(self):
yield from extract_file_features(self.pe)
def is_dotnet_file(self) -> bool:
return bool(self.pe.net)
def is_mixed_mode(self) -> bool:
return not bool(self.pe.net.Flags.CLR_ILONLY)
def get_runtime_version(self) -> Tuple[int, int]:
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")
def get_functions(self):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
def extract_function_features(self, f):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
def get_basic_blocks(self, f):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
def extract_basic_block_features(self, f, bb):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
def get_instructions(self, f, bb):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
def extract_insn_features(self, f, bb, insn):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
def is_library_function(self, va):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
def get_function_name(self, va):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")

View File

@@ -0,0 +1,203 @@
import logging
import itertools
from typing import Tuple, Iterator
import dnfile
import pefile
from dncil.clr.token import Token
import capa.features.extractors.helpers
from capa.features.file import Import, FunctionName
from capa.features.common import (
OS,
OS_ANY,
ARCH_ANY,
ARCH_I386,
ARCH_AMD64,
FORMAT_DOTNET,
Arch,
Class,
Format,
String,
Feature,
Namespace,
Characteristic,
)
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import FeatureExtractor
from capa.features.extractors.dnfile.helpers import (
DnClass,
DnMethod,
iter_dotnet_table,
is_dotnet_mixed_mode,
get_dotnet_managed_imports,
get_dotnet_managed_methods,
calculate_dotnet_token_value,
get_dotnet_unmanaged_imports,
)
logger = logging.getLogger(__name__)
def extract_file_format(**kwargs) -> Iterator[Tuple[Format, 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))
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))
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))
def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Namespace, Address]]:
"""emit namespace features from TypeRef and TypeDef tables"""
# 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 row in iter_dotnet_table(pe, "TypeRef"):
namespaces.add(row.TypeNamespace)
# namespaces may be empty, discard
namespaces.discard("")
for namespace in namespaces:
# namespace do not have an associated token, so we yield 0x0
yield Namespace(namespace), NO_ADDRESS
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, 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))
def extract_file_os(**kwargs) -> Iterator[Tuple[OS, Address]]:
yield OS(OS_ANY), NO_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
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:
yield Arch(ARCH_AMD64), NO_ADDRESS
else:
yield Arch(ARCH_ANY), NO_ADDRESS
def extract_file_strings(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[String, Address]]:
yield from capa.features.extractors.common.extract_file_strings(pe.__data__)
def extract_file_mixed_mode_characteristic_features(
pe: dnfile.dnPE, **kwargs
) -> Iterator[Tuple[Characteristic, Address]]:
if is_dotnet_mixed_mode(pe):
yield Characteristic("mixed mode"), NO_ADDRESS
def extract_file_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
for file_handler in FILE_HANDLERS:
for feature, addr in file_handler(pe=pe): # type: ignore
yield feature, addr
FILE_HANDLERS = (
extract_file_import_names,
extract_file_function_names,
extract_file_strings,
extract_file_format,
extract_file_mixed_mode_characteristic_features,
extract_file_namespace_features,
extract_file_class_features,
)
def extract_global_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
for handler in GLOBAL_HANDLERS:
for feature, va in handler(pe=pe): # type: ignore
yield feature, va
GLOBAL_HANDLERS = (
extract_file_os,
extract_file_arch,
)
class DotnetFileFeatureExtractor(FeatureExtractor):
def __init__(self, path: str):
super(DotnetFileFeatureExtractor, self).__init__()
self.path: str = path
self.pe: dnfile.dnPE = dnfile.dnPE(path)
def get_base_address(self):
return NO_ADDRESS
def get_entry_point(self) -> int:
# self.pe.net.Flags.CLT_NATIVE_ENTRYPOINT
# True: native EP: Token
# False: managed EP: RVA
return self.pe.net.struct.EntryPointTokenOrRva
def extract_global_features(self):
yield from extract_global_features(self.pe)
def extract_file_features(self):
yield from extract_file_features(self.pe)
def is_dotnet_file(self) -> bool:
return bool(self.pe.net)
def is_mixed_mode(self) -> bool:
return is_dotnet_mixed_mode(self.pe)
def get_runtime_version(self) -> Tuple[int, int]:
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")
def get_functions(self):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def extract_function_features(self, f):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def get_basic_blocks(self, f):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def extract_basic_block_features(self, f, bb):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def get_instructions(self, f, bb):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def extract_insn_features(self, f, bb, insn):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def is_library_function(self, va):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def get_function_name(self, va):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -47,7 +47,23 @@ class OS(str, Enum):
NACL = "nacl"
def detect_elf_os(f: BinaryIO) -> str:
# via readelf: https://github.com/bminor/binutils-gdb/blob/c0e94211e1ac05049a4ce7c192c9d14d1764eb3e/binutils/readelf.c#L19635-L19658
# and here: https://github.com/bminor/binutils-gdb/blob/34c54daa337da9fadf87d2706d6a590ae1f88f4d/include/elf/common.h#L933-L939
GNU_ABI_TAG = {
0: OS.LINUX,
1: OS.HURD,
2: OS.SOLARIS,
3: OS.FREEBSD,
4: OS.NETBSD,
5: OS.SYLLABLE,
6: OS.NACL,
}
def detect_elf_os(f) -> str:
"""
f: type Union[BinaryIO, IDAIO]
"""
f.seek(0x0)
file_header = f.read(0x40)
@@ -77,11 +93,13 @@ def detect_elf_os(f: BinaryIO) -> str:
raise CorruptElfFile("not an ELF file: invalid ei_data: 0x%02x" % ei_data)
if bitness == 32:
(e_phoff,) = struct.unpack_from(endian + "I", file_header, 0x1C)
(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,) = struct.unpack_from(endian + "Q", file_header, 0x20)
(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()
@@ -139,7 +157,7 @@ def detect_elf_os(f: BinaryIO) -> str:
PT_NOTE = 0x4
(p_type,) = struct.unpack_from(endian + "I", phent, 0x0)
logger.debug("p_type: 0x%04x", p_type)
logger.debug("ph:p_type: 0x%04x", p_type)
if p_type != PT_NOTE:
continue
@@ -150,7 +168,7 @@ def detect_elf_os(f: BinaryIO) -> str:
else:
raise NotImplementedError()
logger.debug("p_offset: 0x%02x p_filesz: 0x%04x", p_offset, p_filesz)
logger.debug("ph:p_offset: 0x%02x p_filesz: 0x%04x", p_offset, p_filesz)
f.seek(p_offset)
note = f.read(p_filesz)
@@ -162,7 +180,7 @@ def detect_elf_os(f: BinaryIO) -> str:
name_offset = 0xC
desc_offset = name_offset + align(namesz, 0x4)
logger.debug("namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, descsz, type_)
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)
@@ -176,17 +194,6 @@ def detect_elf_os(f: BinaryIO) -> str:
desc = note[desc_offset : desc_offset + descsz]
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(endian + "IIII", desc, 0x0)
# via readelf: https://github.com/bminor/binutils-gdb/blob/c0e94211e1ac05049a4ce7c192c9d14d1764eb3e/binutils/readelf.c#L19635-L19658
# and here: https://github.com/bminor/binutils-gdb/blob/34c54daa337da9fadf87d2706d6a590ae1f88f4d/include/elf/common.h#L933-L939
GNU_ABI_TAG = {
0: OS.LINUX,
1: OS.HURD,
2: OS.SOLARIS,
3: OS.FREEBSD,
4: OS.NETBSD,
5: OS.SYLLABLE,
6: OS.NACL,
}
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
if abi_tag in GNU_ABI_TAG:
@@ -236,6 +243,73 @@ def detect_elf_os(f: BinaryIO) -> str:
# 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
namesz, descsz, type_ = struct.unpack_from(endian + "III", note, 0x0)
name_offset = 0xC
desc_offset = name_offset + align(namesz, 0x4)
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:
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)
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 ret.value if ret is not None else "unknown"

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -15,6 +15,7 @@ from elftools.elf.elffile import ELFFile, SymbolTableSection
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
@@ -39,15 +40,15 @@ def extract_file_import_names(elf, **kwargs):
if symbol.name and symbol.entry.st_info.type == "STT_FUNC":
# TODO symbol address
# TODO symbol version info?
yield Import(symbol.name), 0x0
yield Import(symbol.name), FileOffsetAddress(0x0)
def extract_file_section_names(elf, **kwargs):
for section in elf.iter_sections():
if section.name:
yield Section(section.name), section.header.sh_addr
yield Section(section.name), AbsoluteVirtualAddress(section.header.sh_addr)
elif section.is_null():
yield Section("NULL"), section.header.sh_addr
yield Section("NULL"), AbsoluteVirtualAddress(section.header.sh_addr)
def extract_file_strings(buf, **kwargs):
@@ -58,31 +59,31 @@ def extract_file_os(elf, buf, **kwargs):
# our current approach does not always get an OS value, e.g. for packed samples
# for file limitation purposes, we're more lax here
try:
os = next(capa.features.extractors.common.extract_os(buf))
yield os
os_tuple = next(capa.features.extractors.common.extract_os(buf))
yield os_tuple
except StopIteration:
yield OS("unknown"), 0x0
yield OS("unknown"), NO_ADDRESS
def extract_file_format(**kwargs):
yield Format(FORMAT_ELF), 0x0
yield Format(FORMAT_ELF), NO_ADDRESS
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), 0x0
yield Arch(ElfArch.I386), NO_ADDRESS
elif arch == "x64":
yield Arch(ElfArch.AMD64), 0x0
yield Arch(ElfArch.AMD64), NO_ADDRESS
else:
logger.warning("unsupported architecture: %s", arch)
def extract_file_features(elf: ELFFile, buf: bytes) -> Iterator[Tuple[Feature, int]]:
for file_handler in FILE_HANDLERS:
for feature, va in file_handler(elf=elf, buf=buf): # type: ignore
yield feature, va
for feature, addr in file_handler(elf=elf, buf=buf): # type: ignore
yield feature, addr
FILE_HANDLERS = (
@@ -97,8 +98,8 @@ FILE_HANDLERS = (
def extract_global_features(elf: ELFFile, buf: bytes) -> Iterator[Tuple[Feature, int]]:
for global_handler in GLOBAL_HANDLERS:
for feature, va in global_handler(elf=elf, buf=buf): # type: ignore
yield feature, va
for feature, addr in global_handler(elf=elf, buf=buf): # type: ignore
yield feature, addr
GLOBAL_HANDLERS = (
@@ -118,21 +119,21 @@ class ElfFeatureExtractor(FeatureExtractor):
# virtual address of the first segment with type LOAD
for segment in self.elf.iter_segments():
if segment.header.p_type == "PT_LOAD":
return segment.header.p_vaddr
return AbsoluteVirtualAddress(segment.header.p_vaddr)
def extract_global_features(self):
with open(self.path, "rb") as f:
buf = f.read()
for feature, va in extract_global_features(self.elf, buf):
yield feature, va
for feature, addr in extract_global_features(self.elf, buf):
yield feature, addr
def extract_file_features(self):
with open(self.path, "rb") as f:
buf = f.read()
for feature, va in extract_file_features(self.elf, buf):
yield feature, va
for feature, addr in extract_file_features(self.elf, buf):
yield feature, addr
def get_functions(self):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -29,8 +29,7 @@ def is_aw_function(symbol: str) -> bool:
if symbol[-1] not in ("A", "W"):
return False
# second to last character should be lowercase letter
return "a" <= symbol[-2] <= "z" or "0" <= symbol[-2] <= "9"
return True
def is_ordinal(symbol: str) -> bool:
@@ -52,6 +51,9 @@ def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
- CreateFileA
- CreateFile
"""
# normalize dll name
dll = dll.lower()
# kernel32.CreateFileA
yield "%s.%s" % (dll, symbol)

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -8,22 +8,21 @@
import string
import struct
from typing import Tuple, Iterator
import idaapi
import capa.features.extractors.ida.helpers
from capa.features.common import Characteristic
from capa.features.common import Feature, Characteristic
from capa.features.address import Address
from capa.features.basicblock import BasicBlock
from capa.features.extractors.ida import helpers
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
def get_printable_len(op):
"""Return string length if all operand bytes are ascii or utf16-le printable
args:
op (IDA op_t)
"""
def get_printable_len(op: idaapi.op_t) -> int:
"""Return string length if all operand bytes are ascii or utf16-le printable"""
op_val = capa.features.extractors.ida.helpers.mask_op_val(op)
if op.dtype == idaapi.dt_byte:
@@ -37,12 +36,12 @@ def get_printable_len(op):
else:
raise ValueError("Unhandled operand data type 0x%x." % op.dtype)
def is_printable_ascii(chars):
return all(c < 127 and chr(c) in string.printable for c in chars)
def is_printable_ascii(chars_: bytes):
return all(c < 127 and chr(c) in string.printable for c in chars_)
def is_printable_utf16le(chars):
if all(c == 0x00 for c in chars[1::2]):
return is_printable_ascii(chars[::2])
def is_printable_utf16le(chars_: bytes):
if all(c == 0x00 for c in chars_[1::2]):
return is_printable_ascii(chars_[::2])
if is_printable_ascii(chars):
return idaapi.get_dtype_size(op.dtype)
@@ -53,12 +52,8 @@ def get_printable_len(op):
return 0
def is_mov_imm_to_stack(insn):
"""verify instruction moves immediate onto stack
args:
insn (IDA insn_t)
"""
def is_mov_imm_to_stack(insn: idaapi.insn_t) -> bool:
"""verify instruction moves immediate onto stack"""
if insn.Op2.type != idaapi.o_imm:
return False
@@ -71,14 +66,10 @@ def is_mov_imm_to_stack(insn):
return True
def bb_contains_stackstring(f, bb):
def bb_contains_stackstring(f: idaapi.func_t, bb: idaapi.BasicBlock) -> bool:
"""check basic block for stackstring indicators
true if basic block contains enough moves of constant bytes to the stack
args:
f (IDA func_t)
bb (IDA BasicBlock)
"""
count = 0
for insn in capa.features.extractors.ida.helpers.get_instructions_in_range(bb.start_ea, bb.end_ea):
@@ -89,39 +80,24 @@ def bb_contains_stackstring(f, bb):
return False
def extract_bb_stackstring(f, bb):
"""extract stackstring indicators from basic block
args:
f (IDA func_t)
bb (IDA BasicBlock)
"""
if bb_contains_stackstring(f, bb):
yield Characteristic("stack string"), bb.start_ea
def extract_bb_stackstring(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract stackstring indicators from basic block"""
if bb_contains_stackstring(fh.inner, bbh.inner):
yield Characteristic("stack string"), bbh.address
def extract_bb_tight_loop(f, bb):
"""extract tight loop indicators from a basic block
args:
f (IDA func_t)
bb (IDA BasicBlock)
"""
if capa.features.extractors.ida.helpers.is_basic_block_tight_loop(bb):
yield Characteristic("tight loop"), bb.start_ea
def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract tight loop indicators from a basic block"""
if capa.features.extractors.ida.helpers.is_basic_block_tight_loop(bbh.inner):
yield Characteristic("tight loop"), bbh.address
def extract_features(f, bb):
"""extract basic block features
args:
f (IDA func_t)
bb (IDA BasicBlock)
"""
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract basic block features"""
for bb_handler in BASIC_BLOCK_HANDLERS:
for (feature, ea) in bb_handler(f, bb):
yield feature, ea
yield BasicBlock(), bb.start_ea
for (feature, addr) in bb_handler(fh, bbh):
yield feature, addr
yield BasicBlock(), bbh.address
BASIC_BLOCK_HANDLERS = (
@@ -132,9 +108,10 @@ BASIC_BLOCK_HANDLERS = (
def main():
features = []
for f in helpers.get_functions(skip_thunks=True, skip_libs=True):
for fhandle in helpers.get_functions(skip_thunks=True, skip_libs=True):
f: idaapi.func_t = fhandle.inner
for bb in idaapi.FlowChart(f, flags=idaapi.FC_PREDS):
features.extend(list(extract_features(f, bb)))
features.extend(list(extract_features(fhandle, bb)))
import pprint

View File

@@ -1,10 +1,12 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from typing import List, Tuple, Iterator
import idaapi
import capa.ida.helpers
@@ -14,57 +16,20 @@ import capa.features.extractors.ida.insn
import capa.features.extractors.ida.global_
import capa.features.extractors.ida.function
import capa.features.extractors.ida.basicblock
from capa.features.extractors.base_extractor import FeatureExtractor
class FunctionHandle:
"""this acts like an idaapi.func_t but with __int__()"""
def __init__(self, inner):
self._inner = inner
def __int__(self):
return self.start_ea
def __getattr__(self, name):
return getattr(self._inner, name)
class BasicBlockHandle:
"""this acts like an idaapi.BasicBlock but with __int__()"""
def __init__(self, inner):
self._inner = inner
def __int__(self):
return self.start_ea
def __getattr__(self, name):
return getattr(self._inner, name)
class InstructionHandle:
"""this acts like an idaapi.insn_t but with __int__()"""
def __init__(self, inner):
self._inner = inner
def __int__(self):
return self.ea
def __getattr__(self, name):
return getattr(self._inner, name)
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 IdaFeatureExtractor(FeatureExtractor):
def __init__(self):
super(IdaFeatureExtractor, self).__init__()
self.global_features = []
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.ida.global_.extract_os())
self.global_features.extend(capa.features.extractors.ida.global_.extract_arch())
def get_base_address(self):
return idaapi.get_imagebase()
return AbsoluteVirtualAddress(idaapi.get_imagebase())
def extract_global_features(self):
yield from self.global_features
@@ -72,41 +37,34 @@ class IdaFeatureExtractor(FeatureExtractor):
def extract_file_features(self):
yield from capa.features.extractors.ida.file.extract_features()
def get_functions(self):
def get_functions(self) -> Iterator[FunctionHandle]:
import capa.features.extractors.ida.helpers as ida_helpers
# data structure shared across functions yielded here.
# useful for caching analysis relevant across a single workspace.
ctx = {}
# ignore library functions and thunk functions as identified by IDA
for f in ida_helpers.get_functions(skip_thunks=True, skip_libs=True):
setattr(f, "ctx", ctx)
yield FunctionHandle(f)
yield from ida_helpers.get_functions(skip_thunks=True, skip_libs=True)
@staticmethod
def get_function(ea):
def get_function(ea: int) -> FunctionHandle:
f = idaapi.get_func(ea)
setattr(f, "ctx", {})
return FunctionHandle(f)
return FunctionHandle(address=AbsoluteVirtualAddress(f.start_ea), inner=f)
def extract_function_features(self, f):
yield from capa.features.extractors.ida.function.extract_features(f)
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.ida.function.extract_features(fh)
def get_basic_blocks(self, f):
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
import capa.features.extractors.ida.helpers as ida_helpers
for bb in ida_helpers.get_function_blocks(f):
yield BasicBlockHandle(bb)
for bb in ida_helpers.get_function_blocks(fh.inner):
yield BBHandle(address=AbsoluteVirtualAddress(bb.start_ea), inner=bb)
def extract_basic_block_features(self, f, bb):
yield from capa.features.extractors.ida.basicblock.extract_features(f, bb)
def extract_basic_block_features(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.ida.basicblock.extract_features(fh, bbh)
def get_instructions(self, f, bb):
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
import capa.features.extractors.ida.helpers as ida_helpers
for insn in ida_helpers.get_instructions_in_range(bb.start_ea, bb.end_ea):
yield InstructionHandle(insn)
for insn in ida_helpers.get_instructions_in_range(bbh.inner.start_ea, bbh.inner.end_ea):
yield InsnHandle(address=AbsoluteVirtualAddress(insn.ea), inner=insn)
def extract_insn_features(self, f, bb, insn):
yield from capa.features.extractors.ida.insn.extract_features(f, bb, insn)
def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle):
yield from capa.features.extractors.ida.insn.extract_features(fh, bbh, ih)

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -7,27 +7,26 @@
# See the License for the specific language governing permissions and limitations under the License.
import struct
from typing import Tuple, Iterator
import idc
import idaapi
import idautils
import ida_loader
import capa.features.extractors.common
import capa.features.extractors.helpers
import capa.features.extractors.strings
import capa.features.extractors.ida.helpers
from capa.features.file import Export, Import, Section, FunctionName
from capa.features.common import OS, FORMAT_PE, FORMAT_ELF, OS_WINDOWS, Format, String, Characteristic
from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress
def check_segment_for_pe(seg):
def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]:
"""check segment for embedded PE
adapted for IDA from:
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
args:
seg (IDA segment_t)
"""
seg_max = seg.end_ea
mz_xor = [
@@ -60,13 +59,13 @@ def check_segment_for_pe(seg):
continue
if idc.get_bytes(peoff, 2) == pex:
yield (off, i)
yield off, i
for nextres in capa.features.extractors.ida.helpers.find_byte_sequence(off + 1, seg.end_ea, mzx):
todo.append((nextres, mzx, pex, i))
def extract_file_embedded_pe():
def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]:
"""extract embedded PE features
IDA must load resource sections for this to be complete
@@ -75,16 +74,16 @@ def extract_file_embedded_pe():
"""
for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True):
for (ea, _) in check_segment_for_pe(seg):
yield Characteristic("embedded pe"), ea
yield Characteristic("embedded pe"), FileOffsetAddress(ea)
def extract_file_export_names():
def extract_file_export_names() -> Iterator[Tuple[Feature, Address]]:
"""extract function exports"""
for (_, _, ea, name) in idautils.Entries():
yield Export(name), ea
yield Export(name), AbsoluteVirtualAddress(ea)
def extract_file_import_names():
def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
"""extract function imports
1. imports by ordinal:
@@ -96,11 +95,12 @@ def extract_file_import_names():
- importname
"""
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)
# extract by name here and by ordinal below
for name in capa.features.extractors.helpers.generate_symbols(info[0], info[1]):
yield Import(name), ea
yield Import(name), addr
dll = info[0]
symbol = "#%d" % (info[2])
elif info[1]:
@@ -113,10 +113,10 @@ def extract_file_import_names():
continue
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield Import(name), ea
yield Import(name), addr
def extract_file_section_names():
def extract_file_section_names() -> Iterator[Tuple[Feature, Address]]:
"""extract section names
IDA must load resource sections for this to be complete
@@ -124,10 +124,10 @@ def extract_file_section_names():
- Check 'Load resource sections' when opening binary in IDA manually
"""
for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True):
yield Section(idaapi.get_segm_name(seg)), seg.start_ea
yield Section(idaapi.get_segm_name(seg)), AbsoluteVirtualAddress(seg.start_ea)
def extract_file_strings():
def extract_file_strings() -> Iterator[Tuple[Feature, Address]]:
"""extract ASCII and UTF-16 LE strings
IDA must load resource sections for this to be complete
@@ -137,41 +137,50 @@ def extract_file_strings():
for seg in capa.features.extractors.ida.helpers.get_segments():
seg_buff = capa.features.extractors.ida.helpers.get_segment_buffer(seg)
# differing to common string extractor factor in segment offset here
for s in capa.features.extractors.strings.extract_ascii_strings(seg_buff):
yield String(s.s), (seg.start_ea + s.offset)
yield String(s.s), FileOffsetAddress(seg.start_ea + s.offset)
for s in capa.features.extractors.strings.extract_unicode_strings(seg_buff):
yield String(s.s), (seg.start_ea + s.offset)
yield String(s.s), FileOffsetAddress(seg.start_ea + s.offset)
def extract_file_function_names():
def extract_file_function_names() -> Iterator[Tuple[Feature, Address]]:
"""
extract the names of statically-linked library functions.
"""
for ea in idautils.Functions():
addr = AbsoluteVirtualAddress(ea)
if idaapi.get_func(ea).flags & idaapi.FUNC_LIB:
name = idaapi.get_name(ea)
yield FunctionName(name), ea
yield FunctionName(name), addr
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions.
# extract features for both the mangled and un-mangled representations.
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield FunctionName(name[1:]), addr
def extract_file_format():
format_name = ida_loader.get_file_type_name()
def extract_file_format() -> Iterator[Tuple[Feature, Address]]:
file_info = idaapi.get_inf_structure()
if "PE" in format_name:
yield Format(FORMAT_PE), 0x0
elif "ELF64" in format_name:
yield Format(FORMAT_ELF), 0x0
elif "ELF32" in format_name:
yield Format(FORMAT_ELF), 0x0
if file_info.filetype == idaapi.f_PE:
yield Format(FORMAT_PE), NO_ADDRESS
elif file_info.filetype == idaapi.f_ELF:
yield Format(FORMAT_ELF), NO_ADDRESS
elif file_info.filetype == idaapi.f_BIN:
# no file type to return when processing a binary file, but we want to continue processing
return
else:
raise NotImplementedError("file format: %s", format_name)
raise NotImplementedError("file format: %d" % file_info.filetype)
def extract_features():
def extract_features() -> Iterator[Tuple[Feature, Address]]:
"""extract file features"""
for file_handler in FILE_HANDLERS:
for feature, va in file_handler():
yield feature, va
for feature, addr in file_handler():
yield feature, addr
FILE_HANDLERS = (

View File

@@ -1,35 +1,31 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from typing import Tuple, Iterator
import idaapi
import idautils
import capa.features.extractors.ida.helpers
from capa.features.common import Characteristic
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):
"""extract callers to a function
args:
f (IDA func_t)
"""
for ea in idautils.CodeRefsTo(f.start_ea, True):
yield Characteristic("calls to"), ea
def extract_function_calls_to(fh: FunctionHandle):
"""extract callers to a function"""
for ea in idautils.CodeRefsTo(fh.inner.start_ea, True):
yield Characteristic("calls to"), AbsoluteVirtualAddress(ea)
def extract_function_loop(f):
"""extract loop indicators from a function
args:
f (IDA func_t)
"""
def extract_function_loop(fh: FunctionHandle):
"""extract loop indicators from a function"""
f: idaapi.func_t = fh.inner
edges = []
# construct control flow graph
@@ -38,28 +34,19 @@ def extract_function_loop(f):
edges.append((bb.start_ea, succ.start_ea))
if loops.has_loop(edges):
yield Characteristic("loop"), f.start_ea
yield Characteristic("loop"), fh.address
def extract_recursive_call(f):
"""extract recursive function call
args:
f (IDA func_t)
"""
if capa.features.extractors.ida.helpers.is_function_recursive(f):
yield Characteristic("recursive call"), f.start_ea
def extract_recursive_call(fh: FunctionHandle):
"""extract recursive function call"""
if capa.features.extractors.ida.helpers.is_function_recursive(fh.inner):
yield Characteristic("recursive call"), fh.address
def extract_features(f):
"""extract function features
arg:
f (IDA func_t)
"""
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
for func_handler in FUNCTION_HANDLERS:
for (feature, ea) in func_handler(f):
yield feature, ea
for (feature, addr) in func_handler(fh):
yield feature, addr
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call)
@@ -68,8 +55,8 @@ FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_r
def main():
""" """
features = []
for f in capa.features.extractors.ida.get_functions(skip_thunks=True, skip_libs=True):
features.extend(list(extract_features(f)))
for fhandle in capa.features.extractors.ida.helpers.get_functions(skip_thunks=True, skip_libs=True):
features.extend(list(extract_features(fhandle)))
import pprint

View File

@@ -1,27 +1,29 @@
import logging
import contextlib
from typing import Tuple, Iterator
import idaapi
import ida_loader
import capa.ida.helpers
import capa.features.extractors.elf
from capa.features.common import OS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch
from capa.features.common import OS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature
from capa.features.address import NO_ADDRESS, Address
logger = logging.getLogger(__name__)
def extract_os():
format_name = ida_loader.get_file_type_name()
def extract_os() -> Iterator[Tuple[Feature, Address]]:
format_name: str = ida_loader.get_file_type_name()
if "PE" in format_name:
yield OS(OS_WINDOWS), 0x0
yield OS(OS_WINDOWS), NO_ADDRESS
elif "ELF" in format_name:
with contextlib.closing(capa.ida.helpers.IDAIO()) as f:
os = capa.features.extractors.elf.detect_elf_os(f)
yield OS(os), 0x0
yield OS(os), NO_ADDRESS
else:
# we likely end up here:
@@ -38,13 +40,13 @@ def extract_os():
return
def extract_arch():
info = idaapi.get_inf_structure()
if info.procName == "metapc" and info.is_64bit():
yield Arch(ARCH_AMD64), 0x0
elif info.procName == "metapc" and info.is_32bit():
yield Arch(ARCH_I386), 0x0
elif info.procName == "metapc":
def extract_arch() -> Iterator[Tuple[Feature, Address]]:
info: idaapi.idainfo = idaapi.get_inf_structure()
if info.procname == "metapc" and info.is_64bit():
yield Arch(ARCH_AMD64), NO_ADDRESS
elif info.procname == "metapc" and info.is_32bit():
yield Arch(ARCH_I386), NO_ADDRESS
elif info.procname == "metapc":
logger.debug("unsupported architecture: non-32-bit nor non-64-bit intel")
return
else:
@@ -52,5 +54,5 @@ def extract_arch():
# 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", info.procName)
logger.debug("unsupported architecture: %s", info.procname)
return

View File

@@ -1,18 +1,22 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from typing import Any, Dict, Tuple, Iterator
import idc
import idaapi
import idautils
import ida_bytes
from capa.features.address import AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import FunctionHandle
def find_byte_sequence(start, end, seq):
def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
"""yield all ea of a given byte sequence
args:
@@ -20,32 +24,32 @@ def find_byte_sequence(start, end, seq):
end: max virtual address
seq: bytes to search e.g. b"\x01\x03"
"""
seq = " ".join(["%02x" % b for b in seq])
seqstr = " ".join(["%02x" % b for b in seq])
while True:
ea = idaapi.find_binary(start, end, seq, 0, idaapi.SEARCH_DOWN)
# TODO find_binary: Deprecated. Please use ida_bytes.bin_search() instead.
ea = idaapi.find_binary(start, end, seqstr, 0, idaapi.SEARCH_DOWN)
if ea == idaapi.BADADDR:
break
start = ea + 1
yield ea
def get_functions(start=None, end=None, skip_thunks=False, skip_libs=False):
def get_functions(
start: int = None, end: int = None, skip_thunks: bool = False, skip_libs: bool = False
) -> Iterator[FunctionHandle]:
"""get functions, range optional
args:
start: min virtual address
end: max virtual address
ret:
yield func_t*
"""
for ea in idautils.Functions(start=start, end=end):
f = idaapi.get_func(ea)
if not (skip_thunks and (f.flags & idaapi.FUNC_THUNK) or skip_libs and (f.flags & idaapi.FUNC_LIB)):
yield f
yield FunctionHandle(address=AbsoluteVirtualAddress(ea), inner=f)
def get_segments(skip_header_segments=False):
def get_segments(skip_header_segments=False) -> Iterator[idaapi.segment_t]:
"""get list of segments (sections) in the binary image
args:
@@ -57,7 +61,7 @@ def get_segments(skip_header_segments=False):
yield seg
def get_segment_buffer(seg):
def get_segment_buffer(seg: idaapi.segment_t) -> bytes:
"""return bytes stored in a given segment
decrease buffer size until IDA is able to read bytes from the segment
@@ -75,7 +79,7 @@ def get_segment_buffer(seg):
return buff if buff else b""
def get_file_imports():
def get_file_imports() -> Dict[int, Tuple[str, str, int]]:
"""get file imports"""
imports = {}
@@ -85,10 +89,18 @@ def get_file_imports():
if not library:
continue
# IDA uses section names for the library of ELF imports, like ".dynsym"
library = library.lstrip(".")
def inspect_import(ea, function, ordinal):
if function and function.startswith("__imp_"):
# handle mangled names starting
# handle mangled PE imports
function = function[len("__imp_") :]
if function and "@@" in function:
# handle mangled ELF imports, like "fopen@@glibc_2.2.5"
function, _, _ = function.partition("@@")
imports[ea] = (library.lower(), function, ordinal)
return True
@@ -97,14 +109,12 @@ def get_file_imports():
return imports
def get_instructions_in_range(start, end):
def get_instructions_in_range(start: int, end: int) -> Iterator[idaapi.insn_t]:
"""yield instructions in range
args:
start: virtual address (inclusive)
end: virtual address (exclusive)
yield:
(insn_t*)
"""
for head in idautils.Heads(start, end):
insn = idautils.DecodeInstruction(head)
@@ -112,7 +122,7 @@ def get_instructions_in_range(start, end):
yield insn
def is_operand_equal(op1, op2):
def is_operand_equal(op1: idaapi.op_t, op2: idaapi.op_t) -> bool:
"""compare two IDA op_t"""
if op1.flags != op2.flags:
return False
@@ -138,7 +148,7 @@ def is_operand_equal(op1, op2):
return True
def is_basic_block_equal(bb1, bb2):
def is_basic_block_equal(bb1: idaapi.BasicBlock, bb2: idaapi.BasicBlock) -> bool:
"""compare two IDA BasicBlock"""
if bb1.start_ea != bb2.start_ea:
return False
@@ -152,12 +162,12 @@ def is_basic_block_equal(bb1, bb2):
return True
def basic_block_size(bb):
def basic_block_size(bb: idaapi.BasicBlock) -> int:
"""calculate size of basic block"""
return bb.end_ea - bb.start_ea
def read_bytes_at(ea, count):
def read_bytes_at(ea: int, count: int) -> bytes:
""" """
# check if byte has a value, see get_wide_byte doc
if not idc.is_loaded(ea):
@@ -170,10 +180,10 @@ def read_bytes_at(ea, count):
return idc.get_bytes(ea, count)
def find_string_at(ea, min=4):
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
@@ -187,7 +197,7 @@ def find_string_at(ea, min=4):
return ""
def get_op_phrase_info(op):
def get_op_phrase_info(op: idaapi.op_t) -> Dict:
"""parse phrase features from operand
Pretty much dup of sark's implementation:
@@ -224,23 +234,23 @@ def get_op_phrase_info(op):
return {"base": base, "index": index, "scale": scale, "offset": offset}
def is_op_write(insn, op):
def is_op_write(insn: idaapi.insn_t, op: idaapi.op_t) -> bool:
"""Check if an operand is written to (destination operand)"""
return idaapi.has_cf_chg(insn.get_canon_feature(), op.n)
def is_op_read(insn, op):
def is_op_read(insn: idaapi.insn_t, op: idaapi.op_t) -> bool:
"""Check if an operand is read from (source operand)"""
return idaapi.has_cf_use(insn.get_canon_feature(), op.n)
def is_op_offset(insn, op):
def is_op_offset(insn: idaapi.insn_t, op: idaapi.op_t) -> bool:
"""Check is an operand has been marked as an offset (by auto-analysis or manually)"""
flags = idaapi.get_flags(insn.ea)
return ida_bytes.is_off(flags, op.n)
def is_sp_modified(insn):
def is_sp_modified(insn: idaapi.insn_t) -> bool:
"""determine if instruction modifies SP, ESP, RSP"""
for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,)):
if op.reg == idautils.procregs.sp.reg and is_op_write(insn, op):
@@ -249,7 +259,7 @@ def is_sp_modified(insn):
return False
def is_bp_modified(insn):
def is_bp_modified(insn: idaapi.insn_t) -> bool:
"""check if instruction modifies BP, EBP, RBP"""
for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,)):
if op.reg == idautils.procregs.bp.reg and is_op_write(insn, op):
@@ -258,12 +268,12 @@ def is_bp_modified(insn):
return False
def is_frame_register(reg):
def is_frame_register(reg: int) -> bool:
"""check if register is sp or bp"""
return reg in (idautils.procregs.sp.reg, idautils.procregs.bp.reg)
def get_insn_ops(insn, target_ops=()):
def get_insn_ops(insn: idaapi.insn_t, target_ops: 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:
@@ -274,12 +284,12 @@ def get_insn_ops(insn, target_ops=()):
yield op
def is_op_stack_var(ea, index):
def is_op_stack_var(ea: int, index: int) -> bool:
"""check if operand is a stack variable"""
return idaapi.is_stkvar(idaapi.get_flags(ea), index)
def mask_op_val(op):
def mask_op_val(op: idaapi.op_t) -> int:
"""mask value by data type
necessary due to a bug in AMD64
@@ -299,26 +309,18 @@ def mask_op_val(op):
return masks.get(op.dtype, op.value) & op.value
def is_function_recursive(f):
"""check if function is recursive
args:
f (IDA func_t)
"""
def is_function_recursive(f: idaapi.func_t) -> bool:
"""check if function is recursive"""
for ref in idautils.CodeRefsTo(f.start_ea, True):
if f.contains(ref):
return True
return False
def is_basic_block_tight_loop(bb):
def is_basic_block_tight_loop(bb: idaapi.BasicBlock) -> bool:
"""check basic block loops to self
true if last instruction in basic block branches to basic block start
args:
f (IDA func_t)
bb (IDA BasicBlock)
"""
bb_end = idc.prev_head(bb.end_ea)
if bb.start_ea < bb_end:
@@ -328,7 +330,7 @@ def is_basic_block_tight_loop(bb):
return False
def find_data_reference_from_insn(insn, max_depth=10):
def find_data_reference_from_insn(insn: idaapi.insn_t, max_depth: int = 10) -> int:
"""search for data reference from instruction, return address of instruction if no reference exists"""
depth = 0
ea = insn.ea
@@ -358,19 +360,18 @@ def find_data_reference_from_insn(insn, max_depth=10):
return ea
def get_function_blocks(f):
"""yield basic blocks contained in specified function
args:
f (IDA func_t)
yield:
block (IDA BasicBlock)
"""
def get_function_blocks(f: idaapi.func_t) -> Iterator[idaapi.BasicBlock]:
"""yield basic blocks contained in specified function"""
# leverage idaapi.FC_NOEXT flag to ignore useless external blocks referenced by the function
for block in idaapi.FlowChart(f, flags=(idaapi.FC_PREDS | idaapi.FC_NOEXT)):
yield block
def is_basic_block_return(bb):
def is_basic_block_return(bb: idaapi.BasicBlock) -> bool:
"""check if basic block is return block"""
return bb.type == idaapi.fcb_ret
def has_sib(oper: idaapi.op_t) -> bool:
# via: https://reverseengineering.stackexchange.com/a/14300
return oper.specflag1 == 1

View File

@@ -1,10 +1,11 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from typing import Any, Dict, Tuple, Iterator
import idc
import idaapi
@@ -12,47 +13,23 @@ import idautils
import capa.features.extractors.helpers
import capa.features.extractors.ida.helpers
from capa.features.insn import API, Number, Offset, Mnemonic
from capa.features.common import (
BITNESS_X32,
BITNESS_X64,
MAX_BYTES_FEATURE_SIZE,
THUNK_CHAIN_DEPTH_DELTA,
Bytes,
String,
Characteristic,
)
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
def get_bitness(ctx):
"""
fetch the BITNESS_* constant for the currently open workspace.
via Tamir Bahar/@tmr232
https://reverseengineering.stackexchange.com/a/11398/17194
"""
if "bitness" not in ctx:
info = idaapi.get_inf_structure()
if info.is_64bit():
ctx["bitness"] = BITNESS_X64
elif info.is_32bit():
ctx["bitness"] = BITNESS_X32
else:
raise ValueError("unexpected bitness")
return ctx["bitness"]
def get_imports(ctx):
def get_imports(ctx: Dict[str, Any]) -> Dict[str, 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, insn):
def check_for_api_call(ctx: Dict[str, Any], insn: idaapi.insn_t) -> Iterator[str]:
"""check instruction for API call"""
info = ()
ref = insn.ea
@@ -81,24 +58,22 @@ def check_for_api_call(ctx, insn):
yield "%s.%s" % (info[0], info[1])
def extract_insn_api_features(f, bb, insn):
"""parse instruction API features
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction API features
example:
call dword [0x00473038]
call dword [0x00473038]
"""
insn: idaapi.insn_t = ih.inner
if not insn.get_canon_mnem() in ("call", "jmp"):
return
for api in check_for_api_call(f.ctx, insn):
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):
yield API(name), insn.ea
yield API(name), ih.address
# extract IDA/FLIRT recognized API functions
targets = tuple(idautils.CodeRefsFrom(insn.ea, False))
@@ -113,20 +88,25 @@ def extract_insn_api_features(f, bb, insn):
if target_func.flags & idaapi.FUNC_LIB:
name = idaapi.get_name(target_func.start_ea)
yield API(name), insn.ea
yield API(name), ih.address
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions.
# extract features for both the mangled and un-mangled representations.
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield API(name[1:]), ih.address
def extract_insn_number_features(f, bb, insn):
"""parse instruction number features
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
def extract_insn_number_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction number features
example:
push 3136B0h ; dwControlCode
"""
insn: idaapi.insn_t = ih.inner
if idaapi.is_ret_insn(insn):
# skip things like:
# .text:0042250E retn 8
@@ -137,7 +117,11 @@ def extract_insn_number_features(f, bb, insn):
# .text:00401145 add esp, 0Ch
return
for op in capa.features.extractors.ida.helpers.get_insn_ops(insn, target_ops=(idaapi.o_imm, idaapi.o_mem)):
for i, op in enumerate(insn.ops):
if op.type == idaapi.o_void:
break
if op.type not in (idaapi.o_imm, idaapi.o_mem):
continue
# skip things like:
# .text:00401100 shr eax, offset loc_C
if capa.features.extractors.ida.helpers.is_op_offset(insn, op):
@@ -148,21 +132,27 @@ def extract_insn_number_features(f, bb, insn):
else:
const = op.addr
yield Number(const), insn.ea
yield Number(const, bitness=get_bitness(f.ctx)), insn.ea
yield Number(const), ih.address
yield OperandNumber(i, const), ih.address
if insn.itype == idaapi.NN_add and 0 < const < MAX_STRUCTURE_SIZE and op.type == idaapi.o_imm:
# for pattern like:
#
# add eax, 0x10
#
# assume 0x10 is also an offset (imagine eax is a pointer).
yield Offset(const), ih.address
yield OperandOffset(i, const), ih.address
def extract_insn_bytes_features(f, bb, insn):
"""parse referenced byte sequences
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
def extract_insn_bytes_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse referenced byte sequences
example:
push offset iid_004118d4_IShellLinkA ; riid
"""
insn: idaapi.insn_t = ih.inner
if idaapi.is_call_insn(insn):
return
@@ -170,41 +160,46 @@ def extract_insn_bytes_features(f, bb, insn):
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), insn.ea
yield Bytes(extracted_bytes), ih.address
def extract_insn_string_features(f, bb, insn):
"""parse instruction string features
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
def extract_insn_string_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction string features
example:
push offset aAcr ; "ACR > "
"""
insn: idaapi.insn_t = ih.inner
ref = capa.features.extractors.ida.helpers.find_data_reference_from_insn(insn)
if ref != insn.ea:
found = capa.features.extractors.ida.helpers.find_string_at(ref)
if found:
yield String(found), insn.ea
yield String(found), ih.address
def extract_insn_offset_features(f, bb, insn):
"""parse instruction structure offset features
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
def extract_insn_offset_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction structure offset features
example:
.text:0040112F cmp [esi+4], ebx
"""
for op in capa.features.extractors.ida.helpers.get_insn_ops(insn, target_ops=(idaapi.o_phrase, idaapi.o_displ)):
insn: idaapi.insn_t = ih.inner
for i, op in enumerate(insn.ops):
if op.type == idaapi.o_void:
break
if op.type not in (idaapi.o_phrase, idaapi.o_displ):
continue
if capa.features.extractors.ida.helpers.is_op_stack_var(insn.ea, op.n):
continue
p_info = capa.features.extractors.ida.helpers.get_op_phrase_info(op)
op_off = p_info.get("offset", 0)
if idaapi.is_mapped(op_off):
@@ -217,12 +212,32 @@ def extract_insn_offset_features(f, bb, insn):
# https://stackoverflow.com/questions/31853189/x86-64-assembly-why-displacement-not-64-bits
op_off = capa.features.extractors.helpers.twos_complement(op_off, 32)
yield Offset(op_off), insn.ea
yield Offset(op_off, bitness=get_bitness(f.ctx)), insn.ea
yield Offset(op_off), ih.address
yield OperandOffset(i, op_off), ih.address
if (
insn.itype == idaapi.NN_lea
and i == 1
# o_displ is used for both:
# [eax+1]
# [eax+ebx+2]
and op.type == idaapi.o_displ
# but the SIB is only present for [eax+ebx+2]
# which we don't want
and not capa.features.extractors.ida.helpers.has_sib(op)
):
# for pattern like:
#
# lea eax, [ebx + 1]
#
# assume 1 is also an offset (imagine ebx is a zero register).
yield Number(op_off), ih.address
yield OperandNumber(i, op_off), ih.address
def contains_stack_cookie_keywords(s):
"""check if string contains stack cookie keywords
def contains_stack_cookie_keywords(s: str) -> bool:
"""
check if string contains stack cookie keywords
Examples:
xor ecx, ebp ; StackCookie
@@ -236,7 +251,7 @@ def contains_stack_cookie_keywords(s):
return any(keyword in s for keyword in ("stack", "security"))
def bb_stack_cookie_registers(bb):
def bb_stack_cookie_registers(bb: idaapi.BasicBlock) -> Iterator[int]:
"""scan basic block for stack cookie operations
yield registers ids that may have been used for stack cookie operations
@@ -270,7 +285,7 @@ def bb_stack_cookie_registers(bb):
yield op.reg
def is_nzxor_stack_cookie_delta(f, bb, insn):
def is_nzxor_stack_cookie_delta(f: idaapi.func_t, bb: idaapi.BasicBlock, insn: idaapi.insn_t) -> bool:
"""check if nzxor exists within stack cookie delta"""
# security cookie check should use SP or BP
if not capa.features.extractors.ida.helpers.is_frame_register(insn.Op2.reg):
@@ -293,7 +308,7 @@ def is_nzxor_stack_cookie_delta(f, bb, insn):
return False
def is_nzxor_stack_cookie(f, bb, insn):
def is_nzxor_stack_cookie(f: idaapi.func_t, bb: idaapi.BasicBlock, insn: idaapi.insn_t) -> bool:
"""check if nzxor is related to stack cookie"""
if contains_stack_cookie_keywords(idaapi.get_cmt(insn.ea, False)):
# Example:
@@ -310,37 +325,49 @@ def is_nzxor_stack_cookie(f, bb, insn):
return False
def extract_insn_nzxor_characteristic_features(f, bb, insn):
"""parse instruction non-zeroing XOR instruction
ignore expected non-zeroing XORs, e.g. security cookies
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
def extract_insn_nzxor_characteristic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction non-zeroing XOR instruction
ignore expected non-zeroing XORs, e.g. security cookies
"""
insn: idaapi.insn_t = ih.inner
if insn.itype not in (idaapi.NN_xor, idaapi.NN_xorpd, idaapi.NN_xorps, idaapi.NN_pxor):
return
if capa.features.extractors.ida.helpers.is_operand_equal(insn.Op1, insn.Op2):
return
if is_nzxor_stack_cookie(f, bb, insn):
if is_nzxor_stack_cookie(fh.inner, bbh.inner, insn):
return
yield Characteristic("nzxor"), insn.ea
yield Characteristic("nzxor"), ih.address
def extract_insn_mnemonic_features(f, bb, insn):
"""parse instruction mnemonic features
def extract_insn_mnemonic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction mnemonic features"""
yield Mnemonic(idc.print_insn_mnem(ih.inner.ea)), ih.address
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
def extract_insn_obfs_call_plus_5_characteristic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
yield Mnemonic(idc.print_insn_mnem(insn.ea)), insn.ea
parse call $+5 instruction from the given instruction.
"""
insn: idaapi.insn_t = ih.inner
if not idaapi.is_call_insn(insn):
return
if insn.ea + 5 == idc.get_operand_value(insn.ea, 0):
yield Characteristic("call $+5"), ih.address
def extract_insn_peb_access_characteristic_features(f, bb, insn):
def extract_insn_peb_access_characteristic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction peb access
fs:[0x30] on x86, gs:[0x60] on x64
@@ -348,6 +375,8 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
TODO:
IDA should be able to do this..
"""
insn: idaapi.insn_t = ih.inner
if insn.itype not in (idaapi.NN_push, idaapi.NN_mov):
return
@@ -359,15 +388,19 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
if " fs:30h" in disasm or " gs:60h" in disasm:
# TODO: replace above with proper IDA
yield Characteristic("peb access"), insn.ea
yield Characteristic("peb access"), ih.address
def extract_insn_segment_access_features(f, bb, insn):
def extract_insn_segment_access_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction fs or gs access
TODO:
IDA should be able to do this...
"""
insn: idaapi.insn_t = ih.inner
if all(map(lambda op: op.type != idaapi.o_mem, insn.ops)):
# try to optimize for only memory references
return
@@ -376,23 +409,21 @@ def extract_insn_segment_access_features(f, bb, insn):
if " fs:" in disasm:
# TODO: replace above with proper IDA
yield Characteristic("fs access"), insn.ea
yield Characteristic("fs access"), ih.address
if " gs:" in disasm:
# TODO: replace above with proper IDA
yield Characteristic("gs access"), insn.ea
yield Characteristic("gs access"), ih.address
def extract_insn_cross_section_cflow(f, bb, insn):
"""inspect the instruction for a CALL or JMP that crosses section boundaries
def extract_insn_cross_section_cflow(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""inspect the instruction for a CALL or JMP that crosses section boundaries"""
insn: idaapi.insn_t = ih.inner
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
"""
for ref in idautils.CodeRefsFrom(insn.ea, False):
if ref in get_imports(f.ctx).keys():
if ref in get_imports(fh.ctx).keys():
# ignore API calls
continue
if not idaapi.getseg(ref):
@@ -400,50 +431,40 @@ def extract_insn_cross_section_cflow(f, bb, insn):
continue
if idaapi.getseg(ref) == idaapi.getseg(insn.ea):
continue
yield Characteristic("cross section flow"), insn.ea
yield Characteristic("cross section flow"), ih.address
def extract_function_calls_from(f, bb, insn):
def extract_function_calls_from(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract functions calls from features
most relevant at the function scope, however, its most efficient to extract at the instruction scope
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
"""
insn: idaapi.insn_t = ih.inner
if idaapi.is_call_insn(insn):
for ref in idautils.CodeRefsFrom(insn.ea, False):
yield Characteristic("calls from"), ref
yield Characteristic("calls from"), AbsoluteVirtualAddress(ref)
def extract_function_indirect_call_characteristic_features(f, bb, insn):
def extract_function_indirect_call_characteristic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""extract indirect function calls (e.g., call eax or call dword ptr [edx+4])
does not include calls like => call ds:dword_ABD4974
most relevant at the function or basic block scope;
however, its most efficient to extract at the instruction scope
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
"""
insn: idaapi.insn_t = ih.inner
if idaapi.is_call_insn(insn) and idc.get_operand_type(insn.ea, 0) in (idc.o_reg, idc.o_phrase, idc.o_displ):
yield Characteristic("indirect call"), insn.ea
yield Characteristic("indirect call"), ih.address
def extract_features(f, bb, insn):
"""extract instruction features
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
"""
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, bb, insn):
for (feature, ea) in inst_handler(f, bbh, insn):
yield feature, ea
@@ -455,6 +476,7 @@ INSTRUCTION_HANDLERS = (
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,

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -0,0 +1,77 @@
from typing import Dict, List, Tuple
from dataclasses import dataclass
from capa.features.common import Feature
from capa.features.address import NO_ADDRESS, Address
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
@dataclass
class InstructionFeatures:
features: List[Tuple[Address, Feature]]
@dataclass
class BasicBlockFeatures:
features: List[Tuple[Address, Feature]]
instructions: Dict[Address, InstructionFeatures]
@dataclass
class FunctionFeatures:
features: List[Tuple[Address, Feature]]
basic_blocks: Dict[Address, BasicBlockFeatures]
@dataclass
class NullFeatureExtractor(FeatureExtractor):
"""
An extractor that extracts some user-provided features.
This is useful for testing, as we can provide expected values and see if matching works.
"""
base_address: Address
global_features: List[Feature]
file_features: List[Tuple[Address, Feature]]
functions: Dict[Address, FunctionFeatures]
def get_base_address(self):
return self.base_address
def extract_global_features(self):
for feature in self.global_features:
yield feature, NO_ADDRESS
def extract_file_features(self):
for address, feature in self.file_features:
yield feature, address
def get_functions(self):
for address in sorted(self.functions.keys()):
yield FunctionHandle(address, None)
def extract_function_features(self, f):
for address, feature in self.functions.get(f.address, {}).features:
yield feature, address
def get_basic_blocks(self, f):
for address in sorted(self.functions.get(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:
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()):
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
):
yield feature, address

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -17,6 +17,7 @@ import capa.features.extractors.helpers
import capa.features.extractors.strings
from capa.features.file import Export, Import, Section
from capa.features.common import OS, ARCH_I386, FORMAT_PE, ARCH_AMD64, OS_WINDOWS, Arch, Format, Characteristic
from capa.features.address import NO_ADDRESS, FileOffsetAddress, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import FeatureExtractor
logger = logging.getLogger(__name__)
@@ -24,7 +25,7 @@ logger = logging.getLogger(__name__)
def extract_file_embedded_pe(buf, **kwargs):
for offset, _ in capa.features.extractors.helpers.carve_pe(buf, 1):
yield Characteristic("embedded pe"), offset
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
def extract_file_export_names(pe, **kwargs):
@@ -39,7 +40,7 @@ def extract_file_export_names(pe, **kwargs):
except UnicodeDecodeError:
continue
va = base_address + export.address
yield Export(name), va
yield Export(name), AbsoluteVirtualAddress(va)
def extract_file_import_names(pe, **kwargs):
@@ -71,7 +72,7 @@ def extract_file_import_names(pe, **kwargs):
continue
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
yield Import(name), imp.address
yield Import(name), AbsoluteVirtualAddress(imp.address)
def extract_file_section_names(pe, **kwargs):
@@ -83,7 +84,7 @@ def extract_file_section_names(pe, **kwargs):
except UnicodeDecodeError:
continue
yield Section(name), base_address + section.VirtualAddress
yield Section(name), AbsoluteVirtualAddress(base_address + section.VirtualAddress)
def extract_file_strings(buf, **kwargs):
@@ -103,18 +104,18 @@ def extract_file_function_names(**kwargs):
def extract_file_os(**kwargs):
# assuming PE -> Windows
# though i suppose they're also used by UEFI
yield OS(OS_WINDOWS), 0x0
yield OS(OS_WINDOWS), NO_ADDRESS
def extract_file_format(**kwargs):
yield Format(FORMAT_PE), 0x0
yield Format(FORMAT_PE), NO_ADDRESS
def extract_file_arch(pe, **kwargs):
if pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_I386"]:
yield Arch(ARCH_I386), 0x0
yield Arch(ARCH_I386), NO_ADDRESS
elif pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_AMD64"]:
yield Arch(ARCH_AMD64), 0x0
yield Arch(ARCH_AMD64), NO_ADDRESS
else:
logger.warning("unsupported architecture: %s", pefile.MACHINE_TYPE[pe.FILE_HEADER.Machine])
@@ -176,7 +177,7 @@ class PefileFeatureExtractor(FeatureExtractor):
self.pe = pefile.PE(path)
def get_base_address(self):
return self.pe.OPTIONAL_HEADER.ImageBase
return AbsoluteVirtualAddress(self.pe.OPTIONAL_HEADER.ImageBase)
def extract_global_features(self):
with open(self.path, "rb") as f:

View File

@@ -1,9 +1,12 @@
import string
import struct
from typing import Tuple, Iterator
from capa.features.common import Characteristic
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):
@@ -13,10 +16,10 @@ def _bb_has_tight_loop(f, bb):
return bb.offset in f.blockrefs[bb.offset] if bb.offset in f.blockrefs else False
def extract_bb_tight_loop(f, bb):
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, bb):
yield Characteristic("tight loop"), bb.offset
if _bb_has_tight_loop(f.inner, bb.inner):
yield Characteristic("tight loop"), bb.address
def _bb_has_stackstring(f, bb):
@@ -37,10 +40,10 @@ def get_operands(smda_ins):
return [o.strip() for o in smda_ins.operands.split(",")]
def extract_stackstring(f, bb):
def extract_stackstring(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""check basic block for stackstring indicators"""
if _bb_has_stackstring(f, bb):
yield Characteristic("stack string"), bb.offset
if _bb_has_stackstring(f.inner, bb.inner):
yield Characteristic("stack string"), bb.address
def is_mov_imm_to_stack(smda_ins):
@@ -107,21 +110,21 @@ def get_printable_len(instr):
return 0
def extract_features(f, bb):
def extract_features(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract features from the given basic block.
args:
f (smda.common.SmdaFunction): the function from which to extract features
bb (smda.common.SmdaBasicBlock): the basic block to process.
f: the function from which to extract features
bb: the basic block to process.
yields:
Tuple[Feature, int]: the features and their location found in this basic block.
Tuple[Feature, Address]: the features and their location found in this basic block.
"""
yield BasicBlock(), bb.offset
yield BasicBlock(), bb.address
for bb_handler in BASIC_BLOCK_HANDLERS:
for feature, va in bb_handler(f, bb):
yield feature, va
for feature, addr in bb_handler(f, bb):
yield feature, addr
BASIC_BLOCK_HANDLERS = (

View File

@@ -1,3 +1,5 @@
from typing import List, Tuple
from smda.common.SmdaReport import SmdaReport
import capa.features.extractors.common
@@ -6,7 +8,9 @@ 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.extractors.base_extractor import FeatureExtractor
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):
@@ -18,12 +22,12 @@ class SmdaFeatureExtractor(FeatureExtractor):
self.buf = f.read()
# pre-compute these because we'll yield them at *every* scope.
self.global_features = []
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 self.smda_report.base_addr
return AbsoluteVirtualAddress(self.smda_report.base_addr)
def extract_global_features(self):
yield from self.global_features
@@ -33,21 +37,21 @@ class SmdaFeatureExtractor(FeatureExtractor):
def get_functions(self):
for function in self.smda_report.getFunctions():
yield function
yield FunctionHandle(address=AbsoluteVirtualAddress(function.offset), inner=function)
def extract_function_features(self, f):
yield from capa.features.extractors.smda.function.extract_features(f)
def extract_function_features(self, fh):
yield from capa.features.extractors.smda.function.extract_features(fh)
def get_basic_blocks(self, f):
for bb in f.getBlocks():
yield bb
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, f, bb):
yield from capa.features.extractors.smda.basicblock.extract_features(f, bb)
def extract_basic_block_features(self, fh, bbh):
yield from capa.features.extractors.smda.basicblock.extract_features(fh, bbh)
def get_instructions(self, f, bb):
for smda_ins in bb.getInstructions():
yield smda_ins
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, f, bb, insn):
yield from capa.features.extractors.smda.insn.extract_features(f, bb, insn)
def extract_insn_features(self, fh, bbh, ih):
yield from capa.features.extractors.smda.insn.extract_features(fh, bbh, ih)

View File

@@ -6,11 +6,12 @@ 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"), offset
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
def extract_file_export_names(buf, **kwargs):
@@ -18,7 +19,7 @@ def extract_file_export_names(buf, **kwargs):
if lief_binary is not None:
for function in lief_binary.exported_functions:
yield Export(function.name), function.address
yield Export(function.name), AbsoluteVirtualAddress(function.address)
def extract_file_import_names(smda_report, buf):
@@ -33,10 +34,10 @@ def extract_file_import_names(smda_report, buf):
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), va
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), va
yield Import(name), AbsoluteVirtualAddress(va)
def extract_file_section_names(buf, **kwargs):
@@ -46,7 +47,7 @@ def extract_file_section_names(buf, **kwargs):
if lief_binary and lief_binary.sections:
base_address = lief_binary.optional_header.imagebase
for section in lief_binary.sections:
yield Section(section.name), base_address + section.virtual_address
yield Section(section.name), AbsoluteVirtualAddress(base_address + section.virtual_address)
def extract_file_strings(buf, **kwargs):
@@ -54,10 +55,10 @@ 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), s.offset
yield String(s.s), FileOffsetAddress(s.offset)
for s in capa.features.extractors.strings.extract_unicode_strings(buf):
yield String(s.s), s.offset
yield String(s.s), FileOffsetAddress(s.offset)
def extract_file_function_names(smda_report, **kwargs):
@@ -87,8 +88,8 @@ def extract_features(smda_report, buf):
"""
for file_handler in FILE_HANDLERS:
for feature, va in file_handler(smda_report=smda_report, buf=buf):
yield feature, va
for feature, addr in file_handler(smda_report=smda_report, buf=buf):
yield feature, addr
FILE_HANDLERS = (

View File

@@ -1,38 +1,42 @@
from capa.features.common import Characteristic
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):
for inref in f.inrefs:
yield Characteristic("calls to"), inref
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):
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.blockrefs.items():
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.offset
yield Characteristic("loop"), f.address
def extract_features(f):
def extract_features(f: FunctionHandle):
"""
extract features from the given function.
args:
f (smda.common.SmdaFunction): the function from which to extract features
f: the function from which to extract features
yields:
Tuple[Feature, int]: the features and their location found in this function.
Tuple[Feature, Address]: the features and their location found in this function.
"""
for func_handler in FUNCTION_HANDLERS:
for feature, va in func_handler(f):
yield feature, va
for feature, addr in func_handler(f):
yield feature, addr
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)

View File

@@ -1,6 +1,7 @@
import logging
from capa.features.common import ARCH_I386, ARCH_AMD64, Arch
from capa.features.address import NO_ADDRESS
logger = logging.getLogger(__name__)
@@ -8,9 +9,9 @@ logger = logging.getLogger(__name__)
def extract_arch(smda_report):
if smda_report.architecture == "intel":
if smda_report.bitness == 32:
yield Arch(ARCH_I386), 0x0
yield Arch(ARCH_I386), NO_ADDRESS
elif smda_report.bitness == 64:
yield Arch(ARCH_AMD64), 0x0
yield Arch(ARCH_AMD64), NO_ADDRESS
else:
# we likely end up here:
# 1. handling a new architecture (e.g. aarch64)

View File

@@ -1,20 +1,15 @@
import re
import string
import struct
from typing import Tuple, Iterator
from smda.common.SmdaReport import SmdaReport
import smda
import capa.features.extractors.helpers
from capa.features.insn import API, Number, Offset, Mnemonic
from capa.features.common import (
BITNESS_X32,
BITNESS_X64,
MAX_BYTES_FEATURE_SIZE,
THUNK_CHAIN_DEPTH_DELTA,
Bytes,
String,
Characteristic,
)
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
@@ -23,27 +18,20 @@ PATTERN_HEXNUM = re.compile(r"[+\-] (?P<num>0x[a-fA-F0-9]+)")
PATTERN_SINGLENUM = re.compile(r"[+\-] (?P<num>[0-9])")
def get_bitness(smda_report):
if smda_report.architecture == "intel":
if smda_report.bitness == 32:
return BITNESS_X32
elif smda_report.bitness == 64:
return BITNESS_X64
else:
raise NotImplementedError
def extract_insn_api_features(f, bb, insn):
def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse API features from the given instruction."""
if insn.offset in f.apirefs:
api_entry = f.apirefs[insn.offset]
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), insn.offset
elif insn.offset in f.outrefs:
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):
@@ -62,7 +50,7 @@ def extract_insn_api_features(f, bb, insn):
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), insn.offset
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]
@@ -70,11 +58,14 @@ def extract_insn_api_features(f, bb, insn):
return
def extract_insn_number_features(f, bb, insn):
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:
@@ -82,12 +73,25 @@ def extract_insn_number_features(f, bb, insn):
# .text:00401140 call sub_407E2B
# .text:00401145 add esp, 0Ch
return
for operand in operands:
for i, operand in enumerate(operands):
try:
yield Number(int(operand, 16)), insn.offset
yield Number(int(operand, 16), bitness=get_bitness(f.smda_report)), insn.offset
except:
# 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):
@@ -136,12 +140,15 @@ def derefs(smda_report, p):
p = val
def extract_insn_bytes_features(f, bb, insn):
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)
@@ -150,7 +157,7 @@ def extract_insn_bytes_features(f, bb, insn):
if capa.features.extractors.helpers.all_zeros(bytes_read):
continue
yield Bytes(bytes_read), insn.offset
yield Bytes(bytes_read), ih.address
def detect_ascii_len(smda_report, offset):
@@ -194,30 +201,34 @@ def read_string(smda_report, offset):
return read_bytes(smda_report, offset, ulen).decode("utf-16")
def extract_insn_string_features(f, bb, insn):
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")), insn.offset
yield String(string_read.rstrip("\x00")), ih.address
def extract_insn_offset_features(f, bb, insn):
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 operand in operands:
if not "ptr" in operand:
continue
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)
@@ -227,8 +238,26 @@ def extract_insn_offset_features(f, bb, insn):
elif number_int:
number = int(number_int.group("num"))
number = -1 * number if number_int.group().startswith("-") else number
yield Offset(number), insn.offset
yield Offset(number, bitness=get_bitness(f.smda_report)), insn.offset
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):
@@ -252,11 +281,16 @@ def is_security_cookie(f, bb, insn):
return False
def extract_insn_nzxor_characteristic_features(f, bb, insn):
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
@@ -268,18 +302,35 @@ def extract_insn_nzxor_characteristic_features(f, bb, insn):
if is_security_cookie(f, bb, insn):
return
yield Characteristic("nzxor"), insn.offset
yield Characteristic("nzxor"), ih.address
def extract_insn_mnemonic_features(f, bb, insn):
def extract_insn_mnemonic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse mnemonic features from the given instruction."""
yield Mnemonic(insn.mnemonic), insn.offset
yield Mnemonic(ih.inner.mnemonic), ih.address
def extract_insn_peb_access_characteristic_features(f, bb, insn):
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
@@ -287,65 +338,75 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
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"), insn.offset
yield Characteristic("peb access"), ih.address
elif "gs:" in operand and "0x60" in operand:
yield Characteristic("peb access"), insn.offset
yield Characteristic("peb access"), ih.address
def extract_insn_segment_access_features(f, bb, insn):
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"), insn.offset
yield Characteristic("fs access"), ih.address
elif "gs:" in operand:
yield Characteristic("gs access"), insn.offset
yield Characteristic("gs access"), ih.address
def extract_insn_cross_section_cflow(f, bb, insn):
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 insn.offset in f.apirefs:
if ih.address in f.apirefs:
return
smda_report = insn.smda_function.smda_report
if insn.offset in f.outrefs:
for target in f.outrefs[insn.offset]:
if smda_report.getSection(insn.offset) != smda_report.getSection(target):
yield Characteristic("cross section flow"), insn.offset
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(insn.offset) != smda_report.getSection(target):
yield Characteristic("cross section flow"), insn.offset
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(f, bb, insn):
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 insn.offset in f.outrefs:
for outref in f.outrefs[insn.offset]:
yield Characteristic("calls from"), outref
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"), outref
if insn.offset in f.apirefs:
yield Characteristic("calls from"), insn.offset
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, insn):
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"):
@@ -357,7 +418,7 @@ def extract_function_indirect_call_characteristic_features(f, bb, insn):
# call edx
# call dword ptr [eax+50h]
# call qword ptr [rsp+78h]
yield Characteristic("indirect call"), insn.offset
yield Characteristic("indirect call"), ih.address
def extract_features(f, bb, insn):
@@ -365,16 +426,16 @@ def extract_features(f, bb, insn):
extract features from the given insn.
args:
f (smda.common.SmdaFunction): the function to process.
bb (smda.common.SmdaBasicBlock): the basic block to process.
insn (smda.common.SmdaInstruction): the instruction to process.
f: the function to process.
bb: the basic block to process.
insn: the instruction to process.
yields:
Tuple[Feature, int]: the features and their location found in this insn.
Tuple[Feature, Address]: the features and their location found in this insn.
"""
for insn_handler in INSTRUCTION_HANDLERS:
for feature, va in insn_handler(f, bb, insn):
yield feature, va
for feature, addr in insn_handler(f, bb, insn):
yield feature, addr
INSTRUCTION_HANDLERS = (
@@ -385,6 +446,7 @@ INSTRUCTION_HANDLERS = (
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,

View File

@@ -1,6 +1,6 @@
# strings code from FLOSS, https://github.com/fireeye/flare-floss
# strings code from FLOSS, https://github.com/mandiant/flare-floss
#
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -8,27 +8,30 @@
import string
import struct
from typing import Tuple, Iterator
import envi
import envi.archs.i386.disasm
from capa.features.common import Characteristic
from capa.features.common import Feature, Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress
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 interface_extract_basic_block_XXX(f, bb):
def interface_extract_basic_block_XXX(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse features from the given basic block.
args:
f (viv_utils.Function): the function to process.
bb (viv_utils.BasicBlock): the basic block to process.
f: the function to process.
bb: the basic block to process.
yields:
(Feature, int): the feature and the address at which its found.
(Feature, Address): the feature and the address at which its found.
"""
yield NotImplementedError("feature"), NotImplementedError("virtual address")
...
def _bb_has_tight_loop(f, bb):
@@ -44,10 +47,10 @@ def _bb_has_tight_loop(f, bb):
return False
def extract_bb_tight_loop(f, bb):
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, bb):
yield Characteristic("tight loop"), bb.va
if _bb_has_tight_loop(f, bb.inner):
yield Characteristic("tight loop"), bb.address
def _bb_has_stackstring(f, bb):
@@ -67,10 +70,10 @@ def _bb_has_stackstring(f, bb):
return False
def extract_stackstring(f, bb):
def extract_stackstring(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""check basic block for stackstring indicators"""
if _bb_has_stackstring(f, bb):
yield Characteristic("stack string"), bb.va
if _bb_has_stackstring(f, bb.inner):
yield Characteristic("stack string"), bb.address
def is_mov_imm_to_stack(instr: envi.archs.i386.disasm.i386Opcode) -> bool:
@@ -143,7 +146,7 @@ def is_printable_utf16le(chars: bytes) -> bool:
return False
def extract_features(f, bb):
def extract_features(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract features from the given basic block.
@@ -154,10 +157,10 @@ def extract_features(f, bb):
yields:
Tuple[Feature, int]: the features and their location found in this basic block.
"""
yield BasicBlock(), bb.va
yield BasicBlock(), AbsoluteVirtualAddress(bb.inner.va)
for bb_handler in BASIC_BLOCK_HANDLERS:
for feature, va in bb_handler(f, bb):
yield feature, va
for feature, addr in bb_handler(f, bb):
yield feature, addr
BASIC_BLOCK_HANDLERS = (

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,6 +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.
import logging
from typing import List, Tuple, Iterator
import viv_utils
import viv_utils.flirt
@@ -16,24 +17,13 @@ import capa.features.extractors.viv.insn
import capa.features.extractors.viv.global_
import capa.features.extractors.viv.function
import capa.features.extractors.viv.basicblock
from capa.features.extractors.base_extractor import FeatureExtractor
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
logger = logging.getLogger(__name__)
class InstructionHandle:
"""this acts like a vivisect.Opcode but with an __int__() method"""
def __init__(self, inner):
self._inner = inner
def __int__(self):
return self.va
def __getattr__(self, name):
return getattr(self._inner, name)
class VivisectFeatureExtractor(FeatureExtractor):
def __init__(self, vw, path):
super(VivisectFeatureExtractor, self).__init__()
@@ -43,13 +33,13 @@ class VivisectFeatureExtractor(FeatureExtractor):
self.buf = f.read()
# pre-compute these because we'll yield them at *every* scope.
self.global_features = []
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.viv.global_.extract_arch(self.vw))
def get_base_address(self):
# assume there is only one file loaded into the vw
return list(self.vw.filemeta.values())[0]["imagebase"]
return AbsoluteVirtualAddress(list(self.vw.filemeta.values())[0]["imagebase"])
def extract_global_features(self):
yield from self.global_features
@@ -57,28 +47,33 @@ class VivisectFeatureExtractor(FeatureExtractor):
def extract_file_features(self):
yield from capa.features.extractors.viv.file.extract_features(self.vw, self.buf)
def get_functions(self):
def get_functions(self) -> Iterator[FunctionHandle]:
for va in sorted(self.vw.getFunctions()):
yield viv_utils.Function(self.vw, va)
yield FunctionHandle(address=AbsoluteVirtualAddress(va), inner=viv_utils.Function(self.vw, va))
def extract_function_features(self, f):
yield from capa.features.extractors.viv.function.extract_features(f)
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.viv.function.extract_features(fh)
def get_basic_blocks(self, f):
return f.basic_blocks
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
f: viv_utils.Function = fh.inner
for bb in f.basic_blocks:
yield BBHandle(address=AbsoluteVirtualAddress(bb.va), inner=bb)
def extract_basic_block_features(self, f, bb):
yield from capa.features.extractors.viv.basicblock.extract_features(f, bb)
def extract_basic_block_features(self, fh: FunctionHandle, bbh) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.viv.basicblock.extract_features(fh, bbh)
def get_instructions(self, f, bb):
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
bb: viv_utils.BasicBlock = bbh.inner
for insn in bb.instructions:
yield InstructionHandle(insn)
yield InsnHandle(address=AbsoluteVirtualAddress(insn.va), inner=insn)
def extract_insn_features(self, f, bb, insn):
yield from capa.features.extractors.viv.insn.extract_features(f, bb, insn)
def extract_insn_features(
self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.viv.insn.extract_features(fh, bbh, ih)
def is_library_function(self, va):
return viv_utils.flirt.is_library_function(self.vw, va)
def is_library_function(self, addr):
return viv_utils.flirt.is_library_function(self.vw, addr)
def get_function_name(self, va):
return viv_utils.get_function_name(self.vw, va)
def get_function_name(self, addr):
return viv_utils.get_function_name(self.vw, addr)

View File

@@ -1,10 +1,11 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from typing import Tuple, Iterator
import PE.carve as pe_carve # vivisect PE
import viv_utils
@@ -15,20 +16,21 @@ import capa.features.extractors.common
import capa.features.extractors.helpers
import capa.features.extractors.strings
from capa.features.file import Export, Import, Section, FunctionName
from capa.features.common import String, Characteristic
from capa.features.common import String, Feature, Characteristic
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
def extract_file_embedded_pe(buf, **kwargs):
def extract_file_embedded_pe(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]:
for offset, _ in pe_carve.carve(buf, 1):
yield Characteristic("embedded pe"), offset
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
def extract_file_export_names(vw, **kwargs):
def extract_file_export_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
for va, _, name, _ in vw.getExports():
yield Export(name), va
yield Export(name), AbsoluteVirtualAddress(va)
def extract_file_import_names(vw, **kwargs):
def extract_file_import_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
"""
extract imported function names
1. imports by ordinal:
@@ -44,8 +46,9 @@ def extract_file_import_names(vw, **kwargs):
# replace ord prefix with #
impname = "#%s" % impname[len("ord") :]
addr = AbsoluteVirtualAddress(va)
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
yield Import(name), va
yield Import(name), addr
def is_viv_ord_impname(impname: str) -> bool:
@@ -62,30 +65,37 @@ def is_viv_ord_impname(impname: str) -> bool:
return True
def extract_file_section_names(vw, **kwargs):
def extract_file_section_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
for va, _, segname, _ in vw.getSegments():
yield Section(segname), va
yield Section(segname), AbsoluteVirtualAddress(va)
def extract_file_strings(buf, **kwargs):
def extract_file_strings(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.common.extract_file_strings(buf)
def extract_file_function_names(vw, **kwargs):
def extract_file_function_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
"""
extract the names of statically-linked library functions.
"""
for va in sorted(vw.getFunctions()):
addr = AbsoluteVirtualAddress(va)
if viv_utils.flirt.is_library_function(vw, va):
name = viv_utils.get_function_name(vw, va)
yield FunctionName(name), va
yield FunctionName(name), addr
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions.
# extract features for both the mangled and un-mangled representations.
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield FunctionName(name[1:]), addr
def extract_file_format(buf, **kwargs):
def extract_file_format(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.common.extract_format(buf)
def extract_features(vw, buf: bytes):
def extract_features(vw, buf: bytes) -> Iterator[Tuple[Feature, Address]]:
"""
extract file features from given workspace
@@ -94,12 +104,12 @@ def extract_features(vw, buf: bytes):
buf: the raw input file bytes
yields:
Tuple[Feature, VA]: a feature and its location.
Tuple[Feature, Address]: a feature and its location.
"""
for file_handler in FILE_HANDLERS:
for feature, va in file_handler(vw=vw, buf=buf): # type: ignore
yield feature, va
for feature, addr in file_handler(vw=vw, buf=buf): # type: ignore
yield feature, addr
FILE_HANDLERS = (

View File

@@ -1,40 +1,47 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from typing import Tuple, Iterator
import envi
import viv_utils
import vivisect.const
from capa.features.common import Characteristic
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 interface_extract_function_XXX(f):
def interface_extract_function_XXX(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse features from the given function.
args:
f (viv_utils.Function): the function to process.
f: the function to process.
yields:
(Feature, int): the feature and the address at which its found.
(Feature, Address): the feature and the address at which its found.
"""
yield NotImplementedError("feature"), NotImplementedError("virtual address")
...
def extract_function_calls_to(f):
def extract_function_calls_to(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
f: viv_utils.Function = fhandle.inner
for src, _, _, _ in f.vw.getXrefsTo(f.va, rtype=vivisect.const.REF_CODE):
yield Characteristic("calls to"), src
yield Characteristic("calls to"), AbsoluteVirtualAddress(src)
def extract_function_loop(f):
def extract_function_loop(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse if a function has a loop
"""
f: viv_utils.Function = fhandle.inner
edges = []
for bb in f.basic_blocks:
@@ -50,22 +57,22 @@ def extract_function_loop(f):
edges.append((bb.va, bva))
if edges and loops.has_loop(edges):
yield Characteristic("loop"), f.va
yield Characteristic("loop"), fhandle.address
def extract_features(f):
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract features from the given function.
args:
f (viv_utils.Function): the function from which to extract features
fh: the function handle from which to extract features
yields:
Tuple[Feature, int]: the features and their location found in this function.
"""
for func_handler in FUNCTION_HANDLERS:
for feature, va in func_handler(f):
yield feature, va
for feature, addr in func_handler(fh):
yield feature, addr
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)

View File

@@ -1,19 +1,21 @@
import logging
from typing import Tuple, Iterator
import envi.archs.i386
import envi.archs.amd64
from capa.features.common import ARCH_I386, ARCH_AMD64, Arch
from capa.features.common import ARCH_I386, ARCH_AMD64, Arch, Feature
from capa.features.address import NO_ADDRESS, Address
logger = logging.getLogger(__name__)
def extract_arch(vw):
def extract_arch(vw) -> Iterator[Tuple[Feature, Address]]:
if isinstance(vw.arch, envi.archs.amd64.Amd64Module):
yield Arch(ARCH_AMD64), 0x0
yield Arch(ARCH_AMD64), NO_ADDRESS
elif isinstance(vw.arch, envi.archs.i386.i386Module):
yield Arch(ARCH_I386), 0x0
yield Arch(ARCH_I386), NO_ADDRESS
else:
# we likely end up here:

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -7,7 +7,7 @@
# See the License for the specific language governing permissions and limitations under the License.
import collections
from typing import TYPE_CHECKING, Set, List, Deque, Tuple, Union, Optional
from typing import Set, List, Deque, Tuple, Union, Optional
import envi
import vivisect.const
@@ -15,9 +15,6 @@ import envi.archs.i386.disasm
import envi.archs.amd64.disasm
from vivisect import VivWorkspace
if TYPE_CHECKING:
from capa.features.extractors.viv.extractor import InstructionHandle
# pull out consts for lookup performance
i386RegOper = envi.archs.i386.disasm.i386RegOper
i386ImmOper = envi.archs.i386.disasm.i386ImmOper
@@ -135,16 +132,14 @@ def find_definition(vw: VivWorkspace, va: int, reg: int) -> Tuple[int, Union[int
raise NotFoundError()
def is_indirect_call(vw: VivWorkspace, va: int, insn: Optional["InstructionHandle"] = None) -> bool:
def is_indirect_call(vw: VivWorkspace, va: int, insn: envi.Opcode) -> bool:
if insn is None:
insn = vw.parseOpcode(va)
return insn.mnem in ("call", "jmp") and isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper)
def resolve_indirect_call(
vw: VivWorkspace, va: int, insn: Optional["InstructionHandle"] = None
) -> Tuple[int, Optional[int]]:
def resolve_indirect_call(vw: VivWorkspace, va: int, insn: envi.Opcode) -> Tuple[int, Optional[int]]:
"""
inspect the given indirect call instruction and attempt to resolve the target address.

View File

@@ -1,10 +1,12 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from typing import List, Tuple, Callable, Iterator
import envi
import envi.exc
import viv_utils
@@ -17,16 +19,10 @@ import envi.archs.amd64.disasm
import capa.features.extractors.helpers
import capa.features.extractors.viv.helpers
from capa.features.insn import API, Number, Offset, Mnemonic
from capa.features.common import (
BITNESS_X32,
BITNESS_X64,
MAX_BYTES_FEATURE_SIZE,
THUNK_CHAIN_DEPTH_DELTA,
Bytes,
String,
Characteristic,
)
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
from capa.features.extractors.viv.indirect_calls import NotFoundError, resolve_indirect_call
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
@@ -34,27 +30,21 @@ from capa.features.extractors.viv.indirect_calls import NotFoundError, resolve_i
SECURITY_COOKIE_BYTES_DELTA = 0x40
def get_bitness(vw):
bitness = vw.getMeta("Architecture")
if bitness == "i386":
return BITNESS_X32
elif bitness == "amd64":
return BITNESS_X64
def interface_extract_instruction_XXX(f, bb, insn):
def interface_extract_instruction_XXX(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse features from the given instruction.
args:
f (viv_utils.Function): the function to process.
bb (viv_utils.BasicBlock): the basic block to process.
insn (vivisect...Instruction): the instruction to process.
fh: the function handle to process.
bbh: the basic block handle to process.
ih: the instruction handle to process.
yields:
(Feature, int): the feature and the address at which its found.
(Feature, Address): the feature and the address at which its found.
"""
yield NotImplementedError("feature"), NotImplementedError("virtual address")
...
def get_imports(vw):
@@ -74,12 +64,15 @@ def get_imports(vw):
return imports
def extract_insn_api_features(f, bb, insn):
"""parse API features from the given instruction."""
def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse API features from the given instruction.
# example:
#
# call dword [0x00473038]
example:
call dword [0x00473038]
"""
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
if insn.mnem not in ("call", "jmp"):
return
@@ -96,7 +89,7 @@ def extract_insn_api_features(f, bb, insn):
if target in imports:
dll, symbol = imports[target]
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield API(name), insn.va
yield API(name), ih.address
# call via thunk on x86,
# see 9324d1a8ae37a36ae560c37448c9705a at 0x407985
@@ -118,14 +111,20 @@ def extract_insn_api_features(f, bb, insn):
if viv_utils.flirt.is_library_function(f.vw, target):
name = viv_utils.get_function_name(f.vw, target)
yield API(name), insn.va
yield API(name), ih.address
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions.
# extract features for both the mangled and un-mangled representations.
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield API(name[1:]), ih.address
return
for _ in range(THUNK_CHAIN_DEPTH_DELTA):
if target in imports:
dll, symbol = imports[target]
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield API(name), insn.va
yield API(name), ih.address
# if jump leads to an ENDBRANCH instruction, skip it
if f.vw.getByteDef(target)[1].startswith(b"\xf3\x0f\x1e"):
@@ -145,7 +144,7 @@ def extract_insn_api_features(f, bb, insn):
if target in imports:
dll, symbol = imports[target]
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield API(name), insn.va
yield API(name), ih.address
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper):
try:
@@ -162,38 +161,7 @@ def extract_insn_api_features(f, bb, insn):
if target in imports:
dll, symbol = imports[target]
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield API(name), insn.va
def extract_insn_number_features(f, bb, insn):
"""parse number features from the given instruction."""
# example:
#
# push 3136B0h ; dwControlCode
for oper in insn.opers:
# this is for both x32 and x64
if not isinstance(oper, (envi.archs.i386.disasm.i386ImmOper, envi.archs.i386.disasm.i386ImmMemOper)):
continue
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
v = oper.getOperValue(oper)
else:
v = oper.getOperAddr(oper)
if f.vw.probeMemory(v, 1, envi.memory.MM_READ):
# this is a valid address
# assume its not also a constant.
continue
if insn.mnem == "add" and insn.opers[0].isReg() and insn.opers[0].reg == envi.archs.i386.regs.REG_ESP:
# skip things like:
#
# .text:00401140 call sub_407E2B
# .text:00401145 add esp, 0Ch
return
yield Number(v), insn.va
yield Number(v, bitness=get_bitness(f.vw)), insn.va
yield API(name), ih.address
def derefs(vw, p):
@@ -266,12 +234,15 @@ def read_bytes(vw, va: int) -> bytes:
raise
def extract_insn_bytes_features(f, bb, insn):
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
"""
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
if insn.mnem == "call":
return
@@ -300,7 +271,7 @@ def extract_insn_bytes_features(f, bb, insn):
if capa.features.extractors.helpers.all_zeros(buf):
continue
yield Bytes(buf), insn.va
yield Bytes(buf), ih.address
def read_string(vw, offset: int) -> str:
@@ -334,75 +305,6 @@ def read_string(vw, offset: int) -> str:
raise ValueError("not a string", offset)
def extract_insn_string_features(f, bb, insn):
"""parse string features from the given instruction."""
# example:
#
# push offset aAcr ; "ACR > "
for oper in insn.opers:
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
v = oper.getOperValue(oper)
elif isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper):
# like 0x10056CB4 in `lea eax, dword [0x10056CB4]`
v = oper.imm
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
# like 0x401000 in `mov eax, 0x401000[2 * ebx]`
v = oper.imm
elif isinstance(oper, envi.archs.amd64.disasm.Amd64RipRelOper):
v = oper.getOperAddr(insn)
else:
continue
for v in derefs(f.vw, v):
try:
s = read_string(f.vw, v)
except ValueError:
continue
else:
yield String(s.rstrip("\x00")), insn.va
def extract_insn_offset_features(f, bb, insn):
"""parse structure offset features from the given instruction."""
# example:
#
# .text:0040112F cmp [esi+4], ebx
for oper in insn.opers:
# this is for both x32 and x64
# like [esi + 4]
# reg ^
# disp
if isinstance(oper, envi.archs.i386.disasm.i386RegMemOper):
if oper.reg == envi.archs.i386.regs.REG_ESP:
continue
if oper.reg == envi.archs.i386.regs.REG_EBP:
continue
# TODO: do x64 support for real.
if oper.reg == envi.archs.amd64.regs.REG_RBP:
continue
# viv already decodes offsets as signed
v = oper.disp
yield Offset(v), insn.va
yield Offset(v, bitness=get_bitness(f.vw)), insn.va
# like: [esi + ecx + 16384]
# reg ^ ^
# index ^
# disp
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
# viv already decodes offsets as signed
v = oper.disp
yield Offset(v), insn.va
yield Offset(v, bitness=get_bitness(f.vw)), insn.va
def is_security_cookie(f, bb, insn) -> bool:
"""
check if an instruction is related to security cookie checks
@@ -431,11 +333,17 @@ def is_security_cookie(f, bb, insn) -> bool:
return False
def extract_insn_nzxor_characteristic_features(f, bb, insn):
def extract_insn_nzxor_characteristic_features(
fh: FunctionHandle, bbhandle: 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.
"""
insn: envi.Opcode = ih.inner
bb: viv_utils.BasicBlock = bbhandle.inner
f: viv_utils.Function = fh.inner
if insn.mnem not in ("xor", "xorpd", "xorps", "pxor"):
return
@@ -445,19 +353,40 @@ def extract_insn_nzxor_characteristic_features(f, bb, insn):
if is_security_cookie(f, bb, insn):
return
yield Characteristic("nzxor"), insn.va
yield Characteristic("nzxor"), ih.address
def extract_insn_mnemonic_features(f, bb, insn):
def extract_insn_mnemonic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse mnemonic features from the given instruction."""
yield Mnemonic(insn.mnem), insn.va
yield Mnemonic(ih.inner.mnem), ih.address
def extract_insn_peb_access_characteristic_features(f, bb, insn):
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: envi.Opcode = ih.inner
if insn.mnem != "call":
return
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386PcRelOper):
if insn.va + 5 == insn.opers[0].getOperValue(insn):
yield Characteristic("call $+5"), ih.address
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper) or isinstance(
insn.opers[0], envi.archs.amd64.disasm.Amd64RipRelOper
):
if insn.va + 5 == insn.opers[0].getOperAddr(insn):
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
"""
# TODO handle where fs/gs are loaded into a register or onto the stack and used later
insn: envi.Opcode = ih.inner
if insn.mnem not in ["push", "mov"]:
return
@@ -476,7 +405,7 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
if (isinstance(oper, envi.archs.i386.disasm.i386RegMemOper) and oper.disp == 0x30) or (
isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper) and oper.imm == 0x30
):
yield Characteristic("peb access"), insn.va
yield Characteristic("peb access"), ih.address
elif "gs" in prefix:
for oper in insn.opers:
if (
@@ -484,20 +413,22 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
or (isinstance(oper, envi.archs.amd64.disasm.i386SibOper) and oper.imm == 0x60)
or (isinstance(oper, envi.archs.amd64.disasm.i386ImmMemOper) and oper.imm == 0x60)
):
yield Characteristic("peb access"), insn.va
yield Characteristic("peb access"), ih.address
else:
pass
def extract_insn_segment_access_features(f, bb, insn):
def extract_insn_segment_access_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""parse the instruction for access to fs or gs"""
insn: envi.Opcode = ih.inner
prefix = insn.getPrefixName()
if prefix == "fs":
yield Characteristic("fs access"), insn.va
yield Characteristic("fs access"), ih.address
if prefix == "gs":
yield Characteristic("gs access"), insn.va
yield Characteristic("gs access"), ih.address
def get_section(vw, va: int):
@@ -508,10 +439,13 @@ def get_section(vw, va: int):
raise KeyError(va)
def extract_insn_cross_section_cflow(f, bb, insn):
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.
"""
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
for va, flags in insn.getBranches():
if va is None:
# va may be none for dynamic branches that haven't been resolved, such as `jmp eax`.
@@ -538,7 +472,7 @@ def extract_insn_cross_section_cflow(f, bb, insn):
continue
if get_section(f.vw, insn.va) != get_section(f.vw, va):
yield Characteristic("cross section flow"), insn.va
yield Characteristic("cross section flow"), ih.address
except KeyError:
continue
@@ -546,7 +480,10 @@ def extract_insn_cross_section_cflow(f, bb, insn):
# 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(f, bb, insn):
def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
if insn.mnem != "call":
return
@@ -556,7 +493,7 @@ def extract_function_calls_from(f, bb, insn):
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper):
oper = insn.opers[0]
target = oper.getOperAddr(insn)
yield Characteristic("calls from"), target
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
# call via thunk on x86,
# see 9324d1a8ae37a36ae560c37448c9705a at 0x407985
@@ -565,43 +502,191 @@ def extract_function_calls_from(f, bb, insn):
# see Lab21-01.exe_:0x140001178
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386PcRelOper):
target = insn.opers[0].getOperValue(insn)
yield Characteristic("calls from"), target
if target >= 0:
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
# call via IAT, x64
elif isinstance(insn.opers[0], envi.archs.amd64.disasm.Amd64RipRelOper):
op = insn.opers[0]
target = op.getOperAddr(insn)
yield Characteristic("calls from"), target
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
if target and target == f.va:
# if we found a jump target and it's the function address
# mark as recursive
yield Characteristic("recursive call"), target
yield Characteristic("recursive call"), AbsoluteVirtualAddress(target)
# 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, insn):
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: envi.Opcode = ih.inner
if insn.mnem != "call":
return
# Checks below work for x86 and x64
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper):
# call edx
yield Characteristic("indirect call"), insn.va
yield Characteristic("indirect call"), ih.address
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegMemOper):
# call dword ptr [eax+50h]
yield Characteristic("indirect call"), insn.va
yield Characteristic("indirect call"), ih.address
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386SibOper):
# call qword ptr [rsp+78h]
yield Characteristic("indirect call"), insn.va
yield Characteristic("indirect call"), ih.address
def extract_features(f, bb, insn):
def extract_op_number_features(
fh: FunctionHandle, bb, ih: InsnHandle, i, oper: envi.Operand
) -> Iterator[Tuple[Feature, Address]]:
"""parse number features from the given operand.
example:
push 3136B0h ; dwControlCode
"""
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
# this is for both x32 and x64
if not isinstance(oper, (envi.archs.i386.disasm.i386ImmOper, envi.archs.i386.disasm.i386ImmMemOper)):
return
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
v = oper.getOperValue(oper)
else:
v = oper.getOperAddr(oper)
if f.vw.probeMemory(v, 1, envi.memory.MM_READ):
# this is a valid address
# assume its not also a constant.
return
if insn.mnem == "add" and insn.opers[0].isReg() and insn.opers[0].reg == envi.archs.i386.regs.REG_ESP:
# skip things like:
#
# .text:00401140 call sub_407E2B
# .text:00401145 add esp, 0Ch
return
yield Number(v), ih.address
yield OperandNumber(i, v), ih.address
if insn.mnem == "add" and 0 < v < MAX_STRUCTURE_SIZE and isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
# for pattern like:
#
# add eax, 0x10
#
# assume 0x10 is also an offset (imagine eax is a pointer).
yield Offset(v), ih.address
yield OperandOffset(i, v), ih.address
def extract_op_offset_features(
fh: FunctionHandle, bb, ih: InsnHandle, i, oper: envi.Operand
) -> Iterator[Tuple[Feature, Address]]:
"""parse structure offset features from the given operand."""
# example:
#
# .text:0040112F cmp [esi+4], ebx
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
# this is for both x32 and x64
# like [esi + 4]
# reg ^
# disp
if isinstance(oper, envi.archs.i386.disasm.i386RegMemOper):
if oper.reg == envi.archs.i386.regs.REG_ESP:
return
if oper.reg == envi.archs.i386.regs.REG_EBP:
return
# TODO: do x64 support for real.
if oper.reg == envi.archs.amd64.regs.REG_RBP:
return
# viv already decodes offsets as signed
v = oper.disp
yield Offset(v), ih.address
yield OperandOffset(i, v), ih.address
if insn.mnem == "lea" and i == 1 and not f.vw.probeMemory(v, 1, envi.memory.MM_READ):
# for pattern like:
#
# lea eax, [ebx + 1]
#
# assume 1 is also an offset (imagine ebx is a zero register).
yield Number(v), ih.address
yield OperandNumber(i, v), ih.address
# like: [esi + ecx + 16384]
# reg ^ ^
# index ^
# disp
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
# viv already decodes offsets as signed
v = oper.disp
yield Offset(v), ih.address
yield OperandOffset(i, v), ih.address
def extract_op_string_features(
fh: FunctionHandle, bb, ih: InsnHandle, i, oper: envi.Operand
) -> Iterator[Tuple[Feature, Address]]:
"""parse string features from the given operand."""
# example:
#
# push offset aAcr ; "ACR > "
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
v = oper.getOperValue(oper)
elif isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper):
# like 0x10056CB4 in `lea eax, dword [0x10056CB4]`
v = oper.imm
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
# like 0x401000 in `mov eax, 0x401000[2 * ebx]`
v = oper.imm
elif isinstance(oper, envi.archs.amd64.disasm.Amd64RipRelOper):
v = oper.getOperAddr(insn)
else:
return
for v in derefs(f.vw, v):
try:
s = read_string(f.vw, v)
except ValueError:
continue
else:
yield String(s.rstrip("\x00")), ih.address
def extract_operand_features(f: FunctionHandle, bb, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
for i, oper in enumerate(insn.inner.opers):
for op_handler in OPERAND_HANDLERS:
for feature, addr in op_handler(f, bb, insn, i, oper):
yield feature, addr
OPERAND_HANDLERS: List[
Callable[[FunctionHandle, BBHandle, InsnHandle, int, envi.Operand], Iterator[Tuple[Feature, Address]]]
] = [
extract_op_number_features,
extract_op_offset_features,
extract_op_string_features,
]
def extract_features(f, bb, insn) -> Iterator[Tuple[Feature, Address]]:
"""
extract features from the given insn.
@@ -611,24 +696,23 @@ def extract_features(f, bb, insn):
insn (vivisect...Instruction): the instruction to process.
yields:
Tuple[Feature, int]: the features and their location found in this insn.
Tuple[Feature, Address]: the features and their location found in this insn.
"""
for insn_handler in INSTRUCTION_HANDLERS:
for feature, va in insn_handler(f, bb, insn):
yield feature, va
for feature, addr in insn_handler(f, bb, insn):
yield feature, addr
INSTRUCTION_HANDLERS = (
INSTRUCTION_HANDLERS: List[Callable[[FunctionHandle, BBHandle, InsnHandle], Iterator[Tuple[Feature, Address]]]] = [
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,
)
extract_operand_features,
]

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -1,284 +0,0 @@
"""
capa freeze file format: `| capa0000 | + zlib(utf-8(json(...)))`
json format:
{
'version': 1,
'base address': int(base address),
'functions': {
int(function va): {
'basic blocks': {
int(basic block va): {
'instructions': [instruction va, ...]
},
...
},
...
},
...
},
'scopes': {
'global': [
(str(name), [any(arg), ...], int(va), ()),
...
},
'file': [
(str(name), [any(arg), ...], int(va), ()),
...
},
'function': [
(str(name), [any(arg), ...], int(va), (int(function va), )),
...
],
'basic block': [
(str(name), [any(arg), ...], int(va), (int(function va),
int(basic block va))),
...
],
'instruction': [
(str(name), [any(arg), ...], int(va), (int(function va),
int(basic block va),
int(instruction va))),
...
],
}
}
Copyright (C) 2020 FireEye, 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 json
import zlib
import logging
import capa.features.file
import capa.features.insn
import capa.features.common
import capa.features.basicblock
import capa.features.extractors.base_extractor
from capa.helpers import hex
logger = logging.getLogger(__name__)
def serialize_feature(feature):
return feature.freeze_serialize()
KNOWN_FEATURES = {F.__name__: F for F in capa.features.common.Feature.__subclasses__()}
def deserialize_feature(doc):
F = KNOWN_FEATURES[doc[0]]
return F.freeze_deserialize(doc[1])
def dumps(extractor):
"""
serialize the given extractor to a string
args:
extractor: capa.features.extractors.base_extractor.FeatureExtractor:
returns:
str: the serialized features.
"""
ret = {
"version": 1,
"base address": extractor.get_base_address(),
"functions": {},
"scopes": {
"global": [],
"file": [],
"function": [],
"basic block": [],
"instruction": [],
},
}
for feature, va in extractor.extract_global_features():
ret["scopes"]["global"].append(serialize_feature(feature) + (hex(va), ()))
for feature, va in extractor.extract_file_features():
ret["scopes"]["file"].append(serialize_feature(feature) + (hex(va), ()))
for f in extractor.get_functions():
ret["functions"][hex(f)] = {}
for feature, va in extractor.extract_function_features(f):
ret["scopes"]["function"].append(serialize_feature(feature) + (hex(va), (hex(f),)))
for bb in extractor.get_basic_blocks(f):
ret["functions"][hex(f)][hex(bb)] = []
for feature, va in extractor.extract_basic_block_features(f, bb):
ret["scopes"]["basic block"].append(
serialize_feature(feature)
+ (
hex(va),
(
hex(f),
hex(bb),
),
)
)
for insnva, insn in sorted(
[(int(insn), insn) for insn in extractor.get_instructions(f, bb)], key=lambda p: p[0]
):
ret["functions"][hex(f)][hex(bb)].append(hex(insnva))
for feature, va in extractor.extract_insn_features(f, bb, insn):
ret["scopes"]["instruction"].append(
serialize_feature(feature)
+ (
hex(va),
(
hex(f),
hex(bb),
hex(insnva),
),
)
)
return json.dumps(ret)
def loads(s):
"""deserialize a set of features (as a NullFeatureExtractor) from a string."""
doc = json.loads(s)
if doc.get("version") != 1:
raise ValueError("unsupported freeze format version: %d" % (doc.get("version")))
features = {
"base address": doc.get("base address"),
"global features": [],
"file features": [],
"functions": {},
}
for fva, function in doc.get("functions", {}).items():
fva = int(fva, 0x10)
features["functions"][fva] = {
"features": [],
"basic blocks": {},
}
for bbva, bb in function.items():
bbva = int(bbva, 0x10)
features["functions"][fva]["basic blocks"][bbva] = {
"features": [],
"instructions": {},
}
for insnva in bb:
insnva = int(insnva, 0x10)
features["functions"][fva]["basic blocks"][bbva]["instructions"][insnva] = {
"features": [],
}
# in the following blocks, each entry looks like:
#
# ('MatchedRule', ('foo', ), '0x401000', ('0x401000', ))
# ^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^
# feature name args addr func/bb/insn
for feature in doc.get("scopes", {}).get("global", []):
va, loc = feature[2:]
va = int(va, 0x10)
feature = deserialize_feature(feature[:2])
features["global features"].append((va, feature))
for feature in doc.get("scopes", {}).get("file", []):
va, loc = feature[2:]
va = int(va, 0x10)
feature = deserialize_feature(feature[:2])
features["file features"].append((va, feature))
for feature in doc.get("scopes", {}).get("function", []):
# fetch the pair like:
#
# ('0x401000', ('0x401000', ))
# ^^^^^^^^^^ ^^^^^^^^^^^^^^
# addr func/bb/insn
va, loc = feature[2:]
va = int(va, 0x10)
loc = [int(lo, 0x10) for lo in loc]
# decode the feature from the pair like:
#
# ('MatchedRule', ('foo', ))
# ^^^^^^^^^^^^^ ^^^^^^^^^
# feature name args
feature = deserialize_feature(feature[:2])
features["functions"][loc[0]]["features"].append((va, feature))
for feature in doc.get("scopes", {}).get("basic block", []):
va, loc = feature[2:]
va = int(va, 0x10)
loc = [int(lo, 0x10) for lo in loc]
feature = deserialize_feature(feature[:2])
features["functions"][loc[0]]["basic blocks"][loc[1]]["features"].append((va, feature))
for feature in doc.get("scopes", {}).get("instruction", []):
va, loc = feature[2:]
va = int(va, 0x10)
loc = [int(lo, 0x10) for lo in loc]
feature = deserialize_feature(feature[:2])
features["functions"][loc[0]]["basic blocks"][loc[1]]["instructions"][loc[2]]["features"].append((va, feature))
return capa.features.extractors.base_extractor.NullFeatureExtractor(features)
MAGIC = "capa0000".encode("ascii")
def dump(extractor):
"""serialize the given extractor to a byte array."""
return MAGIC + zlib.compress(dumps(extractor).encode("utf-8"))
def is_freeze(buf: bytes) -> bool:
return buf[: len(MAGIC)] == MAGIC
def load(buf):
"""deserialize a set of features (as a NullFeatureExtractor) from a byte array."""
if not is_freeze(buf):
raise ValueError("missing magic header")
return loads(zlib.decompress(buf[len(MAGIC) :]).decode("utf-8"))
def main(argv=None):
import sys
import argparse
import capa.main
if argv is None:
argv = sys.argv[1:]
parser = argparse.ArgumentParser(description="save capa features to a file")
capa.main.install_common_args(parser, {"sample", "format", "backend", "signatures"})
parser.add_argument("output", type=str, help="Path to output file")
args = parser.parse_args(args=argv)
capa.main.handle_common_args(args)
sigpaths = capa.main.get_signatures(args.signatures)
extractor = capa.main.get_extractor(args.sample, args.format, args.backend, sigpaths, False)
with open(args.output, "wb") as f:
f.write(dump(extractor))
return 0
if __name__ == "__main__":
import sys
sys.exit(main())

View File

@@ -0,0 +1,393 @@
"""
capa freeze file format: `| capa0000 | + zlib(utf-8(json(...)))`
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 zlib
import logging
from enum import Enum
from typing import Any, List, Tuple
import dncil.clr.token
from pydantic import Field, BaseModel
import capa.helpers
import capa.version
import capa.features.file
import capa.features.insn
import capa.features.common
import capa.features.address
import capa.features.basicblock
import capa.features.extractors.base_extractor
from capa.helpers import assert_never
from capa.features.freeze.features import Feature, feature_from_capa
logger = logging.getLogger(__name__)
class HashableModel(BaseModel):
class Config:
frozen = True
class AddressType(str, Enum):
ABSOLUTE = "absolute"
RELATIVE = "relative"
FILE = "file"
DN_TOKEN = "dn token"
DN_TOKEN_OFFSET = "dn token offset"
NO_ADDRESS = "no address"
class Address(HashableModel):
type: AddressType
value: Any
@classmethod
def from_capa(cls, a: capa.features.address.Address) -> "Address":
if isinstance(a, capa.features.address.AbsoluteVirtualAddress):
return cls(type=AddressType.ABSOLUTE, value=int(a))
elif isinstance(a, capa.features.address.RelativeVirtualAddress):
return cls(type=AddressType.RELATIVE, value=int(a))
elif isinstance(a, capa.features.address.FileOffsetAddress):
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)
elif isinstance(a, capa.features.address.DNTokenOffsetAddress):
return cls(type=AddressType.DN_TOKEN_OFFSET, value=(a.token.value, a.offset))
elif a == capa.features.address.NO_ADDRESS or isinstance(a, capa.features.address._NoAddress):
return cls(type=AddressType.NO_ADDRESS, value=None)
elif isinstance(a, capa.features.address.Address) and not issubclass(type(a), capa.features.address.Address):
raise ValueError("don't use an Address instance directly")
elif isinstance(a, capa.features.address.Address):
raise ValueError("don't use an Address instance directly")
else:
assert_never(a)
def to_capa(self) -> capa.features.address.Address:
if self.type is AddressType.ABSOLUTE:
return capa.features.address.AbsoluteVirtualAddress(self.value)
elif self.type is AddressType.RELATIVE:
return capa.features.address.RelativeVirtualAddress(self.value)
elif self.type is AddressType.FILE:
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))
elif self.type is AddressType.DN_TOKEN_OFFSET:
token, offset = self.value
return capa.features.address.DNTokenOffsetAddress(dncil.clr.token.Token(token), offset)
elif self.type is AddressType.NO_ADDRESS:
return capa.features.address.NO_ADDRESS
else:
assert_never(self.type)
def __lt__(self, other: "Address") -> bool:
if self.type != other.type:
return self.type < other.type
if self.type is AddressType.NO_ADDRESS:
return True
else:
return self.value < other.value
class GlobalFeature(HashableModel):
feature: Feature
class FileFeature(HashableModel):
address: Address
feature: Feature
class FunctionFeature(HashableModel):
"""
args:
function: the address of the function to which this feature belongs.
address: the address at which this feature is found.
function != address because, e.g., the feature may be found *within* the scope (function).
versus right at its starting address.
"""
function: Address
address: Address
feature: Feature
class BasicBlockFeature(HashableModel):
"""
args:
basic_block: the address of the basic block to which this feature belongs.
address: the address at which this feature is found.
basic_block != address because, e.g., the feature may be found *within* the scope (basic block).
versus right at its starting address.
"""
basic_block: Address
address: Address
feature: Feature
class InstructionFeature(HashableModel):
"""
args:
instruction: the address of the instruction to which this feature belongs.
address: the address at which this feature is found.
instruction != address because, e.g., the feature may be found *within* the scope (basic block),
versus right at its starting address.
"""
instruction: Address
address: Address
feature: Feature
class InstructionFeatures(BaseModel):
address: Address
features: Tuple[InstructionFeature, ...]
class BasicBlockFeatures(BaseModel):
address: Address
features: Tuple[BasicBlockFeature, ...]
instructions: Tuple[InstructionFeatures, ...]
class FunctionFeatures(BaseModel):
address: Address
features: Tuple[FunctionFeature, ...]
basic_blocks: Tuple[BasicBlockFeatures, ...] = Field(alias="basic block")
class Config:
allow_population_by_field_name = True
class Features(BaseModel):
global_: Tuple[GlobalFeature, ...] = Field(alias="global")
file: Tuple[FileFeature, ...]
functions: Tuple[FunctionFeatures, ...]
class Config:
allow_population_by_field_name = True
class Extractor(BaseModel):
name: str
version: str = capa.version.__version__
class Config:
allow_population_by_field_name = True
class Freeze(BaseModel):
version: int = 2
base_address: Address = Field(alias="base address")
extractor: Extractor
features: Features
class Config:
allow_population_by_field_name = True
def dumps(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -> str:
"""
serialize the given extractor to a string
"""
global_features: List[GlobalFeature] = []
for feature, _ in extractor.extract_global_features():
global_features.append(
GlobalFeature(
feature=feature_from_capa(feature),
)
)
file_features: List[FileFeature] = []
for feature, address in extractor.extract_file_features():
file_features.append(
FileFeature(
feature=feature_from_capa(feature),
address=Address.from_capa(address),
)
)
function_features: List[FunctionFeatures] = []
for f in extractor.get_functions():
faddr = Address.from_capa(f.address)
ffeatures = [
FunctionFeature(
function=faddr,
address=Address.from_capa(addr),
feature=feature_from_capa(feature),
)
for feature, addr in extractor.extract_function_features(f)
]
basic_blocks = []
for bb in extractor.get_basic_blocks(f):
bbaddr = Address.from_capa(bb.address)
bbfeatures = [
BasicBlockFeature(
basic_block=bbaddr,
address=Address.from_capa(addr),
feature=feature_from_capa(feature),
)
for feature, addr in extractor.extract_basic_block_features(f, bb)
]
instructions = []
for insn in extractor.get_instructions(f, bb):
iaddr = Address.from_capa(insn.address)
ifeatures = [
InstructionFeature(
instruction=iaddr,
address=Address.from_capa(addr),
feature=feature_from_capa(feature),
)
for feature, addr in extractor.extract_insn_features(f, bb, insn)
]
instructions.append(
InstructionFeatures(
address=iaddr,
features=ifeatures,
)
)
basic_blocks.append(
BasicBlockFeatures(
address=bbaddr,
features=bbfeatures,
instructions=instructions,
)
)
function_features.append(
FunctionFeatures(
address=faddr,
features=ffeatures,
basic_blocks=basic_blocks,
)
)
features = Features(
global_=global_features,
file=file_features,
functions=function_features,
)
freeze = Freeze(
version=2,
base_address=Address.from_capa(extractor.get_base_address()),
extractor=Extractor(name=extractor.__class__.__name__),
features=features,
)
return freeze.json()
def loads(s: str) -> capa.features.extractors.base_extractor.FeatureExtractor:
"""deserialize a set of features (as a NullFeatureExtractor) from a string."""
import capa.features.extractors.null as null
freeze = Freeze.parse_raw(s)
if freeze.version != 2:
raise ValueError("unsupported freeze format version: %d", freeze.version)
return null.NullFeatureExtractor(
base_address=freeze.base_address.to_capa(),
global_features=[f.feature.to_capa() for f in freeze.features.global_],
file_features=[(f.address.to_capa(), f.feature.to_capa()) for f in freeze.features.file],
functions={
f.address.to_capa(): null.FunctionFeatures(
features=[(fe.address.to_capa(), fe.feature.to_capa()) for fe in f.features],
basic_blocks={
bb.address.to_capa(): null.BasicBlockFeatures(
features=[(fe.address.to_capa(), fe.feature.to_capa()) for fe in bb.features],
instructions={
i.address.to_capa(): null.InstructionFeatures(
features=[(fe.address.to_capa(), fe.feature.to_capa()) for fe in i.features]
)
for i in bb.instructions
},
)
for bb in f.basic_blocks
},
)
for f in freeze.features.functions
},
)
MAGIC = "capa0000".encode("ascii")
def dump(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -> bytes:
"""serialize the given extractor to a byte array."""
return MAGIC + zlib.compress(dumps(extractor).encode("utf-8"))
def is_freeze(buf: bytes) -> bool:
return buf[: len(MAGIC)] == MAGIC
def load(buf: bytes) -> capa.features.extractors.base_extractor.FeatureExtractor:
"""deserialize a set of features (as a NullFeatureExtractor) from a byte array."""
if not is_freeze(buf):
raise ValueError("missing magic header")
return loads(zlib.decompress(buf[len(MAGIC) :]).decode("utf-8"))
def main(argv=None):
import sys
import argparse
import capa.main
if argv is None:
argv = sys.argv[1:]
parser = argparse.ArgumentParser(description="save capa features to a file")
capa.main.install_common_args(parser, {"sample", "format", "backend", "signatures"})
parser.add_argument("output", type=str, help="Path to output file")
args = parser.parse_args(args=argv)
capa.main.handle_common_args(args)
sigpaths = capa.main.get_signatures(args.signatures)
extractor = capa.main.get_extractor(args.sample, args.format, args.backend, sigpaths, False)
with open(args.output, "wb") as f:
f.write(dump(extractor))
return 0
if __name__ == "__main__":
import sys
sys.exit(main())

View File

@@ -0,0 +1,332 @@
import binascii
from typing import Union, Optional
from pydantic import Field, BaseModel
import capa.features.file
import capa.features.insn
import capa.features.common
import capa.features.basicblock
class FeatureModel(BaseModel):
class Config:
frozen = True
allow_population_by_field_name = True
def to_capa(self) -> capa.features.common.Feature:
if isinstance(self, OSFeature):
return capa.features.common.OS(self.os, description=self.description)
elif isinstance(self, ArchFeature):
return capa.features.common.Arch(self.arch, description=self.description)
elif isinstance(self, FormatFeature):
return capa.features.common.Format(self.format, description=self.description)
elif isinstance(self, MatchFeature):
return capa.features.common.MatchedRule(self.match, description=self.description)
elif isinstance(
self,
CharacteristicFeature,
):
return capa.features.common.Characteristic(self.characteristic, description=self.description)
elif isinstance(self, ExportFeature):
return capa.features.file.Export(self.export, description=self.description)
elif isinstance(self, ImportFeature):
return capa.features.file.Import(self.import_, description=self.description)
elif isinstance(self, SectionFeature):
return capa.features.file.Section(self.section, description=self.description)
elif isinstance(self, FunctionNameFeature):
return capa.features.file.FunctionName(self.function_name, description=self.description)
elif isinstance(self, SubstringFeature):
return capa.features.common.Substring(self.substring, description=self.description)
elif isinstance(self, RegexFeature):
return capa.features.common.Regex(self.regex, description=self.description)
elif isinstance(self, StringFeature):
return capa.features.common.String(self.string, description=self.description)
elif isinstance(self, ClassFeature):
return capa.features.common.Class(self.class_, description=self.description)
elif isinstance(self, NamespaceFeature):
return capa.features.common.Namespace(self.namespace, description=self.description)
elif isinstance(self, BasicBlockFeature):
return capa.features.basicblock.BasicBlock(description=self.description)
elif isinstance(self, APIFeature):
return capa.features.insn.API(self.api, description=self.description)
elif isinstance(self, NumberFeature):
return capa.features.insn.Number(self.number, description=self.description)
elif isinstance(self, BytesFeature):
return capa.features.common.Bytes(binascii.unhexlify(self.bytes), description=self.description)
elif isinstance(self, OffsetFeature):
return capa.features.insn.Offset(self.offset, description=self.description)
elif isinstance(self, MnemonicFeature):
return capa.features.insn.Mnemonic(self.mnemonic, description=self.description)
elif isinstance(self, OperandNumberFeature):
return capa.features.insn.OperandNumber(
self.index,
self.operand_number,
description=self.description,
)
elif isinstance(self, OperandOffsetFeature):
return capa.features.insn.OperandOffset(
self.index,
self.operand_offset,
description=self.description,
)
else:
raise NotImplementedError(f"Feature.to_capa({type(self)}) not implemented")
def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
if isinstance(f, capa.features.common.OS):
return OSFeature(os=f.value, description=f.description)
elif isinstance(f, capa.features.common.Arch):
return ArchFeature(arch=f.value, description=f.description)
elif isinstance(f, capa.features.common.Format):
return FormatFeature(format=f.value, description=f.description)
elif isinstance(f, capa.features.common.MatchedRule):
return MatchFeature(match=f.value, description=f.description)
elif isinstance(f, capa.features.common.Characteristic):
return CharacteristicFeature(characteristic=f.value, description=f.description)
elif isinstance(f, capa.features.file.Export):
return ExportFeature(export=f.value, description=f.description)
elif isinstance(f, capa.features.file.Import):
return ImportFeature(import_=f.value, description=f.description)
elif isinstance(f, capa.features.file.Section):
return SectionFeature(section=f.value, description=f.description)
elif isinstance(f, capa.features.file.FunctionName):
return FunctionNameFeature(function_name=f.value, description=f.description)
# must come before check for String due to inheritance
elif isinstance(f, capa.features.common.Substring):
return SubstringFeature(substring=f.value, description=f.description)
# must come before check for String due to inheritance
elif isinstance(f, capa.features.common.Regex):
return RegexFeature(regex=f.value, description=f.description)
elif isinstance(f, capa.features.common.String):
return StringFeature(string=f.value, description=f.description)
elif isinstance(f, capa.features.common.Class):
return ClassFeature(class_=f.value, description=f.description)
elif isinstance(f, capa.features.common.Namespace):
return NamespaceFeature(namespace=f.value, description=f.description)
elif isinstance(f, capa.features.basicblock.BasicBlock):
return BasicBlockFeature(description=f.description)
elif isinstance(f, capa.features.insn.API):
return APIFeature(api=f.value, description=f.description)
elif isinstance(f, capa.features.insn.Number):
return NumberFeature(number=f.value, description=f.description)
elif isinstance(f, capa.features.common.Bytes):
buf = f.value
assert isinstance(buf, bytes)
return BytesFeature(bytes=binascii.hexlify(buf).decode("ascii"), description=f.description)
elif isinstance(f, capa.features.insn.Offset):
return OffsetFeature(offset=f.value, description=f.description)
elif isinstance(f, capa.features.insn.Mnemonic):
return MnemonicFeature(mnemonic=f.value, description=f.description)
elif isinstance(f, capa.features.insn.OperandNumber):
return OperandNumberFeature(index=f.index, operand_number=f.value, description=f.description)
elif isinstance(f, capa.features.insn.OperandOffset):
return OperandOffsetFeature(index=f.index, operand_offset=f.value, description=f.description)
else:
raise NotImplementedError(f"feature_from_capa({type(f)}) not implemented")
class OSFeature(FeatureModel):
type: str = "os"
os: str
description: Optional[str]
class ArchFeature(FeatureModel):
type: str = "arch"
arch: str
description: Optional[str]
class FormatFeature(FeatureModel):
type: str = "format"
format: str
description: Optional[str]
class MatchFeature(FeatureModel):
type: str = "match"
match: str
description: Optional[str]
class CharacteristicFeature(FeatureModel):
type: str = "characteristic"
characteristic: str
description: Optional[str]
class ExportFeature(FeatureModel):
type: str = "export"
export: str
description: Optional[str]
class ImportFeature(FeatureModel):
type: str = "import"
import_: str = Field(alias="import")
description: Optional[str]
class SectionFeature(FeatureModel):
type: str = "section"
section: str
description: Optional[str]
class FunctionNameFeature(FeatureModel):
type: str = "function name"
function_name: str = Field(alias="function name")
description: Optional[str]
class SubstringFeature(FeatureModel):
type: str = "substring"
substring: str
description: Optional[str]
class RegexFeature(FeatureModel):
type: str = "regex"
regex: str
description: Optional[str]
class StringFeature(FeatureModel):
type: str = "string"
string: str
description: Optional[str]
class ClassFeature(FeatureModel):
type: str = "class"
class_: str = Field(alias="class")
description: Optional[str]
class NamespaceFeature(FeatureModel):
type: str = "namespace"
namespace: str
description: Optional[str]
class BasicBlockFeature(FeatureModel):
type: str = "basic block"
description: Optional[str]
class APIFeature(FeatureModel):
type: str = "api"
api: str
description: Optional[str]
class NumberFeature(FeatureModel):
type: str = "number"
number: Union[int, float]
description: Optional[str]
class BytesFeature(FeatureModel):
type: str = "bytes"
bytes: str
description: Optional[str]
class OffsetFeature(FeatureModel):
type: str = "offset"
offset: int
description: Optional[str]
class MnemonicFeature(FeatureModel):
type: str = "mnemonic"
mnemonic: str
description: Optional[str]
class OperandNumberFeature(FeatureModel):
type: str = "operand number"
index: int
operand_number: int = Field(alias="operand number")
description: Optional[str]
class OperandOffsetFeature(FeatureModel):
type: str = "operand offset"
index: int
operand_offset: int = Field(alias="operand offset")
description: Optional[str]
Feature = Union[
OSFeature,
ArchFeature,
FormatFeature,
MatchFeature,
CharacteristicFeature,
ExportFeature,
ImportFeature,
SectionFeature,
FunctionNameFeature,
SubstringFeature,
RegexFeature,
StringFeature,
ClassFeature,
NamespaceFeature,
APIFeature,
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?
BasicBlockFeature,
]

View File

@@ -1,41 +1,103 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import abc
from typing import Union
import capa.render.utils
from capa.features.common import Feature
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
class API(Feature):
def __init__(self, name: str, description=None):
# Downcase library name if given
if "." in name:
modname, _, impname = name.rpartition(".")
name = modname.lower() + "." + impname
super(API, self).__init__(name, description=description)
class Number(Feature):
def __init__(self, value: int, bitness=None, description=None):
super(Number, self).__init__(value, bitness=bitness, description=description)
def __init__(self, value: Union[int, float], description=None):
super(Number, self).__init__(value, description=description)
def get_value_str(self):
return capa.render.utils.hex(self.value)
if isinstance(self.value, int):
return hex(self.value)
elif isinstance(self.value, float):
return str(self.value)
else:
raise ValueError("invalid value type")
# max recognized structure size (and therefore, offset size)
MAX_STRUCTURE_SIZE = 0x10000
class Offset(Feature):
def __init__(self, value: int, bitness=None, description=None):
super(Offset, self).__init__(value, bitness=bitness, description=description)
def __init__(self, value: int, description=None):
super(Offset, self).__init__(value, description=description)
def get_value_str(self):
return capa.render.utils.hex(self.value)
return hex(self.value)
class Mnemonic(Feature):
def __init__(self, value: str, description=None):
super(Mnemonic, self).__init__(value, description=description)
# max number of operands to consider for a given instrucion.
# 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
MAX_OPERAND_INDEX = MAX_OPERAND_COUNT - 1
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)
self.index = index
def __hash__(self):
return hash((self.name, self.value))
def __eq__(self, other):
return super().__eq__(other) and self.index == other.index
class OperandNumber(_Operand):
# cached names so we don't do extra string formatting every ctor
NAMES = ["operand[%d].number" % i for i in range(MAX_OPERAND_COUNT)]
# operand[i].number: 0x12
def __init__(self, index: int, value: int, description=None):
super(OperandNumber, self).__init__(index, value, description=description)
self.name = self.NAMES[index]
def get_value_str(self) -> str:
assert isinstance(self.value, int)
return hex(self.value)
class OperandOffset(_Operand):
# cached names so we don't do extra string formatting every ctor
NAMES = ["operand[%d].offset" % i for i in range(MAX_OPERAND_COUNT)]
# operand[i].offset: 0x12
def __init__(self, index: int, value: int, description=None):
super(OperandOffset, self).__init__(index, value, description=description)
self.name = self.NAMES[index]
def get_value_str(self) -> str:
assert isinstance(self.value, int)
return hex(self.value)

View File

@@ -1,12 +1,22 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import os
import logging
from typing import NoReturn
from capa.exceptions import UnsupportedFormatError
from capa.features.common import FORMAT_SC32, FORMAT_SC64, FORMAT_UNKNOWN
EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32")
EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64")
EXTENSIONS_ELF = "elf_"
logger = logging.getLogger("capa")
_hex = hex
@@ -30,3 +40,79 @@ def is_runtime_ida():
return False
else:
return True
def assert_never(value: NoReturn) -> NoReturn:
assert False, f"Unhandled value: {value} ({type(value).__name__})"
def get_format_from_extension(sample: str) -> str:
if sample.endswith(EXTENSIONS_SHELLCODE_32):
return FORMAT_SC32
elif sample.endswith(EXTENSIONS_SHELLCODE_64):
return FORMAT_SC64
return FORMAT_UNKNOWN
def get_auto_format(path: str) -> str:
format_ = get_format(path)
if format_ == FORMAT_UNKNOWN:
format_ = get_format_from_extension(path)
if format_ == FORMAT_UNKNOWN:
raise UnsupportedFormatError()
return format_
def get_format(sample: str) -> str:
# imported locally to avoid import cycle
from capa.features.extractors.common import extract_format
with open(sample, "rb") as f:
buf = f.read()
for feature, _ in extract_format(buf):
assert isinstance(feature.value, str)
return feature.value
return FORMAT_UNKNOWN
def log_unsupported_format_error():
logger.error("-" * 80)
logger.error(" Input file does not appear to be a PE or ELF file.")
logger.error(" ")
logger.error(
" capa currently only supports analyzing PE and ELF files (or shellcode, when using --format sc32|sc64)."
)
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
logger.error("-" * 80)
def log_unsupported_os_error():
logger.error("-" * 80)
logger.error(" Input file does not appear to target a supported OS.")
logger.error(" ")
logger.error(
" capa currently only supports analyzing executables for some operating systems (including Windows and Linux)."
)
logger.error("-" * 80)
def log_unsupported_arch_error():
logger.error("-" * 80)
logger.error(" Input file does not appear to target a supported architecture.")
logger.error(" ")
logger.error(" capa currently only supports analyzing x86 (32- and 64-bit).")
logger.error("-" * 80)
def log_unsupported_runtime_error():
logger.error("-" * 80)
logger.error(" Unsupported runtime or Python interpreter.")
logger.error(" ")
logger.error(" capa supports running under Python 3.7 and higher.")
logger.error(" ")
logger.error(
" If you're seeing this message on the command line, please ensure you're running a supported Python version."
)
logger.error("-" * 80)

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -8,6 +8,7 @@
import logging
import datetime
import contextlib
import idc
import idaapi
@@ -21,13 +22,6 @@ import capa.features.common
logger = logging.getLogger("capa")
# IDA version as returned by idaapi.get_kernel_version()
SUPPORTED_IDA_VERSIONS = (
"7.4",
"7.5",
"7.6",
)
# file type as returned by idainfo.file_type
SUPPORTED_FILE_TYPES = (
idaapi.f_PE,
@@ -45,13 +39,11 @@ def inform_user_ida_ui(message):
def is_supported_ida_version():
version = idaapi.get_kernel_version()
if version not in SUPPORTED_IDA_VERSIONS:
version = float(idaapi.get_kernel_version())
if version < 7.4 or version >= 9:
warning_msg = "This plugin does not support your IDA Pro version"
logger.warning(warning_msg)
logger.warning(
"Your IDA Pro version is: %s. Supported versions are: %s." % (version, ", ".join(SUPPORTED_IDA_VERSIONS))
)
logger.warning("Your IDA Pro version is: %s. Supported versions are: IDA >= 7.4 and IDA < 9.0." % version)
return False
return True
@@ -116,14 +108,31 @@ def get_file_sha256():
return sha256
def collect_metadata():
def collect_metadata(rules):
""" """
md5 = get_file_md5()
sha256 = get_file_sha256()
info: idaapi.idainfo = idaapi.get_inf_structure()
if info.procname == "metapc" and info.is_64bit():
arch = "x86_64"
elif info.procname == "metapc" and info.is_32bit():
arch = "x86"
else:
arch = "unknown arch"
format_name: str = ida_loader.get_file_type_name()
if "PE" in format_name:
os = "windows"
elif "ELF" in format_name:
with contextlib.closing(capa.ida.helpers.IDAIO()) as f:
os = capa.features.extractors.elf.detect_elf_os(f)
else:
os = "unknown os"
return {
"timestamp": datetime.datetime.now().isoformat(),
# "argv" is not relevant here
"argv": [],
"sample": {
"md5": md5,
"sha1": "", # not easily accessible
@@ -132,8 +141,23 @@ def collect_metadata():
},
"analysis": {
"format": idaapi.get_file_type_name(),
"arch": arch,
"os": os,
"extractor": "ida",
"rules": rules,
"base_address": idaapi.get_imagebase(),
"layout": {
# this is updated after capabilities have been collected.
# will look like:
#
# "functions": { 0x401000: { "matched_basic_blocks": [ 0x401000, 0x401005, ... ] }, ... }
},
# ignore these for now - not used by IDA plugin.
"feature_counts": {
"file": {},
"functions": {},
},
"library_functions": {},
},
"version": capa.version.__version__,
}

View File

@@ -28,24 +28,24 @@ to modify the rule text directly and the `Editor` pane to construct and rearrang
![](../../../doc/img/rulegen_expanded.png)
For more information on the FLARE team's open-source framework, capa, check out the overview in our first [blog](https://www.fireeye.com/blog/threat-research/2020/07/capa-automatically-identify-malware-capabilities.html).
For more information on the FLARE team's open-source framework, capa, check out the overview in our first [blog](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities).
## Getting Started
### Requirements
capa explorer supports Python versions >= 3.6.x and the following IDA Pro versions:
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.6.x). Based on our testing the following matrix shows the Python versions supported
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.6.x | Yes | Yes | Yes |
| 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 |
@@ -54,7 +54,7 @@ To use capa explorer with IDA 7.4 and Python 3.8.x you must follow the instructi
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/fireeye/capa/issues).
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
@@ -86,8 +86,8 @@ You can install capa explorer using the following steps:
```
$ pip install flare-capa
```
3. Download the [standard collection of capa rules](https://github.com/fireeye/capa-rules) (capa explorer needs capa rules to analyze a database)
4. Copy [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory
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
### Usage
@@ -99,7 +99,7 @@ You can install capa explorer using the following steps:
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/fireeye/capa-rules) when getting started with the plugin.
downloading and using the [standard collection of capa rules](https://github.com/mandiant/capa-rules) when getting started with the plugin.
#### Tips for Program Analysis
@@ -125,15 +125,15 @@ downloading and using the [standard collection of capa rules](https://github.com
## 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
installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md#method-3-inspecting-the-capa-source-code). Once installed, copy [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py)
installation guide](https://github.com/mandiant/capa/blob/master/doc/installation.md#method-3-inspecting-the-capa-source-code). Once installed, copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py)
to your plugins directory to install capa explorer in IDA.
### Components
capa explorer consists of two main components:
* An [feature extractor](https://github.com/fireeye/capa/tree/master/capa/features/extractors/ida) built on top of IDA's binary analysis engine
* This component uses IDAPython to extract [capa features](https://github.com/fireeye/capa-rules/blob/master/doc/format.md#extracted-features) from your IDBs such as strings,
* An [feature extractor](https://github.com/mandiant/capa/tree/master/capa/features/extractors/ida) built on top of IDA's binary analysis engine
* This component uses IDAPython to extract [capa features](https://github.com/mandiant/capa-rules/blob/master/doc/format.md#extracted-features) from your IDBs such as strings,
disassembly, and control flow; these extracted features are used by capa to find feature combinations that result in a rule match
* An [interactive user interface](https://github.com/fireeye/capa/tree/master/capa/ida/plugin) for displaying and exploring capa rule matches
* An [interactive user interface](https://github.com/mandiant/capa/tree/master/capa/ida/plugin) for displaying and exploring capa rule matches
* This component integrates the feature extractor and capa, providing an interactive user interface to dissect rule matches found by capa using features extracted directly from your IDBs

View File

@@ -1,11 +1,10 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
import idaapi
@@ -27,8 +26,8 @@ class CapaExplorerPlugin(idaapi.plugin_t):
wanted_name = PLUGIN_NAME
wanted_hotkey = "ALT-F5"
comment = "IDA Pro plugin for the FLARE team's capa tool to identify capabilities in executable files."
website = "https://github.com/fireeye/capa"
help = "See https://github.com/fireeye/capa/blob/master/doc/usage.md"
website = "https://github.com/mandiant/capa"
help = "See https://github.com/mandiant/capa/blob/master/doc/usage.md"
version = ""
flags = 0

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -8,10 +8,10 @@
import os
import copy
import json
import logging
import itertools
import collections
from typing import Set, Dict, Optional
import idaapi
import ida_kernwin
@@ -26,16 +26,20 @@ import capa.render.json
import capa.features.common
import capa.render.result_document
import capa.features.extractors.ida.extractor
from capa.engine import FeatureSet
from capa.features.common import Feature
from capa.ida.plugin.icon import QICON
from capa.ida.plugin.view import (
CapaExplorerQtreeView,
CapaExplorerRulgenEditor,
CapaExplorerRulgenPreview,
CapaExplorerRulegenEditor,
CapaExplorerRulegenPreview,
CapaExplorerRulegenFeatures,
)
from capa.features.address import NO_ADDRESS, Address
from capa.ida.plugin.hooks import CapaExplorerIdaHooks
from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearchProxyModel
from capa.features.extractors.base_extractor import FunctionHandle
logger = logging.getLogger(__name__)
settings = ida_settings.IDASettings("capa")
@@ -66,32 +70,32 @@ def trim_function_name(f, max_length=25):
return n
def find_func_features(f, extractor):
def find_func_features(fh: FunctionHandle, extractor):
""" """
func_features = collections.defaultdict(set)
bb_features = collections.defaultdict(dict)
func_features: Dict[Feature, Set] = collections.defaultdict(set)
bb_features: Dict[Address, Dict] = collections.defaultdict(dict)
for (feature, ea) in extractor.extract_function_features(f):
func_features[feature].add(ea)
for (feature, addr) in extractor.extract_function_features(fh):
func_features[feature].add(addr)
for bb in extractor.get_basic_blocks(f):
for bbh in extractor.get_basic_blocks(fh):
_bb_features = collections.defaultdict(set)
for (feature, ea) in extractor.extract_basic_block_features(f, bb):
_bb_features[feature].add(ea)
func_features[feature].add(ea)
for (feature, addr) in extractor.extract_basic_block_features(fh, bbh):
_bb_features[feature].add(addr)
func_features[feature].add(addr)
for insn in extractor.get_instructions(f, bb):
for (feature, ea) in extractor.extract_insn_features(f, bb, insn):
_bb_features[feature].add(ea)
func_features[feature].add(ea)
for insn in extractor.get_instructions(fh, bbh):
for (feature, addr) in extractor.extract_insn_features(fh, bbh, insn):
_bb_features[feature].add(addr)
func_features[feature].add(addr)
bb_features[int(bb)] = _bb_features
bb_features[bbh.address] = _bb_features
return func_features, bb_features
def find_func_matches(f, ruleset, func_features, bb_features):
def find_func_matches(f: FunctionHandle, ruleset, func_features, bb_features):
""" """
func_matches = collections.defaultdict(list)
bb_matches = collections.defaultdict(list)
@@ -108,7 +112,7 @@ def find_func_matches(f, ruleset, func_features, bb_features):
func_features[capa.features.common.MatchedRule(name)].add(ea)
# find rule matches for function, function features include rule matches for basic blocks
_, matches = capa.engine.match(ruleset.function_rules, func_features, int(f))
_, matches = capa.engine.match(ruleset.function_rules, func_features, f.address)
for (name, res) in matches.items():
func_matches[name].extend(res)
@@ -117,19 +121,19 @@ def find_func_matches(f, ruleset, func_features, bb_features):
def find_file_features(extractor):
""" """
file_features = collections.defaultdict(set)
for (feature, ea) in extractor.extract_file_features():
if ea:
file_features[feature].add(ea)
file_features = collections.defaultdict(set) # type: FeatureSet
for (feature, addr) in extractor.extract_file_features():
if addr:
file_features[feature].add(addr)
else:
if feature not in file_features:
file_features[feature] = set()
return file_features
def find_file_matches(ruleset, file_features):
def find_file_matches(ruleset, file_features: FeatureSet):
""" """
_, matches = capa.engine.match(ruleset.file_rules, file_features, 0x0)
_, matches = capa.engine.match(ruleset.file_rules, file_features, NO_ADDRESS)
return matches
@@ -173,9 +177,9 @@ class CapaExplorerFeatureExtractor(capa.features.extractors.ida.extractor.IdaFea
super(CapaExplorerFeatureExtractor, self).__init__()
self.indicator = CapaExplorerProgressIndicator()
def extract_function_features(self, f):
self.indicator.update("function at 0x%X" % f.start_ea)
return super(CapaExplorerFeatureExtractor, self).extract_function_features(f)
def extract_function_features(self, fh: FunctionHandle):
self.indicator.update("function at 0x%X" % fh.inner.start_ea)
return super(CapaExplorerFeatureExtractor, self).extract_function_features(fh)
class QLineEditClicked(QtWidgets.QLineEdit):
@@ -245,8 +249,9 @@ class CapaExplorerForm(idaapi.PluginForm):
self.parent = None
self.ida_hooks = None
self.doc = None
self.doc: Optional[capa.render.result_document.ResultDocument] = None
self.rule_paths = None
self.rules_cache = None
self.ruleset_cache = None
@@ -484,8 +489,8 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_rulegen_header_label.setText("Features")
self.view_rulegen_header_label.setFont(font)
self.view_rulegen_preview = CapaExplorerRulgenPreview(parent=self.parent)
self.view_rulegen_editor = CapaExplorerRulgenEditor(self.view_rulegen_preview, parent=self.parent)
self.view_rulegen_preview = CapaExplorerRulegenPreview(parent=self.parent)
self.view_rulegen_editor = CapaExplorerRulegenEditor(self.view_rulegen_preview, parent=self.parent)
self.view_rulegen_features = CapaExplorerRulegenFeatures(self.view_rulegen_editor, parent=self.parent)
self.view_rulegen_preview.textChanged.connect(self.slot_rulegen_preview_update)
@@ -617,6 +622,7 @@ class CapaExplorerForm(idaapi.PluginForm):
def load_capa_rules(self):
""" """
self.rule_paths = None
self.ruleset_cache = None
self.rules_cache = None
@@ -627,7 +633,7 @@ class CapaExplorerForm(idaapi.PluginForm):
path = self.ask_user_directory()
if not path:
logger.warning(
"You must select a file directory containing capa rules before analysis can be run. The standard collection of capa rules can be downloaded from https://github.com/fireeye/capa-rules."
"You must select a file directory containing capa rules before analysis can be run. The standard collection of capa rules can be downloaded from https://github.com/mandiant/capa-rules."
)
return False
settings.user[CAPA_SETTINGS_RULE_PATH] = path
@@ -650,10 +656,12 @@ class CapaExplorerForm(idaapi.PluginForm):
rule_paths.append(rule_path)
elif os.path.isdir(rule_path):
for root, dirs, files in os.walk(rule_path):
if ".github" in root:
if ".git" in root:
# the .github directory contains CI config in capa-rules
# this includes some .yml files
# these are not rules
# additionally, .git has files that are not .yml and generate the warning
# skip those too
continue
for file in files:
if not file.endswith(".yml"):
@@ -694,11 +702,24 @@ class CapaExplorerForm(idaapi.PluginForm):
)
logger.error("Failed to load rules from %s (error: %s).", settings.user[CAPA_SETTINGS_RULE_PATH], e)
logger.error(
"Make sure your file directory contains properly formatted capa rules. You can download the standard collection of capa rules from https://github.com/fireeye/capa-rules."
"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."
)
logger.error(
"Please ensure you're using the rules that correspond to your major version of capa (%s)",
capa.version.get_major_version(),
)
logger.error(
"You can check out these rules with the following command:\n %s",
capa.version.get_rules_checkout_command(),
)
logger.error(
"Or, for more details, see the rule set documentation here: %s",
"https://github.com/mandiant/capa/blob/master/doc/rules.md",
)
settings.user[CAPA_SETTINGS_RULE_PATH] = ""
return False
self.rule_paths = rule_paths
self.ruleset_cache = ruleset
self.rules_cache = rules
@@ -748,9 +769,10 @@ class CapaExplorerForm(idaapi.PluginForm):
update_wait_box("extracting features")
try:
meta = capa.ida.helpers.collect_metadata()
meta = capa.ida.helpers.collect_metadata(self.rule_paths)
capabilities, counts = capa.main.find_capabilities(self.ruleset_cache, extractor, disable_progress=True)
meta["analysis"].update(counts)
meta["analysis"]["layout"] = capa.main.compute_layout(self.ruleset_cache, extractor, capabilities)
except UserCancelledError:
logger.info("User cancelled analysis.")
return False
@@ -794,11 +816,9 @@ class CapaExplorerForm(idaapi.PluginForm):
update_wait_box("rendering results")
try:
self.doc = capa.render.result_document.convert_capabilities_to_result_document(
meta, self.ruleset_cache, capabilities
)
self.doc = capa.render.result_document.ResultDocument.from_capa(meta, self.ruleset_cache, capabilities)
except Exception as e:
logger.error("Failed to render results (error: %s)", e)
logger.error("Failed to collect results (error: %s)", e, exc_info=True)
return False
try:
@@ -807,7 +827,7 @@ class CapaExplorerForm(idaapi.PluginForm):
"capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache))
)
except Exception as e:
logger.error("Failed to render results (error: %s)", e)
logger.error("Failed to render results (error: %s)", e, exc_info=True)
return False
return True
@@ -860,7 +880,7 @@ class CapaExplorerForm(idaapi.PluginForm):
# must use extractor to get function, as capa analysis requires casted object
extractor = CapaExplorerFeatureExtractor()
except Exception as e:
logger.error("Failed to load IDA feature extractor (error: %s)" % e)
logger.error("Failed to load IDA feature extractor (error: %s)", e)
return False
if ida_kernwin.user_cancelled():
@@ -871,10 +891,10 @@ class CapaExplorerForm(idaapi.PluginForm):
try:
f = idaapi.get_func(idaapi.get_screen_ea())
if f:
f = extractor.get_function(f.start_ea)
self.rulegen_current_function = f
fh: FunctionHandle = extractor.get_function(f.start_ea)
self.rulegen_current_function = fh
func_features, bb_features = find_func_features(f, extractor)
func_features, bb_features = find_func_features(fh, extractor)
self.rulegen_func_features_cache = collections.defaultdict(set, copy.copy(func_features))
self.rulegen_bb_features_cache = collections.defaultdict(dict, copy.copy(bb_features))
@@ -885,15 +905,15 @@ class CapaExplorerForm(idaapi.PluginForm):
try:
# add function and bb rule matches to function features, for display purposes
func_matches, bb_matches = find_func_matches(f, self.ruleset_cache, func_features, bb_features)
for (name, res) in itertools.chain(func_matches.items(), bb_matches.items()):
func_matches, bb_matches = find_func_matches(fh, self.ruleset_cache, func_features, bb_features)
for (name, addrs) in itertools.chain(func_matches.items(), bb_matches.items()):
rule = self.ruleset_cache[name]
if rule.meta.get("capa/subscope-rule"):
if rule.is_subscope_rule():
continue
for (ea, _) in res:
func_features[capa.features.common.MatchedRule(name)].add(ea)
for (addr, _) in addrs:
func_features[capa.features.common.MatchedRule(name)].add(addr)
except Exception as e:
logger.error("Failed to match function/basic block rule scope (error: %s)" % e)
logger.error("Failed to match function/basic block rule scope (error: %s)", e)
return False
else:
func_features = {}
@@ -901,7 +921,7 @@ class CapaExplorerForm(idaapi.PluginForm):
logger.info("User cancelled analysis.")
return False
except Exception as e:
logger.error("Failed to extract function features (error: %s)" % e)
logger.error("Failed to extract function features (error: %s)", e)
return False
if ida_kernwin.user_cancelled():
@@ -911,7 +931,7 @@ class CapaExplorerForm(idaapi.PluginForm):
try:
file_features = find_file_features(extractor)
self.rulegen_file_features_cache = collections.defaultdict(dict, copy.copy(file_features))
self.rulegen_file_features_cache = copy.copy(file_features)
if ida_kernwin.user_cancelled():
logger.info("User cancelled analysis.")
@@ -920,17 +940,17 @@ class CapaExplorerForm(idaapi.PluginForm):
try:
# add file matches to file features, for display purposes
for (name, res) in find_file_matches(self.ruleset_cache, file_features).items():
for (name, addrs) in find_file_matches(self.ruleset_cache, file_features).items():
rule = self.ruleset_cache[name]
if rule.meta.get("capa/subscope-rule"):
if rule.is_subscope_rule():
continue
for (ea, _) in res:
file_features[capa.features.common.MatchedRule(name)].add(ea)
for (addr, _) in addrs:
file_features[capa.features.common.MatchedRule(name)].add(addr)
except Exception as e:
logger.error("Failed to match file scope rules (error: %s)" % e)
logger.error("Failed to match file scope rules (error: %s)", e)
return False
except Exception as e:
logger.error("Failed to extract file features (error: %s)" % e)
logger.error("Failed to extract file features (error: %s)", e)
return False
if ida_kernwin.user_cancelled():
@@ -941,7 +961,7 @@ class CapaExplorerForm(idaapi.PluginForm):
try:
# load preview and feature tree
self.view_rulegen_preview.load_preview_meta(
f.start_ea if f else None,
fh.address if fh else None,
settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, "<insert_author>"),
settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function"),
)
@@ -952,7 +972,7 @@ class CapaExplorerForm(idaapi.PluginForm):
"capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache))
)
except Exception as e:
logger.error("Failed to render views (error: %s)" % e)
logger.error("Failed to render views (error: %s)", e, exc_info=True)
return False
return True
@@ -1150,7 +1170,7 @@ class CapaExplorerForm(idaapi.PluginForm):
idaapi.info("No program analysis to save.")
return
s = json.dumps(self.doc, sort_keys=True, cls=capa.render.json.CapaJsonObjectEncoder).encode("utf-8")
s = self.doc.json().encode("utf-8")
path = self.ask_user_capa_json_file()
if not path:

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -7,12 +7,14 @@
# See the License for the specific language governing permissions and limitations under the License.
import codecs
from typing import List, Iterator, Optional
import idc
import idaapi
from PyQt5 import QtCore
import capa.ida.helpers
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
def info_to_name(display):
@@ -26,19 +28,19 @@ def info_to_name(display):
return ""
def location_to_hex(location):
"""convert location to hex for display"""
return "%08X" % location
def ea_to_hex(ea):
"""convert effective address (ea) to hex for display"""
return "%08X" % ea
class CapaExplorerDataItem:
"""store data for CapaExplorerDataModel"""
def __init__(self, parent, data, can_check=True):
def __init__(self, parent: "CapaExplorerDataItem", data: List[str], can_check=True):
"""initialize item"""
self.pred = parent
self._data = data
self.children = []
self._children: List["CapaExplorerDataItem"] = []
self._checked = False
self._can_check = can_check
@@ -76,29 +78,29 @@ class CapaExplorerDataItem:
"""get item is checked"""
return self._checked
def appendChild(self, item):
def appendChild(self, item: "CapaExplorerDataItem"):
"""add a new child to specified item
@param item: CapaExplorerDataItem
"""
self.children.append(item)
self._children.append(item)
def child(self, row):
def child(self, row: int) -> "CapaExplorerDataItem":
"""get child row
@param row: row number
"""
return self.children[row]
return self._children[row]
def childCount(self):
def childCount(self) -> int:
"""get child count"""
return len(self.children)
return len(self._children)
def columnCount(self):
def columnCount(self) -> int:
"""get column count"""
return len(self._data)
def data(self, column):
def data(self, column: int) -> Optional[str]:
"""get data at column
@param: column number
@@ -108,17 +110,17 @@ class CapaExplorerDataItem:
except IndexError:
return None
def parent(self):
def parent(self) -> "CapaExplorerDataItem":
"""get parent"""
return self.pred
def row(self):
def row(self) -> int:
"""get row location"""
if self.pred:
return self.pred.children.index(self)
return self.pred._children.index(self)
return 0
def setData(self, column, value):
def setData(self, column: int, value: str):
"""set data in column
@param column: column number
@@ -126,14 +128,14 @@ class CapaExplorerDataItem:
"""
self._data[column] = value
def children(self):
def children(self) -> Iterator["CapaExplorerDataItem"]:
"""yield children"""
for child in self.children:
for child in self._children:
yield child
def removeChildren(self):
"""remove children"""
del self.children[:]
del self._children[:]
def __str__(self):
"""get string representation of columns
@@ -148,7 +150,7 @@ class CapaExplorerDataItem:
return self._data[0]
@property
def location(self):
def location(self) -> Optional[int]:
"""return data stored in location column"""
try:
# address stored as str, convert to int before return
@@ -167,7 +169,9 @@ class CapaExplorerRuleItem(CapaExplorerDataItem):
fmt = "%s (%d matches)"
def __init__(self, parent, name, namespace, count, source, can_check=True):
def __init__(
self, parent: CapaExplorerDataItem, name: str, namespace: str, count: int, source: str, can_check=True
):
"""initialize item
@param parent: parent node
@@ -189,7 +193,7 @@ class CapaExplorerRuleItem(CapaExplorerDataItem):
class CapaExplorerRuleMatchItem(CapaExplorerDataItem):
"""store data for rule match"""
def __init__(self, parent, display, source=""):
def __init__(self, parent: CapaExplorerDataItem, display: str, source=""):
"""initialize item
@param parent: parent node
@@ -210,14 +214,16 @@ class CapaExplorerFunctionItem(CapaExplorerDataItem):
fmt = "function(%s)"
def __init__(self, parent, location, can_check=True):
def __init__(self, parent: CapaExplorerDataItem, location: Address, can_check=True):
"""initialize item
@param parent: parent node
@param location: virtual address of function as seen by IDA
"""
assert isinstance(location, AbsoluteVirtualAddress)
ea = int(location)
super(CapaExplorerFunctionItem, self).__init__(
parent, [self.fmt % idaapi.get_name(location), location_to_hex(location), ""], can_check
parent, [self.fmt % idaapi.get_name(ea), ea_to_hex(ea), ""], can_check
)
@property
@@ -243,7 +249,7 @@ class CapaExplorerSubscopeItem(CapaExplorerDataItem):
fmt = "subscope(%s)"
def __init__(self, parent, scope):
def __init__(self, parent: CapaExplorerDataItem, scope):
"""initialize item
@param parent: parent node
@@ -257,19 +263,23 @@ class CapaExplorerBlockItem(CapaExplorerDataItem):
fmt = "basic block(loc_%08X)"
def __init__(self, parent, location):
def __init__(self, parent: CapaExplorerDataItem, location: Address):
"""initialize item
@param parent: parent node
@param location: virtual address of basic block as seen by IDA
"""
super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % location, location_to_hex(location), ""])
assert isinstance(location, AbsoluteVirtualAddress)
ea = int(location)
super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % ea, ea_to_hex(ea), ""])
class CapaExplorerDefaultItem(CapaExplorerDataItem):
"""store data for default match e.g. statement (and, or)"""
def __init__(self, parent, display, details="", location=None):
def __init__(
self, parent: CapaExplorerDataItem, display: str, details: str = "", location: Optional[Address] = None
):
"""initialize item
@param parent: parent node
@@ -277,14 +287,22 @@ class CapaExplorerDefaultItem(CapaExplorerDataItem):
@param details: text to display in details section of UI
@param location: virtual address as seen by IDA
"""
location = location_to_hex(location) if location else ""
super(CapaExplorerDefaultItem, self).__init__(parent, [display, location, details])
ea = None
if location:
assert isinstance(location, AbsoluteVirtualAddress)
ea = int(location)
super(CapaExplorerDefaultItem, self).__init__(
parent, [display, ea_to_hex(ea) if ea is not None else "", details]
)
class CapaExplorerFeatureItem(CapaExplorerDataItem):
"""store data for feature match"""
def __init__(self, parent, display, location="", details=""):
def __init__(
self, parent: CapaExplorerDataItem, display: str, location: Optional[Address] = None, details: str = ""
):
"""initialize item
@param parent: parent node
@@ -292,14 +310,18 @@ class CapaExplorerFeatureItem(CapaExplorerDataItem):
@param details: text to display in details section of UI
@param location: virtual address as seen by IDA
"""
location = location_to_hex(location) if location else ""
super(CapaExplorerFeatureItem, self).__init__(parent, [display, location, details])
if location:
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
ea = int(location)
super(CapaExplorerFeatureItem, self).__init__(parent, [display, ea_to_hex(ea), details])
else:
super(CapaExplorerFeatureItem, self).__init__(parent, [display, "", details])
class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
"""store data for instruction match"""
def __init__(self, parent, display, location):
def __init__(self, parent: CapaExplorerDataItem, display: str, location: Address):
"""initialize item
details section shows disassembly view for match
@@ -308,15 +330,17 @@ class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
@param display: text to display in UI
@param location: virtual address as seen by IDA
"""
details = capa.ida.helpers.get_disasm_line(location)
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)
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
"""store data for byte match"""
def __init__(self, parent, display, location):
def __init__(self, parent: CapaExplorerDataItem, display: str, location: Address):
"""initialize item
details section shows byte preview for match
@@ -325,7 +349,10 @@ class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
@param display: text to display in UI
@param location: virtual address as seen by IDA
"""
byte_snap = idaapi.get_bytes(location, 32)
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
ea = int(location)
byte_snap = idaapi.get_bytes(ea, 32)
details = ""
if byte_snap:
@@ -333,18 +360,21 @@ class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
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)
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
class CapaExplorerStringViewItem(CapaExplorerFeatureItem):
"""store data for string match"""
def __init__(self, parent, display, location, value):
def __init__(self, parent: CapaExplorerDataItem, display: str, location: Address, value: str):
"""initialize item
@param parent: parent node
@param display: text to display in UI
@param location: virtual address as seen by IDA
"""
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
ea = int(location)
super(CapaExplorerStringViewItem, self).__init__(parent, display, location=location, details=value)
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,7 +6,8 @@
# 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 collections import deque, defaultdict
from typing import Set, Dict, List, Tuple
from collections import deque
import idc
import idaapi
@@ -16,6 +17,9 @@ import capa.rules
import capa.ida.helpers
import capa.render.utils as rutils
import capa.features.common
import capa.features.freeze as frz
import capa.render.result_document as rd
import capa.features.freeze.features as frzf
from capa.ida.plugin.item import (
CapaExplorerDataItem,
CapaExplorerRuleItem,
@@ -29,6 +33,7 @@ from capa.ida.plugin.item import (
CapaExplorerStringViewItem,
CapaExplorerInstructionViewItem,
)
from capa.features.address import Address, AbsoluteVirtualAddress
# default highlight color used in IDA window
DEFAULT_HIGHLIGHT = 0xE6C700
@@ -342,7 +347,14 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
return item.childCount()
def render_capa_doc_statement_node(self, parent, statement, locations, doc):
def render_capa_doc_statement_node(
self,
parent: CapaExplorerDataItem,
match: rd.Match,
statement: rd.Statement,
locations: List[Address],
doc: rd.ResultDocument,
):
"""render capa statement read from doc
@param parent: parent to which new child is assigned
@@ -350,132 +362,143 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
@param locations: locations of children (applies to range only?)
@param doc: result doc
"""
if statement["type"] in ("and", "or", "optional"):
display = statement["type"]
if statement.get("description"):
display += " (%s)" % statement["description"]
if isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement)):
display = statement.type
if statement.description:
display += " (%s)" % statement.description
return CapaExplorerDefaultItem(parent, display)
elif statement["type"] == "not":
elif isinstance(statement, rd.NotStatement):
# TODO: do we display 'not'
pass
elif statement["type"] == "some":
display = "%d or more" % statement["count"]
if statement.get("description"):
display += " (%s)" % statement["description"]
elif isinstance(statement, rd.SomeStatement):
display = "%d or more" % statement.count
if statement.description:
display += " (%s)" % statement.description
return CapaExplorerDefaultItem(parent, display)
elif statement["type"] == "range":
elif isinstance(statement, rd.RangeStatement):
# `range` is a weird node, its almost a hybrid of statement + feature.
# it is a specific feature repeated multiple times.
# there's no additional logic in the feature part, just the existence of a feature.
# so, we have to inline some of the feature rendering here.
display = "count(%s): " % self.capa_doc_feature_to_display(statement["child"])
display = "count(%s): " % self.capa_doc_feature_to_display(statement.child)
if statement["max"] == statement["min"]:
display += "%d" % (statement["min"])
elif statement["min"] == 0:
display += "%d or fewer" % (statement["max"])
elif statement["max"] == (1 << 64 - 1):
display += "%d or more" % (statement["min"])
if statement.max == statement.min:
display += "%d" % (statement.min)
elif statement.min == 0:
display += "%d or fewer" % (statement.max)
elif statement.max == (1 << 64 - 1):
display += "%d or more" % (statement.min)
else:
display += "between %d and %d" % (statement["min"], statement["max"])
display += "between %d and %d" % (statement.min, statement.max)
if statement.get("description"):
display += " (%s)" % statement["description"]
if statement.description:
display += " (%s)" % statement.description
parent2 = CapaExplorerFeatureItem(parent, display=display)
for location in locations:
# for each location render child node for range statement
self.render_capa_doc_feature(parent2, statement["child"], location, doc)
self.render_capa_doc_feature(parent2, match, statement.child, location, doc)
return parent2
elif statement["type"] == "subscope":
display = statement[statement["type"]]
if statement.get("description"):
display += " (%s)" % statement["description"]
elif isinstance(statement, rd.SubscopeStatement):
display = str(statement.scope)
if statement.description:
display += " (%s)" % statement.description
return CapaExplorerSubscopeItem(parent, display)
else:
raise RuntimeError("unexpected match statement type: " + str(statement))
def render_capa_doc_match(self, parent, match, doc):
def render_capa_doc_match(self, parent: CapaExplorerDataItem, match: rd.Match, doc: rd.ResultDocument):
"""render capa match read from doc
@param parent: parent node to which new child is assigned
@param match: match read from doc
@param doc: result doc
"""
if not match["success"]:
if not match.success:
# TODO: display failed branches at some point? Help with debugging rules?
return
# optional statement with no successful children is empty
if match["node"].get("statement", {}).get("type") == "optional" and not any(
map(lambda m: m["success"], match["children"])
):
return
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
if not any(map(lambda m: m.success, match.children)):
return
if match["node"]["type"] == "statement":
if isinstance(match.node, rd.StatementNode):
parent2 = self.render_capa_doc_statement_node(
parent, match["node"]["statement"], match.get("locations", []), doc
parent, match, match.node.statement, [addr.to_capa() for addr in match.locations], doc
)
elif match["node"]["type"] == "feature":
elif isinstance(match.node, rd.FeatureNode):
parent2 = self.render_capa_doc_feature_node(
parent, match["node"]["feature"], match.get("locations", []), doc
parent, match, match.node.feature, [addr.to_capa() for addr in match.locations], doc
)
else:
raise RuntimeError("unexpected node type: " + str(match["node"]["type"]))
raise RuntimeError("unexpected node type: " + str(match.node.type))
for child in match.get("children", []):
for child in match.children:
self.render_capa_doc_match(parent2, child, doc)
def render_capa_doc_by_function(self, doc):
def render_capa_doc_by_function(self, doc: rd.ResultDocument):
""" """
matches_by_function = {}
matches_by_function: Dict[int, Tuple[CapaExplorerFunctionItem, Set[str]]] = {}
for rule in rutils.capability_rules(doc):
for ea in rule["matches"].keys():
for location_, _ in rule.matches:
location = location_.to_capa()
if not isinstance(location, AbsoluteVirtualAddress):
# only handle matches with a VA
continue
ea = int(location)
ea = capa.ida.helpers.get_func_start_ea(ea)
if ea is None:
# file scope, skip rendering in this mode
continue
if not matches_by_function.get(ea, ()):
# new function root
matches_by_function[ea] = (CapaExplorerFunctionItem(self.root_node, ea, can_check=False), [])
matches_by_function[ea] = (
CapaExplorerFunctionItem(self.root_node, location, can_check=False),
set(),
)
function_root, match_cache = matches_by_function[ea]
if rule["meta"]["name"] in match_cache:
if rule.meta.name in match_cache:
# rule match already rendered for this function root, skip it
continue
match_cache.append(rule["meta"]["name"])
match_cache.add(rule.meta.name)
CapaExplorerRuleItem(
function_root,
rule["meta"]["name"],
rule["meta"].get("namespace"),
len(rule["matches"]),
rule["source"],
rule.meta.name,
rule.meta.namespace or "",
len(rule.matches),
rule.source,
can_check=False,
)
def render_capa_doc_by_program(self, doc):
def render_capa_doc_by_program(self, doc: rd.ResultDocument):
""" """
for rule in rutils.capability_rules(doc):
rule_name = rule["meta"]["name"]
rule_namespace = rule["meta"].get("namespace")
parent = CapaExplorerRuleItem(
self.root_node, rule_name, rule_namespace, len(rule["matches"]), rule["source"]
)
rule_name = rule.meta.name
rule_namespace = rule.meta.namespace or ""
parent = CapaExplorerRuleItem(self.root_node, rule_name, rule_namespace, len(rule.matches), rule.source)
for (location, match) in doc["rules"][rule["meta"]["name"]]["matches"].items():
if rule["meta"]["scope"] == capa.rules.FILE_SCOPE:
for (location_, match) in rule.matches:
location = location_.to_capa()
parent2: CapaExplorerDataItem
if rule.meta.scope == capa.rules.FILE_SCOPE:
parent2 = parent
elif rule["meta"]["scope"] == capa.rules.FUNCTION_SCOPE:
elif rule.meta.scope == capa.rules.FUNCTION_SCOPE:
parent2 = CapaExplorerFunctionItem(parent, location)
elif rule["meta"]["scope"] == capa.rules.BASIC_BLOCK_SCOPE:
elif rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE:
parent2 = CapaExplorerBlockItem(parent, location)
else:
raise RuntimeError("unexpected rule scope: " + str(rule["meta"]["scope"]))
raise RuntimeError("unexpected rule scope: " + str(rule.meta.scope))
self.render_capa_doc_match(parent2, match, doc)
def render_capa_doc(self, doc, by_function):
def render_capa_doc(self, doc: rd.ResultDocument, by_function: bool):
"""render capa features specified in doc
@param doc: capa result doc
@@ -491,27 +514,36 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
# inform model changes have ended
self.endResetModel()
def capa_doc_feature_to_display(self, feature):
def capa_doc_feature_to_display(self, feature: frzf.Feature):
"""convert capa doc feature type string to display string for ui
@param feature: capa feature read from doc
"""
key = feature["type"]
value = feature[feature["type"]]
key = feature.type
value = getattr(feature, feature.type)
if value:
if key == "string":
if isinstance(feature, frzf.StringFeature):
value = '"%s"' % capa.features.common.escape_string(value)
if feature.get("description", ""):
return "%s(%s = %s)" % (key, value, feature["description"])
if feature.description:
return "%s(%s = %s)" % (key, value, feature.description)
else:
return "%s(%s)" % (key, value)
else:
return "%s" % key
def render_capa_doc_feature_node(self, parent, feature, locations, doc):
def render_capa_doc_feature_node(
self,
parent: CapaExplorerDataItem,
match: rd.Match,
feature: frzf.Feature,
locations: List[Address],
doc: rd.ResultDocument,
):
"""process capa doc feature node
@param parent: parent node to which child is assigned
@param match: match information
@param feature: capa doc feature node
@param locations: locations identified for feature
@param doc: capa doc
@@ -522,6 +554,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
# only one location for feature so no need to nest children
parent2 = self.render_capa_doc_feature(
parent,
match,
feature,
next(iter(locations)),
doc,
@@ -532,81 +565,112 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
parent2 = CapaExplorerFeatureItem(parent, display)
for location in sorted(locations):
self.render_capa_doc_feature(parent2, feature, location, doc)
self.render_capa_doc_feature(parent2, match, feature, location, doc)
return parent2
def render_capa_doc_feature(self, parent, feature, location, doc, display="-"):
def render_capa_doc_feature(
self,
parent: CapaExplorerDataItem,
match: rd.Match,
feature: frzf.Feature,
location: Address,
doc: rd.ResultDocument,
display="-",
):
"""render capa feature read from doc
@param parent: parent node to which new child is assigned
@param match: match information
@param feature: feature read from doc
@param doc: capa feature doc
@param location: address of feature
@param display: text to display in plugin UI
"""
# special handling for characteristic pending type
if feature["type"] == "characteristic":
if feature[feature["type"]] in ("embedded pe",):
if isinstance(feature, frzf.CharacteristicFeature):
characteristic = feature.characteristic
if characteristic in ("embedded pe",):
return CapaExplorerByteViewItem(parent, display, location)
if feature[feature["type"]] in ("loop", "recursive call", "tight loop"):
if characteristic in ("loop", "recursive call", "tight loop"):
return CapaExplorerFeatureItem(parent, display=display)
# default to instruction view for all other characteristics
return CapaExplorerInstructionViewItem(parent, display, location)
if feature["type"] == "match":
elif isinstance(feature, frzf.MatchFeature):
# display content of rule for all rule matches
return CapaExplorerRuleMatchItem(
parent, display, source=doc["rules"].get(feature[feature["type"]], {}).get("source", "")
)
matched_rule_source = ""
if feature["type"] in ("regex", "substring"):
for s, locations in feature["matches"].items():
if location in locations:
return CapaExplorerStringViewItem(
parent, display, location, '"' + capa.features.common.escape_string(s) + '"'
)
# check if match is a matched rule
matched_rule = doc.rules.get(feature.match, None)
if matched_rule is not None:
matched_rule_source = matched_rule.source
return CapaExplorerRuleMatchItem(parent, display, source=matched_rule_source)
elif isinstance(feature, (frzf.RegexFeature, frzf.SubstringFeature)):
for capture, addrs in sorted(match.captures.items()):
for addr in addrs:
assert isinstance(addr, frz.Address)
if location == addr.value:
return CapaExplorerStringViewItem(
parent, display, location, '"' + capa.features.common.escape_string(capture) + '"'
)
# programming error: the given location should always be found in the regex matches
raise ValueError("regex match at location not found")
if feature["type"] == "basicblock":
elif isinstance(feature, frzf.BasicBlockFeature):
return CapaExplorerBlockItem(parent, location)
if feature["type"] in (
"bytes",
"api",
"mnemonic",
"number",
"offset",
"number/x32",
"number/x64",
"offset/x32",
"offset/x64",
elif isinstance(
feature,
(
frzf.BytesFeature,
frzf.APIFeature,
frzf.MnemonicFeature,
frzf.NumberFeature,
frzf.OffsetFeature,
),
):
# display instruction preview
return CapaExplorerInstructionViewItem(parent, display, location)
if feature["type"] in ("section",):
elif isinstance(feature, frzf.SectionFeature):
# display byte preview
return CapaExplorerByteViewItem(parent, display, location)
if feature["type"] in ("string",):
elif isinstance(feature, frzf.StringFeature):
# display string preview
return CapaExplorerStringViewItem(
parent, display, location, '"%s"' % capa.features.common.escape_string(feature[feature["type"]])
parent, display, location, '"%s"' % capa.features.common.escape_string(feature.string)
)
if feature["type"] in ("import", "export", "function-name"):
elif isinstance(
feature,
(
frzf.ImportFeature,
frzf.ExportFeature,
frzf.FunctionNameFeature,
),
):
# display no preview
return CapaExplorerFeatureItem(parent, location=location, display=display)
if feature["type"] in ("arch", "os", "format"):
elif isinstance(
feature,
(
frzf.ArchFeature,
frzf.OSFeature,
frzf.FormatFeature,
),
):
return CapaExplorerFeatureItem(parent, display=display)
raise RuntimeError("unexpected feature type: " + str(feature["type"]))
raise RuntimeError("unexpected feature type: " + str(feature.type))
def update_function_name(self, old_name, new_name):
"""update all instances of old function name with new function name

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -18,6 +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.ida.plugin.model import CapaExplorerDataModel
MAX_SECTION_SIZE = 750
@@ -172,13 +173,13 @@ def resize_columns_to_content(header):
header.resizeSection(0, MAX_SECTION_SIZE)
class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
INDENT = " " * 2
def __init__(self, parent=None):
""" """
super(CapaExplorerRulgenPreview, self).__init__(parent)
super(CapaExplorerRulegenPreview, self).__init__(parent)
self.setFont(QtGui.QFont("Courier", weight=QtGui.QFont.Bold))
self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
@@ -196,7 +197,8 @@ class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
" meta:",
" name: <insert_name>",
" namespace: <insert_namespace>",
" author: %s" % author,
" authors:",
" - %s" % author,
" scope: %s" % scope,
" references: <insert_references>",
" examples:",
@@ -282,7 +284,7 @@ class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
self.set_selection(select_start_ppos, select_end_ppos, len(self.toPlainText()))
self.verticalScrollBar().setSliderPosition(scroll_ppos)
else:
super(CapaExplorerRulgenPreview, self).keyPressEvent(e)
super(CapaExplorerRulegenPreview, self).keyPressEvent(e)
def count_previous_lines_from_block(self, block):
"""calculate number of lines preceding block"""
@@ -302,13 +304,13 @@ class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
self.setTextCursor(cursor)
class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
updated = QtCore.pyqtSignal()
def __init__(self, preview, parent=None):
""" """
super(CapaExplorerRulgenEditor, self).__init__(parent)
super(CapaExplorerRulegenEditor, self).__init__(parent)
self.preview = preview
@@ -372,18 +374,18 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
def dragMoveEvent(self, e):
""" """
super(CapaExplorerRulgenEditor, self).dragMoveEvent(e)
super(CapaExplorerRulegenEditor, self).dragMoveEvent(e)
def dragEventEnter(self, e):
""" """
super(CapaExplorerRulgenEditor, self).dragEventEnter(e)
super(CapaExplorerRulegenEditor, self).dragEventEnter(e)
def dropEvent(self, e):
""" """
if not self.indexAt(e.pos()).isValid():
return
super(CapaExplorerRulgenEditor, self).dropEvent(e)
super(CapaExplorerRulegenEditor, self).dropEvent(e)
self.update_preview()
expand_tree(self.invisibleRootItem())
@@ -437,7 +439,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
""" """
expression, o = action.data()
if "basic block" in expression and "basic block" not in o.text(
CapaExplorerRulgenEditor.get_column_feature_index()
CapaExplorerRulegenEditor.get_column_feature_index()
):
# current expression is "basic block", and not changing to "basic block" expression
children = o.takeChildren()
@@ -445,7 +447,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
for child in children:
new_parent.addChild(child)
new_parent.setExpanded(True)
o.setText(CapaExplorerRulgenEditor.get_column_feature_index(), expression)
o.setText(CapaExplorerRulegenEditor.get_column_feature_index(), expression)
def slot_clear_all(self, action):
""" """
@@ -456,7 +458,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
if not self.indexAt(pos).isValid():
# user selected invalid index
self.load_custom_context_menu_invalid_index(pos)
elif self.itemAt(pos).capa_type == CapaExplorerRulgenEditor.get_node_type_expression():
elif self.itemAt(pos).capa_type == CapaExplorerRulegenEditor.get_node_type_expression():
# user selected expression node
self.load_custom_context_menu_expression(pos)
else:
@@ -468,8 +470,8 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
def slot_item_double_clicked(self, o, column):
""" """
if column in (
CapaExplorerRulgenEditor.get_column_comment_index(),
CapaExplorerRulgenEditor.get_column_description_index(),
CapaExplorerRulegenEditor.get_column_comment_index(),
CapaExplorerRulegenEditor.get_column_description_index(),
):
o.setFlags(o.flags() | QtCore.Qt.ItemIsEditable)
self.editItem(o, column)
@@ -555,7 +557,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
font = QtGui.QFont()
font.setBold(True)
o.setFont(CapaExplorerRulgenEditor.get_column_feature_index(), font)
o.setFont(CapaExplorerRulegenEditor.get_column_feature_index(), font)
def style_feature_node(self, o):
""" """
@@ -566,8 +568,8 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
font.setWeight(QtGui.QFont.Medium)
brush.setColor(QtGui.QColor(*COLOR_GREEN_RGB))
o.setFont(CapaExplorerRulgenEditor.get_column_feature_index(), font)
o.setForeground(CapaExplorerRulgenEditor.get_column_feature_index(), brush)
o.setFont(CapaExplorerRulegenEditor.get_column_feature_index(), font)
o.setForeground(CapaExplorerRulegenEditor.get_column_feature_index(), brush)
def style_comment_node(self, o):
""" """
@@ -575,22 +577,22 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
font.setBold(True)
font.setFamily("Courier")
o.setFont(CapaExplorerRulgenEditor.get_column_feature_index(), font)
o.setFont(CapaExplorerRulegenEditor.get_column_feature_index(), font)
def set_expression_node(self, o):
""" """
setattr(o, "capa_type", CapaExplorerRulgenEditor.get_node_type_expression())
setattr(o, "capa_type", CapaExplorerRulegenEditor.get_node_type_expression())
self.style_expression_node(o)
def set_feature_node(self, o):
""" """
setattr(o, "capa_type", CapaExplorerRulgenEditor.get_node_type_feature())
setattr(o, "capa_type", CapaExplorerRulegenEditor.get_node_type_feature())
o.setFlags(o.flags() & ~QtCore.Qt.ItemIsDropEnabled)
self.style_feature_node(o)
def set_comment_node(self, o):
""" """
setattr(o, "capa_type", CapaExplorerRulgenEditor.get_node_type_comment())
setattr(o, "capa_type", CapaExplorerRulegenEditor.get_node_type_comment())
o.setFlags(o.flags() & ~QtCore.Qt.ItemIsDropEnabled)
self.style_comment_node(o)
@@ -692,11 +694,11 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
# we need to set our own type so we can control the GUI accordingly
if feature.startswith(("- and:", "- or:", "- not:", "- basic block:", "- optional:")):
setattr(node, "capa_type", CapaExplorerRulgenEditor.get_node_type_expression())
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_expression())
elif feature.startswith("#"):
setattr(node, "capa_type", CapaExplorerRulgenEditor.get_node_type_comment())
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_comment())
else:
setattr(node, "capa_type", CapaExplorerRulgenEditor.get_node_type_feature())
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_feature())
# format the node based on its type
(self.set_expression_node, self.set_feature_node, self.set_comment_node)[node.capa_type](node)
@@ -758,7 +760,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
""" """
for feature in filter(
lambda o: o.capa_type
in (CapaExplorerRulgenEditor.get_node_type_feature(), CapaExplorerRulgenEditor.get_node_type_comment()),
in (CapaExplorerRulegenEditor.get_node_type_feature(), CapaExplorerRulegenEditor.get_node_type_comment()),
tuple(iterate_tree(self)),
):
if feature in ignore:
@@ -770,7 +772,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
def get_expressions(self, selected=False, ignore=()):
""" """
for expression in filter(
lambda o: o.capa_type == CapaExplorerRulgenEditor.get_node_type_expression(), tuple(iterate_tree(self))
lambda o: o.capa_type == CapaExplorerRulegenEditor.get_node_type_expression(), tuple(iterate_tree(self))
):
if expression in ignore:
continue
@@ -787,7 +789,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
self.parent_items = {}
self.editor = editor
self.setHeaderLabels(["Feature", "Virtual Address"])
self.setHeaderLabels(["Feature", "Address"])
self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}")
# configure view columns to auto-resize
@@ -1010,7 +1012,8 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
self.parent_items = {}
def format_address(e):
return "%X" % e if e else ""
assert isinstance(e, Address)
return "%X" % e if not isinstance(e, _NoAddress) else ""
def format_feature(feature):
""" """
@@ -1020,7 +1023,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
value = '"%s"' % capa.features.common.escape_string(value)
return "%s(%s)" % (name, value)
for (feature, eas) 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
@@ -1032,7 +1035,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
# level 1
if feature not in self.parent_items:
if len(eas) > 1:
if len(addrs) > 1:
self.parent_items[feature] = self.new_parent_node(
self.parent_items[type(feature)], (format_feature(feature),), feature=feature
)
@@ -1042,18 +1045,18 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
)
# level n > 1
if len(eas) > 1:
for ea in sorted(eas):
if len(addrs) > 1:
for addr in sorted(addrs):
self.new_leaf_node(
self.parent_items[feature], (format_feature(feature), format_address(ea)), feature=feature
self.parent_items[feature], (format_feature(feature), format_address(addr)), feature=feature
)
else:
if eas:
ea = eas.pop()
if addrs:
addr = addrs.pop()
else:
# some features may not have an address e.g. "format"
ea = ""
for (i, v) in enumerate((format_feature(feature), format_address(ea))):
addr = _NoAddress()
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)

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -10,13 +10,16 @@ See the License for the specific language governing permissions and limitations
"""
import os
import sys
import time
import hashlib
import logging
import os.path
import argparse
import datetime
import textwrap
import warnings
import itertools
import contextlib
import collections
from typing import Any, Dict, List, Tuple
@@ -26,6 +29,7 @@ import colorama
from pefile import PEFormatError
from elftools.common.exceptions import ELFError
import capa.perf
import capa.rules
import capa.engine
import capa.version
@@ -38,23 +42,61 @@ import capa.render.vverbose
import capa.features.extractors
import capa.features.extractors.common
import capa.features.extractors.pefile
import capa.features.extractors.dnfile_
import capa.features.extractors.elffile
from capa.rules import Rule, RuleSet
import capa.features.extractors.dotnetfile
import capa.features.extractors.base_extractor
from capa.rules import Rule, Scope, RuleSet
from capa.engine import FeatureSet, MatchResults
from capa.helpers import get_file_taste
from capa.features.extractors.base_extractor import FunctionHandle, FeatureExtractor
from capa.helpers import (
get_format,
get_file_taste,
get_auto_format,
log_unsupported_os_error,
log_unsupported_arch_error,
log_unsupported_format_error,
)
from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError, UnsupportedRuntimeError
from capa.features.common import (
FORMAT_PE,
FORMAT_ELF,
FORMAT_AUTO,
FORMAT_SC32,
FORMAT_SC64,
FORMAT_DOTNET,
FORMAT_FREEZE,
)
from capa.features.address import NO_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"
EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32")
EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64")
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
logger = logging.getLogger("capa")
@contextlib.contextmanager
def timing(msg: str):
t0 = time.time()
yield
t1 = time.time()
logger.debug("perf: %s: %0.2fs", msg, t1 - t0)
def set_vivisect_log_level(level):
logging.getLogger("vivisect").setLevel(level)
logging.getLogger("vivisect.base").setLevel(level)
@@ -62,47 +104,112 @@ def set_vivisect_log_level(level):
logging.getLogger("vtrace").setLevel(level)
logging.getLogger("envi").setLevel(level)
logging.getLogger("envi.codeflow").setLevel(level)
logging.getLogger("Elf").setLevel(level)
def find_function_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, f: FunctionHandle):
# contains features from:
# - insns
# - function
def find_instruction_capabilities(
ruleset: RuleSet, extractor: FeatureExtractor, f: FunctionHandle, bb: BBHandle, insn: InsnHandle
) -> Tuple[FeatureSet, MatchResults]:
"""
find matches for the given rules for the given instruction.
returns: tuple containing (features for instruction, match results for instruction)
"""
# all features found for the instruction.
features = collections.defaultdict(set) # type: FeatureSet
for feature, addr in itertools.chain(
extractor.extract_insn_features(f, bb, insn), extractor.extract_global_features()
):
features[feature].add(addr)
# matches found at this instruction.
_, matches = ruleset.match(Scope.INSTRUCTION, features, insn.address)
for rule_name, res in matches.items():
rule = ruleset[rule_name]
for addr, _ in res:
capa.engine.index_rule_matches(features, rule, [addr])
return features, matches
def find_basic_block_capabilities(
ruleset: RuleSet, extractor: FeatureExtractor, f: FunctionHandle, bb: BBHandle
) -> Tuple[FeatureSet, MatchResults, MatchResults]:
"""
find matches for the given rules within the given basic block.
returns: tuple containing (features for basic block, match results for basic block, match results for instructions)
"""
# all features found within this basic block,
# includes features found within instructions.
features = collections.defaultdict(set) # type: FeatureSet
# matches found at the instruction scope.
# might be found at different instructions, thats ok.
insn_matches = collections.defaultdict(list) # type: MatchResults
for insn in extractor.get_instructions(f, bb):
ifeatures, imatches = find_instruction_capabilities(ruleset, extractor, f, bb, insn)
for feature, vas in ifeatures.items():
features[feature].update(vas)
for rule_name, res in imatches.items():
insn_matches[rule_name].extend(res)
for feature, va in itertools.chain(
extractor.extract_basic_block_features(f, bb), extractor.extract_global_features()
):
features[feature].add(va)
# matches found within this basic block.
_, matches = ruleset.match(Scope.BASIC_BLOCK, features, bb.address)
for rule_name, res in matches.items():
rule = ruleset[rule_name]
for va, _ in res:
capa.engine.index_rule_matches(features, rule, [va])
return features, matches, insn_matches
def find_code_capabilities(
ruleset: RuleSet, extractor: FeatureExtractor, fh: FunctionHandle
) -> Tuple[MatchResults, MatchResults, MatchResults, int]:
"""
find matches for the given rules within the given function.
returns: tuple containing (match results for function, match results for basic blocks, match results for instructions, number of features)
"""
# all features found within this function,
# includes features found within basic blocks (and instructions).
function_features = collections.defaultdict(set) # type: FeatureSet
# matches found at the basic block scope.
# might be found at different basic blocks, thats ok.
bb_matches = collections.defaultdict(list) # type: MatchResults
for feature, va in itertools.chain(extractor.extract_function_features(f), extractor.extract_global_features()):
# matches found at the instruction scope.
# might be found at different instructions, thats ok.
insn_matches = collections.defaultdict(list) # type: MatchResults
for bb in extractor.get_basic_blocks(fh):
features, bmatches, imatches = find_basic_block_capabilities(ruleset, extractor, fh, bb)
for feature, vas in features.items():
function_features[feature].update(vas)
for rule_name, res in bmatches.items():
bb_matches[rule_name].extend(res)
for rule_name, res in imatches.items():
insn_matches[rule_name].extend(res)
for feature, va in itertools.chain(extractor.extract_function_features(fh), extractor.extract_global_features()):
function_features[feature].add(va)
for bb in extractor.get_basic_blocks(f):
# contains features from:
# - insns
# - basic blocks
bb_features = collections.defaultdict(set)
for feature, va in itertools.chain(
extractor.extract_basic_block_features(f, bb), extractor.extract_global_features()
):
bb_features[feature].add(va)
function_features[feature].add(va)
for insn in extractor.get_instructions(f, bb):
for feature, va in itertools.chain(
extractor.extract_insn_features(f, bb, insn), extractor.extract_global_features()
):
bb_features[feature].add(va)
function_features[feature].add(va)
_, matches = capa.engine.match(ruleset.basic_block_rules, bb_features, int(bb))
for rule_name, res in matches.items():
bb_matches[rule_name].extend(res)
rule = ruleset[rule_name]
for va, _ in res:
capa.engine.index_rule_matches(function_features, rule, [va])
_, function_matches = capa.engine.match(ruleset.function_rules, function_features, int(f))
return function_matches, bb_matches, len(function_features)
_, function_matches = ruleset.match(Scope.FUNCTION, function_features, fh.address)
return function_matches, bb_matches, insn_matches, len(function_features)
def find_file_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, function_features: FeatureSet):
@@ -122,13 +229,14 @@ def find_file_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, functi
file_features.update(function_features)
_, matches = capa.engine.match(ruleset.file_rules, file_features, 0x0)
_, matches = ruleset.match(Scope.FILE, file_features, NO_ADDRESS)
return matches, len(file_features)
def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_progress=None) -> Tuple[MatchResults, Any]:
all_function_matches = collections.defaultdict(list) # type: MatchResults
all_bb_matches = collections.defaultdict(list) # type: MatchResults
all_insn_matches = collections.defaultdict(list) # type: MatchResults
meta = {
"feature_counts": {
@@ -149,31 +257,33 @@ def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_pro
pb = pbar(functions, desc="matching", unit=" functions", postfix="skipped 0 library functions")
for f in pb:
function_address = int(f)
if extractor.is_library_function(function_address):
function_name = extractor.get_function_name(function_address)
logger.debug("skipping library function 0x%x (%s)", function_address, function_name)
meta["library_functions"][function_address] = function_name
if extractor.is_library_function(f.address):
function_name = extractor.get_function_name(f.address)
logger.debug("skipping library function 0x%x (%s)", f.address, function_name)
meta["library_functions"][f.address] = function_name
n_libs = len(meta["library_functions"])
percentage = 100 * (n_libs / n_funcs)
if isinstance(pb, tqdm.tqdm):
pb.set_postfix_str("skipped %d library functions (%d%%)" % (n_libs, percentage))
continue
function_matches, bb_matches, feature_count = find_function_capabilities(ruleset, extractor, f)
meta["feature_counts"]["functions"][function_address] = feature_count
logger.debug("analyzed function 0x%x and extracted %d features", function_address, feature_count)
function_matches, bb_matches, insn_matches, feature_count = find_code_capabilities(ruleset, extractor, f)
meta["feature_counts"]["functions"][f.address] = feature_count
logger.debug("analyzed function 0x%x and extracted %d features", f.address, feature_count)
for rule_name, res in function_matches.items():
all_function_matches[rule_name].extend(res)
for rule_name, res in bb_matches.items():
all_bb_matches[rule_name].extend(res)
for rule_name, res in insn_matches.items():
all_insn_matches[rule_name].extend(res)
# collection of features that captures the rule matches within function and BB scopes.
# collection of features that captures the rule matches within function, BB, and instruction scopes.
# mapping from feature (matched rule) to set of addresses at which it matched.
function_and_lower_features: FeatureSet = collections.defaultdict(set)
for rule_name, results in itertools.chain(all_function_matches.items(), all_bb_matches.items()):
for rule_name, results in itertools.chain(
all_function_matches.items(), all_bb_matches.items(), all_insn_matches.items()
):
locations = set(map(lambda p: p[0], results))
rule = ruleset[rule_name]
capa.engine.index_rule_matches(function_and_lower_features, rule, locations)
@@ -187,6 +297,7 @@ def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_pro
# each rule exists in exactly one scope,
# so there won't be any overlap among these following MatchResults,
# and we can merge the dictionaries naively.
all_insn_matches.items(),
all_bb_matches.items(),
all_function_matches.items(),
all_file_matches.items(),
@@ -196,6 +307,7 @@ def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_pro
return matches, meta
# TODO move all to helpers?
def has_rule_with_namespace(rules, capabilities, rule_cat):
for rule_name in capabilities.keys():
if rules.rules[rule_name].meta.get("namespace", "").startswith(rule_cat):
@@ -243,17 +355,6 @@ def is_supported_format(sample: str) -> bool:
return len(list(capa.features.extractors.common.extract_format(taste))) == 1
def get_format(sample: str) -> str:
with open(sample, "rb") as f:
buf = f.read()
for feature, _ in capa.features.extractors.common.extract_format(buf):
assert isinstance(feature.value, str)
return feature.value
return "unknown"
def is_supported_arch(sample: str) -> bool:
with open(sample, "rb") as f:
buf = f.read()
@@ -342,19 +443,7 @@ def get_default_signatures() -> List[str]:
return ret
class UnsupportedFormatError(ValueError):
pass
class UnsupportedArchError(ValueError):
pass
class UnsupportedOSError(ValueError):
pass
def get_workspace(path, format, sigpaths):
def get_workspace(path, format_, sigpaths):
"""
load the program at the given path into a vivisect workspace using the given format.
also apply the given FLIRT signatures.
@@ -374,21 +463,22 @@ def get_workspace(path, format, sigpaths):
import viv_utils
logger.debug("generating vivisect workspace for: %s", path)
if format == "auto":
# TODO should not be auto at this point, anymore
if format_ == FORMAT_AUTO:
if not is_supported_format(path):
raise UnsupportedFormatError()
# don't analyze, so that we can add our Flirt function analyzer first.
vw = viv_utils.getWorkspace(path, analyze=False, should_save=False)
elif format in {"pe", "elf"}:
elif format_ in {FORMAT_PE, FORMAT_ELF}:
vw = viv_utils.getWorkspace(path, analyze=False, should_save=False)
elif format == "sc32":
elif format_ == FORMAT_SC32:
# these are not analyzed nor saved.
vw = viv_utils.getShellcodeWorkspaceFromFile(path, arch="i386", analyze=False)
elif format == "sc64":
elif format_ == FORMAT_SC64:
vw = viv_utils.getShellcodeWorkspaceFromFile(path, arch="amd64", analyze=False)
else:
raise ValueError("unexpected format: " + format)
raise ValueError("unexpected format: " + format_)
viv_utils.flirt.register_flirt_signature_analyzers(vw, sigpaths)
@@ -398,12 +488,9 @@ def get_workspace(path, format, sigpaths):
return vw
class UnsupportedRuntimeError(RuntimeError):
pass
# TODO get_extractors -> List[FeatureExtractor]?
def get_extractor(
path: str, format: str, backend: str, sigpaths: List[str], should_save_workspace=False, disable_progress=False
path: str, format_: str, backend: str, sigpaths: List[str], should_save_workspace=False, disable_progress=False
) -> FeatureExtractor:
"""
raises:
@@ -411,7 +498,7 @@ def get_extractor(
UnsupportedArchError
UnsupportedOSError
"""
if format not in ("sc32", "sc64"):
if format_ not in (FORMAT_SC32, FORMAT_SC64):
if not is_supported_format(path):
raise UnsupportedFormatError()
@@ -421,12 +508,19 @@ def get_extractor(
if not is_supported_os(path):
raise UnsupportedOSError()
if format_ == FORMAT_DOTNET:
import capa.features.extractors.dnfile.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()
@@ -439,7 +533,7 @@ def get_extractor(
import capa.features.extractors.viv.extractor
with halo.Halo(text="analyzing program", spinner="simpleDots", stream=sys.stderr, enabled=not disable_progress):
vw = get_workspace(path, format, sigpaths)
vw = get_workspace(path, format_, sigpaths)
if should_save_workspace:
logger.debug("saving workspace")
@@ -454,6 +548,22 @@ def get_extractor(
return capa.features.extractors.viv.extractor.VivisectFeatureExtractor(vw, path)
def get_file_extractors(sample: str, format_: str) -> List[FeatureExtractor]:
file_extractors: List[FeatureExtractor] = list()
if format_ == capa.features.extractors.common.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_ == capa.features.extractors.common.FORMAT_ELF:
file_extractors.append(capa.features.extractors.elffile.ElfFeatureExtractor(sample))
return file_extractors
def is_nursery_rule_path(path: str) -> bool:
"""
The nursery is a spot for rules that have not yet been fully polished.
@@ -467,32 +577,33 @@ def is_nursery_rule_path(path: str) -> bool:
return "nursery" in path
def get_rules(rule_path: str, disable_progress=False) -> List[Rule]:
if not os.path.exists(rule_path):
raise IOError("rule path %s does not exist or cannot be accessed" % rule_path)
def get_rules(rule_paths: List[str], disable_progress=False) -> List[Rule]:
rule_file_paths = []
for rule_path in rule_paths:
if not os.path.exists(rule_path):
raise IOError("rule path %s does not exist or cannot be accessed" % rule_path)
rule_paths = []
if os.path.isfile(rule_path):
rule_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):
if ".github" in root:
# the .github directory contains CI config in capa-rules
# this includes some .yml files
# these are not rules
continue
for file in files:
if not file.endswith(".yml"):
if not (file.startswith(".git") or file.endswith((".git", ".md", ".txt"))):
# expect to see .git* files, readme.md, format.md, and maybe a .git directory
# other things maybe are rules, but are mis-named.
logger.warning("skipping non-.yml file: %s", file)
if os.path.isfile(rule_path):
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):
if ".git" in root:
# the .github directory contains CI config in capa-rules
# this includes some .yml files
# these are not rules
# additionally, .git has files that are not .yml and generate the warning
# skip those too
continue
rule_path = os.path.join(root, file)
rule_paths.append(rule_path)
for file in files:
if not file.endswith(".yml"):
if not (file.startswith(".git") or file.endswith((".git", ".md", ".txt"))):
# expect to see .git* files, readme.md, format.md, and maybe a .git directory
# other things maybe are rules, but are mis-named.
logger.warning("skipping non-.yml file: %s", file)
continue
rule_path = os.path.join(root, file)
rule_file_paths.append(rule_path)
rules = [] # type: List[Rule]
@@ -502,14 +613,14 @@ def get_rules(rule_path: str, disable_progress=False) -> List[Rule]:
# to disable progress completely
pbar = lambda s, *args, **kwargs: s
for rule_path in pbar(list(rule_paths), desc="loading ", unit=" rules"):
for rule_file_path in pbar(list(rule_file_paths), desc="loading ", unit=" rules"):
try:
rule = capa.rules.Rule.from_yaml_file(rule_path)
rule = capa.rules.Rule.from_yaml_file(rule_file_path)
except capa.rules.InvalidRule:
raise
else:
rule.meta["capa/path"] = rule_path
if is_nursery_rule_path(rule_path):
rule.meta["capa/path"] = rule_file_path
if is_nursery_rule_path(rule_file_path):
rule.meta["capa/nursery"] = True
rules.append(rule)
@@ -546,7 +657,12 @@ def get_signatures(sigs_path):
return paths
def collect_metadata(argv, sample_path, rules_path, extractor):
def collect_metadata(
argv: List[str],
sample_path: str,
rules_path: List[str],
extractor: capa.features.extractors.base_extractor.FeatureExtractor,
):
md5 = hashlib.md5()
sha1 = hashlib.sha1()
sha256 = hashlib.sha256()
@@ -558,10 +674,10 @@ def collect_metadata(argv, sample_path, rules_path, extractor):
sha1.update(buf)
sha256.update(buf)
if rules_path != RULES_PATH_DEFAULT_STRING:
rules_path = os.path.abspath(os.path.normpath(rules_path))
if rules_path != [RULES_PATH_DEFAULT_STRING]:
rules_path = [os.path.abspath(os.path.normpath(r)) for r in rules_path]
format = get_format(sample_path)
format_ = get_format(sample_path)
arch = get_arch(sample_path)
os_ = get_os(sample_path)
@@ -576,16 +692,61 @@ def collect_metadata(argv, sample_path, rules_path, extractor):
"path": os.path.normpath(sample_path),
},
"analysis": {
"format": format,
"format": format_,
"arch": arch,
"os": os_,
"extractor": extractor.__class__.__name__,
"rules": rules_path,
"base_address": extractor.get_base_address(),
"layout": {
# this is updated after capabilities have been collected.
# will look like:
#
# "functions": { 0x401000: { "matched_basic_blocks": [ 0x401000, 0x401005, ... ] }, ... }
},
},
}
def compute_layout(rules, extractor, capabilities):
"""
compute a metadata structure that links basic blocks
to the functions in which they're found.
only collect the basic blocks at which some rule matched.
otherwise, we may pollute the json document with
a large amount of un-referenced data.
"""
functions_by_bb = {}
bbs_by_function = {}
for f in extractor.get_functions():
bbs_by_function[f.address] = []
for bb in extractor.get_basic_blocks(f):
functions_by_bb[bb.address] = f.address
bbs_by_function[f.address].append(bb.address)
matched_bbs = set()
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:
assert addr in functions_by_bb
matched_bbs.add(addr)
layout = {
"functions": {
f: {
"matched_basic_blocks": [bb for bb in bbs if bb in matched_bbs]
# this object is open to extension in the future,
# such as with the function name, etc.
}
for f, bbs in bbs_by_function.items()
}
}
return layout
def install_common_args(parser, wanted=None):
"""
register a common set of command line arguments for re-use by main & scripts.
@@ -646,19 +807,20 @@ def install_common_args(parser, wanted=None):
if "format" in wanted:
formats = [
("auto", "(default) detect file type automatically"),
("pe", "Windows PE file"),
("elf", "Executable and Linkable Format"),
("sc32", "32-bit shellcode"),
("sc64", "64-bit shellcode"),
("freeze", "features previously frozen by capa"),
(FORMAT_AUTO, "(default) detect file type automatically"),
(FORMAT_PE, "Windows PE file"),
(FORMAT_DOTNET, ".NET PE file"),
(FORMAT_ELF, "Executable and Linkable Format"),
(FORMAT_SC32, "32-bit shellcode"),
(FORMAT_SC64, "64-bit shellcode"),
(FORMAT_FREEZE, "features previously frozen by capa"),
]
format_help = ", ".join(["%s: %s" % (f[0], f[1]) for f in formats])
parser.add_argument(
"-f",
"--format",
choices=[f[0] for f in formats],
default="auto",
default=FORMAT_AUTO,
help="select sample format, %s" % format_help,
)
@@ -677,7 +839,8 @@ def install_common_args(parser, wanted=None):
"-r",
"--rules",
type=str,
default=RULES_PATH_DEFAULT_STRING,
default=[RULES_PATH_DEFAULT_STRING],
action="append",
help="path to rule file or directory, use embedded rules by default",
)
@@ -718,7 +881,7 @@ def handle_common_args(args):
# disable vivisect-related logging, it's verbose and not relevant for capa users
set_vivisect_log_level(logging.CRITICAL)
# Since Python 3.8 cp65001 is an alias to utf_8, but not for Pyhton < 3.8
# Since Python 3.8 cp65001 is an alias to utf_8, but not for Python < 3.8
# TODO: remove this code when only supporting Python 3.8+
# https://stackoverflow.com/a/3259271/87207
import codecs
@@ -739,29 +902,38 @@ def handle_common_args(args):
raise RuntimeError("unexpected --color value: " + args.color)
if hasattr(args, "rules"):
if args.rules == RULES_PATH_DEFAULT_STRING:
rules_paths: List[str] = []
if args.rules == [RULES_PATH_DEFAULT_STRING]:
logger.debug("-" * 80)
logger.debug(" Using default embedded rules.")
logger.debug(" To provide your own rules, use the form `capa.exe -r ./path/to/rules/ /path/to/mal.exe`.")
logger.debug(" You can see the current default rule set here:")
logger.debug(" https://github.com/fireeye/capa-rules")
logger.debug(" https://github.com/mandiant/capa-rules")
logger.debug("-" * 80)
rules_path = os.path.join(get_default_root(), "rules")
default_rule_path = os.path.join(get_default_root(), "rules")
if not os.path.exists(rules_path):
if not os.path.exists(default_rule_path):
# when a users installs capa via pip,
# this pulls down just the source code - not the default rules.
# i'm not sure the default rules should even be written to the library directory,
# so in this case, we require the user to use -r to specify the rule directory.
logger.error("default embedded rules not found! (maybe you installed capa as a library?)")
logger.error("provide your own rule set via the `-r` option.")
return -1
else:
rules_path = args.rules
logger.debug("using rules path: %s", rules_path)
return E_MISSING_RULES
args.rules = rules_path
rules_paths.append(default_rule_path)
else:
rules_paths = args.rules
if RULES_PATH_DEFAULT_STRING in rules_paths:
rules_paths.remove(RULES_PATH_DEFAULT_STRING)
for rule_path in rules_paths:
logger.debug("using rules path: %s", rule_path)
args.rules = rules_paths
if hasattr(args, "signatures"):
if args.signatures == SIGNATURES_PATH_DEFAULT_STRING:
@@ -781,8 +953,8 @@ def handle_common_args(args):
def main(argv=None):
if sys.version_info < (3, 6):
raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.6+")
if sys.version_info < (3, 7):
raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.7+")
if argv is None:
argv = sys.argv[1:]
@@ -792,7 +964,7 @@ def main(argv=None):
"""
By default, capa uses a default set of embedded rules.
You can see the rule set here:
https://github.com/fireeye/capa-rules
https://github.com/mandiant/capa-rules
To provide your own rule set, use the `-r` flag:
capa --rules /path/to/rules suspicious.exe
@@ -822,25 +994,36 @@ def main(argv=None):
install_common_args(parser, {"sample", "format", "backend", "signatures", "rules", "tag"})
parser.add_argument("-j", "--json", action="store_true", help="emit JSON instead of text")
args = parser.parse_args(args=argv)
handle_common_args(args)
ret = handle_common_args(args)
if ret is not None and ret != 0:
return ret
try:
taste = get_file_taste(args.sample)
_ = get_file_taste(args.sample)
except IOError as e:
# per our research there's not a programmatic way to render the IOError with non-ASCII filename unless we
# handle the IOError separately and reach into the args
logger.error("%s", e.args[0])
return -1
return E_MISSING_FILE
format_ = args.format
if format_ == FORMAT_AUTO:
try:
format_ = get_auto_format(args.sample)
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)
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: "capa/subscope-rule" not in r.meta, rules.rules.values())]),
len([i for i in filter(lambda r: not r.is_subscope_rule(), rules.rules.values())]),
)
if args.tag:
rules = rules.filter_rules_by_meta(args.tag)
@@ -850,36 +1033,48 @@ def main(argv=None):
logger.debug(" %d. %s", i, r)
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
logger.error("%s", str(e))
return -1
logger.error(
"Please ensure you're using the rules that correspond to your major version of capa (%s)",
capa.version.get_major_version(),
)
logger.error(
"You can check out these rules with the following command:\n %s",
capa.version.get_rules_checkout_command(),
)
logger.error(
"Or, for more details, see the rule set documentation here: %s",
"https://github.com/mandiant/capa/blob/master/doc/rules.md",
)
return E_INVALID_RULE
file_extractor = None
if args.format == "pe" or (args.format == "auto" and taste.startswith(b"MZ")):
# these pefile and elffile file feature extractors are pretty light weight: they don't do any code analysis.
# so we can fairly quickly determine if the given file has "pure" file-scope rules
# that indicate a limitation (like "file is packed based on section names")
# and avoid doing a full code analysis on difficult/impossible binaries.
try:
file_extractor = capa.features.extractors.pefile.PefileFeatureExtractor(args.sample)
except PEFormatError as e:
logger.error("Input file '%s' is not a valid PE file: %s", args.sample, str(e))
return -1
# file feature extractors are pretty lightweight: they don't do any code analysis.
# so we can fairly quickly determine if the given file has "pure" file-scope rules
# that indicate a limitation (like "file is packed based on section names")
# and avoid doing a full code analysis on difficult/impossible binaries.
#
# this pass can inspect multiple file extractors, e.g., dotnet and pe to identify
# various limitations
try:
file_extractors = get_file_extractors(args.sample, format_)
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 (ELFError, OverflowError) as e:
logger.error("Input file '%s' is not a valid ELF file: %s", args.sample, str(e))
return E_CORRUPT_FILE
elif args.format == "elf" or (args.format == "auto" and taste.startswith(b"\x7fELF")):
try:
file_extractor = capa.features.extractors.elffile.ElfFeatureExtractor(args.sample)
except (ELFError, OverflowError) as e:
logger.error("Input file '%s' is not a valid ELF file: %s", args.sample, str(e))
return -1
if file_extractor:
for file_extractor in file_extractors:
try:
pure_file_capabilities, _ = find_file_capabilities(rules, file_extractor, {})
except PEFormatError as e:
logger.error("Input file '%s' is not a valid PE file: %s", args.sample, str(e))
return -1
return E_CORRUPT_FILE
except (ELFError, OverflowError) as e:
logger.error("Input file '%s' is not a valid ELF file: %s", args.sample, str(e))
return -1
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.
@@ -888,72 +1083,49 @@ def main(argv=None):
# do show the output in verbose mode, though.
if not (args.verbose or args.vverbose or args.json):
logger.debug("file limitation short circuit, won't analyze fully.")
return -1
return E_FILE_LIMITATION
try:
if args.format == "pe" or (args.format == "auto" and taste.startswith(b"MZ")):
sig_paths = get_signatures(args.signatures)
else:
sig_paths = []
logger.debug("skipping library code matching: only have PE signatures")
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"
if format_ == FORMAT_FREEZE:
with open(args.sample, "rb") as f:
extractor = capa.features.freeze.load(f.read())
else:
format = args.format
if format == "auto" and args.sample.endswith(EXTENSIONS_SHELLCODE_32):
format = "sc32"
elif format == "auto" and args.sample.endswith(EXTENSIONS_SHELLCODE_64):
format = "sc64"
try:
if format_ == FORMAT_PE:
sig_paths = get_signatures(args.signatures)
else:
sig_paths = []
logger.debug("skipping library code matching: only have native PE signatures")
except IOError as e:
logger.error("%s", str(e))
return E_INVALID_SIG
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
try:
extractor = get_extractor(
args.sample, format, args.backend, sig_paths, should_save_workspace, disable_progress=args.quiet
args.sample, format_, args.backend, sig_paths, should_save_workspace, disable_progress=args.quiet
)
except UnsupportedFormatError:
logger.error("-" * 80)
logger.error(" Input file does not appear to be a PE or ELF file.")
logger.error(" ")
logger.error(
" capa currently only supports analyzing PE and ELF files (or shellcode, when using --format sc32|sc64)."
)
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
logger.error("-" * 80)
return -1
log_unsupported_format_error()
return E_INVALID_FILE_TYPE
except UnsupportedArchError:
logger.error("-" * 80)
logger.error(" Input file does not appear to target a supported architecture.")
logger.error(" ")
logger.error(" capa currently only supports analyzing x86 (32- and 64-bit).")
logger.error("-" * 80)
return -1
log_unsupported_arch_error()
return E_INVALID_FILE_ARCH
except UnsupportedOSError:
logger.error("-" * 80)
logger.error(" Input file does not appear to target a supported OS.")
logger.error(" ")
logger.error(
" capa currently only supports analyzing executables for some operating systems (including Windows and Linux)."
)
logger.error("-" * 80)
return -1
log_unsupported_os_error()
return E_INVALID_FILE_OS
meta = collect_metadata(argv, args.sample, args.rules, extractor)
capabilities, counts = find_capabilities(rules, extractor, disable_progress=args.quiet)
meta["analysis"].update(counts)
meta["analysis"]["layout"] = compute_layout(rules, extractor, capabilities)
if has_file_limitation(rules, capabilities):
# bail if capa encountered file limitation e.g. a packed binary
# do show the output in verbose mode, though.
if not (args.verbose or args.vverbose or args.json):
return -1
return E_FILE_LIMITATION
if args.json:
print(capa.render.json.render(meta, rules, capabilities))
@@ -980,16 +1152,16 @@ def ida_main():
logging.getLogger().setLevel(logging.INFO)
if not capa.ida.helpers.is_supported_ida_version():
return -1
return E_UNSUPPORTED_IDA_VERSION
if not capa.ida.helpers.is_supported_file_type():
return -1
return E_INVALID_FILE_TYPE
logger.debug("-" * 80)
logger.debug(" Using default embedded rules.")
logger.debug(" ")
logger.debug(" You can see the current default rule set here:")
logger.debug(" https://github.com/fireeye/capa-rules")
logger.debug(" https://github.com/mandiant/capa-rules")
logger.debug("-" * 80)
rules_path = os.path.join(get_default_root(), "rules")
@@ -997,7 +1169,7 @@ def ida_main():
rules = get_rules(rules_path)
rules = capa.rules.RuleSet(rules)
meta = capa.ida.helpers.collect_metadata()
meta = capa.ida.helpers.collect_metadata([rules_path])
capabilities, counts = find_capabilities(rules, capa.features.extractors.ida.extractor.IdaFeatureExtractor())
meta["analysis"].update(counts)

70
capa/optimizer.py Normal file
View File

@@ -0,0 +1,70 @@
import logging
import capa.engine as ceng
import capa.features.common
logger = logging.getLogger(__name__)
def get_node_cost(node):
if isinstance(node, (capa.features.common.OS, capa.features.common.Arch, capa.features.common.Format)):
# we assume these are the most restrictive features:
# authors commonly use them at the start of rules to restrict the category of samples to inspect
return 0
# elif "everything else":
# return 1
#
# this should be all hash-lookup features.
# see below.
elif isinstance(node, (capa.features.common.Substring, capa.features.common.Regex, capa.features.common.Bytes)):
# substring and regex features require a full scan of each string
# which we anticipate is more expensive then a hash lookup feature (e.g. mnemonic or count).
#
# TODO: compute the average cost of these feature relative to hash feature
# and adjust the factor accordingly.
return 2
elif isinstance(node, (ceng.Not, ceng.Range)):
# the cost of these nodes are defined by the complexity of their single child.
return 1 + get_node_cost(node.child)
elif isinstance(node, (ceng.And, ceng.Or, ceng.Some)):
# the cost of these nodes is the full cost of their children
# as this is the worst-case scenario.
return 1 + sum(map(get_node_cost, node.children))
else:
# this should be all hash-lookup features.
# we give this a arbitrary weight of 1.
# the only thing more "important" than this is checking OS/Arch/Format.
return 1
def optimize_statement(statement):
# this routine operates in-place
if isinstance(statement, (ceng.And, ceng.Or, ceng.Some)):
# has .children
statement.children = sorted(statement.children, key=lambda n: get_node_cost(n))
return
elif isinstance(statement, (ceng.Not, ceng.Range)):
# has .child
optimize_statement(statement.child)
return
else:
# appears to be "simple"
return
def optimize_rule(rule):
# this routine operates in-place
optimize_statement(rule.statement)
def optimize_rules(rules):
logger.debug("optimizing %d rules", len(rules))
for rule in rules:
optimize_rule(rule)
return rules

10
capa/perf.py Normal file
View File

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

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -11,7 +11,9 @@ import collections
import tabulate
import capa.render.utils as rutils
import capa.render.result_document
import capa.features.freeze as frz
import capa.render.result_document as rd
import capa.features.freeze.features as frzf
from capa.rules import RuleSet
from capa.engine import MatchResults
from capa.render.utils import StringIO
@@ -27,50 +29,49 @@ def width(s: str, character_count: int) -> str:
return s
def render_meta(doc, ostream: StringIO):
def render_meta(doc: rd.ResultDocument, ostream: StringIO):
rows = [
(width("md5", 22), width(doc["meta"]["sample"]["md5"], 82)),
("sha1", doc["meta"]["sample"]["sha1"]),
("sha256", doc["meta"]["sample"]["sha256"]),
("os", doc["meta"]["analysis"]["os"]),
("format", doc["meta"]["analysis"]["format"]),
("arch", doc["meta"]["analysis"]["arch"]),
("path", doc["meta"]["sample"]["path"]),
(width("md5", 22), width(doc.meta.sample.md5, 82)),
("sha1", doc.meta.sample.sha1),
("sha256", doc.meta.sample.sha256),
("os", doc.meta.analysis.os),
("format", doc.meta.analysis.format),
("arch", doc.meta.analysis.arch),
("path", doc.meta.sample.path),
]
ostream.write(tabulate.tabulate(rows, tablefmt="psql"))
ostream.write("\n")
def find_subrule_matches(doc):
def find_subrule_matches(doc: rd.ResultDocument):
"""
collect the rule names that have been matched as a subrule match.
this way we can avoid displaying entries for things that are too specific.
"""
matches = set([])
def rec(node):
if not node["success"]:
def rec(match: rd.Match):
if not match.success:
# there's probably a bug here for rules that do `not: match: ...`
# but we don't have any examples of this yet
return
elif node["node"]["type"] == "statement":
for child in node["children"]:
elif isinstance(match.node, rd.StatementNode):
for child in match.children:
rec(child)
elif node["node"]["type"] == "feature":
if node["node"]["feature"]["type"] == "match":
matches.add(node["node"]["feature"]["match"])
elif isinstance(match.node, rd.FeatureNode) and isinstance(match.node.feature, frzf.MatchFeature):
matches.add(match.node.feature.match)
for rule in rutils.capability_rules(doc):
for node in rule["matches"].values():
rec(node)
for address, match in rule.matches:
rec(match)
return matches
def render_capabilities(doc, ostream: StringIO):
def render_capabilities(doc: rd.ResultDocument, ostream: StringIO):
"""
example::
@@ -86,18 +87,18 @@ def render_capabilities(doc, ostream: StringIO):
rows = []
for rule in rutils.capability_rules(doc):
if rule["meta"]["name"] in subrule_matches:
if rule.meta.name in subrule_matches:
# rules that are also matched by other rules should not get rendered by default.
# this cuts down on the amount of output while giving approx the same detail.
# see #224
continue
count = len(rule["matches"])
count = len(rule.matches)
if count == 1:
capability = rutils.bold(rule["meta"]["name"])
capability = rutils.bold(rule.meta.name)
else:
capability = "%s (%d matches)" % (rutils.bold(rule["meta"]["name"]), count)
rows.append((capability, rule["meta"]["namespace"]))
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
rows.append((capability, rule.meta.namespace))
if rows:
ostream.write(
@@ -108,7 +109,7 @@ def render_capabilities(doc, ostream: StringIO):
ostream.writeln(rutils.bold("no capabilities found"))
def render_attack(doc, ostream: StringIO):
def render_attack(doc: rd.ResultDocument, ostream: StringIO):
"""
example::
@@ -126,17 +127,14 @@ def render_attack(doc, ostream: StringIO):
"""
tactics = collections.defaultdict(set)
for rule in rutils.capability_rules(doc):
if not rule["meta"].get("att&ck"):
continue
for attack in rule["meta"]["att&ck"]:
tactics[attack["tactic"]].add((attack["technique"], attack.get("subtechnique"), attack["id"]))
for attack in rule.meta.attack:
tactics[attack.tactic].add((attack.technique, attack.subtechnique, attack.id))
rows = []
for tactic, techniques in sorted(tactics.items()):
inner_rows = []
for (technique, subtechnique, id) in sorted(techniques):
if subtechnique is None:
if not subtechnique:
inner_rows.append("%s %s" % (rutils.bold(technique), id))
else:
inner_rows.append("%s::%s %s" % (rutils.bold(technique), subtechnique, id))
@@ -156,7 +154,7 @@ def render_attack(doc, ostream: StringIO):
ostream.write("\n")
def render_mbc(doc, ostream: StringIO):
def render_mbc(doc: rd.ResultDocument, ostream: StringIO):
"""
example::
@@ -172,17 +170,14 @@ def render_mbc(doc, ostream: StringIO):
"""
objectives = collections.defaultdict(set)
for rule in rutils.capability_rules(doc):
if not rule["meta"].get("mbc"):
continue
for mbc in rule["meta"]["mbc"]:
objectives[mbc["objective"]].add((mbc["behavior"], mbc.get("method"), mbc["id"]))
for mbc in rule.meta.mbc:
objectives[mbc.objective].add((mbc.behavior, mbc.method, mbc.id))
rows = []
for objective, behaviors in sorted(objectives.items()):
inner_rows = []
for (behavior, method, id) in sorted(behaviors):
if method is None:
if not method:
inner_rows.append("%s [%s]" % (rutils.bold(behavior), id))
else:
inner_rows.append("%s::%s [%s]" % (rutils.bold(behavior), method, id))
@@ -200,7 +195,7 @@ def render_mbc(doc, ostream: StringIO):
ostream.write("\n")
def render_default(doc):
def render_default(doc: rd.ResultDocument):
ostream = rutils.StringIO()
render_meta(doc, ostream)
@@ -215,5 +210,5 @@ def render_default(doc):
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
doc = rd.ResultDocument.from_capa(meta, rules, capabilities)
return render_default(doc)

View File

@@ -1,33 +1,14 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import json
import capa.render.result_document
import capa.render.result_document as rd
from capa.rules import RuleSet
from capa.engine import MatchResults
class CapaJsonObjectEncoder(json.JSONEncoder):
"""JSON encoder that emits Python sets as sorted lists"""
def default(self, obj):
if isinstance(obj, (list, dict, int, float, bool, type(None))) or isinstance(obj, str):
return json.JSONEncoder.default(self, obj)
elif isinstance(obj, set):
return list(sorted(obj))
else:
# probably will TypeError
return json.JSONEncoder.default(self, obj)
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
return json.dumps(
capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities),
cls=CapaJsonObjectEncoder,
sort_keys=True,
)
return rd.ResultDocument.from_capa(meta, rules, capabilities).json(exclude_none=True)

View File

@@ -1,329 +1,557 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import copy
import datetime
from typing import Any, Dict, Tuple, Union, Optional
from pydantic import Field, BaseModel
import capa.rules
import capa.engine
import capa.render.utils
import capa.features.common
import capa.features.freeze as frz
import capa.features.address
from capa.rules import RuleSet
from capa.engine import MatchResults
from capa.helpers import assert_never
def convert_statement_to_result_document(statement):
"""
"statement": {
"type": "or"
},
"statement": {
"max": 9223372036854775808,
"min": 2,
"type": "range"
},
"""
statement_type = statement.name.lower()
result = {"type": statement_type}
if statement.description:
result["description"] = statement.description
if statement_type == "some" and statement.count == 0:
result["type"] = "optional"
elif statement_type == "some":
result["count"] = statement.count
elif statement_type == "range":
result["min"] = statement.min
result["max"] = statement.max
result["child"] = convert_feature_to_result_document(statement.child)
elif statement_type == "subscope":
result["subscope"] = statement.scope
return result
class FrozenModel(BaseModel):
class Config:
frozen = True
def convert_feature_to_result_document(feature):
"""
"feature": {
"number": 6,
"type": "number"
},
"feature": {
"api": "ws2_32.WSASocket",
"type": "api"
},
"feature": {
"match": "create TCP socket",
"type": "match"
},
"feature": {
"characteristic": [
"loop",
true
],
"type": "characteristic"
},
"""
result = {"type": feature.name, feature.name: feature.get_value_str()}
if feature.description:
result["description"] = feature.description
if feature.name in ("regex", "substring"):
result["matches"] = feature.matches
return result
class Sample(FrozenModel):
md5: str
sha1: str
sha256: str
path: str
def convert_node_to_result_document(node):
"""
"node": {
"type": "statement",
"statement": { ... }
},
class BasicBlockLayout(FrozenModel):
address: frz.Address
"node": {
"type": "feature",
"feature": { ... }
},
"""
if isinstance(node, capa.engine.Statement):
return {
"type": "statement",
"statement": convert_statement_to_result_document(node),
}
elif isinstance(node, capa.features.common.Feature):
return {
"type": "feature",
"feature": convert_feature_to_result_document(node),
}
class FunctionLayout(FrozenModel):
address: frz.Address
matched_basic_blocks: Tuple[BasicBlockLayout, ...]
class Layout(FrozenModel):
functions: Tuple[FunctionLayout, ...]
class LibraryFunction(FrozenModel):
address: frz.Address
name: str
class FunctionFeatureCount(FrozenModel):
address: frz.Address
count: int
class FeatureCounts(FrozenModel):
file: int
functions: Tuple[FunctionFeatureCount, ...]
class Analysis(FrozenModel):
format: str
arch: str
os: str
extractor: str
rules: Tuple[str, ...]
base_address: frz.Address
layout: Layout
feature_counts: FeatureCounts
library_functions: Tuple[LibraryFunction, ...]
class Metadata(FrozenModel):
timestamp: datetime.datetime
version: str
argv: Tuple[str, ...]
sample: Sample
analysis: Analysis
@classmethod
def from_capa(cls, meta: Any) -> "Metadata":
return cls(
timestamp=meta["timestamp"],
version=meta["version"],
argv=meta["argv"] if "argv" in meta else None,
sample=Sample(
md5=meta["sample"]["md5"],
sha1=meta["sample"]["sha1"],
sha256=meta["sample"]["sha256"],
path=meta["sample"]["path"],
),
analysis=Analysis(
format=meta["analysis"]["format"],
arch=meta["analysis"]["arch"],
os=meta["analysis"]["os"],
extractor=meta["analysis"]["extractor"],
rules=meta["analysis"]["rules"],
base_address=frz.Address.from_capa(meta["analysis"]["base_address"]),
layout=Layout(
functions=[
FunctionLayout(
address=frz.Address.from_capa(address),
matched_basic_blocks=[
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=[
FunctionFeatureCount(address=frz.Address.from_capa(address), count=count)
for address, count in meta["analysis"]["feature_counts"]["functions"].items()
],
),
library_functions=[
LibraryFunction(address=frz.Address.from_capa(address), name=name)
for address, name in meta["analysis"]["library_functions"].items()
],
),
)
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 SomeStatement(StatementModel):
type = "some"
description: Optional[str]
count: int
class OptionalStatement(StatementModel):
type = "optional"
description: Optional[str]
class RangeStatement(StatementModel):
type = "range"
description: Optional[str]
min: int
max: int
child: frz.Feature
class SubscopeStatement(StatementModel):
type = "subscope"
description: Optional[str]
scope = capa.rules.Scope
Statement = Union[
OptionalStatement,
AndStatement,
OrStatement,
NotStatement,
SomeStatement,
RangeStatement,
SubscopeStatement,
]
class StatementNode(FrozenModel):
type = "statement"
statement: Statement
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)
elif isinstance(node, capa.engine.Some):
if node.count == 0:
return OptionalStatement(description=node.description)
else:
return SomeStatement(
description=node.description,
count=node.count,
)
elif isinstance(node, capa.engine.Range):
return RangeStatement(
description=node.description,
min=node.min,
max=node.max,
child=frz.feature_from_capa(node.child),
)
elif isinstance(node, capa.engine.Subscope):
return SubscopeStatement(
description=node.description,
scope=capa.rules.Scope(node.scope),
)
else:
raise RuntimeError("unexpected match node type")
raise NotImplementedError(f"statement_from_capa({type(node)}) not implemented")
def convert_match_to_result_document(rules, capabilities, result):
class FeatureNode(FrozenModel):
type = "feature"
feature: frz.Feature
Node = Union[StatementNode, FeatureNode]
def node_from_capa(node: Union[capa.engine.Statement, capa.engine.Feature]) -> Node:
if isinstance(node, capa.engine.Statement):
return StatementNode(statement=statement_from_capa(node))
elif isinstance(node, capa.engine.Feature):
return FeatureNode(feature=frz.feature_from_capa(node))
else:
assert_never(node)
class Match(BaseModel):
"""
convert the given Result instance into a common, Python-native data structure.
this will become part of the "result document" format that can be emitted to JSON.
args:
success: did the node match?
node: the logic node or feature node.
children: any children of the logic node. not relevent for features, can be empty.
locations: where the feature matched. not relevant for logic nodes (except range), can be empty.
captures: captured values from the string/regex feature, and the locations of those values.
"""
doc = {
"success": bool(result.success),
"node": convert_node_to_result_document(result.statement),
"children": [convert_match_to_result_document(rules, capabilities, child) for child in result.children],
}
# logic expression, like `and`, don't have locations - their children do.
# so only add `locations` to feature nodes.
if isinstance(result.statement, capa.features.common.Feature):
if bool(result.success):
doc["locations"] = result.locations
elif isinstance(result.statement, capa.engine.Range):
if bool(result.success):
doc["locations"] = result.locations
success: bool
node: Node
children: Tuple["Match", ...]
locations: Tuple[frz.Address, ...]
captures: Dict[str, Tuple[frz.Address, ...]]
# if we have a `match` statement, then we're referencing another rule or namespace.
# this could an external rule (written by a human), or
# rule generated to support a subscope (basic block, etc.)
# we still want to include the matching logic in this tree.
#
# so, we need to lookup the other rule results
# and then filter those down to the address used here.
# finally, splice that logic into this tree.
if (
doc["node"]["type"] == "feature"
and doc["node"]["feature"]["type"] == "match"
# only add subtree on success,
# because there won't be results for the other rule on failure.
and doc["success"]
):
@classmethod
def from_capa(
cls,
rules: RuleSet,
capabilities: MatchResults,
result: capa.engine.Result,
) -> "Match":
success = bool(result)
name = doc["node"]["feature"]["match"]
node = node_from_capa(result.statement)
children = [Match.from_capa(rules, capabilities, child) for child in result.children]
if name in rules:
# this is a rule that we're matching
#
# pull matches from the referenced rule into our tree here.
rule_name = doc["node"]["feature"]["match"]
# logic expression, like `and`, don't have locations - their children do.
# so only add `locations` to feature nodes.
locations = []
if isinstance(node, FeatureNode) and success:
locations = list(map(frz.Address.from_capa, result.locations))
elif isinstance(node, StatementNode) and isinstance(node.statement, RangeStatement) and success:
locations = list(map(frz.Address.from_capa, result.locations))
captures = {}
if isinstance(result.statement, (capa.features.common._MatchedSubstring, capa.features.common._MatchedRegex)):
captures = {
capture: list(map(frz.Address.from_capa, locs)) for capture, locs in result.statement.matches.items()
}
# if we have a `match` statement, then we're referencing another rule or namespace.
# this could an external rule (written by a human), or
# rule generated to support a subscope (basic block, etc.)
# we still want to include the matching logic in this tree.
#
# so, we need to lookup the other rule results
# and then filter those down to the address used here.
# finally, splice that logic into this tree.
if (
isinstance(node, FeatureNode)
and isinstance(node.feature, frz.features.MatchFeature)
# only add subtree on success,
# because there won't be results for the other rule on failure.
and success
):
name = node.feature.match
if name in rules:
# this is a rule that we're matching
#
# pull matches from the referenced rule into our tree here.
rule_name = name
rule = rules[rule_name]
rule_matches = {address: result for (address, result) in capabilities[rule_name]}
if rule.is_subscope_rule():
# for a subscope rule, fixup the node to be a scope node, rather than a match feature node.
#
# e.g. `contain loop/30c4c78e29bf4d54894fc74f664c62e8` -> `basic block`
#
# note! replace `node`
node = StatementNode(
statement=SubscopeStatement(
scope=rule.meta["scope"],
)
)
for location in result.locations:
children.append(Match.from_capa(rules, capabilities, rule_matches[location]))
else:
# this is a namespace that we're matching
#
# check for all rules in the namespace,
# seeing if they matched.
# if so, pull their matches into our match tree here.
ns_name = name
ns_rules = rules.rules_by_namespace[ns_name]
for rule in ns_rules:
if rule.name in capabilities:
# the rule matched, so splice results into our tree here.
#
# note, there's a shortcoming in our result document schema here:
# we lose the name of the rule that matched in a namespace.
# for example, if we have a statement: `match: runtime/dotnet`
# and we get matches, we can say the following:
#
# match: runtime/dotnet @ 0x0
# or:
# import: mscoree._CorExeMain @ 0x402000
#
# however, we lose the fact that it was rule
# "compiled to the .NET platform"
# that contained this logic and did the match.
#
# we could introduce an intermediate node here.
# this would be a breaking change and require updates to the renderers.
# in the meantime, the above might be sufficient.
rule_matches = {address: result for (address, result) in capabilities[rule.name]}
for location in result.locations:
# doc[locations] contains all matches for the given namespace.
# for example, the feature might be `match: anti-analysis/packer`
# which matches against "generic unpacker" and "UPX".
# in this case, doc[locations] contains locations for *both* of thse.
#
# rule_matches contains the matches for the specific rule.
# this is a subset of doc[locations].
#
# so, grab only the locations for current rule.
if location in rule_matches:
children.append(Match.from_capa(rules, capabilities, rule_matches[location]))
return cls(
success=success,
node=node,
children=children,
locations=locations,
captures=captures,
)
def parse_parts_id(s: str):
id = ""
parts = s.split("::")
if len(parts) > 0:
last = parts.pop()
last, _, id = last.rpartition(" ")
id = id.lstrip("[").rstrip("]")
parts.append(last)
return parts, id
class AttackSpec(FrozenModel):
"""
given an ATT&CK spec like: `Tactic::Technique::Subtechnique [Identifier]`
e.g., `Execution::Command and Scripting Interpreter::Python [T1059.006]`
args:
tactic: like `Tactic` above, perhaps "Execution"
technique: like `Technique` above, perhaps "Command and Scripting Interpreter"
subtechnique: like `Subtechnique` above, perhaps "Python"
id: like `Identifier` above, perhaps "T1059.006"
"""
parts: Tuple[str, ...]
tactic: str
technique: str
subtechnique: str
id: str
@classmethod
def from_str(cls, s) -> "AttackSpec":
tactic = ""
technique = ""
subtechnique = ""
parts, id = parse_parts_id(s)
if len(parts) > 0:
tactic = parts[0]
if len(parts) > 1:
technique = parts[1]
if len(parts) > 2:
subtechnique = parts[2]
return cls(
parts=parts,
tactic=tactic,
technique=technique,
subtechnique=subtechnique,
id=id,
)
class MBCSpec(FrozenModel):
"""
given an MBC spec like: `Objective::Behavior::Method [Identifier]`
e.g., `Collection::Input Capture::Mouse Events [E1056.m01]`
args:
objective: like `Objective` above, perhaps "Collection"
behavior: like `Behavior` above, perhaps "Input Capture"
method: like `Method` above, perhaps "Mouse Events"
id: like `Identifier` above, perhaps "E1056.m01"
"""
parts: Tuple[str, ...]
objective: str
behavior: str
method: str
id: str
@classmethod
def from_str(cls, s) -> "MBCSpec":
objective = ""
behavior = ""
method = ""
parts, id = parse_parts_id(s)
if len(parts) > 0:
objective = parts[0]
if len(parts) > 1:
behavior = parts[1]
if len(parts) > 2:
method = parts[2]
return cls(
parts=parts,
objective=objective,
behavior=behavior,
method=method,
id=id,
)
class MaecMetadata(FrozenModel):
analysis_conclusion: Optional[str] = Field(None, alias="analysis-conclusion")
analysis_conclusion_ov: Optional[str] = Field(None, alias="analysis-conclusion-ov")
malware_family: Optional[str] = Field(None, alias="malware-family")
malware_category: Optional[str] = Field(None, alias="malware-category")
malware_category_ov: Optional[str] = Field(None, alias="malware-category-ov")
class Config:
frozen = True
allow_population_by_field_name = True
class RuleMetadata(FrozenModel):
name: str
namespace: Optional[str]
authors: Tuple[str, ...]
scope: capa.rules.Scope
attack: Tuple[AttackSpec, ...] = Field(alias="att&ck")
mbc: Tuple[MBCSpec, ...]
references: Tuple[str, ...]
examples: Tuple[str, ...]
description: str
lib: bool = Field(False, alias="lib")
is_subscope_rule: bool = Field(False, alias="capa/subscope")
maec: MaecMetadata
@classmethod
def from_capa(cls, rule: capa.rules.Rule) -> "RuleMetadata":
return cls(
name=rule.meta.get("name"),
namespace=rule.meta.get("namespace"),
authors=rule.meta.get("authors"),
scope=capa.rules.Scope(rule.meta.get("scope")),
attack=list(map(AttackSpec.from_str, rule.meta.get("att&ck", []))),
mbc=list(map(MBCSpec.from_str, rule.meta.get("mbc", []))),
references=rule.meta.get("references", []),
examples=rule.meta.get("examples", []),
description=rule.meta.get("description", ""),
lib=rule.meta.get("lib", False),
capa_subscope=rule.meta.get("capa/subscope", False),
maec=MaecMetadata(
analysis_conclusion=rule.meta.get("maec/analysis-conclusion"),
analysis_conclusion_ov=rule.meta.get("maec/analysis-conclusion-ov"),
malware_family=rule.meta.get("maec/malware-family"),
malware_category=rule.meta.get("maec/malware-category"),
malware_category_ov=rule.meta.get("maec/malware-category-ov"),
),
)
class Config:
frozen = True
allow_population_by_field_name = True
class RuleMatches(BaseModel):
"""
args:
meta: the metadata from the rule
source: the raw rule text
"""
meta: RuleMetadata
source: str
matches: Tuple[Tuple[frz.Address, Match], ...]
class ResultDocument(BaseModel):
meta: Metadata
rules: Dict[str, RuleMatches]
@classmethod
def from_capa(cls, meta, rules: RuleSet, capabilities: MatchResults) -> "ResultDocument":
rule_matches: Dict[str, RuleMatches] = {}
for rule_name, matches in capabilities.items():
rule = rules[rule_name]
rule_matches = {address: result for (address, result) in capabilities[rule_name]}
if rule.meta.get("capa/subscope-rule"):
# for a subscope rule, fixup the node to be a scope node, rather than a match feature node.
#
# e.g. `contain loop/30c4c78e29bf4d54894fc74f664c62e8` -> `basic block`
scope = rule.meta["scope"]
doc["node"] = {
"type": "statement",
"statement": {
"type": "subscope",
"subscope": scope,
},
}
continue
for location in doc["locations"]:
doc["children"].append(convert_match_to_result_document(rules, capabilities, rule_matches[location]))
else:
# this is a namespace that we're matching
#
# check for all rules in the namespace,
# seeing if they matched.
# if so, pull their matches into our match tree here.
ns_name = doc["node"]["feature"]["match"]
ns_rules = rules.rules_by_namespace[ns_name]
rule_matches[rule_name] = RuleMatches(
meta=RuleMetadata.from_capa(rule),
source=rule.definition,
matches=[
(frz.Address.from_capa(addr), Match.from_capa(rules, capabilities, match))
for addr, match in matches
],
)
for rule in ns_rules:
if rule.name in capabilities:
# the rule matched, so splice results into our tree here.
#
# note, there's a shortcoming in our result document schema here:
# we lose the name of the rule that matched in a namespace.
# for example, if we have a statement: `match: runtime/dotnet`
# and we get matches, we can say the following:
#
# match: runtime/dotnet @ 0x0
# or:
# import: mscoree._CorExeMain @ 0x402000
#
# however, we lose the fact that it was rule
# "compiled to the .NET platform"
# that contained this logic and did the match.
#
# we could introduce an intermediate node here.
# this would be a breaking change and require updates to the renderers.
# in the meantime, the above might be sufficient.
rule_matches = {address: result for (address, result) in capabilities[rule.name]}
for location in doc["locations"]:
# doc[locations] contains all matches for the given namespace.
# for example, the feature might be `match: anti-analysis/packer`
# which matches against "generic unpacker" and "UPX".
# in this case, doc[locations] contains locations for *both* of thse.
#
# rule_matches contains the matches for the specific rule.
# this is a subset of doc[locations].
#
# so, grab only the locations for current rule.
if location in rule_matches:
doc["children"].append(
convert_match_to_result_document(rules, capabilities, rule_matches[location])
)
return doc
def convert_meta_to_result_document(meta):
# make a copy so that we don't modify the given parameter
meta = copy.deepcopy(meta)
attacks = meta.get("att&ck", [])
meta["att&ck"] = [parse_canonical_attack(attack) for attack in attacks]
mbcs = meta.get("mbc", [])
meta["mbc"] = [parse_canonical_mbc(mbc) for mbc in mbcs]
return meta
def parse_canonical_attack(attack: str):
"""
parse capa's canonical ATT&CK representation: `Tactic::Technique::Subtechnique [Identifier]`
"""
tactic = ""
technique = ""
subtechnique = ""
parts, id = capa.render.utils.parse_parts_id(attack)
if len(parts) > 0:
tactic = parts[0]
if len(parts) > 1:
technique = parts[1]
if len(parts) > 2:
subtechnique = parts[2]
return {
"parts": parts,
"id": id,
"tactic": tactic,
"technique": technique,
"subtechnique": subtechnique,
}
def parse_canonical_mbc(mbc: str):
"""
parse capa's canonical MBC representation: `Objective::Behavior::Method [Identifier]`
"""
objective = ""
behavior = ""
method = ""
parts, id = capa.render.utils.parse_parts_id(mbc)
if len(parts) > 0:
objective = parts[0]
if len(parts) > 1:
behavior = parts[1]
if len(parts) > 2:
method = parts[2]
return {
"parts": parts,
"id": id,
"objective": objective,
"behavior": behavior,
"method": method,
}
def convert_capabilities_to_result_document(meta, rules: RuleSet, capabilities: MatchResults):
"""
convert the given rule set and capabilities result to a common, Python-native data structure.
this format can be directly emitted to JSON, or passed to the other `capa.render.*.render()` routines
to render as text.
see examples of substructures in above routines.
schema:
```json
{
"meta": {...},
"rules: {
$rule-name: {
"meta": {...copied from rule.meta...},
"matches: {
$address: {...match details...},
...
}
},
...
}
}
```
Args:
meta (Dict[str, Any]):
rules (RuleSet):
capabilities (Dict[str, List[Tuple[int, Result]]]):
"""
doc = {
"meta": meta,
"rules": {},
}
for rule_name, matches in capabilities.items():
rule = rules[rule_name]
if rule.meta.get("capa/subscope-rule"):
continue
rule_meta = convert_meta_to_result_document(rule.meta)
doc["rules"][rule_name] = {
"meta": rule_meta,
"source": rule.definition,
"matches": {
addr: convert_match_to_result_document(rules, capabilities, match) for (addr, match) in matches
},
}
return doc
return ResultDocument(meta=Metadata.from_capa(meta), rules=rule_matches)

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -7,9 +7,12 @@
# See the License for the specific language governing permissions and limitations under the License.
import io
from typing import Union, Iterator
import termcolor
import capa.render.result_document as rd
def bold(s: str) -> str:
"""draw attention to the given string"""
@@ -29,40 +32,29 @@ def hex(n: int) -> str:
return "0x%X" % n
def parse_parts_id(s: str):
id = ""
parts = s.split("::")
if len(parts) > 0:
last = parts.pop()
last, _, id = last.rpartition(" ")
id = id.lstrip("[").rstrip("]")
parts.append(last)
return parts, id
def format_parts_id(data):
def format_parts_id(data: Union[rd.AttackSpec, rd.MBCSpec]):
"""
format canonical representation of ATT&CK/MBC parts and ID
"""
return "%s [%s]" % ("::".join(data["parts"]), data["id"])
return "%s [%s]" % ("::".join(data.parts), data.id)
def capability_rules(doc):
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"].get("namespace", ""), rule["meta"]["name"], rule), doc["rules"].values())
):
if rule["meta"].get("lib"):
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"].get("capa/subscope"):
if rule.meta.is_subscope_rule:
continue
if rule["meta"].get("maec/analysis-conclusion"):
if rule.meta.maec.analysis_conclusion:
continue
if rule["meta"].get("maec/analysis-conclusion-ov"):
if rule.meta.maec.analysis_conclusion_ov:
continue
if rule["meta"].get("maec/malware-category"):
if rule.meta.maec.malware_family:
continue
if rule["meta"].get("maec/malware-category-ov"):
if rule.meta.maec.malware_category:
continue
if rule.meta.maec.malware_category_ov:
continue
yield rule

View File

@@ -3,7 +3,7 @@ example::
send data
namespace communication
author william.ballenthin@fireeye.com
author william.ballenthin@mandiant.com
description all known techniques for sending data to a potential C2 server
scope function
examples BFB9B5391A13D0AFD787E87AB90F14F5:0x13145D60
@@ -14,7 +14,7 @@ example::
0x10003415
0x10003797
Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -23,15 +23,39 @@ Unless required by applicable law or agreed to in writing, software distributed
See the License for the specific language governing permissions and limitations under the License.
"""
import tabulate
import dnfile.mdtable
import dncil.clr.token
import capa.rules
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
def render_meta(ostream, doc):
def format_address(address: frz.Address) -> str:
if address.type == frz.AddressType.ABSOLUTE:
return rutils.hex(address.value)
elif address.type == frz.AddressType.RELATIVE:
return f"base address+{rutils.hex(address.value)}"
elif address.type == frz.AddressType.FILE:
return f"file+{rutils.hex(address.value)}"
elif address.type == frz.AddressType.DN_TOKEN:
token = dncil.clr.token.Token(address.value)
return f"token({rutils.hex(token.value)})"
elif address.type == frz.AddressType.DN_TOKEN_OFFSET:
token, offset = address.value
token = dncil.clr.token.Token(token)
return f"token({rutils.hex(token.value)})+{rutils.hex(offset)}"
elif address.type == frz.AddressType.NO_ADDRESS:
return "global"
else:
raise ValueError("unexpected address type")
def render_meta(ostream, doc: rd.ResultDocument):
"""
like:
@@ -51,30 +75,31 @@ def render_meta(ostream, doc):
total feature count 1918
"""
rows = [
("md5", doc["meta"]["sample"]["md5"]),
("sha1", doc["meta"]["sample"]["sha1"]),
("sha256", doc["meta"]["sample"]["sha256"]),
("path", doc["meta"]["sample"]["path"]),
("timestamp", doc["meta"]["timestamp"]),
("capa version", doc["meta"]["version"]),
("os", doc["meta"]["analysis"]["os"]),
("format", doc["meta"]["analysis"]["format"]),
("arch", doc["meta"]["analysis"]["arch"]),
("extractor", doc["meta"]["analysis"]["extractor"]),
("base address", hex(doc["meta"]["analysis"]["base_address"])),
("rules", doc["meta"]["analysis"]["rules"]),
("function count", len(doc["meta"]["analysis"]["feature_counts"]["functions"])),
("library function count", len(doc["meta"]["analysis"]["library_functions"])),
("md5", doc.meta.sample.md5),
("sha1", doc.meta.sample.sha1),
("sha256", doc.meta.sample.sha256),
("path", doc.meta.sample.path),
("timestamp", doc.meta.timestamp),
("capa version", doc.meta.version),
("os", doc.meta.analysis.os),
("format", doc.meta.analysis.format),
("arch", doc.meta.analysis.arch),
("extractor", doc.meta.analysis.extractor),
("base address", format_address(doc.meta.analysis.base_address)),
("rules", "\n".join(doc.meta.analysis.rules)),
("function count", len(doc.meta.analysis.feature_counts.functions)),
("library function count", len(doc.meta.analysis.library_functions)),
(
"total feature count",
doc["meta"]["analysis"]["feature_counts"]["file"]
+ sum(doc["meta"]["analysis"]["feature_counts"]["functions"].values()),
doc.meta.analysis.feature_counts.file
+ sum(map(lambda f: f.count, doc.meta.analysis.feature_counts.functions)),
),
]
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
def render_rules(ostream, doc):
def render_rules(ostream, doc: rd.ResultDocument):
"""
like:
@@ -87,28 +112,29 @@ def render_rules(ostream, doc):
"""
had_match = False
for rule in rutils.capability_rules(doc):
count = len(rule["matches"])
count = len(rule.matches)
if count == 1:
capability = rutils.bold(rule["meta"]["name"])
capability = rutils.bold(rule.meta.name)
else:
capability = "%s (%d matches)" % (rutils.bold(rule["meta"]["name"]), count)
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
ostream.writeln(capability)
had_match = True
rows = []
for key in ("namespace", "description", "scope"):
if key == "name" or key not in rule["meta"]:
v = getattr(rule.meta, key)
if not v:
continue
v = rule["meta"][key]
if isinstance(v, list) and len(v) == 1:
v = v[0]
rows.append((key, v))
if rule["meta"]["scope"] != capa.rules.FILE_SCOPE:
locations = doc["rules"][rule["meta"]["name"]]["matches"].keys()
rows.append(("matches", "\n".join(map(rutils.hex, locations))))
if rule.meta.scope != capa.rules.FILE_SCOPE:
locations = list(map(lambda m: m[0], doc.rules[rule.meta.name].matches))
rows.append(("matches", "\n".join(map(format_address, locations))))
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
ostream.write("\n")
@@ -117,7 +143,7 @@ def render_rules(ostream, doc):
ostream.writeln(rutils.bold("no capabilities found"))
def render_verbose(doc):
def render_verbose(doc: rd.ResultDocument):
ostream = rutils.StringIO()
render_meta(ostream, doc)
@@ -130,5 +156,4 @@ def render_verbose(doc):
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
return render_verbose(doc)
return render_verbose(rd.ResultDocument.from_capa(meta, rules, capabilities))

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,96 +6,135 @@
# 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
import tabulate
import capa.rules
import capa.render.utils as rutils
import capa.render.verbose
import capa.features.common
import capa.render.result_document
import capa.features.freeze as frz
import capa.features.address
import capa.render.result_document as rd
import capa.features.freeze.features as frzf
from capa.rules import RuleSet
from capa.engine import MatchResults
def render_locations(ostream, match):
def render_locations(ostream, locations: Iterable[frz.Address]):
import capa.render.verbose as v
# its possible to have an empty locations array here,
# such as when we're in MODE_FAILURE and showing the logic
# under a `not` statement (which will have no matched locations).
locations = list(sorted(match.get("locations", [])))
locations = list(sorted(locations))
if len(locations) == 0:
return
ostream.write(" @ ")
if len(locations) == 1:
ostream.write(" @ ")
ostream.write(rutils.hex(locations[0]))
ostream.write(v.format_address(locations[0]))
elif len(locations) > 4:
# don't display too many locations, because it becomes very noisy.
# probably only the first handful of locations will be useful for inspection.
ostream.write(", ".join(map(v.format_address, locations[0:4])))
ostream.write(", and %d more..." % (len(locations) - 4))
elif len(locations) > 1:
ostream.write(" @ ")
if len(locations) > 4:
# don't display too many locations, because it becomes very noisy.
# probably only the first handful of locations will be useful for inspection.
ostream.write(", ".join(map(rutils.hex, locations[0:4])))
ostream.write(", and %d more..." % (len(locations) - 4))
else:
ostream.write(", ".join(map(rutils.hex, locations)))
ostream.write(", ".join(map(v.format_address, locations)))
else:
raise RuntimeError("unreachable")
def render_statement(ostream, match, statement, indent=0):
def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0):
ostream.write(" " * indent)
if statement["type"] in ("and", "or", "optional", "not", "subscope"):
ostream.write(statement["type"])
if isinstance(statement, rd.SubscopeStatement):
# emit `basic block:`
# rather than `subscope:`
ostream.write(statement.scope)
ostream.write(":")
if statement.get("description"):
ostream.write(" = %s" % statement["description"])
if statement.description:
ostream.write(" = %s" % statement.description)
ostream.writeln("")
elif statement["type"] == "some":
ostream.write("%d or more:" % (statement["count"]))
if statement.get("description"):
ostream.write(" = %s" % statement["description"])
elif isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement, rd.NotStatement)):
# emit `and:` `or:` `optional:` `not:`
ostream.write(statement.type)
ostream.write(":")
if statement.description:
ostream.write(" = %s" % statement.description)
ostream.writeln("")
elif statement["type"] == "range":
elif isinstance(statement, rd.SomeStatement):
ostream.write("%d or more:" % (statement.count))
if statement.description:
ostream.write(" = %s" % statement.description)
ostream.writeln("")
elif isinstance(statement, rd.RangeStatement):
# `range` is a weird node, its almost a hybrid of statement+feature.
# it is a specific feature repeated multiple times.
# there's no additional logic in the feature part, just the existence of a feature.
# so, we have to inline some of the feature rendering here.
child = statement["child"]
child = statement.child
value = getattr(child, child.type)
if value:
if isinstance(child, frzf.StringFeature):
value = '"%s"' % capa.features.common.escape_string(value)
if child[child["type"]]:
if child["type"] == "string":
value = '"%s"' % capa.features.common.escape_string(child[child["type"]])
else:
value = child[child["type"]]
value = rutils.bold2(value)
if child.get("description"):
ostream.write("count(%s(%s = %s)): " % (child["type"], value, child["description"]))
if child.description:
ostream.write("count(%s(%s = %s)): " % (child.type, value, child.description))
else:
ostream.write("count(%s(%s)): " % (child["type"], value))
ostream.write("count(%s(%s)): " % (child.type, value))
else:
ostream.write("count(%s): " % child["type"])
ostream.write("count(%s): " % child.type)
if statement["max"] == statement["min"]:
ostream.write("%d" % (statement["min"]))
elif statement["min"] == 0:
ostream.write("%d or fewer" % (statement["max"]))
elif statement["max"] == (1 << 64 - 1):
ostream.write("%d or more" % (statement["min"]))
if statement.max == statement.min:
ostream.write("%d" % (statement.min))
elif statement.min == 0:
ostream.write("%d or fewer" % (statement.max))
elif statement.max == (1 << 64 - 1):
ostream.write("%d or more" % (statement.min))
else:
ostream.write("between %d and %d" % (statement["min"], statement["max"]))
ostream.write("between %d and %d" % (statement.min, statement.max))
if statement.get("description"):
ostream.write(" = %s" % statement["description"])
render_locations(ostream, match)
if statement.description:
ostream.write(" = %s" % statement.description)
render_locations(ostream, match.locations)
ostream.writeln("")
else:
raise RuntimeError("unexpected match statement type: " + str(statement))
def render_string_value(s):
def render_string_value(s: str) -> str:
return '"%s"' % capa.features.common.escape_string(s)
def render_feature(ostream, match, feature, indent=0):
def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
ostream.write(" " * indent)
key = feature["type"]
value = feature[feature["type"]]
key = feature.type
if isinstance(feature, frzf.ImportFeature):
# fixup access to Python reserved name
value = feature.import_
if isinstance(feature, frzf.ClassFeature):
value = feature.class_
else:
value = getattr(feature, key)
if key not in ("regex", "substring"):
# like:
@@ -103,18 +142,22 @@ def render_feature(ostream, match, feature, indent=0):
if key == "string":
value = render_string_value(value)
if key == "number":
assert isinstance(value, int)
value = hex(value)
ostream.write(key)
ostream.write(": ")
if value:
ostream.write(rutils.bold2(value))
if "description" in feature:
if feature.description:
ostream.write(capa.rules.DESCRIPTION_SEPARATOR)
ostream.write(feature["description"])
ostream.write(feature.description)
if key not in ("os", "arch"):
render_locations(ostream, match)
render_locations(ostream, match.locations)
ostream.write("\n")
else:
# like:
@@ -126,19 +169,19 @@ def render_feature(ostream, match, feature, indent=0):
ostream.write(value)
ostream.write("\n")
for match, locations in sorted(feature["matches"].items(), key=lambda p: p[0]):
for capture, locations in sorted(match.captures.items()):
ostream.write(" " * (indent + 1))
ostream.write("- ")
ostream.write(rutils.bold2(render_string_value(match)))
render_locations(ostream, {"locations": locations})
ostream.write(rutils.bold2(render_string_value(capture)))
render_locations(ostream, locations)
ostream.write("\n")
def render_node(ostream, match, node, indent=0):
if node["type"] == "statement":
render_statement(ostream, match, node["statement"], indent=indent)
elif node["type"] == "feature":
render_feature(ostream, match, node["feature"], indent=indent)
def render_node(ostream, match: rd.Match, node: rd.Node, indent=0):
if isinstance(node, rd.StatementNode):
render_statement(ostream, match, node.statement, indent=indent)
elif isinstance(node, rd.FeatureNode):
render_feature(ostream, match, node.feature, indent=indent)
else:
raise RuntimeError("unexpected node type: " + str(node))
@@ -151,103 +194,147 @@ MODE_SUCCESS = "success"
MODE_FAILURE = "failure"
def render_match(ostream, match, indent=0, mode=MODE_SUCCESS):
def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
child_mode = mode
if mode == MODE_SUCCESS:
# display only nodes that evaluated successfully.
if not match["success"]:
if not match.success:
return
# optional statement with no successful children is empty
if match["node"].get("statement", {}).get("type") == "optional" and not any(
map(lambda m: m["success"], match["children"])
):
return
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
if not any(map(lambda m: m.success, match.children)):
return
# not statement, so invert the child mode to show failed evaluations
if match["node"].get("statement", {}).get("type") == "not":
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
child_mode = MODE_FAILURE
elif mode == MODE_FAILURE:
# display only nodes that did not evaluate to True
if match["success"]:
if match.success:
return
# optional statement with successful children is not relevant
if match["node"].get("statement", {}).get("type") == "optional" and any(
map(lambda m: m["success"], match["children"])
):
return
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
if any(map(lambda m: m.success, match.children)):
return
# not statement, so invert the child mode to show successful evaluations
if match["node"].get("statement", {}).get("type") == "not":
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
child_mode = MODE_SUCCESS
else:
raise RuntimeError("unexpected mode: " + mode)
render_node(ostream, match, match["node"], indent=indent)
render_node(ostream, match, match.node, indent=indent)
for child in match["children"]:
for child in match.children:
render_match(ostream, child, indent=indent + 1, mode=child_mode)
def render_rules(ostream, doc):
def render_rules(ostream, doc: rd.ResultDocument):
"""
like:
## rules
check for OutputDebugString error
namespace anti-analysis/anti-debugging/debugger-detection
author michael.hunhoff@fireeye.com
author michael.hunhoff@mandiant.com
scope function
mbc Anti-Behavioral Analysis::Detect Debugger::OutputDebugString
examples Practical Malware Analysis Lab 16-02.exe_:0x401020
function @ 0x10004706
and:
api: kernel32.SetLastError @ 0x100047C2
api: kernel32.GetLastError @ 0x10004A87
api: kernel32.OutputDebugString @ 0x10004767, 0x10004787, 0x10004816, 0x10004895
"""
functions_by_bb: Dict[capa.features.address.Address, capa.features.address.Address] = {}
for finfo in doc.meta.analysis.layout.functions:
faddress = finfo.address.to_capa()
for bb in finfo.matched_basic_blocks:
bbaddress = bb.address.to_capa()
functions_by_bb[bbaddress] = faddress
had_match = False
for rule in rutils.capability_rules(doc):
count = len(rule["matches"])
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.
#
# still ignore subscope rules because they're stitched into the final document.
if rule.meta.is_subscope_rule:
continue
count = len(rule.matches)
if count == 1:
capability = rutils.bold(rule["meta"]["name"])
capability = rutils.bold(rule.meta.name)
else:
capability = "%s (%d matches)" % (rutils.bold(rule["meta"]["name"]), count)
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
ostream.writeln(capability)
had_match = True
rows = []
for key in capa.rules.META_KEYS:
if key == "name" or key not in rule["meta"]:
continue
rows.append(("namespace", rule.meta.namespace))
v = rule["meta"][key]
if not v:
continue
if rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov:
rows.append(
(
"maec/analysis-conclusion",
rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov,
)
)
if key in ("att&ck", "mbc"):
v = [rutils.format_parts_id(vv) for vv in v]
if rule.meta.maec.malware_family:
rows.append(("maec/malware-family", rule.meta.maec.malware_family))
if isinstance(v, list) and len(v) == 1:
v = v[0]
elif isinstance(v, list) and len(v) > 1:
v = ", ".join(v)
rows.append((key, v))
if rule.meta.maec.malware_category or rule.meta.maec.malware_category:
rows.append(
("maec/malware-category", rule.meta.maec.malware_category or rule.meta.maec.malware_category_ov)
)
rows.append(("author", ", ".join(rule.meta.authors)))
rows.append(("scope", rule.meta.scope.value))
if rule.meta.attack:
rows.append(("att&ck", ", ".join([rutils.format_parts_id(v) for v in rule.meta.attack])))
if rule.meta.mbc:
rows.append(("mbc", ", ".join([rutils.format_parts_id(v) for v in rule.meta.mbc])))
if rule.meta.references:
rows.append(("references", ", ".join(rule.meta.references)))
if rule.meta.description:
rows.append(("description", rule.meta.description))
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
if rule["meta"]["scope"] == capa.rules.FILE_SCOPE:
matches = list(doc["rules"][rule["meta"]["name"]]["matches"].values())
if rule.meta.scope == capa.rules.FILE_SCOPE:
matches = doc.rules[rule.meta.name].matches
if len(matches) != 1:
# i think there should only ever be one match per file-scope rule,
# because we do the file-scope evaluation a single time.
# but i'm not 100% sure if this is/will always be true.
# so, lets be explicit about our assumptions and raise an exception if they fail.
raise RuntimeError("unexpected file scope match count: %d" % (len(matches)))
render_match(ostream, matches[0], indent=0)
first_address, first_match = matches[0]
render_match(ostream, first_match, indent=0)
else:
for location, match in sorted(doc["rules"][rule["meta"]["name"]]["matches"].items()):
ostream.write(rule["meta"]["scope"])
for location, match in sorted(doc.rules[rule.meta.name].matches):
ostream.write(rule.meta.scope)
ostream.write(" @ ")
ostream.writeln(rutils.hex(location))
ostream.write(capa.render.verbose.format_address(location))
if rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE:
ostream.write(
" in function "
+ capa.render.verbose.format_address(frz.Address.from_capa(functions_by_bb[location.to_capa()]))
)
ostream.write("\n")
render_match(ostream, match, indent=1)
ostream.write("\n")
@@ -255,7 +342,7 @@ def render_rules(ostream, doc):
ostream.writeln(rutils.bold("no capabilities found"))
def render_vverbose(doc):
def render_vverbose(doc: rd.ResultDocument):
ostream = rutils.StringIO()
capa.render.verbose.render_meta(ostream, doc)
@@ -268,5 +355,4 @@ def render_vverbose(doc):
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
return render_vverbose(doc)
return render_vverbose(rd.ResultDocument.from_capa(meta, rules, capabilities))

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -12,8 +12,10 @@ import uuid
import codecs
import logging
import binascii
import functools
import collections
from enum import Enum
from capa.helpers import assert_never
try:
from functools import lru_cache
@@ -22,19 +24,22 @@ except ImportError:
# https://github.com/python/mypy/issues/1153
from backports.functools_lru_cache import lru_cache # type: ignore
from typing import Any, Dict, List, Union, Iterator
from typing import Any, Set, Dict, List, Tuple, Union, Iterator
import yaml
import ruamel.yaml
import capa.perf
import capa.engine as ceng
import capa.features
import capa.optimizer
import capa.features.file
import capa.features.insn
import capa.features.common
import capa.features.basicblock
from capa.engine import Statement, FeatureSet
from capa.features.common import MAX_BYTES_FEATURE_SIZE, Feature
from capa.features.address import Address
logger = logging.getLogger(__name__)
@@ -43,12 +48,12 @@ logger = logging.getLogger(__name__)
META_KEYS = (
"name",
"namespace",
"rule-category",
"maec/analysis-conclusion",
"maec/analysis-conclusion-ov",
"maec/malware-family",
"maec/malware-category",
"maec/malware-category-ov",
"author",
"authors",
"description",
"lib",
"scope",
@@ -64,12 +69,29 @@ META_KEYS = (
HIDDEN_META_KEYS = ("capa/nursery", "capa/path")
FILE_SCOPE = "file"
FUNCTION_SCOPE = "function"
BASIC_BLOCK_SCOPE = "basic block"
class Scope(str, Enum):
FILE = "file"
FUNCTION = "function"
BASIC_BLOCK = "basic block"
INSTRUCTION = "instruction"
SUPPORTED_FEATURES = {
FILE_SCOPE = Scope.FILE.value
FUNCTION_SCOPE = Scope.FUNCTION.value
BASIC_BLOCK_SCOPE = Scope.BASIC_BLOCK.value
INSTRUCTION_SCOPE = Scope.INSTRUCTION.value
# used only to specify supported features per scope.
# not used to validate rules.
GLOBAL_SCOPE = "global"
SUPPORTED_FEATURES: Dict[str, Set] = {
GLOBAL_SCOPE: {
# these will be added to other scopes, see below.
capa.features.common.OS,
capa.features.common.Arch,
capa.features.common.Format,
},
FILE_SCOPE: {
capa.features.common.MatchedRule,
capa.features.file.Export,
@@ -78,21 +100,26 @@ SUPPORTED_FEATURES = {
capa.features.file.FunctionName,
capa.features.common.Characteristic("embedded pe"),
capa.features.common.String,
capa.features.common.Format,
capa.features.common.OS,
capa.features.common.Arch,
capa.features.common.Class,
capa.features.common.Namespace,
capa.features.common.Characteristic("mixed mode"),
},
FUNCTION_SCOPE: {
# plus basic block scope features, see below
capa.features.common.MatchedRule,
capa.features.basicblock.BasicBlock,
capa.features.common.Characteristic("calls from"),
capa.features.common.Characteristic("calls to"),
capa.features.common.Characteristic("loop"),
capa.features.common.Characteristic("recursive call"),
capa.features.common.OS,
capa.features.common.Arch,
# plus basic block scope features, see below
},
BASIC_BLOCK_SCOPE: {
capa.features.common.MatchedRule,
capa.features.common.Characteristic("tight loop"),
capa.features.common.Characteristic("stack string"),
# plus instruction scope features, see below
},
INSTRUCTION_SCOPE: {
capa.features.common.MatchedRule,
capa.features.insn.API,
capa.features.insn.Number,
@@ -100,19 +127,29 @@ SUPPORTED_FEATURES = {
capa.features.common.Bytes,
capa.features.insn.Offset,
capa.features.insn.Mnemonic,
capa.features.insn.OperandNumber,
capa.features.insn.OperandOffset,
capa.features.common.Characteristic("nzxor"),
capa.features.common.Characteristic("peb access"),
capa.features.common.Characteristic("fs access"),
capa.features.common.Characteristic("gs access"),
capa.features.common.Characteristic("cross section flow"),
capa.features.common.Characteristic("tight loop"),
capa.features.common.Characteristic("stack string"),
capa.features.common.Characteristic("indirect call"),
capa.features.common.OS,
capa.features.common.Arch,
capa.features.common.Characteristic("call $+5"),
capa.features.common.Characteristic("cross section flow"),
capa.features.common.Characteristic("unmanaged call"),
capa.features.common.Class,
capa.features.common.Namespace,
},
}
# global scope features are available in all other scopes
SUPPORTED_FEATURES[INSTRUCTION_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
SUPPORTED_FEATURES[FUNCTION_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
SUPPORTED_FEATURES[FILE_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
# all instruction scope features are also basic block features
SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE].update(SUPPORTED_FEATURES[INSTRUCTION_SCOPE])
# all basic block scope features are also function scope features
SUPPORTED_FEATURES[FUNCTION_SCOPE].update(SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE])
@@ -225,20 +262,8 @@ def parse_feature(key: str):
return capa.features.common.Bytes
elif key == "number":
return capa.features.insn.Number
elif key.startswith("number/"):
bitness = key.partition("/")[2]
# the other handlers here return constructors for features,
# and we want to as well,
# however, we need to preconfigure one of the arguments (`bitness`).
# so, instead we return a partially-applied function that
# provides `bitness` to the feature constructor.
# it forwards any other arguments provided to the closure along to the constructor.
return functools.partial(capa.features.insn.Number, bitness=bitness)
elif key == "offset":
return capa.features.insn.Offset
elif key.startswith("offset/"):
bitness = key.partition("/")[2]
return functools.partial(capa.features.insn.Offset, bitness=bitness)
elif key == "mnemonic":
return capa.features.insn.Mnemonic
elif key == "basic blocks":
@@ -260,8 +285,11 @@ def parse_feature(key: str):
elif key == "format":
return capa.features.common.Format
elif key == "arch":
return capa.features.common.Arch
elif key == "class":
return capa.features.common.Class
elif key == "namespace":
return capa.features.common.Namespace
else:
raise InvalidRule("unexpected statement: %s" % key)
@@ -328,7 +356,14 @@ def parse_description(s: Union[str, int, bytes], value_type: str, description=No
# the string "10" that needs to become the number 10.
if value_type == "bytes":
value = parse_bytes(value)
elif value_type in ("number", "offset") or value_type.startswith(("number/", "offset/")):
elif (
value_type in ("number", "offset")
or value_type.startswith(("number/", "offset/"))
or (
value_type.startswith("operand[")
and (value_type.endswith("].number") or value_type.endswith("].offset"))
)
):
try:
value = parse_int(value)
except ValueError:
@@ -406,7 +441,7 @@ def build_statements(d, scope: str):
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(FUNCTION_SCOPE, build_statements(d[key][0], FUNCTION_SCOPE))
return ceng.Subscope(FUNCTION_SCOPE, build_statements(d[key][0], FUNCTION_SCOPE), description=description)
elif key == "basic block":
if scope != FUNCTION_SCOPE:
@@ -415,7 +450,30 @@ def build_statements(d, scope: str):
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(BASIC_BLOCK_SCOPE, build_statements(d[key][0], BASIC_BLOCK_SCOPE))
return ceng.Subscope(BASIC_BLOCK_SCOPE, build_statements(d[key][0], BASIC_BLOCK_SCOPE), description=description)
elif key == "instruction":
if scope not in (FUNCTION_SCOPE, BASIC_BLOCK_SCOPE):
raise InvalidRule("instruction subscope supported only for function and basic block scope")
if len(d[key]) == 1:
statements = build_statements(d[key][0], INSTRUCTION_SCOPE)
else:
# for instruction subscopes, we support a shorthand in which the top level AND is implied.
# the following are equivalent:
#
# - instruction:
# - and:
# - arch: i386
# - mnemonic: cmp
#
# - instruction:
# - arch: i386
# - mnemonic: cmp
#
statements = ceng.And([build_statements(dd, INSTRUCTION_SCOPE) for dd in d[key]])
return ceng.Subscope(INSTRUCTION_SCOPE, statements, description=description)
elif key.startswith("count(") and key.endswith(")"):
# e.g.:
@@ -472,6 +530,37 @@ def build_statements(d, scope: str):
raise InvalidRule("unexpected range: %s" % (count))
elif key == "string" and not isinstance(d[key], str):
raise InvalidRule("ambiguous string value %s, must be defined as explicit string" % d[key])
elif key.startswith("operand[") and key.endswith("].number"):
index = key[len("operand[") : -len("].number")]
try:
index = int(index)
except ValueError:
raise InvalidRule("operand index must be an integer")
value, description = parse_description(d[key], key, d.get("description"))
try:
feature = capa.features.insn.OperandNumber(index, value, description=description)
except ValueError as e:
raise InvalidRule(str(e))
ensure_feature_valid_for_scope(scope, feature)
return feature
elif key.startswith("operand[") and key.endswith("].offset"):
index = key[len("operand[") : -len("].offset")]
try:
index = int(index)
except ValueError:
raise InvalidRule("operand index must be an integer")
value, description = parse_description(d[key], key, d.get("description"))
try:
feature = capa.features.insn.OperandOffset(index, value, description=description)
except ValueError as e:
raise InvalidRule(str(e))
ensure_feature_valid_for_scope(scope, feature)
return feature
elif (
(key == "os" and d[key] not in capa.features.common.VALID_OS)
or (key == "format" and d[key] not in capa.features.common.VALID_FORMAT)
@@ -595,6 +684,9 @@ class Rule:
for new_rule in self._extract_subscope_rules_rec(child):
yield new_rule
def is_subscope_rule(self):
return bool(self.meta.get("capa/subscope-rule", False))
def extract_subscope_rules(self):
"""
scan through the statements of this rule,
@@ -619,8 +711,10 @@ class Rule:
for new_rule in self._extract_subscope_rules_rec(self.statement):
yield new_rule
def evaluate(self, features: FeatureSet):
return self.statement.evaluate(features)
def evaluate(self, features: FeatureSet, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.rule"] += 1
return self.statement.evaluate(features, short_circuit=short_circuit)
@classmethod
def from_dict(cls, d, definition):
@@ -958,12 +1052,27 @@ class RuleSet:
if len(rules) == 0:
raise InvalidRuleSet("no rules selected")
rules = capa.optimizer.optimize_rules(rules)
self.file_rules = self._get_rules_for_scope(rules, FILE_SCOPE)
self.function_rules = self._get_rules_for_scope(rules, FUNCTION_SCOPE)
self.basic_block_rules = self._get_rules_for_scope(rules, BASIC_BLOCK_SCOPE)
self.instruction_rules = self._get_rules_for_scope(rules, INSTRUCTION_SCOPE)
self.rules = {rule.name: rule for rule in rules}
self.rules_by_namespace = index_rules_by_namespace(rules)
# unstable
(self._easy_file_rules_by_feature, self._hard_file_rules) = self._index_rules_by_feature(self.file_rules)
(self._easy_function_rules_by_feature, self._hard_function_rules) = self._index_rules_by_feature(
self.function_rules
)
(self._easy_basic_block_rules_by_feature, self._hard_basic_block_rules) = self._index_rules_by_feature(
self.basic_block_rules
)
(self._easy_instruction_rules_by_feature, self._hard_instruction_rules) = self._index_rules_by_feature(
self.instruction_rules
)
def __len__(self):
return len(self.rules)
@@ -973,6 +1082,156 @@ class RuleSet:
def __contains__(self, rulename):
return rulename in self.rules
@staticmethod
def _index_rules_by_feature(rules) -> Tuple[Dict[Feature, Set[str]], List[str]]:
"""
split the given rules into two structures:
- "easy rules" are indexed by feature,
such that you can quickly find the rules that contain a given feature.
- "hard rules" are those that contain substring/regex/bytes features or match statements.
these continue to be ordered topologically.
a rule evaluator can use the "easy rule" index to restrict the
candidate rules that might match a given set of features.
at this time, a rule evaluator can't do anything special with
the "hard rules". it must still do a full top-down match of each
rule, in topological order.
this does not index global features, because these are not selective, and
won't be used as the sole feature used to match.
"""
# we'll do a couple phases:
#
# 1. recursively visit all nodes in all rules,
# a. indexing all features
# b. recording the types of features found per rule
# 2. compute the easy and hard rule sets
# 3. remove hard rules from the rules-by-feature index
# 4. construct the topologically ordered list of hard rules
rules_with_easy_features: Set[str] = set()
rules_with_hard_features: Set[str] = set()
rules_by_feature: Dict[Feature, Set[str]] = collections.defaultdict(set)
def rec(rule_name: str, node: Union[Feature, Statement]):
"""
walk through a rule's logic tree, indexing the easy and hard rules,
and the features referenced by easy rules.
"""
if isinstance(
node,
(
# these are the "hard features"
# substring: scanning feature
capa.features.common.Substring,
# regex: scanning feature
capa.features.common.Regex,
# bytes: scanning feature
capa.features.common.Bytes,
# match: dependency on another rule,
# which we have to evaluate first,
# and is therefore tricky.
capa.features.common.MatchedRule,
),
):
# hard feature: requires scan or match lookup
rules_with_hard_features.add(rule_name)
elif isinstance(node, capa.features.common.Feature):
if capa.features.common.is_global_feature(node):
# we don't want to index global features
# because they're not very selective.
#
# they're global, so if they match at one location in a file,
# they'll match at every location in a file.
# so thats not helpful to decide how to downselect.
#
# and, a global rule will never be the sole selector in a rule.
# TODO: probably want a lint for this.
pass
else:
# easy feature: hash lookup
rules_with_easy_features.add(rule_name)
rules_by_feature[node].add(rule_name)
elif isinstance(node, (ceng.Not)):
# `not:` statements are tricky to deal with.
#
# first, features found under a `not:` should not be indexed,
# because they're not wanted to be found.
# second, `not:` can be nested under another `not:`, or two, etc.
# third, `not:` at the root or directly under an `or:`
# means the rule will match against *anything* not specified there,
# which is a difficult set of things to compute and index.
#
# so, if a rule has a `not:` statement, its hard.
# as of writing, this is an uncommon statement, with only 6 instances in 740 rules.
rules_with_hard_features.add(rule_name)
elif isinstance(node, (ceng.Some)) and node.count == 0:
# `optional:` and `0 or more:` are tricky to deal with.
#
# when a subtree is optional, it may match, but not matching
# doesn't have any impact either.
# now, our rule authors *should* not put this under `or:`
# and this is checked by the linter,
# but this could still happen (e.g. private rule set without linting)
# and would be hard to trace down.
#
# so better to be safe than sorry and consider this a hard case.
rules_with_hard_features.add(rule_name)
elif isinstance(node, (ceng.Range)) and node.min == 0:
# `count(foo): 0 or more` are tricky to deal with.
# because the min is 0,
# this subtree *can* match just about any feature
# (except the given one)
# which is a difficult set of things to compute and index.
rules_with_hard_features.add(rule_name)
elif isinstance(node, (ceng.Range)):
rec(rule_name, node.child)
elif isinstance(node, (ceng.And, ceng.Or, ceng.Some)):
for child in node.children:
rec(rule_name, child)
elif isinstance(node, ceng.Statement):
# unhandled type of statement.
# this should only happen if a new subtype of `Statement`
# has since been added to capa.
#
# ideally, we'd like to use mypy for exhaustiveness checking
# for all the subtypes of `Statement`.
# but, as far as i can tell, mypy does not support this type
# of checking.
#
# in a way, this makes some intuitive sense:
# the set of subtypes of type A is unbounded,
# because any user might come along and create a new subtype B,
# so mypy can't reason about this set of types.
assert False, f"Unhandled value: {node} ({type(node).__name__})"
else:
# programming error
assert_never(node)
for rule in rules:
rule_name = rule.meta["name"]
root = rule.statement
rec(rule_name, root)
# if a rule has a hard feature,
# dont consider it easy, and therefore,
# don't index any of its features.
#
# otherwise, its an easy rule, and index its features
for rules_with_feature in rules_by_feature.values():
rules_with_feature.difference_update(rules_with_hard_features)
easy_rules_by_feature = rules_by_feature
# `rules` is already topologically ordered,
# so extract our hard set into the topological ordering.
hard_rules = []
for rule in rules:
if rule.meta["name"] in rules_with_hard_features:
hard_rules.append(rule.meta["name"])
return (easy_rules_by_feature, hard_rules)
@staticmethod
def _get_rules_for_scope(rules, scope):
"""
@@ -989,7 +1248,7 @@ class RuleSet:
# at lower scope, e.g. function scope.
# so, we find all dependencies of all rules, and later will filter them down.
for rule in rules:
if rule.meta.get("capa/subscope-rule", False):
if rule.is_subscope_rule():
continue
scope_rules.update(get_rules_and_dependencies(rules, rule.name))
@@ -1034,4 +1293,76 @@ class RuleSet:
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)))
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)))
break
return RuleSet(list(rules_filtered))
def match(self, scope: Scope, features: FeatureSet, addr: Address) -> Tuple[FeatureSet, ceng.MatchResults]:
"""
match rules from this ruleset at the given scope against the given features.
this routine should act just like `capa.engine.match`,
except that it may be more performant.
"""
easy_rules_by_feature = {}
if scope is Scope.FILE:
easy_rules_by_feature = self._easy_file_rules_by_feature
hard_rule_names = self._hard_file_rules
elif scope is Scope.FUNCTION:
easy_rules_by_feature = self._easy_function_rules_by_feature
hard_rule_names = self._hard_function_rules
elif scope is Scope.BASIC_BLOCK:
easy_rules_by_feature = self._easy_basic_block_rules_by_feature
hard_rule_names = self._hard_basic_block_rules
elif scope is Scope.INSTRUCTION:
easy_rules_by_feature = self._easy_instruction_rules_by_feature
hard_rule_names = self._hard_instruction_rules
else:
assert_never(scope)
candidate_rule_names = set()
for feature in features:
easy_rule_names = easy_rules_by_feature.get(feature)
if easy_rule_names:
candidate_rule_names.update(easy_rule_names)
# first, match against the set of rules that have at least one
# feature shared with our feature set.
candidate_rules = [self.rules[name] for name in candidate_rule_names]
features2, easy_matches = ceng.match(candidate_rules, features, addr)
# note that we've stored the updated feature set in `features2`.
# this contains a superset of the features in `features`;
# it contains additional features for any easy rule matches.
# we'll pass this feature set to hard rule matching, since one
# of those rules might rely on an easy rule match.
#
# the updated feature set from hard matching will go into `features3`.
# this is a superset of `features2` is a superset of `features`.
# ultimately, this is what we'll return to the caller.
#
# in each case, we could have assigned the updated feature set back to `features`,
# but this is slightly more explicit how we're tracking the data.
# now, match against (topologically ordered) list of rules
# that we can't really make any guesses about.
# these are rules with hard features, like substring/regex/bytes and match statements.
hard_rules = [self.rules[name] for name in hard_rule_names]
features3, hard_matches = ceng.match(hard_rules, features2, addr)
# note that above, we probably are skipping matching a bunch of
# rules that definitely would never hit.
# specifically, "easy rules" that don't share any features with
# feature set.
# MatchResults doesn't technically have an .update() method
# but a dict does.
matches = {} # type: ignore
matches.update(easy_matches)
matches.update(hard_matches)
return (features3, matches)

View File

@@ -1 +1,13 @@
__version__ = "3.0.2"
__version__ = "4.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

@@ -1,8 +1,8 @@
# Installation
You can install capa in a few different ways. First, if you simply want to use capa, just download the [standalone binary](https://github.com/fireeye/capa/releases). If you want to use capa as a Python library, you can install the package directly from GitHub using `pip`. If you'd like to contribute patches or features to capa, you can work with a local copy of the source code.
You can install capa in a few different ways. First, if you simply want to use capa, just download the [standalone binary](https://github.com/mandiant/capa/releases). If you want to use capa as a Python library, you can install the package directly from GitHub using `pip`. If you'd like to contribute patches or features to capa, you can work with a local copy of the source code.
## Method 1: Standalone installation
If you simply want to use capa, use the standalone binaries we host on GitHub: https://github.com/fireeye/capa/releases. These binary executable files contain all the source code, Python interpreter, and associated resources needed to make capa run. This means you can run it without any installation! Just invoke the file using your terminal shell to see the help documentation.
If you simply want to use capa, use the standalone binaries we host on GitHub: https://github.com/mandiant/capa/releases. These binary executable files contain all the source code, Python interpreter, and associated resources needed to make capa run. This means you can run it without any installation! Just invoke the file using your terminal shell to see the help documentation.
We use PyInstaller to create these packages.
@@ -26,8 +26,14 @@ To install capa as a Python library use `pip` to fetch the `flare-capa` module.
#### *Note*:
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/fireeye/capa-rules/) and pass the directory to the entrypoint using `-r` or set the rules path in the IDA Pro plugin.
This technique also doesn't set up the default library identification [signatures](https://github.com/fireeye/capa/tree/master/sigs). You can pass the signature directory using the `-s` argument.
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:
```console
$ git clone https://github.com/mandiant/capa-rules.git -b v3 /local/path/to/rules
$ capa -r /local/path/to/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:
capa -r /path/to/capa-rules -s /path/to/capa-sigs suspicious.exe
@@ -44,16 +50,16 @@ If you'd like to review and modify the capa source code, you'll need to check it
### 1. Check out source code
Next, clone the capa git repository.
We use submodules to separate [code](https://github.com/fireeye/capa), [rules](https://github.com/fireeye/capa-rules), and [test data](https://github.com/fireeye/capa-testfiles).
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.
- `$ git clone --recurse-submodules https://github.com/fireeye/capa.git /local/path/to/src` (HTTPS)
- `$ git clone --recurse-submodules git@github.com:fireeye/capa.git /local/path/to/src` (SSH)
- `$ 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:
- clone repository
- `$ git clone https://github.com/fireeye/capa.git /local/path/to/src` (HTTPS)
- `$ git clone git@github.com:fireeye/capa.git /local/path/to/src` (SSH)
- `$ 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`
- `$ git submodule update --init rules`
@@ -87,7 +93,7 @@ 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`
- [dos2unix](https://linux.die.net/man/1/dos2unix) for UNIX-style LF newlines
- [capafmt](https://github.com/fireeye/capa/blob/master/scripts/capafmt.py) rule formatter
- [capafmt](https://github.com/mandiant/capa/blob/master/scripts/capafmt.py) rule formatter
To install these development dependencies, run:

View File

@@ -5,7 +5,7 @@ If capa detects that a program may be packed using its rules it warns the user.
# Installers, run-time programs, etc.
capa cannot handle installers, run-time programs like .NET applications, or other packaged applications like AutoIt well. This means that the results may be misleading or incomplete.
capa cannot handle installers, run-time programs, or other packaged applications like AutoIt well. This means that the results may be misleading or incomplete.
If capa detects an installer, run-time program, etc. it warns the user.
@@ -46,6 +46,6 @@ We need more practical use cases and test samples to justify the additional work
# ATT&CK, MAEC, MBC, and other capability tagging
capa uses namespaces to group capabilities (see https://github.com/fireeye/capa-rules/tree/master#namespace-organization).
capa uses namespaces to group capabilities (see https://github.com/mandiant/capa-rules/tree/master#namespace-organization).
The `rule.meta` field also supports `att&ck`, `mbc`, and `maec` fields to associate rules with the respective taxonomy (see https://github.com/fireeye/capa-rules/blob/master/doc/format.md#meta-block).
The `rule.meta` field also supports `att&ck`, `mbc`, and `maec` fields to associate rules with the respective taxonomy (see https://github.com/mandiant/capa-rules/blob/master/doc/format.md#meta-block).

View File

@@ -1,13 +1,13 @@
# Release checklist
- [ ] Ensure all [milestoned issues/PRs](https://github.com/fireeye/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/fireeye/capa/pulls) and [capa-rules](https://github.com/fireeye/capa-rules/pulls).
- [ ] Ensure the [CI workflow succeeds in master](https://github.com/fireeye/capa/actions/workflows/tests.yml?query=branch%3Amaster).
- [ ] 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).
- [ ] Review changes
- capa https://github.com/fireeye/capa/compare/\<last-release\>...master
- capa-rules https://github.com/fireeye/capa-rules/compare/\<last-release>\...master
- [ ] Update [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md)
- capa https://github.com/mandiant/capa/compare/\<last-release\>...master
- capa-rules https://github.com/mandiant/capa-rules/compare/\<last-release>\...master
- [ ] Update [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md)
- Do not forget to add a nice introduction thanking contributors
- Remember that we need a major release if we introduce breaking changes
- Sections: see template below
@@ -31,13 +31,19 @@
### Development
### Raw diffs
- [capa <release>...master](https://github.com/fireeye/capa/compare/<release>...master)
- [capa-rules <release>...master](https://github.com/fireeye/capa-rules/compare/<release>...master)
- [capa <release>...master](https://github.com/mandiant/capa/compare/<release>...master)
- [capa-rules <release>...master](https://github.com/mandiant/capa-rules/compare/<release>...master)
```
- [ ] Update [capa/version.py](https://github.com/fireeye/capa/blob/master/capa/version.py)
- [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/fireeye/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/fireeye/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md).
- [ ] Verify GH actions [upload artifacts](https://github.com/fireeye/capa/releases), [publish to PyPI](https://pypi.org/project/flare-capa) and [create a tag in capa rules](https://github.com/fireeye/capa-rules/tags) upon completion.
- [ ] 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
```
- [ ] [Spread the word](https://twitter.com)
- [ ] Update internal service

72
doc/rules.md Normal file
View File

@@ -0,0 +1,72 @@
### 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.
When you download a standalone version of capa, this standard library is embedded within the executable and capa will use these rules by default:
```console
$ 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
- collect a private selection of rules not shared publicly.
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
```
You can collect the standard set of rules in two ways:
- [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.
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
ERROR:lint:invalid rule: injection.yml: invalid rule: unexpected statement: instruction
```
You can check the version of capa you're currently using like this:
```console
$ 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: f04491001d...ad4da12d90

View File

@@ -47,7 +47,7 @@ usage:
parallelism factor
--no-mp disable subprocesses
Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -68,6 +68,7 @@ import capa
import capa.main
import capa.rules
import capa.render.json
import capa.render.result_document as rd
logger = logging.getLogger("capa")
@@ -126,18 +127,14 @@ def get_capa_results(args):
"error": "unexpected error: %s" % (e),
}
meta = capa.main.collect_metadata("", path, "", extractor)
meta = capa.main.collect_metadata([], path, [], extractor)
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
meta["analysis"].update(counts)
meta["analysis"]["layout"] = capa.main.compute_layout(rules, extractor, capabilities)
return {
"path": path,
"status": "ok",
"ok": {
"meta": meta,
"capabilities": capabilities,
},
}
doc = rd.ResultDocument.from_capa(meta, rules, capabilities)
return {"path": path, "status": "ok", "ok": doc.dict(exclude_none=True)}
def main(argv=None):
@@ -204,11 +201,7 @@ def main(argv=None):
if result["status"] == "error":
logger.warning(result["error"])
elif result["status"] == "ok":
meta = result["ok"]["meta"]
capabilities = result["ok"]["capabilities"]
# our renderer expects to emit a json document for a single sample
# so we deserialize the json document, store it in a larger dict, and we'll subsequently re-encode.
results[result["path"]] = json.loads(capa.render.json.render(meta, rules, capabilities))
results[result["path"]] = rd.ResultDocument.parse_obj(result["ok"]).json(exclude_none=True)
else:
raise ValueError("unexpected status: %s" % (result["status"]))

View File

@@ -21,7 +21,7 @@ optional arguments:
-t TAG, --tag TAG filter on rule meta field values
Copyright (C) 2020, 2021 Arnim Rupp (@ruppde) and FireEye, Inc. All Rights Reserved.
Copyright (C) 2020, 2021 Arnim Rupp (@ruppde) and 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
@@ -43,7 +43,6 @@ import capa.rules
import capa.engine
import capa.features
import capa.features.insn
from capa.features.common import BITNESS_X32, BITNESS_X64, String
logger = logging.getLogger("capa2yara")
@@ -57,8 +56,8 @@ var_names = ["".join(letters) for letters in itertools.product(string.ascii_lowe
unsupported = ["characteristic", "mnemonic", "offset", "subscope", "Range"]
# TODO shorten this list, possible stuff:
# - 2 or more strings: e.g.
# -- https://github.com/fireeye/capa-rules/blob/master/collection/file-managers/gather-direct-ftp-information.yml
# -- https://github.com/fireeye/capa-rules/blob/master/collection/browser/gather-firefox-profile-information.yml
# -- https://github.com/mandiant/capa-rules/blob/master/collection/file-managers/gather-direct-ftp-information.yml
# -- https://github.com/mandiant/capa-rules/blob/master/collection/browser/gather-firefox-profile-information.yml
# - count(string (1 rule: /executable/subfile/pe/contain-an-embedded-pe-file.yml)
# - count(match( could be done by creating the referenced rule a 2nd time with the condition, that it hits x times (only 1 rule: ./anti-analysis/anti-disasm/contain-anti-disasm-techniques.yml)
# - it would be technically possible to get the "basic blocks" working, but the rules contain mostly other non supported statements in there => not worth the effort.
@@ -104,7 +103,7 @@ def check_feature(statement, rulename):
def get_rule_url(path):
path = re.sub(r"\.\.\/", "", path)
path = re.sub(r"capa-rules\/", "", path)
return "https://github.com/fireeye/capa-rules/blob/master/" + path
return "https://github.com/mandiant/capa-rules/blob/master/" + path
def convert_capa_number_to_yara_bytes(number):
@@ -176,12 +175,20 @@ def convert_rule(rule, rulename, cround, depth):
elif s_type == "api" or s_type == "import":
# TODO: is it possible in YARA to make a difference between api & import?
# https://github.com/fireeye/capa-rules/blob/master/doc/format.md#api
# https://github.com/mandiant/capa-rules/blob/master/doc/format.md#api
api = kid.value
logger.info("doing api: " + repr(api))
# e.g. kernel32.CreateNamedPipe => look for kernel32.dll and CreateNamedPipe
if "." in api:
# TODO: improve .NET API call handling
if "::" in api:
mod, api = api.split("::")
var_name = "api_" + var_names.pop(0)
yara_strings += "\t$" + var_name + " = /\\b" + api + "(A|W)?\\b/ ascii wide\n"
yara_condition += "\t$" + var_name + " "
elif api.count(".") == 1:
dll, api = api.split(".")
# usage of regex is needed and /i because string search for "CreateMutex" in imports() doesn't look for e.g. CreateMutexA
@@ -208,7 +215,7 @@ def convert_rule(rule, rulename, cround, depth):
yara_condition += '\tpe.exports("' + export + '") '
elif s_type == "section":
# https://github.com/fireeye/capa-rules/blob/master/doc/format.md#section
# https://github.com/mandiant/capa-rules/blob/master/doc/format.md#section
section = kid.value
logger.info("doing section: " + repr(section))
@@ -220,7 +227,7 @@ def convert_rule(rule, rulename, cround, depth):
)
elif s_type == "match":
# https://github.com/fireeye/capa-rules/blob/master/doc/format.md#matching-prior-rule-matches-and-namespaces
# https://github.com/mandiant/capa-rules/blob/master/doc/format.md#matching-prior-rule-matches-and-namespaces
match = kid.value
logger.info("doing match: " + repr(match))
@@ -311,7 +318,7 @@ def convert_rule(rule, rulename, cround, depth):
return yara_strings, yara_condition
############################## end def do_statement
# end: def do_statement
yara_strings_list = []
yara_condition_list = []
@@ -390,7 +397,9 @@ def convert_rule(rule, rulename, cround, depth):
logger.info("kid coming: " + repr(kid.name))
# logger.info("grandchildren: " + repr(kid.children))
##### here we go into RECURSION ##################################################################################
#
# here we go into RECURSION
#
yara_strings_sub, yara_condition_sub, rule_comment_sub, incomplete_sub = convert_rule(
kid, rulename, cround, depth
)
@@ -496,9 +505,7 @@ def convert_rule(rule, rulename, cround, depth):
yara_condition = "\n\t" + yara_condition_list[0]
logger.info(
f"################# end of convert_rule() #strings: {len(yara_strings_list)} #conditions: {len(yara_condition_list)}"
)
logger.info(f"# end of convert_rule() #strings: {len(yara_strings_list)} #conditions: {len(yara_condition_list)}")
logger.info(f"strings: {yara_strings} conditions: {yara_condition}")
return yara_strings, yara_condition, rule_comment, incomplete
@@ -535,7 +542,7 @@ def convert_rules(rules, namespaces, cround):
rule_name = convert_rule_name(rule.name)
if rule.meta.get("capa/subscope-rule", False):
if rule.is_subscope_rule():
logger.info("skipping sub scope rule capa: " + rule.name)
continue
@@ -617,7 +624,7 @@ def convert_rules(rules, namespaces, cround):
# 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
if not value in seen_hashes:
if value not in seen_hashes:
yara_meta += "\t" + meta_name + ' = "' + value + '"\n'
seen_hashes.append(value)
@@ -703,7 +710,7 @@ def main(argv=None):
logging.getLogger("capa2yara").setLevel(level)
try:
rules = capa.main.get_rules(args.rules, disable_progress=True)
rules = capa.main.get_rules([args.rules], disable_progress=True)
namespaces = capa.rules.index_rules_by_namespace(list(rules))
rules = capa.rules.RuleSet(rules)
logger.info("successfully loaded %s rules (including subscope rules which will be ignored)", len(rules))
@@ -717,7 +724,7 @@ def main(argv=None):
return -1
output_yar(
"// Rules from FireEye's https://github.com/fireeye/capa-rules converted to YARA using https://github.com/fireeye/capa/blob/master/scripts/capa2yara.py by Arnim Rupp"
"// Rules from Mandiant's https://github.com/mandiant/capa-rules converted to YARA using https://github.com/mandiant/capa/blob/master/scripts/capa2yara.py by Arnim Rupp"
)
output_yar(
"// Beware: These are less rules than capa (because not all fit into YARA, stats at EOF) and is less precise because e.g. capas function scopes are applied to the whole file"

View File

@@ -2,6 +2,7 @@
import json
import collections
from typing import Any, Dict
import capa.main
import capa.rules
@@ -10,52 +11,48 @@ import capa.features
import capa.render.json
import capa.render.utils as rutils
import capa.render.default
import capa.render.result_document
import capa.render.result_document as rd
import capa.features.freeze.features as frzf
from capa.engine import *
# edit this to set the path for file to analyze and rule directory
RULES_PATH = "/tmp/capa/rules/"
# load rules from disk
rules = capa.rules.RuleSet(capa.main.get_rules(RULES_PATH, disable_progress=True))
# == Render ddictionary helpers
def render_meta(doc, ostream):
ostream["md5"] = doc["meta"]["sample"]["md5"]
ostream["sha1"] = doc["meta"]["sample"]["sha1"]
ostream["sha256"] = doc["meta"]["sample"]["sha256"]
ostream["path"] = doc["meta"]["sample"]["path"]
def render_meta(doc: rd.ResultDocument, result):
result["md5"] = doc.meta.sample.md5
result["sha1"] = doc.meta.sample.sha1
result["sha256"] = doc.meta.sample.sha256
result["path"] = doc.meta.sample.path
def find_subrule_matches(doc):
def find_subrule_matches(doc: rd.ResultDocument) -> Set[str]:
"""
collect the rule names that have been matched as a subrule match.
this way we can avoid displaying entries for things that are too specific.
"""
matches = set([])
def rec(node):
if not node["success"]:
def rec(node: rd.Match):
if not node.success:
# there's probably a bug here for rules that do `not: match: ...`
# but we don't have any examples of this yet
return
elif node["node"]["type"] == "statement":
for child in node["children"]:
elif isinstance(node.node, rd.StatementNode):
for child in node.children:
rec(child)
elif node["node"]["type"] == "feature":
if node["node"]["feature"]["type"] == "match":
matches.add(node["node"]["feature"]["match"])
elif isinstance(node.node, rd.FeatureNode):
if isinstance(node.node.feature, frzf.MatchFeature):
matches.add(node.node.feature.match)
for rule in rutils.capability_rules(doc):
for node in rule["matches"].values():
for _, node in rule.matches:
rec(node)
return matches
def render_capabilities(doc, ostream):
def render_capabilities(doc: rd.ResultDocument, result):
"""
example::
{'CAPABILITY': {'accept command line arguments': 'host-interaction/cli',
@@ -68,25 +65,25 @@ def render_capabilities(doc, ostream):
"""
subrule_matches = find_subrule_matches(doc)
ostream["CAPABILITY"] = dict()
result["CAPABILITY"] = dict()
for rule in rutils.capability_rules(doc):
if rule["meta"]["name"] in subrule_matches:
if rule.meta.name in subrule_matches:
# rules that are also matched by other rules should not get rendered by default.
# this cuts down on the amount of output while giving approx the same detail.
# see #224
continue
count = len(rule["matches"])
count = len(rule.matches)
if count == 1:
capability = rule["meta"]["name"]
capability = rule.meta.name
else:
capability = "%s (%d matches)" % (rule["meta"]["name"], count)
capability = "%s (%d matches)" % (rule.meta.name, count)
ostream["CAPABILITY"].setdefault(rule["meta"]["namespace"], list())
ostream["CAPABILITY"][rule["meta"]["namespace"]].append(capability)
result["CAPABILITY"].setdefault(rule.meta.namespace, list())
result["CAPABILITY"][rule.meta.namespace].append(capability)
def render_attack(doc, ostream):
def render_attack(doc, result):
"""
example::
{'ATT&CK': {'COLLECTION': ['Input Capture::Keylogging [T1056.001]'],
@@ -99,13 +96,13 @@ def render_attack(doc, ostream):
'EXECUTION': ['Shared Modules [T1129]']}
}
"""
ostream["ATTCK"] = dict()
result["ATTCK"] = dict()
tactics = collections.defaultdict(set)
for rule in rutils.capability_rules(doc):
if not rule["meta"].get("att&ck"):
if not rule.meta.attack:
continue
for attack in rule["meta"]["att&ck"]:
tactics[attack["tactic"]].add((attack["technique"], attack.get("subtechnique"), attack["id"]))
for attack in rule.meta.attack:
tactics[attack.tactic].add((attack.technique, attack.subtechnique, attack.id))
for tactic, techniques in sorted(tactics.items()):
inner_rows = []
@@ -114,10 +111,10 @@ def render_attack(doc, ostream):
inner_rows.append("%s %s" % (technique, id))
else:
inner_rows.append("%s::%s %s" % (technique, subtechnique, id))
ostream["ATTCK"].setdefault(tactic.upper(), inner_rows)
result["ATTCK"].setdefault(tactic.upper(), inner_rows)
def render_mbc(doc, ostream):
def render_mbc(doc, result):
"""
example::
{'MBC': {'ANTI-BEHAVIORAL ANALYSIS': ['Debugger Detection::Timing/Delay Check '
@@ -132,14 +129,14 @@ def render_mbc(doc, ostream):
'[C0021.004]']}
}
"""
ostream["MBC"] = dict()
result["MBC"] = dict()
objectives = collections.defaultdict(set)
for rule in rutils.capability_rules(doc):
if not rule["meta"].get("mbc"):
if not rule.meta.mbc:
continue
for mbc in rule["meta"]["mbc"]:
objectives[mbc["objective"]].add((mbc["behavior"], mbc.get("method"), mbc["id"]))
for mbc in rule.meta.mbc:
objectives[mbc.objective].add((mbc.behavior, mbc.method, mbc.id))
for objective, behaviors in sorted(objectives.items()):
inner_rows = []
@@ -148,34 +145,37 @@ def render_mbc(doc, ostream):
inner_rows.append("%s [%s]" % (behavior, id))
else:
inner_rows.append("%s::%s [%s]" % (behavior, method, id))
ostream["MBC"].setdefault(objective.upper(), inner_rows)
result["MBC"].setdefault(objective.upper(), inner_rows)
def render_dictionary(doc):
ostream = dict()
render_meta(doc, ostream)
render_attack(doc, ostream)
render_mbc(doc, ostream)
render_capabilities(doc, ostream)
def render_dictionary(doc: rd.ResultDocument) -> Dict[str, Any]:
result: Dict[str, Any] = dict()
render_meta(doc, result)
render_attack(doc, result)
render_mbc(doc, result)
render_capabilities(doc, result)
return ostream
return result
# ==== render dictionary helpers
def capa_details(file_path, output_format="dictionary"):
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))
# extract features and find capabilities
extractor = capa.main.get_extractor(file_path, "auto", capa.main.BACKEND_VIV, [], False, disable_progress=True)
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
# collect metadata (used only to make rendering more complete)
meta = capa.main.collect_metadata("", file_path, RULES_PATH, extractor)
meta = capa.main.collect_metadata([], file_path, rules_path, extractor)
meta["analysis"].update(counts)
meta["analysis"]["layout"] = capa.main.compute_layout(rules, extractor, capabilities)
capa_output = False
if output_format == "dictionary":
# ...as python dictionary, simplified as textable but in dictionary
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
doc = rd.ResultDocument.from_capa(meta, rules, capabilities)
capa_output = render_dictionary(doc)
elif output_format == "json":
# render results
@@ -186,3 +186,22 @@ def capa_details(file_path, output_format="dictionary"):
capa_output = capa.render.default.render(meta, rules, capabilities)
return capa_output
if __name__ == "__main__":
import sys
import os.path
import argparse
RULES_PATH = os.path.join(os.path.dirname(__file__), "..", "rules")
parser = argparse.ArgumentParser(description="Extract capabilities from a file")
parser.add_argument("file", help="file to extract capabilities from")
parser.add_argument("--rules", help="path to rules directory", default=os.path.abspath(RULES_PATH))
parser.add_argument(
"--output", help="output format", choices=["dictionary", "json", "texttable"], default="dictionary"
)
args = parser.parse_args()
print(capa_details(args.rules, args.file, args.output))
sys.exit(0)

View File

@@ -6,7 +6,7 @@ Usage:
$ python capafmt.py -i foo.yml
Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python2
"""
Copyright (C) 2021 FireEye, Inc. All Rights Reserved.
Copyright (C) 2021 Mandiant, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at: [package root]/LICENSE.txt

View File

@@ -20,7 +20,7 @@ Adapted for Binary Ninja by @psifertex
This script will verify that the report matches the workspace.
Check the log window for any errors, and/or the summary of changes.
Derived from: https://github.com/fireeye/capa/blob/master/scripts/import-to-ida.py
Derived from: https://github.com/mandiant/capa/blob/master/scripts/import-to-ida.py
"""
import os
import json

View File

@@ -20,7 +20,7 @@ and then select the existing capa report from the file system.
This script will verify that the report matches the workspace.
Check the output window for any errors, and/or the summary of changes.
Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at: [package root]/LICENSE.txt

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