Compare commits

...

736 Commits

Author SHA1 Message Date
mr-tz
481ae685e1 move sigs to capa directory 2024-01-18 12:31:55 +01:00
Moritz
12b628318d Merge pull request #1930 from mandiant/dependabot/pip/pytest-7.4.4
build(deps-dev): bump pytest from 7.4.3 to 7.4.4
2024-01-18 10:17:21 +01:00
Moritz
be30117030 Merge pull request #1931 from mandiant/dependabot/pip/ruff-0.1.13
build(deps-dev): bump ruff from 0.1.9 to 0.1.13
2024-01-18 10:17:05 +01:00
Capa Bot
6b41e02d63 Sync capa rules submodule 2024-01-17 08:22:01 +00:00
Capa Bot
d2ca130060 Sync capa rules submodule 2024-01-17 08:10:13 +00:00
Moritz
50dcf7ca20 Merge pull request #1932 from mandiant/update-lint-data-20241
update lint data
2024-01-17 09:07:48 +01:00
mr-tz
9bc04ec612 update data via script 2024-01-16 15:29:25 +01:00
dependabot[bot]
966976d97c build(deps-dev): bump ruff from 0.1.9 to 0.1.13
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.9 to 0.1.13.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.9...v0.1.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-15 14:08:54 +00:00
dependabot[bot]
05d7083890 build(deps-dev): bump pytest from 7.4.3 to 7.4.4
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.3 to 7.4.4.
- [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.4.3...7.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-15 14:08:38 +00:00
Willi Ballenthin
1dc72a3183 elf: detect linux via GCC .ident directives (#1928)
* elf: detect linux via GCC .ident directives

* changelog

* pep8
2024-01-11 16:15:26 +01:00
Capa Bot
efc26be196 Sync capa rules submodule 2024-01-11 14:20:33 +00:00
Willi Ballenthin
f3bc132565 render: show human readable flavor name (#1925) 2024-01-11 14:06:39 +01:00
Willi Ballenthin
ad46b33bb7 com: move database into python files (#1924)
* com: move database into python files

* com: pep8 and lints

* com: fix generated string feature type

* pyinstaller: remove reference to old assets directory
2024-01-11 14:06:24 +01:00
dependabot[bot]
9e5cc07a48 build(deps-dev): bump types-tabulate from 0.9.0.3 to 0.9.0.20240106 (#1923)
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.9.0.3 to 0.9.0.20240106.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-tabulate
  dependency-type: direct:production
  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>
2024-01-09 12:51:03 -07:00
Moritz
f4fecf43bf Merge pull request #1922 from mandiant/dependabot/pip/types-requests-2.31.0.20240106
build(deps-dev): bump types-requests from 2.31.0.10 to 2.31.0.20240106
2024-01-09 16:20:10 +01:00
Moritz
7426574741 Merge pull request #1921 from mandiant/dependabot/pip/flake8-7.0.0
build(deps-dev): bump flake8 from 6.1.0 to 7.0.0
2024-01-09 16:19:57 +01:00
Moritz
9ab7a24153 Merge pull request #1920 from mandiant/dependabot/pip/wcwidth-0.2.13
build(deps-dev): bump wcwidth from 0.2.12 to 0.2.13
2024-01-09 16:19:42 +01:00
Mike Hunhoff
f37b598010 fix: do not trim api names that include :: (#1897) 2024-01-08 10:59:24 -07:00
dependabot[bot]
5ca59634f3 build(deps-dev): bump types-requests from 2.31.0.10 to 2.31.0.20240106
Bumps [types-requests](https://github.com/python/typeshed) from 2.31.0.10 to 2.31.0.20240106.
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-08 14:46:29 +00:00
dependabot[bot]
42c1a307f3 build(deps-dev): bump flake8 from 6.1.0 to 7.0.0
Bumps [flake8](https://github.com/pycqa/flake8) from 6.1.0 to 7.0.0.
- [Commits](https://github.com/pycqa/flake8/compare/6.1.0...7.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-08 14:46:23 +00:00
dependabot[bot]
ef5063171b build(deps-dev): bump wcwidth from 0.2.12 to 0.2.13
Bumps [wcwidth](https://github.com/jquast/wcwidth) from 0.2.12 to 0.2.13.
- [Release notes](https://github.com/jquast/wcwidth/releases)
- [Commits](https://github.com/jquast/wcwidth/compare/0.2.12...0.2.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-08 14:46:19 +00:00
Blas
7584e4a5e6 dotnet: emit enclosing class information for nested classes (#1913)
* Update helpers.py

* Update helpers.py

* TypeRef correction in helpers.py

* Fixed TypeRef to proper functionality

* Accounts for TypeRef updated tuple

* Corrected TypeDef tuple creation in helpers.py

* Update types.py

* Update types.py

* Create helpers_draft.py

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update helper functions, variables, and draft further implementations

* Update helpers.py

* Update types.py

* Directly access TypeDef and TypeRef tables

* Update helpers.py

* Update helpers.py

* Delete capa/features/extractors/dnfile/helpers_draft.py

* Update types.py

* Update dotnetfile.py

* Update types.py comment

* Clean extract_file_class_features in dotnetfile.py

* Cleaned up callers, var names, and other small items

* Update dotnetfile.py

* Clean up caller logic in dotnetfile.py

* Clean up callers and update helper logic in helpers.py

* Linter corrections for types.py

* Linter corrections for dotnetfile.py

* Linter corrections and caller functions cleanup for helpers.py

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update capa/features/extractors/dnfile/helpers.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* Update helpers.py

* Update dotnetfile.py

* Update tuple type in types.py

* Update dotnetfile.py

* Update return value annotations in helpers.py

* Linting update types.py

* Linting update dotnetfile.py

* Added unit tests to fixtures.py

* Update types.py

* Linting fix for types.py

* Update CHANGELOG.md

* Small changes to return types in helpers.py

---------

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2024-01-05 10:09:38 -07:00
Capa Bot
62474c764a Sync capa-testfiles submodule 2024-01-05 14:24:40 +00:00
Capa Bot
1fc26b4f27 Sync capa rules submodule 2024-01-04 13:07:27 +00:00
Capa Bot
037a97381c Sync capa-testfiles submodule 2024-01-04 08:16:43 +00:00
Capa Bot
ef65f14260 Sync capa-testfiles submodule 2024-01-03 16:36:36 +00:00
Capa Bot
3214ecf0ee Sync capa rules submodule 2024-01-03 16:32:40 +00:00
dependabot[bot]
23c5e6797f build(deps-dev): bump ruff from 0.1.7 to 0.1.9 (#1915)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.7 to 0.1.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.7...v0.1.9)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  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>
2024-01-02 10:31:29 -07:00
dependabot[bot]
e940890c29 build(deps-dev): bump mypy from 1.7.1 to 1.8.0 (#1916)
Bumps [mypy](https://github.com/python/mypy) from 1.7.1 to 1.8.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.7.1...v1.8.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  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>
2024-01-02 09:05:49 -07:00
dependabot[bot]
21b76fc91e build(deps-dev): bump setuptools from 69.0.2 to 69.0.3 (#1917)
Bumps [setuptools](https://github.com/pypa/setuptools) from 69.0.2 to 69.0.3.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v69.0.2...v69.0.3)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
  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>
2024-01-02 09:05:27 -07:00
dependabot[bot]
05ef952129 build(deps-dev): bump black from 23.12.0 to 23.12.1 (#1918)
Bumps [black](https://github.com/psf/black) from 23.12.0 to 23.12.1.
- [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/23.12.0...23.12.1)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  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>
2024-01-02 09:05:09 -07:00
Mike Hunhoff
22f4251ad6 ghidra: improve instruction string and bytes feature extraction (#1885)
* ghidra: improve instruction string and bytes feature extraction

* focus on data references only

* remove unneeded check
2023-12-24 18:24:54 -08:00
dependabot[bot]
92478d2469 build(deps-dev): bump black from 23.11.0 to 23.12.0 (#1911)
Bumps [black](https://github.com/psf/black) from 23.11.0 to 23.12.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/23.11.0...23.12.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  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>
2023-12-19 12:29:40 -07:00
dependabot[bot]
2aaba6ef16 build(deps-dev): bump isort from 5.13.0 to 5.13.2 (#1910)
Bumps [isort](https://github.com/pycqa/isort) from 5.13.0 to 5.13.2.
- [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.13.0...5.13.2)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  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>
2023-12-19 11:04:49 -07:00
dependabot[bot]
8120fb796e build(deps-dev): bump flake8-bugbear from 23.11.26 to 23.12.2 (#1892)
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 23.11.26 to 23.12.2.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/23.11.26...23.12.2)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-type: direct:production
  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>
2023-12-19 11:03:51 -07:00
dependabot[bot]
f3c38ae300 build(deps-dev): bump termcolor from 2.3.0 to 2.4.0 (#1891)
Bumps [termcolor](https://github.com/termcolor/termcolor) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/termcolor/termcolor/releases)
- [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md)
- [Commits](https://github.com/termcolor/termcolor/compare/2.3.0...2.4.0)

---
updated-dependencies:
- dependency-name: termcolor
  dependency-type: direct:production
  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>
2023-12-19 11:03:39 -07:00
Capa Bot
bf56ee0311 Sync capa rules submodule 2023-12-18 06:54:41 +00:00
Capa Bot
4a84660e76 Sync capa rules submodule 2023-12-18 06:54:07 +00:00
Mike Hunhoff
382c20cd58 ghidra: fix UnboundLocalError exception (#1881) 2023-12-15 17:03:43 -08:00
Mike Hunhoff
2dbac05716 ghidra: fix IndexError exception (#1879)
* ghidra: fix IndexError exception
2023-12-15 16:23:19 -08:00
dependabot[bot]
3f449f3c0f build(deps-dev): bump isort from 5.11.4 to 5.13.0 (#1900)
Bumps [isort](https://github.com/pycqa/isort) from 5.11.4 to 5.13.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.11.4...5.13.0)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  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>
2023-12-13 15:56:24 +01:00
dependabot[bot]
51b63b465b build(deps-dev): bump ruff from 0.1.6 to 0.1.7 (#1902)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.6 to 0.1.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.6...v0.1.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  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>
2023-12-13 15:56:16 +01:00
dependabot[bot]
afb3426e96 build(deps-dev): bump pyinstaller from 6.2.0 to 6.3.0 (#1901)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v6.2.0...v6.3.0)

---
updated-dependencies:
- dependency-name: pyinstaller
  dependency-type: direct:production
  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>
2023-12-13 15:56:04 +01:00
Arnim Rupp
1d3ae1f216 Update capa2yara.py (#1904)
Extend unhandled strings to allow capa2yara to run through
2023-12-13 15:51:56 +01:00
Capa Bot
f229c8ecb8 Sync capa rules submodule 2023-12-13 11:04:32 +00:00
Capa Bot
e3da2d88d0 Sync capa rules submodule 2023-12-11 16:07:10 +00:00
Capa Bot
e4eb4340b1 Sync capa rules submodule 2023-12-09 06:53:06 +00:00
Capa Bot
a8e7611252 Sync capa rules submodule 2023-12-08 21:41:12 +00:00
aaronatp
8531acd7c5 Only show stack trace in debug mode (#1860)
* Only show stack trace in dev mode

* Update custom exception handler to handle KeyboardInterrupts
2023-12-08 22:07:16 +01:00
Mike Hunhoff
d6f7d2180f dotnet: combine dnfile_.py and dotnetfile.py (#1895) 2023-12-07 14:06:54 -07:00
Moritz
d1b213aaac Merge pull request #1890 from mandiant/fix-dlls
fix symbol generation, ordinals
2023-12-03 21:05:01 +01:00
mr-tz
51ddadbc87 fix symbol generation, ordinals 2023-12-03 17:49:54 +02:00
Moritz
cd52b1937b Merge pull request #1887 from mandiant/fix/dynamic/1882
dynamic: fix UnboundLocalError exception
2023-12-01 14:52:55 +01:00
Mike Hunhoff
ca14dab804 dynamic: fix UnboundLocalError exception 2023-11-30 14:52:18 -07:00
Moritz
fbe0440361 add build for Python 3.11 for linux (#1877)
* add build for Python 3.11 for linux
2023-11-29 22:42:56 +01:00
Moritz
4c3586b5e9 Merge pull request #1697 from mandiant/dynamic-feature-extraction
add dynamic analysis
2023-11-29 17:45:24 +01:00
mr-tz
47019e4d7c Merge branch 'master' into dynamic-feature-extraction 2023-11-29 16:28:12 +01:00
Capa Bot
a236a952bc Sync capa rules submodule 2023-11-29 15:24:54 +00:00
mr-tz
73ea822123 Merge branch 'master' into dynamic-feature-extraction 2023-11-29 16:17:09 +01:00
Willi Ballenthin
3c159a1f52 ci: revert temporary CI event subscription 2023-11-29 14:26:53 +00:00
Capa Bot
7db40c3af8 Sync capa rules submodule 2023-11-29 13:53:18 +00:00
Willi Ballenthin
9a996d07c7 Merge branch 'dynamic-feature-extraction' of public.github.com:mandiant/capa into dynamic-feature-extraction 2023-11-29 13:46:47 +00:00
Willi Ballenthin
93cfb6ef8c sync testfiles submodule 2023-11-29 13:46:29 +00:00
Capa Bot
a29c320f95 Sync capa-testfiles submodule 2023-11-29 13:45:44 +00:00
Capa Bot
277d7e0687 Sync capa rules submodule 2023-11-29 13:33:01 +00:00
Yacine
e66c2efcf5 add documentation for dynamic capa capabilties (#1837)
* README: adapt for dynamic capa

* README.md: fix duplication error

* Update README.md

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>

* documentation: add review suggestions

* documentation: newline fix

* Update README.md

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>

* Update README.md

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>

* Update README.md

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>

---------

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-11-29 14:26:29 +01:00
Willi Ballenthin
583f8b5688 Merge branch 'dynamic-feature-extraction' of public.github.com:mandiant/capa into dynamic-feature-extraction 2023-11-29 13:13:04 +00:00
Willi Ballenthin
b4c6bf859e changelog 2023-11-29 13:12:30 +00:00
Moritz
ba9da0dd82 Merge pull request #1876 from mandiant/fix/1867
set os, arch, format in meta table
2023-11-29 13:44:43 +01:00
mr-tz
92770dd5c7 set os, arch, format in meta table 2023-11-28 17:09:14 +01:00
Moritz
8946cb633e Merge pull request #1874 from mandiant/fix/global-features
only check and display file limitation once
2023-11-28 15:19:10 +01:00
mr-tz
8f0eb5676e only check and display file limitation once 2023-11-28 15:00:47 +01:00
Willi Ballenthin
cb1a037502 Merge pull request #1869 from mandiant/dependabot/pip/flake8-encodings-0.5.1
build(deps-dev): bump flake8-encodings from 0.5.0.post1 to 0.5.1
2023-11-28 12:38:19 +00:00
dependabot[bot]
c8d0071443 build(deps-dev): bump flake8-encodings from 0.5.0.post1 to 0.5.1
Bumps [flake8-encodings](https://github.com/python-formate/flake8-encodings) from 0.5.0.post1 to 0.5.1.
- [Release notes](https://github.com/python-formate/flake8-encodings/releases)
- [Commits](https://github.com/python-formate/flake8-encodings/compare/v0.5.0.post1...v0.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-28 12:37:42 +00:00
Willi Ballenthin
e6b8a3e505 Merge pull request #1870 from mandiant/dependabot/pip/wcwidth-0.2.12
build(deps-dev): bump wcwidth from 0.2.10 to 0.2.12
2023-11-28 12:37:16 +00:00
Willi Ballenthin
f328df1bc4 Merge pull request #1871 from mandiant/dependabot/pip/setuptools-69.0.2
build(deps-dev): bump setuptools from 68.0.0 to 69.0.2
2023-11-28 12:37:06 +00:00
Willi Ballenthin
d1aa1557b2 Merge pull request #1872 from mandiant/dependabot/pip/flake8-bugbear-23.11.26
build(deps-dev): bump flake8-bugbear from 23.9.16 to 23.11.26
2023-11-28 12:36:58 +00:00
Willi Ballenthin
a0929124ec Merge pull request #1873 from mandiant/dependabot/pip/mypy-1.7.1
build(deps-dev): bump mypy from 1.7.0 to 1.7.1
2023-11-28 12:36:47 +00:00
dependabot[bot]
84ed6c8d24 build(deps-dev): bump mypy from 1.7.0 to 1.7.1
Bumps [mypy](https://github.com/python/mypy) from 1.7.0 to 1.7.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.7.0...v1.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-27 14:56:45 +00:00
dependabot[bot]
61c8e30f65 build(deps-dev): bump flake8-bugbear from 23.9.16 to 23.11.26
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 23.9.16 to 23.11.26.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/23.9.16...23.11.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-27 14:56:29 +00:00
dependabot[bot]
6a4994f1ef build(deps-dev): bump setuptools from 68.0.0 to 69.0.2
Bumps [setuptools](https://github.com/pypa/setuptools) from 68.0.0 to 69.0.2.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v68.0.0...v69.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-27 14:56:01 +00:00
dependabot[bot]
fce105060d build(deps-dev): bump wcwidth from 0.2.10 to 0.2.12
Bumps [wcwidth](https://github.com/jquast/wcwidth) from 0.2.10 to 0.2.12.
- [Release notes](https://github.com/jquast/wcwidth/releases)
- [Commits](https://github.com/jquast/wcwidth/compare/0.2.10...0.2.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-27 14:55:45 +00:00
Moritz
d84457eac7 Merge pull request #1868 from mandiant/fix/global-features
Fix global features and display
2023-11-27 14:06:01 +01:00
mr-tz
890c879e7c only check and display file limitation once 2023-11-27 13:28:36 +01:00
mr-tz
f201ef1d22 actually get global feature values 2023-11-27 13:28:06 +01:00
Moritz
f763d14266 Merge pull request #1862 from mandiant/dependabot/pip/wcwidth-0.2.10
build(deps-dev): bump wcwidth from 0.2.9 to 0.2.10
2023-11-23 12:28:16 +01:00
Moritz
6f0be06f86 Merge pull request #1861 from mandiant/dependabot/pip/ruff-0.1.6
build(deps-dev): bump ruff from 0.1.5 to 0.1.6
2023-11-23 12:28:05 +01:00
Capa Bot
347687579c Sync capa rules submodule 2023-11-22 18:05:52 +00:00
Capa Bot
d61d1dc591 Sync capa rules submodule 2023-11-22 13:10:44 +00:00
Capa Bot
235a3bede0 Sync capa rules submodule 2023-11-21 10:52:38 +00:00
dependabot[bot]
cf35d2c497 build(deps-dev): bump wcwidth from 0.2.9 to 0.2.10
Bumps [wcwidth](https://github.com/jquast/wcwidth) from 0.2.9 to 0.2.10.
- [Release notes](https://github.com/jquast/wcwidth/releases)
- [Commits](https://github.com/jquast/wcwidth/compare/0.2.9...0.2.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 14:20:59 +00:00
dependabot[bot]
f6048b9e99 build(deps-dev): bump ruff from 0.1.5 to 0.1.6
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.5 to 0.1.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.5...v0.1.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 14:20:47 +00:00
Capa Bot
9d1e60d4a2 Sync capa-testfiles submodule 2023-11-20 11:40:22 +00:00
Capa Bot
fb1235d26f Sync capa rules submodule 2023-11-20 10:27:11 +00:00
Capa Bot
3fe2328bd2 Sync capa rules submodule 2023-11-17 23:27:52 +00:00
Willi Ballenthin
647abb669f Merge pull request #1858 from doomedraven/patch-1 2023-11-16 14:16:16 +01:00
doomedraven
a5e1eca8cc Create pip-audit.yml 2023-11-16 13:27:25 +01:00
Willi Ballenthin
fdb96709ae Merge pull request #1856 from doomedraven/patch-1
fix pydantic vuln (ReDoS)
2023-11-16 13:20:01 +01:00
doomedraven
490271e50b fix pydantic vuln (ReDoS)
Regular Expression Denial of Service (ReDoS)
MEDIUM SEVERITY
Package Manager: pip
Vulnerable module: pydantic
Remediation
Upgrade pydantic to version 1.10.13, 2.4.0 or higher.
2023-11-16 10:54:59 +01:00
Willi Ballenthin
a870c92a2f sync submodule rules 2023-11-15 11:00:51 +00:00
Willi Ballenthin
de5f08871e sync submodule rules 2023-11-15 10:57:16 +00:00
Capa Bot
2f60ec03af Sync capa rules submodule 2023-11-15 09:25:02 +00:00
Willi Ballenthin
987eb2d358 sync rules submodule 2023-11-14 14:34:08 +00:00
Willi Ballenthin
6e3fff4bae use latest rules migration 2023-11-14 14:29:34 +00:00
Willi Ballenthin
a705bf9eab Merge pull request #1825 from mandiant/fix/issue-1816
verbose: show process name and other human-level details
2023-11-14 12:33:41 +01:00
Willi Ballenthin
c68c68d5cb Merge branch 'dynamic-feature-extraction' into fix/issue-1816 2023-11-14 11:36:24 +01:00
Willi Ballenthin
82013f0e24 submodule: tests: data: sync 2023-11-14 10:35:18 +00:00
Willi Ballenthin
210a13d94e Merge pull request #1850 from mandiant/dependabot/pip/mypy-1.7.0
build(deps-dev): bump mypy from 1.6.1 to 1.7.0
2023-11-14 11:29:59 +01:00
dependabot[bot]
0d5ff45c76 build(deps-dev): bump mypy from 1.6.1 to 1.7.0
Bumps [mypy](https://github.com/python/mypy) from 1.6.1 to 1.7.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.6.1...v1.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-14 10:29:20 +00:00
Willi Ballenthin
11b98cb0b1 Merge pull request #1849 from mandiant/dependabot/pip/black-23.11.0
build(deps-dev): bump black from 23.10.1 to 23.11.0
2023-11-14 11:29:12 +01:00
dependabot[bot]
3c9ab63521 build(deps-dev): bump black from 23.10.1 to 23.11.0
Bumps [black](https://github.com/psf/black) from 23.10.1 to 23.11.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/23.10.1...23.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-14 10:29:05 +00:00
Willi Ballenthin
a2fde921aa Merge pull request #1848 from mandiant/dependabot/pip/ruff-0.1.5
build(deps-dev): bump ruff from 0.1.4 to 0.1.5
2023-11-14 11:28:25 +01:00
Willi Ballenthin
d4f7c77be8 Merge pull request #1847 from mandiant/dependabot/pip/pyinstaller-6.2.0
build(deps-dev): bump pyinstaller from 6.1.0 to 6.2.0
2023-11-14 11:28:08 +01:00
dependabot[bot]
f0f95824ac build(deps-dev): bump ruff from 0.1.4 to 0.1.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.4 to 0.1.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.4...v0.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 14:21:13 +00:00
dependabot[bot]
0ba5c23847 build(deps-dev): bump pyinstaller from 6.1.0 to 6.2.0
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v6.1.0...v6.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 14:20:52 +00:00
Moritz
dee0aa73eb Merge pull request #1844 from mandiant/mr-tz-patch-1
fix whitespace removal in format check
2023-11-11 19:53:44 +01:00
Moritz
41a397661f fix whitespace removal in format check 2023-11-10 11:40:55 +01:00
Moritz
52997e70a0 fix imports according to ruff 2023-11-08 16:58:40 +01:00
Moritz
1acc2d1959 Merge branch 'dynamic-feature-extraction' into fix/issue-1816 2023-11-08 16:56:05 +01:00
Moritz
74f70856a6 Merge pull request #1840 from mandiant/dependabot/pip/wcwidth-0.2.9
build(deps-dev): bump wcwidth from 0.2.8 to 0.2.9
2023-11-08 15:38:27 +01:00
Moritz
e5b7ee96fc Merge pull request #1839 from mandiant/dependabot/pip/black-23.10.1
build(deps-dev): bump black from 23.10.0 to 23.10.1
2023-11-08 15:38:02 +01:00
Moritz
92d43f5327 Merge pull request #1838 from mandiant/dependabot/pip/ruamel-yaml-0.18.5
build(deps-dev): bump ruamel-yaml from 0.18.3 to 0.18.5
2023-11-08 15:37:31 +01:00
dependabot[bot]
48abd297a8 build(deps-dev): bump black from 23.10.0 to 23.10.1
Bumps [black](https://github.com/psf/black) from 23.10.0 to 23.10.1.
- [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/23.10.0...23.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-07 13:16:09 +00:00
Willi Ballenthin
d64a10a287 Merge pull request #1841 from mandiant/dependabot/pip/ruff-0.1.4
build(deps-dev): bump ruff from 0.0.291 to 0.1.4
2023-11-07 14:15:24 +01:00
dependabot[bot]
abf83fe8cf build(deps-dev): bump ruff from 0.0.291 to 0.1.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.291 to 0.1.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.0.291...v0.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 14:42:18 +00:00
dependabot[bot]
6380d936ae build(deps-dev): bump wcwidth from 0.2.8 to 0.2.9
Bumps [wcwidth](https://github.com/jquast/wcwidth) from 0.2.8 to 0.2.9.
- [Release notes](https://github.com/jquast/wcwidth/releases)
- [Commits](https://github.com/jquast/wcwidth/compare/0.2.8...0.2.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 14:42:06 +00:00
dependabot[bot]
18ab8d28d9 build(deps-dev): bump ruamel-yaml from 0.18.3 to 0.18.5
Bumps [ruamel-yaml]() from 0.18.3 to 0.18.5.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 14:41:55 +00:00
Willi Ballenthin
a52af3895a verbose: remove TODOs 2023-11-06 10:37:22 +00:00
Willi Ballenthin
5d31bc462b verbose: render dynamic match locations 2023-11-06 10:34:26 +00:00
Willi Ballenthin
7678897334 tests: fix render tests 2023-11-06 10:32:44 +00:00
Willi Ballenthin
75ff58edaa vverbose: better render pid/tid/call index 2023-11-06 10:09:23 +00:00
Willi Ballenthin
eb12ec43f0 mypy 2023-11-06 09:52:00 +00:00
Willi Ballenthin
f7c72cd1c3 vverbose: don't repeat rendered calls when in call scope 2023-11-06 09:52:00 +00:00
Willi Ballenthin
0da614aa4f vverbose: dynamic: show rendered matching API call 2023-11-06 09:52:00 +00:00
Willi Ballenthin
9c81ccf88a vverbose: make missing names an error 2023-11-06 09:52:00 +00:00
Willi Ballenthin
c141f7ec6e verbose: better render scopes 2023-11-06 09:52:00 +00:00
Willi Ballenthin
274a710bb1 report: better compute dynamic layout 2023-11-06 09:52:00 +00:00
Willi Ballenthin
4a7e488e4c Update capa/render/vverbose.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-11-01 12:19:13 +01:00
Willi Ballenthin
348120dea9 Merge pull request #1835 from mandiant/dependabot/pip/ruamel-yaml-0.18.3
build(deps-dev): bump ruamel-yaml from 0.17.35 to 0.18.3
2023-11-01 12:17:22 +01:00
Willi Ballenthin
435eea1b80 Merge pull request #1834 from mandiant/dependabot/pip/pytest-7.4.3
build(deps-dev): bump pytest from 7.4.2 to 7.4.3
2023-11-01 12:17:12 +01:00
Willi Ballenthin
621d42a093 Merge pull request #1831 from mandiant/dependabot/pip/flake8-no-implicit-concat-0.3.5
build(deps-dev): bump flake8-no-implicit-concat from 0.3.4 to 0.3.5
2023-11-01 12:17:04 +01:00
Willi Ballenthin
15701c6d12 Merge pull request #1829 from mandiant/dependabot/pip/mypy-1.6.1
build(deps-dev): bump mypy from 1.6.0 to 1.6.1
2023-11-01 12:16:55 +01:00
Willi Ballenthin
ec7fc86dc5 Merge pull request #1828 from mandiant/dependabot/pip/types-requests-2.31.0.10
build(deps-dev): bump types-requests from 2.31.0.2 to 2.31.0.10
2023-11-01 12:16:46 +01:00
dependabot[bot]
8d55c2f249 build(deps-dev): bump ruamel-yaml from 0.17.35 to 0.18.3
Bumps [ruamel-yaml]() from 0.17.35 to 0.18.3.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-30 14:11:50 +00:00
dependabot[bot]
66607f1412 build(deps-dev): bump pytest from 7.4.2 to 7.4.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.2 to 7.4.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.4.2...7.4.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-30 14:11:00 +00:00
Yacine
0097822e51 Merge pull request #1820 from yelhamer/capabilities-module
add a capabilities module
2023-10-27 13:39:49 +02:00
Yacine Elhamer
e559cc27d5 capa.rules: remove redundant ceng.MatchResults import 2023-10-26 19:43:26 +02:00
Yacine Elhamer
a0cec3f07d capa.rules: remove redundant is_internal_rule() and has_file_limitations() from capa source code 2023-10-26 19:41:09 +02:00
dependabot[bot]
874faf0901 build(deps-dev): bump mypy from 1.6.0 to 1.6.1
Bumps [mypy](https://github.com/python/mypy) from 1.6.0 to 1.6.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.6.0...v1.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:48:35 +00:00
Moritz
4750913fad Merge pull request #1827 from mandiant/dependabot/pip/black-23.10.0
build(deps-dev): bump black from 23.9.1 to 23.10.0
2023-10-24 21:47:52 +02:00
dependabot[bot]
e7198b2aaf build(deps-dev): bump flake8-no-implicit-concat from 0.3.4 to 0.3.5
Bumps [flake8-no-implicit-concat](https://github.com/10sr/flake8-no-implicit-concat) from 0.3.4 to 0.3.5.
- [Release notes](https://github.com/10sr/flake8-no-implicit-concat/releases)
- [Changelog](https://github.com/10sr/flake8-no-implicit-concat/blob/master/CHANGELOG.md)
- [Commits](https://github.com/10sr/flake8-no-implicit-concat/compare/v0.3.4...v0.3.5)

---
updated-dependencies:
- dependency-name: flake8-no-implicit-concat
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 14:47:26 +00:00
dependabot[bot]
426931c392 build(deps-dev): bump types-requests from 2.31.0.2 to 2.31.0.10
Bumps [types-requests](https://github.com/python/typeshed) from 2.31.0.2 to 2.31.0.10.
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 14:47:03 +00:00
dependabot[bot]
fec1e6a947 build(deps-dev): bump black from 23.9.1 to 23.10.0
Bumps [black](https://github.com/psf/black) from 23.9.1 to 23.10.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/23.9.1...23.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 14:46:59 +00:00
Moritz
db53424548 Merge pull request #1826 from mandiant/fix-model-hexint
fix parsing base 10/16
2023-10-23 09:02:21 +02:00
Yacine Elhamer
8029fed31c Merge branch 'capabilities-module' of https://github.com/yelhamer/capa into capabilities-module 2023-10-20 20:11:28 +02:00
Yacine Elhamer
3572b512d9 test_capabilities.py: add missing test_com_feature_matching() test 2023-10-20 20:11:08 +02:00
Yacine Elhamer
ab06c94d80 capa/main.py: move has_rule_with_namespace() to capa.rules.RuleSet 2023-10-20 20:10:29 +02:00
Willi Ballenthin
9e6919f33c layout: capture call names
so that they can be rendered to output
2023-10-20 14:21:13 +00:00
mr-tz
99042f232d fix parsing base 10/16 2023-10-20 15:26:11 +02:00
Willi Ballenthin
393b0e63f0 layout: capture process name 2023-10-20 12:39:28 +00:00
Willi Ballenthin
ee4f02908c layout: capture process name 2023-10-20 12:38:35 +00:00
Moritz
c9df78252a Ignore DLL names for API features (#1824)
* ignore DLL name for api features

* keep DLL name for import features

* fix tests
2023-10-20 13:39:15 +02:00
Willi Ballenthin
788251ba2b vverbose: render scope for humans 2023-10-20 11:37:42 +00:00
Willi Ballenthin
62d4b008c5 Merge pull request #1822 from mandiant/fix/dynamic-freeze
update freeze for dynamic
2023-10-20 13:16:48 +02:00
Capa Bot
be6f87318e Sync capa rules submodule 2023-10-20 09:50:07 +00:00
Yacine Elhamer
aae72667a3 Merge branch 'capabilities-module' of https://github.com/yelhamer/capa into capabilities-module 2023-10-20 10:16:41 +02:00
Yacine Elhamer
d6c5d98b0d move is_file_limitation_rule() to the rules module (Rule class) 2023-10-20 10:16:09 +02:00
Yacine Elhamer
d5ae2ffd91 capa.capabilities: move has_file_limitations() from capa.main to the capabilities module 2023-10-20 10:15:20 +02:00
Yacine Elhamer
96fb204d9d move capa.features.capabilities to capa.capabilities, and update scripts 2023-10-20 09:54:24 +02:00
Yacine
20604c4b41 Update capa/capabilities/static.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-10-20 09:28:13 +02:00
Yacine
423d942bd0 Update capa/capabilities/dynamic.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-10-20 09:28:05 +02:00
Yacine
f9b87417e6 Update capa/capabilities/common.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-10-20 09:27:58 +02:00
Willi Ballenthin
fc4618e234 Merge branch 'dynamic-feature-extraction' into fix/dynamic-freeze 2023-10-20 09:16:07 +02:00
Willi Ballenthin
1143f2ba56 changelog 2023-10-20 07:11:42 +00:00
Willi Ballenthin
10dc4b92b1 freeze: update freeze format v3 2023-10-20 06:59:53 +00:00
Willi Ballenthin
bfecf414fb freeze: add dynamic tests 2023-10-20 06:59:34 +00:00
Willi Ballenthin
0231ceef87 null extractor: fix typings 2023-10-20 06:59:16 +00:00
Yacine
0ae8f34aff Merge branch 'dynamic-feature-extraction' into capabilities-module 2023-10-20 08:55:49 +02:00
Moritz
b8b55f4e19 identify potential JSON object data start (#1819)
* identify potential JSON object data start
2023-10-19 17:17:57 +02:00
Willi Ballenthin
d42829d7e7 Merge pull request #1765 from mandiant/fix/dynamic-proto
protobuf: add dynamic support
2023-10-19 13:37:45 +02:00
Willi Ballenthin
c724a4b311 ci: only run BN and Ghidra tests after others complete
these are much less likely to fail because they're
changed less often, so don't run them until we know
other tests also pass.
2023-10-19 11:35:42 +00:00
Willi Ballenthin
84e22b187d doc 2023-10-19 11:29:30 +00:00
Willi Ballenthin
b6a0d6e1f3 pre-commit: fix stages 2023-10-19 11:26:22 +00:00
Willi Ballenthin
1cb3ca61cd pre-commit: only run fast checks during commit 2023-10-19 10:35:57 +00:00
Willi Ballenthin
288313a300 changelog 2023-10-19 10:28:37 +00:00
Willi Ballenthin
2cc6a37713 ci: run fast tests before the full suite 2023-10-19 10:23:03 +00:00
Willi Ballenthin
fbeb33a91f Merge branch 'dynamic-feature-extraction' into fix/dynamic-proto 2023-10-19 10:05:26 +00:00
Willi Ballenthin
3519125e03 tests: fix COM tests with dynamic scope 2023-10-19 10:04:26 +00:00
Willi Ballenthin
98360328f9 proto: fix serialization of call address 2023-10-19 09:59:18 +00:00
Willi Ballenthin
3d4facd9a3 Merge branch 'dynamic-feature-extraction' into fix/dynamic-proto 2023-10-19 09:24:37 +00:00
Willi Ballenthin
8b0ba1e656 tests: rename freeze tests 2023-10-19 09:24:18 +00:00
Willi Ballenthin
7bc3fba7b0 Merge branch 'dynamic-feature-extraction' into fix/dynamic-proto 2023-10-19 09:20:15 +00:00
Willi Ballenthin
d5e187bc70 Merge branch 'master' into dynamic-feature-extraction 2023-10-19 09:15:57 +00:00
Yacine Elhamer
85610a82c5 changelog fix 2023-10-19 10:59:45 +02:00
Yacine Elhamer
f2011c162c fix styling issues 2023-10-19 10:58:30 +02:00
Yacine Elhamer
37caeb2736 capabilities: add a test file for the new capabilities module, and move the corresponding tests from main to there 2023-10-19 10:54:53 +02:00
Yacine Elhamer
5c48f38208 capa/main.py: add a capabilities module and move all of the capability extraction there 2023-10-19 10:39:14 +02:00
Moritz
8687c740d5 Merge pull request #1817 from mandiant/improve-vv-render
improve vverbose rendering
2023-10-19 09:41:31 +02:00
Yacine
9609d63f8a Update tests/test_main.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-10-19 08:10:29 +02:00
Capa Bot
772f806eb6 Sync capa rules submodule 2023-10-18 15:01:37 +00:00
Willi Ballenthin
5eaba611d1 Merge pull request #1738 from Aayush-Goel-04/Aayush-Goel-04/Issue#322
add com class/interface features
2023-10-18 17:00:39 +02:00
mr-tz
b6f13f3489 improve vverbose rendering 2023-10-18 13:37:56 +02:00
Aayush Goel
178cfce456 Merge branch 'Aayush-Goel-04/Issue#322' of https://github.com/Aayush-Goel-04/capa into Aayush-Goel-04/Issue#322 2023-10-18 16:33:37 +05:30
Aayush Goel
94cf53a1e3 Update __init__.py 2023-10-18 16:33:31 +05:30
Moritz
2cfd45022a improve and fix various dynamic parts (#1809)
* improve and fix various dynamic parts
2023-10-18 10:59:41 +02:00
Aayush Goel
26a2d1b4d1 Merge branch 'mandiant:master' into Aayush-Goel-04/Issue#322 2023-10-17 21:09:07 +05:30
Aayush Goel
6dbd3768ce Update __init__.py 2023-10-17 21:04:21 +05:30
Willi Ballenthin
21f9e0736d isort 2023-10-17 15:07:34 +00:00
Aayush Goel
7cd5aa1c40 Added Enum for comType 2023-10-17 20:28:49 +05:30
Willi Ballenthin
55e4fddc51 mypy 2023-10-17 14:46:33 +00:00
Willi Ballenthin
1aac4a1a69 mypy 2023-10-17 14:42:58 +00:00
Willi Ballenthin
92daf3a530 elffile: fix property access 2023-10-17 14:28:52 +00:00
Willi Ballenthin
547502051f dynamic: fix tests 2023-10-17 14:27:36 +00:00
Aayush Goel
884b714be2 loading com db only once
avoid loading db multiple times by caching it.
2023-10-17 19:48:06 +05:30
Willi Ballenthin
7205bc26ef submodule: rules: update 2023-10-17 12:28:45 +00:00
Willi Ballenthin
e1b3a3f6b4 rules: fix rendering of yaml 2023-10-17 12:22:32 +00:00
Willi Ballenthin
cb5fa36fc8 flake8 2023-10-17 11:44:48 +00:00
Willi Ballenthin
8ee97acf2a dynamic: fix some tests 2023-10-17 11:43:09 +00:00
Willi Ballenthin
44d05f9498 dynamic: fix some tests 2023-10-17 11:41:40 +00:00
Willi Ballenthin
bf233c1c7a integrate Ghidra backend with dynamic analysis 2023-10-17 10:56:35 +00:00
Willi Ballenthin
182a9868ca merge master 2023-10-17 10:32:25 +00:00
Willi Ballenthin
40d9587fa4 Merge pull request #1808 from mandiant/dependabot/pip/ruamel-yaml-0.17.35
build(deps-dev): bump ruamel-yaml from 0.17.32 to 0.17.35
2023-10-17 09:59:41 +02:00
Willi Ballenthin
430fdb074b Merge pull request #1807 from mandiant/dependabot/pip/pre-commit-3.5.0
build(deps-dev): bump pre-commit from 3.4.0 to 3.5.0
2023-10-17 09:59:30 +02:00
Willi Ballenthin
0324d24490 Merge pull request #1806 from mandiant/dependabot/pip/flake8-simplify-0.21.0
build(deps-dev): bump flake8-simplify from 0.20.0 to 0.21.0
2023-10-17 09:59:21 +02:00
Willi Ballenthin
41c286d1a3 Merge pull request #1805 from mandiant/dependabot/pip/pyinstaller-6.1.0
build(deps-dev): bump pyinstaller from 6.0.0 to 6.1.0
2023-10-17 09:59:13 +02:00
Willi Ballenthin
187cf40d6f Merge pull request #1804 from mandiant/dependabot/pip/mypy-1.6.0
build(deps-dev): bump mypy from 1.5.1 to 1.6.0
2023-10-17 09:58:44 +02:00
Capa Bot
c37a0e525c Sync capa rules submodule 2023-10-16 14:53:14 +00:00
dependabot[bot]
de0c35b6ad build(deps-dev): bump ruamel-yaml from 0.17.32 to 0.17.35
Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.32 to 0.17.35.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 14:18:33 +00:00
dependabot[bot]
d99b454c0e build(deps-dev): bump pre-commit from 3.4.0 to 3.5.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.4.0...v3.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 14:18:11 +00:00
dependabot[bot]
44f156925a build(deps-dev): bump flake8-simplify from 0.20.0 to 0.21.0
Bumps [flake8-simplify](https://github.com/MartinThoma/flake8-simplify) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/MartinThoma/flake8-simplify/releases)
- [Changelog](https://github.com/MartinThoma/flake8-simplify/blob/main/CHANGELOG.md)
- [Commits](https://github.com/MartinThoma/flake8-simplify/commits/0.21.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 14:17:47 +00:00
dependabot[bot]
599c115767 build(deps-dev): bump pyinstaller from 6.0.0 to 6.1.0
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v6.0.0...v6.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 14:17:36 +00:00
dependabot[bot]
6ecc9b77b9 build(deps-dev): bump mypy from 1.5.1 to 1.6.0
Bumps [mypy](https://github.com/python/mypy) from 1.5.1 to 1.6.0.
- [Commits](https://github.com/python/mypy/compare/v1.5.1...v1.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 14:17:01 +00:00
Aayush Goel
412d296d6b Merge branch 'mandiant:master' into Aayush-Goel-04/Issue#322 2023-10-16 16:38:18 +05:30
Aayush Goel
db32d90480 tests updated 2023-10-16 16:35:30 +05:30
Yacine Elhamer
9a66c265db cape/file.py: fix flake8 issue of using '+' for logging 2023-10-16 12:11:07 +02:00
Yacine Elhamer
a1aca3aeb3 Merge branch 'dynamic-feature-extraction' of https://github.com/mandiant/capa into dynamic-feature-extraction 2023-10-16 12:04:47 +02:00
Yacine Elhamer
ffe6ab6842 main.py: load signatures only for the static context 2023-10-16 12:04:38 +02:00
Yacine
d1b7afbe13 Update capa/render/verbose.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-10-14 09:36:55 +02:00
Capa Bot
77de088ac9 Sync capa rules submodule 2023-10-12 09:01:30 +00:00
Capa Bot
40ba6679f0 Sync capa-testfiles submodule 2023-10-11 14:36:05 +00:00
Moritz
8b6fa35e9f Merge pull request #1794 from mandiant/dependabot/pip/pyinstaller-6.0.0
build(deps-dev): bump pyinstaller from 5.10.1 to 6.0.0
2023-10-11 13:58:48 +02:00
Moritz
f85ea915bf Update pyinstaller.spec 2023-10-11 12:29:18 +02:00
Moritz
312ad48041 Merge pull request #1801 from mandiant/dependabot/pip/dnfile-0.14.1
build(deps-dev): bump dnfile from 0.13.0 to 0.14.1
2023-10-11 12:20:07 +02:00
Moritz
65b80d4d13 Merge pull request #1800 from mandiant/dependabot/pip/flake8-bugbear-23.9.16
build(deps-dev): bump flake8-bugbear from 23.7.10 to 23.9.16
2023-10-11 12:19:51 +02:00
Moritz
fb098fde5f Merge pull request #1799 from mandiant/dependabot/pip/black-23.9.1
build(deps-dev): bump black from 23.7.0 to 23.9.1
2023-10-11 12:19:36 +02:00
Moritz
eedec933c2 Merge pull request #1798 from mandiant/dependabot/pip/wcwidth-0.2.8
build(deps-dev): bump wcwidth from 0.2.6 to 0.2.8
2023-10-11 12:19:20 +02:00
Yacine Elhamer
559f2fd162 cape/file.py: flake8 fixes 2023-10-11 11:56:49 +02:00
Yacine Elhamer
953b2e82d2 rendering: several fixes and added types/classes 2023-10-11 11:52:16 +02:00
Capa Bot
cd268d6327 Sync capa rules submodule 2023-10-10 13:34:52 +00:00
Aayush Goel
23ecb248a5 Update __init__.py 2023-10-10 18:08:07 +05:30
Aayush Goel
bc165331db Update __init__.py 2023-10-10 17:56:18 +05:30
Capa Bot
5d66a389d3 Sync capa rules submodule 2023-10-10 10:09:36 +00:00
Capa Bot
248a51c15f Sync capa rules submodule 2023-10-10 09:55:31 +00:00
Aayush Goel
8a0628f357 Update CHANGELOG.md 2023-10-10 04:16:38 +05:30
Aayush Goel
2ec87f717a Merge branch 'mandiant:master' into Aayush-Goel-04/Issue#322 2023-10-10 04:06:28 +05:30
Capa Bot
4430fce314 Sync capa rules submodule 2023-10-09 18:13:48 +00:00
Capa Bot
174c8121ca Sync capa rules submodule 2023-10-09 18:01:23 +00:00
Capa Bot
fa1371cfa8 Sync capa rules submodule 2023-10-09 18:00:29 +00:00
Capa Bot
a0a2b07b85 Sync capa rules submodule 2023-10-09 16:35:56 +00:00
Moritz
a9daa92c9a Merge branch 'master' into Aayush-Goel-04/Issue#322 2023-10-09 18:22:46 +02:00
Capa Bot
b315aacd73 Sync capa rules submodule 2023-10-09 16:22:26 +00:00
Capa Bot
3dd051582a Sync capa rules submodule 2023-10-09 16:01:44 +00:00
Capa Bot
5f7b4fbf74 Sync capa rules submodule 2023-10-06 15:20:18 +00:00
Yacine Elhamer
8b287c1704 scripts/profile_time.py: revert restriction that sample extractors can only be static ones 2023-10-04 10:51:53 +02:00
Yacine Elhamer
28a722d4c3 scripts/profile_time.py: revert restriction that frozen extractors can only be static ones 2023-10-04 10:51:02 +02:00
Yacine Elhamer
35f64f37bb cape/global_.py: throw exceptions for unrecognized OSes, formats, and architectures 2023-10-04 10:36:08 +02:00
Yacine Elhamer
7d9ae57692 check for pid and ppid reuse 2023-10-04 10:28:10 +02:00
dependabot[bot]
838205b375 build(deps-dev): bump dnfile from 0.13.0 to 0.14.1
Bumps [dnfile](https://github.com/malwarefrank/dnfile) from 0.13.0 to 0.14.1.
- [Changelog](https://github.com/malwarefrank/dnfile/blob/master/HISTORY.rst)
- [Commits](https://github.com/malwarefrank/dnfile/compare/v0.13.0...v0.14.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-02 14:42:47 +00:00
dependabot[bot]
0fbec49708 build(deps-dev): bump flake8-bugbear from 23.7.10 to 23.9.16
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 23.7.10 to 23.9.16.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/23.7.10...23.9.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-02 14:42:40 +00:00
dependabot[bot]
0bdc727dce build(deps-dev): bump black from 23.7.0 to 23.9.1
Bumps [black](https://github.com/psf/black) from 23.7.0 to 23.9.1.
- [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/23.7.0...23.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-02 14:42:28 +00:00
dependabot[bot]
8ea7708a38 build(deps-dev): bump wcwidth from 0.2.6 to 0.2.8
Bumps [wcwidth](https://github.com/jquast/wcwidth) from 0.2.6 to 0.2.8.
- [Release notes](https://github.com/jquast/wcwidth/releases)
- [Commits](https://github.com/jquast/wcwidth/compare/0.2.6...0.2.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-02 14:42:16 +00:00
dependabot[bot]
c6c54c316f build(deps-dev): bump pyinstaller from 5.10.1 to 6.0.0
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.10.1 to 6.0.0.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v5.10.1...v6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-27 06:50:58 +00:00
Aayush Goel
8331ed6ea0 Merge branch 'mandiant:master' into Aayush-Goel-04/Issue#322 2023-09-06 16:35:29 +05:30
Willi Ballenthin
72e836166f proto: better convert to/from proto 2023-09-05 10:24:53 +00:00
Willi Ballenthin
d64ab41dfd tests: proto: add more dynamic proto tests 2023-09-05 10:23:55 +00:00
Willi Ballenthin
5b4c167489 proto: add additional types 2023-09-05 10:23:30 +00:00
Willi Ballenthin
2a757b0cbb submodule: test data: update 2023-09-05 10:22:59 +00:00
Willi Ballenthin
69836a0f13 proto: add dynamic test 2023-09-05 10:22:33 +00:00
Willi Ballenthin
866c7c5ce4 proto: deprecate metadata.analysis 2023-09-05 08:39:37 +00:00
Willi Ballenthin
3725618d50 render: proto: use Static/Dynamic analysis types 2023-09-05 08:37:11 +00:00
Willi Ballenthin
766b05e5c3 Merge branch 'dynamic-feature-extraction' into fix/dynamic-proto 2023-09-05 08:18:51 +00:00
Yacine Elhamer
dd0eadb438 freeze/__init__.py: bump freeze version to 3 2023-09-04 11:51:22 +02:00
Yacine Elhamer
f905ed611b Merge branch 'dynamic-feature-extraction' of https://github.com/mandiant/capa into dynamic-feature-extraction 2023-09-04 11:04:38 +02:00
Yacine Elhamer
cfa703eaae remove type comment 2023-09-04 11:04:09 +02:00
Yacine Elhamer
9ec1bf3e42 point rules towards dynamic-syntax 2023-09-04 10:38:01 +02:00
Yacine Elhamer
d83c0e70de main.py: remove comment type annotations 2023-09-04 09:59:29 +02:00
Yacine Elhamer
1d8e650d7b freeze/__init__.py: bump freeze version to 3 2023-09-04 09:50:29 +02:00
Yacine
99caa87a3d Update capa/main.py
Co-authored-by: Willi Ballenthin <wballenthin@google.com>
2023-09-04 09:46:41 +02:00
Aayush Goel
6317153ef0 Update tests/test_rules.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-08-30 21:48:55 +05:30
Aayush Goel
24dad6bcc4 Update capa/rules/__init__.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-08-30 21:48:48 +05:30
Yacine Elhamer
73c158ad68 point submodules towards the right branch 2023-08-30 11:42:43 +02:00
Yacine Elhamer
47330e69d4 verbose.py render_dynamic_meta(): s/doc: rd.ResultDocument/meta: rd.MetaData/g 2023-08-29 22:42:18 +02:00
Yacine Elhamer
0987673bf3 verbose.py: temporarily add a mypy-related assert to render_static_meta() 2023-08-29 22:38:14 +02:00
Yacine Elhamer
2c75f786c3 main.py rdoc.Metadata creation: revert to usage of as_posix() within the call to rdoc.Sample() 2023-08-29 22:35:49 +02:00
Yacine Elhamer
09afcfbac1 render/verbose.py: remove frz.AddressType.FREEZE 2023-08-29 22:31:16 +02:00
Aayush Goel
ab3747e448 added com prefix CLSID, IID 2023-08-30 01:00:07 +05:30
Yacine
9dc457e61e Update capa/features/freeze/__init__.py
Co-authored-by: Willi Ballenthin <wballenthin@google.com>
2023-08-28 15:40:31 +02:00
Yacine Elhamer
9eb88e6ca7 Merge branch 'dynamic-feature-extraction' of https://github.com/mandiant/capa into dynamic-feature-extraction 2023-08-28 13:24:58 +02:00
Yacine Elhamer
214a355b9c binja extractor: remove unused pathlib.Path import 2023-08-28 13:24:54 +02:00
Yacine
4d538b939e Update scripts/import-to-ida.py
Co-authored-by: Willi Ballenthin <wballenthin@google.com>
2023-08-27 14:59:10 +02:00
Yacine Elhamer
8c9e676868 binja: use binja api's methods to get the file hash 2023-08-27 14:31:43 +02:00
Yacine Elhamer
b0133f0aa1 various fixes 2023-08-26 19:28:07 +02:00
Yacine Elhamer
49adecb25c add yaml representer for the Scope class, as well as other bugfixes 2023-08-26 18:11:35 +02:00
Yacine Elhamer
e9a9b3a6b6 point the data file to the latest PR 2023-08-26 13:04:45 +02:00
Yacine
d5daa79547 Merge pull request #1764 from mandiant/fix/scope-enum-usage
rules: use Scope enum instead of constants
2023-08-25 20:58:34 +03:00
Aayush Goel
90df85b332 test for com_feature
matching a file as expected
generating the bytes/strings
if an unknown COM class/interface is provided?
2023-08-25 20:59:58 +05:30
Willi Ballenthin
88ee6e661e wip: proto: add Metadata.[static, dynamic]_analysis 2023-08-25 14:40:50 +00:00
Willi Ballenthin
08c9bbcc91 proto: deprecate RuleMetadata.scope 2023-08-25 13:22:48 +00:00
Willi Ballenthin
f96b9e6a6e proto: add RuleMetadata.scopes 2023-08-25 13:20:46 +00:00
Willi Ballenthin
9bbd3184b0 rules: handle unsupported scopes again 2023-08-25 13:15:55 +00:00
Willi Ballenthin
e4c1361d42 Merge branch 'fix/scope-enum-usage' into fix/dynamic-proto 2023-08-25 13:01:49 +00:00
Willi Ballenthin
17e4765728 changelog 2023-08-25 13:00:34 +00:00
Willi Ballenthin
7e258a91ec Merge branch 'dynamic-feature-extraction' into fix/scope-enum-usage 2023-08-25 14:59:18 +02:00
Willi Ballenthin
b88853f327 changelog 2023-08-25 14:59:03 +02:00
Willi Ballenthin
a60401fc7e Merge branch 'master' into dynamic-feature-extraction 2023-08-25 14:58:35 +02:00
Willi Ballenthin
a734358377 rules: use Scope enum instead of constants 2023-08-25 12:54:57 +00:00
Willi Ballenthin
ebcbad3ae3 proto: add new scopes 2023-08-25 12:21:37 +00:00
Willi Ballenthin
8ff74d4a04 proto: regenerate using 3.21 protoc 2023-08-25 12:20:51 +00:00
Aayush Goel
bd0d8eb403 Update __init__.py
added parse_description for com feature
Update CHANGELOG.md
added comments, dealt with errors
2023-08-25 16:04:25 +05:30
Aayush Goel
9b79aa1983 Merge branch 'Aayush-Goel-04/Issue#322' of https://github.com/Aayush-Goel-04/capa into Aayush-Goel-04/Issue#322 2023-08-25 15:42:17 +05:30
Aayush Goel
172968c77e Update CHANGELOG.md 2023-08-25 15:42:02 +05:30
Aayush Goel
f1a7049ab5 Merge branch 'master' into Aayush-Goel-04/Issue#322 2023-08-25 15:39:03 +05:30
Aayush Goel
155a2904fb Update CHANGELOG.md 2023-08-25 15:38:00 +05:30
Aayush Goel
4c2e8fd718 Merge branch 'Aayush-Goel-04/Issue#322' of https://github.com/Aayush-Goel-04/capa into Aayush-Goel-04/Issue#322 2023-08-25 15:33:52 +05:30
Aayush Goel
95e279a03b update com db
moved code to rules/init.py , create db for coms
2023-08-25 15:32:40 +05:30
Willi Ballenthin
f2909c82f3 proto: reenable tests and linters 2023-08-25 09:41:25 +00:00
Willi Ballenthin
164b08276c extractor: tweak hashes to fix mypy 2023-08-25 09:38:23 +00:00
Willi Ballenthin
b930523d44 freeze: add TODO issue link 2023-08-25 11:32:56 +02:00
Yacine Elhamer
f34b0355e7 test_result_document.py: re-enable result-document related tests 2023-08-25 10:56:12 +02:00
Yacine
3ee56e3bee Merge pull request #1762 from yelhamer/modify-sample-hashes
Modify sample hashes
2023-08-25 10:29:38 +03:00
Yacine Elhamer
49bf2eb6d4 base_extractor.py: replace dunder with single underscore for sample_hashes attribute 2023-08-25 10:14:25 +02:00
Yacine Elhamer
707dee4c3f base_Extractor.py: make sample_hashes attribute private 2023-08-25 09:53:08 +02:00
Yacine Elhamer
0ded827290 modify null extractor 2023-08-25 08:50:34 +02:00
Yacine Elhamer
f74107d960 initial commit 2023-08-25 08:37:57 +02:00
Yacine
acd3a30d27 Merge pull request #1758 from yelhamer/fix-cape2fmt
Add dynamic scopes to capa2fmt
2023-08-24 15:43:34 +03:00
Yacine Elhamer
b636f23e3c Merge branch 'fix-cape2fmt' of https://github.com/yelhamer/capa into fix-cape2fmt 2023-08-24 15:01:00 +02:00
Yacine Elhamer
70eae1a6f0 freeze/__init__.py: fix missing space 2023-08-24 15:00:34 +02:00
Yacine Elhamer
3574bd49bd Merge remote-tracking branch 'parentrepo/dynamic-feature-extraction' into fix-cape2fmt 2023-08-24 14:48:07 +02:00
Yacine Elhamer
46217a3acb test_main.py: remove unused pytest 2023-08-24 14:47:40 +02:00
Yacine Elhamer
9eb1255b29 cape2yara.py: update for use of scopes, and fix bug 2023-08-24 14:32:49 +02:00
Yacine
d66f834e54 Update tests/test_scripts.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-08-24 13:48:32 +02:00
Yacine Elhamer
7c101f01e5 test_binja.py: revert ruleset-related xfails 2023-08-24 13:36:53 +02:00
Yacine Elhamer
42689ef1da test_main.py: revert ruleset-related xfails 2023-08-24 13:30:22 +02:00
Yacine
5ba7325646 Merge pull request #1753 from yelhamer/update-linter
Update the rules linter
2023-08-23 11:50:51 +03:00
Yacine
86effec1a2 capa/rules/__init__.py: merge features from small scopes into larger ones
Co-authored-by: Willi Ballenthin <wballenthin@google.com>
2023-08-23 08:49:36 +03:00
Yacine
cdb469eca0 capa/features/freeze/__init__.py: remove comment
Co-authored-by: Willi Ballenthin <wballenthin@google.com>
2023-08-23 08:45:21 +03:00
Yacine
39c8fd8286 Update capa/features/freeze/__init__.py
Co-authored-by: Willi Ballenthin <wballenthin@google.com>
2023-08-23 08:43:36 +03:00
Yacine Elhamer
5730e5515f lint.py: update recommendation messages 2023-08-23 01:42:22 +02:00
Yacine Elhamer
901ba551bc lint.py: fix boolean statement 2023-08-23 01:41:44 +02:00
Yacine Elhamer
77b3fadf79 lint.py: add 'unsupported' keyword 2023-08-23 01:39:14 +02:00
Yacine Elhamer
44fc3357d1 initial commit 2023-08-23 01:32:01 +02:00
Willi Ballenthin
25414044ef Merge pull request #1748 from mandiant/feat/issue-1744
rules: add scope terms "unsupported" and "unspecified"
2023-08-22 15:59:57 +02:00
Yacine Elhamer
d1068991e3 test_rules_insn_scope.py: update rules missing the dynamic scope 2023-08-22 16:26:54 +02:00
Willi Ballenthin
4ab240e990 rules: add scope terms "unsupported" and "unspecified"
closes #1744
2023-08-22 12:58:06 +00:00
Willi Ballenthin
9489927bed Merge pull request #1746 from mandiant/fix/issue-1745
fix detection of CAPE reports
2023-08-22 14:34:23 +02:00
Willi Ballenthin
c160f45849 main: fix rendering of logging message 2023-08-22 12:32:53 +00:00
Willi Ballenthin
5b585c0e39 cape: better detect CAPE reports
fixes #1745
2023-08-22 12:32:30 +00:00
Aayush Goel
c6ee919619 Update capa/features/common.py
Co-authored-by: Willi Ballenthin <wballenthin@google.com>
2023-08-22 15:52:04 +05:30
Willi Ballenthin
675ad364ac point submodule rules to branch dynamic-syntax 2023-08-22 08:50:18 +00:00
Willi Ballenthin
21cefa0932 Merge branch 'master' into dynamic-feature-extraction 2023-08-22 09:53:42 +02:00
Willi Ballenthin
89c8c6d212 Update capa/rules/__init__.py 2023-08-22 09:38:41 +02:00
Willi Ballenthin
e5af7165ea Update capa/features/freeze/__init__.py 2023-08-22 09:31:35 +02:00
Willi Ballenthin
ee936f9257 Merge pull request #1729 from mandiant/feat/cape-pydantic
add Pydantic models for CAPE sandbox
2023-08-22 09:25:02 +02:00
Aayush Goel
6482848fa4 Merge branch 'Aayush-Goel-04/Issue#322' of https://github.com/Aayush-Goel-04/capa into Aayush-Goel-04/Issue#322 2023-08-20 00:39:50 +05:30
Aayush Goel
7c2a736c4b Update CHANGELOG.md 2023-08-20 00:38:35 +05:30
Aayush Goel
918ec22667 Merge branch 'master' into Aayush-Goel-04/Issue#322 2023-08-20 00:38:26 +05:30
Aayush Goel
1027da9be0 add new feature for com 2023-08-20 00:36:37 +05:30
Yacine Elhamer
521bd25d31 remove file-limitations checks for dynamic extractors 2023-08-18 15:23:19 +02:00
Yacine Elhamer
e7c0bea6e5 Match.from_capa(): remove reliance on the meta field to get the scope 2023-08-18 15:05:15 +02:00
Yacine Elhamer
a8bd5b1119 disable packed-sample warning for dynamic feature extractors 2023-08-18 14:31:32 +02:00
Yacine Elhamer
9144d12e51 add error message for invalid report files 2023-08-18 14:28:02 +02:00
Yacine Elhamer
d741544514 result_document.py: use the scopes attribute instead of meta["scope"] 2023-08-18 14:15:36 +02:00
Willi Ballenthin
5e31f0df23 cape: models: more fixes thanks to avast 2023-08-18 10:19:07 +00:00
Willi Ballenthin
18dff9d664 cape: models: more fixes thanks to avast 2023-08-18 10:15:12 +00:00
Yacine Elhamer
350094759a main.py: look up rules scope with scopes attribute, not their meta field 2023-08-18 12:37:42 +02:00
Willi Ballenthin
b10275e851 black 2023-08-18 08:23:21 +00:00
Willi Ballenthin
05cf7201ad Merge branch 'dynamic-feature-extraction' into feat/cape-pydantic 2023-08-18 10:22:55 +02:00
Willi Ballenthin
8cd5e03e87 ci: pre-commit: show-diff-on-failure 2023-08-18 08:19:27 +00:00
Willi Ballenthin
120917e0b5 cape: models: tweaks from Avast dataset 2023-08-18 08:10:55 +00:00
Yacine
264958ebfe Update capa/features/common.py
Co-authored-by: Willi Ballenthin <wballenthin@google.com>
2023-08-16 16:12:26 +02:00
Willi Ballenthin
3614ce1409 cape: fix test failures 2023-08-16 11:43:45 +00:00
Willi Ballenthin
c80542ded3 cape: call: fix argument type switch 2023-08-16 11:37:41 +00:00
Willi Ballenthin
3350a936b7 ida: use ida_nalt not idaapi
closes #1730
2023-08-16 13:33:01 +02:00
Willi Ballenthin
724db83920 cape: require PE analysis 2023-08-16 13:23:00 +02:00
Willi Ballenthin
8788a40d12 Merge branch 'dynamic-feature-extraction' into feat/cape-pydantic 2023-08-16 13:13:29 +02:00
Willi Ballenthin
6f7bf96776 cape: use pydantic model 2023-08-16 11:12:05 +00:00
Willi Ballenthin
e943a71dff cape: models: relax deserializing FlexibleModels 2023-08-16 10:04:20 +00:00
Willi Ballenthin
4be1c89c5b cape: models: more data shapes 2023-08-16 09:50:13 +00:00
Willi Ballenthin
2eda053c79 cape: models: more data shapes 2023-08-16 09:41:36 +00:00
Willi Ballenthin
26539e68d9 cape: models: add tests 2023-08-16 08:57:54 +00:00
Willi Ballenthin
046427cf55 cape: model: document the data we'll use in cape 2023-08-16 08:57:17 +00:00
Willi Ballenthin
25aabcd7e4 cape: models: more shapes 2023-08-16 07:48:59 +00:00
Willi Ballenthin
d8bea816dd cape: models: add more fields 2023-08-15 14:36:49 +00:00
Willi Ballenthin
bb2b1824a9 Merge branch 'master' into dynamic-feature-extraction 2023-08-15 14:01:30 +02:00
Willi Ballenthin
59a129d6d6 cape: add pydantic model for v2.2 2023-08-15 11:54:15 +00:00
Willi Ballenthin
db40d9bc7a wip: add initial CAPE model 2023-08-15 11:41:11 +00:00
Willi Ballenthin
827b4b29b4 test_rules: fix rule scoping logic 2023-08-15 09:21:49 +00:00
Willi Ballenthin
2a31b16567 merge 2023-08-15 08:56:41 +00:00
Willi Ballenthin
c001c883f7 Merge pull request #1714 from mandiant/fix/issue-1697-1
rule scoping tweaks
2023-08-15 10:16:01 +02:00
Willi Ballenthin
476c7ff749 main: provide encoding to open
fixes flake8 warning
2023-08-15 08:13:22 +00:00
Willi Ballenthin
4978aa74e7 tests: temporarily xfail script test
closes #1717
2023-08-15 08:13:14 +00:00
Yacine Elhamer
4411911664 Merge remote-tracking branch 'parentrepo/dynamic-feature-extraction' into fix/issue-1697-1 2023-08-15 09:57:13 +02:00
Yacine
0e1ce21488 Merge pull request #1715 from mandiant/fix/issue-1710
fix rendering of scope in vverbose mode
2023-08-15 09:51:53 +02:00
Yacine
88aa17fa7b Merge pull request #1716 from mandiant/fix/issue-1697-2
remove dynamic return address concept
2023-08-15 08:55:12 +02:00
Willi Ballenthin
d648fdf6c0 Merge pull request #1713 from mandiant/fix/issue-1711
record and show the analysis flavor
2023-08-14 16:44:42 +02:00
Yacine Elhamer
846bd62817 Merge remote-tracking branch 'parentrepo/dynamic-feature-extraction' into fix/issue-1711 2023-08-14 16:05:20 +02:00
Yacine
84cddc70fd Merge pull request #1709 from mandiant/fix/issue-1702
fix rendering of call and return addresses
2023-08-14 16:00:16 +03:00
Yacine
2a83f1fc23 Merge pull request #1708 from mandiant/fix/issue-1707
tests: create workspaces only during tests, not import
2023-08-14 12:24:02 +03:00
Yacine Elhamer
751231b730 fixtures.py: fix the path of '0000a567' in get_data_path_by_name() method 2023-08-14 12:37:15 +03:00
Willi Ballenthin
c6d400bcf3 address: remove dynamic return address concept, as its unused today 2023-08-11 11:18:54 +00:00
Willi Ballenthin
fd1cd05b99 vverbose: render relevant scope at top of match tree
closes #1710
2023-08-11 10:59:44 +00:00
Willi Ballenthin
8202e9e921 main: don't use analysis flavor to filter rules
im worried this will interact poorly with our rule cache,
unless we add more handling there, which needs more testing.
so, since the filtering likely has only a small impact on performance,
revert the rule filtering changes for simplicity.
2023-08-11 10:36:59 +00:00
Willi Ballenthin
3c069a6784 rules: don't change passed-in argument
make a local copy of the scopes dict
2023-08-11 10:35:40 +00:00
Willi Ballenthin
e100a63cc8 rules: use set instead of tuple, add doc
since the primary operation is `contain()`,
set is more appropriate than tuple.
2023-08-11 10:34:41 +00:00
Willi Ballenthin
3057b5fb9d render: show analysis flavor
closes #1711
2023-08-11 09:49:13 +00:00
Willi Ballenthin
c91dc71e75 result document: wire analysis flavor through metadata
ref #1711
2023-08-11 09:33:30 +00:00
Willi Ballenthin
f48e4a8ad8 render: verbose: render dynamic call return address 2023-08-11 09:07:11 +00:00
Willi Ballenthin
dafbefb325 render: verbose: render call address
closes #1702
2023-08-11 09:02:29 +00:00
Willi Ballenthin
6de23a9748 tests: main: demonstrate CAPE analysis (and bug #1702) 2023-08-11 08:56:06 +00:00
Willi Ballenthin
1cf33e4343 tests: create workspaces only during tests, not import
closes #1707
2023-08-11 08:38:06 +00:00
Willi Ballenthin
34db63171f sync submodule testfiles 2023-08-11 08:36:29 +00:00
Willi Ballenthin
19495f69d7 freeze: pydantic v2 fixes 2023-08-10 13:29:52 +00:00
Willi Ballenthin
c1fbb27d73 Merge branch 'master' into dynamic-feature-extraction 2023-08-10 13:21:49 +00:00
Willi Ballenthin
3cf748a135 vverbose: render both scopes nicely 2023-08-10 11:39:56 +02:00
Willi Ballenthin
85b58d041b process: simplify string enumeration loop 2023-08-10 11:38:43 +02:00
Willi Ballenthin
ae9d773e04 add TODO for typing.TypeAlias 2023-08-10 11:37:50 +02:00
Willi Ballenthin
582bb7c897 docstrings: improve wording 2023-08-10 11:36:51 +02:00
Willi Ballenthin
681d4fb007 Merge pull request #1678 from yelhamer/call-scope
Add a call scope
2023-08-07 11:31:29 +02:00
Yacine Elhamer
a185341a4d features/address.py: rename CallAddress DynamicCallAddress 2023-08-07 09:48:11 +01:00
Yacine Elhamer
aacd9f51b3 delete empty files 2023-08-07 09:48:11 +01:00
Yacine
95148d445a test_rules.py: update rules' formatting
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-08-07 09:47:57 +01:00
Yacine
65ac422e36 test_rules.py: update rules' fomratting
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-08-07 09:47:37 +01:00
Willi Ballenthin
5ffb6ca0cd Merge branch 'dynamic-feature-extraction' into call-scope 2023-08-07 10:40:53 +02:00
Willi Ballenthin
85f151303a merge 2023-08-07 08:40:03 +00:00
Willi Ballenthin
216cd01b3c sync test data submodule 2023-08-07 08:37:23 +00:00
Yacine
23bd2e7cd4 cape/call.py: remove use of the description keyword for features
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-08-07 09:13:07 +01:00
Yacine Elhamer
f461f65a86 move thread-scope features into the call-scope 2023-08-06 18:12:29 +01:00
Yacine Elhamer
8dc4adbb5e fix test_rules.py yaml identation bug 2023-08-04 16:20:37 +01:00
Yacine Elhamer
8b36cd1e35 add call-scope tests 2023-08-04 16:20:37 +01:00
Yacine
cd700a1782 Merge branch 'dynamic-feature-extraction' into call-scope 2023-08-03 15:27:44 +01:00
Yacine
60e94adeb1 base_extractor.py: fix ProcessHandle documentation comment
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-08-03 14:39:53 +01:00
Yacine
eafed0f1d4 build_statements(): fix call-scope InvalidRule message typo
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-08-03 14:38:38 +01:00
Yacine Elhamer
7c14c51012 cape/call.py: update extract_call_features() comment 2023-08-03 14:20:18 +01:00
Yacine Elhamer
4f9d24598f bugfix 2023-08-03 11:24:24 +01:00
Yacine Elhamer
4277b4bef8 include an address' parent in comparisons 2023-08-03 11:21:58 +01:00
Yacine Elhamer
3c3205adf1 add call address to show-features.py script 2023-08-02 23:10:27 +01:00
Yacine Elhamer
4e1527df95 update changelog 2023-08-02 22:48:38 +01:00
Yacine Elhamer
ca2760fb46 Initial commit 2023-08-02 22:46:54 +01:00
Willi Ballenthin
61924672e2 Merge pull request #1671 from yelhamer/rule-statement-building 2023-08-01 22:15:03 +02:00
Yacine Elhamer
7fdd988e4f remove redundant imports 2023-08-01 20:12:15 +01:00
Yacine Elhamer
a85e0523f8 remove Scopes LRU caching 2023-08-01 20:09:42 +01:00
Yacine Elhamer
462024ad03 update tests to explicitely specify scopes 2023-08-01 07:41:47 +01:00
Yacine Elhamer
f0d09899a1 rules/__init__.py: invalidate rules with no scopes field 2023-08-01 07:19:11 +01:00
Yacine Elhamer
b8212b3da7 main.py: replace | operator with Optional 2023-07-27 16:00:52 +01:00
Yacine Elhamer
3d812edc4d use weakrefs for Scopes instantiation; fix test_rules() 2023-07-27 15:52:39 +01:00
Yacine Elhamer
2efb7f2975 fix flake8 issues 2023-07-27 15:10:01 +01:00
Yacine Elhamer
44c5e96cf0 RuleSet: remove irrelevant rules after dependecies have been checked 2023-07-27 12:44:07 +01:00
Yacine Elhamer
97c878db22 update CHANGELOG 2023-07-27 10:33:34 +01:00
Yacine Elhamer
16e32f8441 add tests 2023-07-27 10:31:45 +01:00
Yacine Elhamer
d6aced5ec7 RulSet: add flavor-based rule filtering 2023-07-27 10:24:08 +01:00
Yacine Elhamer
b843382065 rules/__init__.py: update Scopes class 2023-07-26 17:20:51 +01:00
Willi Ballenthin
f4bdff0824 Merge pull request #1644 from yelhamer/find-dynamic-capabilities 2023-07-21 20:08:22 +02:00
Yacine Elhamer
d8c28e80eb add get_sample_hashes() to elf extractor 2023-07-21 15:50:09 +01:00
yelhamer
344b3e9931 Update capa/features/extractors/base_extractor.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-21 15:43:56 +01:00
yelhamer
c32ac19c0d Update capa/features/extractors/ida/extractor.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-21 15:43:41 +01:00
yelhamer
d13114e907 remove SampleHashes __iter__method
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-21 15:43:22 +01:00
yelhamer
90298fe2c8 Update capa/features/extractors/base_extractor.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-21 15:39:30 +01:00
Yacine Elhamer
3d1a1fb9fa add get_sample_hashes() to NullFeatureExtractor 2023-07-21 14:54:54 +01:00
Yacine Elhamer
830bad54bd fix bugs 2023-07-21 14:41:07 +01:00
Yacine Elhamer
c4ba5afe6b replace : FeatureSet annotations with a comment type annotation 2023-07-21 14:32:42 +01:00
Yacine Elhamer
4ec39d49aa fix linting issues 2023-07-21 14:03:57 +01:00
Yacine Elhamer
ab585ef951 add the skipif mark back 2023-07-21 14:00:58 +01:00
Yacine Elhamer
674122999f migrate the get_sample_hashes() function to each individual extractor 2023-07-21 14:00:01 +01:00
Yacine Elhamer
8085caef35 remove the usage of SampleHashes's __iter__() method 2023-07-21 13:48:48 +01:00
Yacine Elhamer
3ab3c61d5e use ida's hash-extraction functions 2023-07-21 13:48:48 +01:00
Yacine Elhamer
736b2cd689 address @mr-tz main.py review comments 2023-07-21 13:48:48 +01:00
yelhamer
bd8331678c update compute_static_layout with the appropriate types
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-21 13:16:51 +01:00
yelhamer
6f3fb42385 update compute_dynamic_layout with the appropriate type
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-21 13:15:55 +01:00
yelhamer
da4e887aee fix comment typo
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-07-21 12:40:02 +01:00
Yacine Elhamer
b1e468dae4 add tests for the get_sample_hashes() method 2023-07-21 11:04:21 +01:00
Yacine Elhamer
6d1a885864 update static freeze test 2023-07-21 08:48:18 +01:00
Yacine Elhamer
24b3abd706 add get_sample_hashes() to base extractor 2023-07-21 08:45:14 +01:00
yelhamer
806bc1853d Update mypy.ini: add TODO comment 2023-07-20 22:13:06 +01:00
Yacine Elhamer
6ee1dfd656 address review comments: rename SampleHashes's from_sample() method to from_bytes() method 2023-07-20 21:53:28 +01:00
Yacine Elhamer
ab092cb536 add sample_hashes attribute to the base extractors 2023-07-20 21:51:37 +01:00
Yacine Elhamer
b4cf50fb6e fix mypy issues 2023-07-20 21:48:05 +01:00
yelhamer
2b2b2b6545 Update capa/features/extractors/base_extractor.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-20 21:47:30 +01:00
yelhamer
fd7b926a33 Update capa/features/extractors/base_extractor.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-20 21:47:23 +01:00
Yacine Elhamer
482e0d386b use pathlib.Path() in binja and ida extractors 2023-07-20 21:42:14 +01:00
Yacine Elhamer
d99b16ed5e add copyright and remove old test 2023-07-20 21:41:16 +01:00
Yacine Elhamer
0a4fe58ac6 fix tests 2023-07-20 20:25:11 +01:00
Yacine Elhamer
8ac9caf45c fix bugs 2023-07-20 20:20:33 +01:00
Yacine Elhamer
1029b369f2 Merge remote-tracking branch 'parentrepo/dynamic-feature-extraction' into find-dynamic-capabilities 2023-07-20 20:02:49 +01:00
Willi Ballenthin
5ae588deaa Merge pull request #1658 from mandiant/sync-1657
sync
2023-07-20 14:05:22 +02:00
Willi Ballenthin
a2f31ab8ae update testfiles submodule 2023-07-20 11:52:15 +00:00
Willi Ballenthin
666c9c21a1 update testfiles submodule 2023-07-20 11:49:20 +00:00
Yacine Elhamer
a675c4c7a1 remove redundant code block 2023-07-20 11:27:07 +01:00
Yacine Elhamer
16eab6b5e5 remove unused commit 2023-07-20 11:24:07 +01:00
Yacine Elhamer
d520bfc753 fix bugs and add copyrights 2023-07-20 11:19:54 +01:00
Yacine Elhamer
301b10d261 fix style issues 2023-07-20 10:52:43 +01:00
Yacine Elhamer
e38e56ccf6 Merge remote-tracking branch 'parentrepo/dynamic-feature-extraction' into sync-1657 2023-07-20 09:33:48 +01:00
yelhamer
7de223f116 Update capa/features/extractors/ida/extractor.py: add call to get_input_file_path()
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-19 15:39:06 +01:00
Yacine Elhamer
c5d08ec0d1 update extractors and tests 2023-07-19 14:00:45 +01:00
Yacine Elhamer
4e4b1235c3 mypy.ini: ignore proto issues 2023-07-18 21:04:51 +01:00
Yacine Elhamer
e5d7903475 add removed tests 2023-07-18 20:38:54 +01:00
Yacine Elhamer
bc46bf3202 add vverbose rendering 2023-07-18 11:26:20 +01:00
yelhamer
4af84e53d5 bugfixes 2023-07-17 12:25:12 +01:00
Yacine Elhamer
e3f60ea0fb initial commit 2023-07-17 11:50:49 +01:00
Moritz
ce15a2b01e Merge pull request #1580 from yelhamer/analysis-flavor
add flavored scopes
2023-07-12 17:24:38 +02:00
Yacine Elhamer
9c878458b8 fix typo: replace 'rules' with 'rule' 2023-07-12 15:43:32 +01:00
Yacine Elhamer
53d897da09 ida/plugin/form.py: replace list comprehension in any() with a generator 2023-07-12 15:39:56 +01:00
Yacine Elhamer
17030395c6 ida/plugin/form.py: replace usage of '==' with usage of 'in' operator 2023-07-12 15:36:28 +01:00
Yacine Elhamer
34d3d6c1f9 Merge remote-tracking branch 'origin/analysis-flavor' into yelhamer-analysis-flavor 2023-07-12 15:27:13 +01:00
Willi Ballenthin
e335c9f977 Merge pull request #1612 from yelhamer/process-thread-addresses
add process and thread addresses
2023-07-12 10:54:14 +02:00
Yacine Elhamer
4ee38cbe29 fix linting issues 2023-07-11 14:52:04 +01:00
Yacine Elhamer
12c9154f55 fix flake8 linting issues 2023-07-11 14:40:56 +01:00
Yacine Elhamer
0e312d6dfe replace unused variable 'r' with '_' 2023-07-11 14:38:52 +01:00
Yacine Elhamer
7e18eeddba update ruff.toml 2023-07-11 14:33:19 +01:00
Yacine Elhamer
0db7141e33 remove redundant import 2023-07-11 14:33:07 +01:00
Yacine Elhamer
1ef0b16f11 Update ruff.toml 2023-07-11 14:32:33 +01:00
Yacine Elhamer
37c1bf98eb fix ruff F401 pytes issues 2023-07-11 14:26:59 +01:00
Yacine Elhamer
85d4c00096 fix ruff linting issues with test_static_freeze 2023-07-11 14:07:08 +01:00
Yacine Elhamer
078978a5b5 fix fixtures issue 2023-07-11 13:33:48 +01:00
Yacine Elhamer
841d393f8b fix non-matching type issue 2023-07-11 12:49:15 +01:00
Yacine Elhamer
740d1f6d4e fix imports: import TypeAlias from typing_extensions 2023-07-11 12:40:58 +01:00
Yacine Elhamer
b615c103ef fix flake8 linting: replace unused 'variable' with '_' 2023-07-11 12:37:01 +01:00
Yacine Elhamer
f879f53a6b fix linting issues 2023-07-11 12:33:37 +01:00
Yacine Elhamer
42baa10bcb Merge branch 'process-thread-addresses' of https://github.com/yelhamer/capa into yelhamer-process-thread-addresses 2023-07-11 12:07:20 +01:00
Yacine Elhamer
6feb9f540f fix ruff linting issues 2023-07-11 10:58:00 +01:00
Yacine Elhamer
f86ecfe446 Merge remote-tracking branch 'parentrepo/dynamic-feature-extraction' into analysis-flavor 2023-07-11 10:43:31 +01:00
Yacine Elhamer
64a16314ab Update capa/features/address.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-07-10 16:24:30 +01:00
Yacine Elhamer
dccebaeff8 Update CHANGELOG.md: include PR number 2023-07-10 16:18:59 +01:00
Yacine Elhamer
d2e5dea3e2 update magic header 2023-07-10 16:15:37 +01:00
Yacine Elhamer
ec59886031 Update capa/rules/__init__.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-10 15:58:27 +01:00
Yacine Elhamer
917dd8b0db Update scripts/lint.py
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-10 15:58:17 +01:00
Yacine Elhamer
63e273efd4 fix bugs and mypy issues 2023-07-10 15:52:33 +01:00
Yacine Elhamer
9394194031 address review comments 2023-07-10 14:12:56 +01:00
Yacine Elhamer
af256bc0e9 fix mypy issues and bugs 2023-07-10 14:11:10 +01:00
Yacine Elhamer
37e4b913b0 address review comments 2023-07-10 13:22:47 +01:00
Yacine Elhamer
722ee2f3d0 remove redundant print
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-10 12:54:15 +01:00
Yacine Elhamer
e5f5d542d0 replace ppid and pid fields with process in thread address
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-10 12:53:27 +01:00
Yacine Elhamer
1ac64aca10 feature freeze: fix Addres.from_capa() not returning bug 2023-07-10 12:44:27 +01:00
Yacine Elhamer
78054eea5a update changelog 2023-07-10 12:18:16 +01:00
Yacine Elhamer
ff63b0ff1a rename test_freeze.py to test_static_freeze.py 2023-07-10 12:15:38 +01:00
Yacine Elhamer
e2e367f091 update tests 2023-07-10 12:15:06 +01:00
Yacine Elhamer
5aa1a1afc7 initial commit: add ProcessAddress and ThreadAddress 2023-07-10 12:14:53 +01:00
Willi Ballenthin
a2d6bd693b Merge branch 'dynamic-feature-extraction' into analysis-flavor 2023-07-10 10:23:49 +02:00
Willi Ballenthin
7f57fccefb fix lints after sync with master 2023-07-10 02:55:50 +02:00
Willi Ballenthin
72e123e319 sync master 2023-07-10 02:50:18 +02:00
Willi Ballenthin
d29e7140b6 Merge pull request #1596 from mandiant/sync-master
Sync master
2023-07-10 10:30:23 +02:00
mr-tz
b6580f99db sync submodule 2023-07-07 19:37:25 +02:00
Yacine Elhamer
605fbaf803 add import asdict from dataclasses 2023-07-07 15:33:05 +01:00
Yacine Elhamer
03b0493d29 Scopes class: remove __eq__ operator overriding and override __in__ instead 2023-07-07 15:31:45 +01:00
Yacine Elhamer
5e295f59a4 DEV_SCOPE: add todo comment 2023-07-07 15:31:45 +01:00
mr-tz
f3135630d1 Merge branch 'master' into sync-master 2023-07-07 14:28:13 +02:00
Moritz
e140fba5df enhance various dynamic-related functions (#1590)
* enhance various dynamic-related functions

* test_cape_features(): update API(NtQueryValueKey) feature count to 7

---------

Co-authored-by: Yacine Elhamer <elhamer.yacine@gmail.com>
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-07 13:59:12 +02:00
Yacine Elhamer
fa7a7c294e replace usage of __dict__ with dataclasses.asdict()
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-07-07 11:01:02 +01:00
Yacine Elhamer
9dd65bfcb9 extract_subscope_rules(): use DEV_SCOPE 2023-07-07 08:54:19 +01:00
Yacine Elhamer
a8f722c4de xfail tests that require the old ruleset 2023-07-06 18:15:02 +01:00
Yacine Elhamer
0c56291e4a update linter 2023-07-06 17:50:57 +01:00
Yacine Elhamer
c916e3b07f update the linter 2023-07-06 17:27:45 +01:00
Yacine Elhamer
32f936ce8c address review comments 2023-07-06 17:17:18 +01:00
Yacine Elhamer
47aebcbdd4 fix show-capabilities-by-function 2023-07-06 00:48:22 +01:00
Yacine Elhamer
4649c9a61d rename rule.scope to rule.scope in ida plugin 2023-07-06 00:09:23 +01:00
Yacine Elhamer
9300e68225 fix mypy issues in test_rules.py 2023-07-06 00:05:20 +01:00
Yacine Elhamer
19e40a3383 address review comments 2023-07-05 23:58:08 +01:00
Yacine Elhamer
9ffe85fd9c build_statements: add support for scope flavors 2023-07-05 15:57:57 +01:00
Yacine Elhamer
8ba86e9cea add update Scopes class and switch scope to scopes 2023-07-05 15:00:14 +01:00
Yacine Elhamer
c042a28af1 rename Flavor to Scopes 2023-07-03 19:21:08 +01:00
Yacine Elhamer
1b59efc79a Apply suggestions from code review: rename Flavor to Scopes
Co-authored-by: Willi Ballenthin (Google) <118457858+wballenthin@users.noreply.github.com>
2023-07-03 11:11:14 +01:00
Yacine Elhamer
f1d7ac36eb Update test_rules.py 2023-07-03 02:48:24 +01:00
Yacine Elhamer
21cecb2aec tests: add unit tests for flavored scopes 2023-07-01 01:51:44 +01:00
Yacine Elhamer
8a93a06b71 fix mypy issues 2023-07-01 01:41:19 +01:00
Yacine Elhamer
d2ff0af34a Revert "tests: add unit tests for flavored scopes"
This reverts commit 6f0566581e.
2023-07-01 01:39:54 +01:00
Yacine Elhamer
ae5f2ec104 fix mypy issues 2023-07-01 01:38:37 +01:00
Yacine Elhamer
6f0566581e tests: add unit tests for flavored scopes 2023-07-01 00:57:01 +01:00
Yacine Elhamer
e726c7894c ensure_feature_valid_for_scope(): add support for flavored scopes 2023-07-01 00:56:35 +01:00
Yacine Elhamer
c4bb4d9508 update changelog 2023-06-30 20:28:40 +01:00
Yacine Elhamer
cfad228d3c scope flavors: add a Flavor class 2023-06-30 20:26:55 +01:00
Willi Ballenthin
670faf1d1d Merge pull request #1576 from yelhamer/process-scope 2023-06-28 16:34:15 +02:00
Yacine Elhamer
659163a93c thread scope: fix feature inheritance error 2023-06-28 14:52:00 +01:00
Yacine Elhamer
2b163edc0e add thread scope 2023-06-28 13:08:11 +01:00
Yacine Elhamer
0d38f85db7 process scope: add MatchedRule feature 2023-06-28 11:27:08 +01:00
Willi Ballenthin
1dc2825a75 Merge pull request #1577 from mandiant/master
sync dynamic-feature-extraction
2023-06-28 11:16:01 +02:00
Willi Ballenthin
630e2d23c9 Merge pull request #1569 from yelhamer/static-extractor
add a StaticFeatureExtractor class
2023-06-28 11:13:46 +02:00
Yacine Elhamer
c73187e7d4 Update capa/rules/__init__.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-28 10:08:29 +01:00
Yacine Elhamer
e18afe5d1e Merge branch 'dynamic-feature-extraction' into process-scope 2023-06-28 01:46:39 +01:00
Yacine Elhamer
7534e3f739 update changelog 2023-06-28 01:41:13 +01:00
Yacine Elhamer
0e01d91cec update changelog 2023-06-28 01:39:11 +01:00
Yacine Elhamer
06aea6b97c fix mypy and codestyle issues 2023-06-27 11:32:21 +01:00
Yacine Elhamer
a99ff813cb DynamicFeatureExtractor: remove get_base_address() method
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-27 11:22:35 +01:00
Yacine Elhamer
92734416a6 update base_extractor.py example
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-27 11:20:41 +01:00
Yacine Elhamer
2f32d4fe49 Update base_extractor.py with review comments
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-27 11:20:02 +01:00
Willi Ballenthin
81d35eb645 Merge branch 'dynamic-feature-extraction' into static-extractor 2023-06-27 09:42:16 +02:00
Willi Ballenthin
ac24ac2507 Merge pull request #1566 from yelhamer/dynamic-show-features
integrate the CAPE extractor with the show-features.py script
2023-06-27 09:37:27 +02:00
Yacine Elhamer
b172f9a354 FeatureExtractor alias: fix mypy typing issues by adding ininstance-based assert statements 2023-06-26 22:46:27 +01:00
Yacine Elhamer
63e4d3d5eb fix TypeAlias importing: import from typing_extensions to support Python 3.9 and lower 2023-06-26 21:14:17 +01:00
Yacine Elhamer
c74c8871f8 scripts: add type-related assert statements 2023-06-26 21:06:35 +01:00
Yacine Elhamer
3f5d08aedb base_extractor.py: add TypeAlias keyword, use union instead of bar operator, add an extract_file_features() and extract_global_features() methods 2023-06-26 20:57:51 +01:00
Yacine Elhamer
ddcb299834 main.py: address review suggestions (using elif for type casts, renaming to find_static_capabilities()) 2023-06-26 20:53:41 +01:00
Yacine Elhamer
a9f70dd1e5 main.py: update extractor type casting
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-26 20:01:30 +01:00
Yacine Elhamer
aff0c6b49b show-featurex.py: bugfix in ida_main() 2023-06-26 09:41:14 +01:00
Yacine Elhamer
417bb42ac8 show_features.py: rename show_{function,process}_features to show_{static,dynamic}_features.py 2023-06-26 09:16:59 +01:00
Yacine Elhamer
040ed4fa57 get_format_from_report(): use strings instead of literals
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-26 09:05:20 +01:00
Yacine Elhamer
94fc7b4e9a FeatureExtractor alias: add type casts to either StaticFeatureExtractor or DynamicFeatureExtractor 2023-06-26 01:23:01 +01:00
Yacine Elhamer
172e7a7649 update changelog 2023-06-25 23:03:13 +01:00
Yacine Elhamer
37ed138dcf base_extractor(): add a StaticFeatureExtractor and DynamicFeatureExtractor base classes, as well as a FeatureExtractor type alias 2023-06-25 22:57:39 +01:00
Yacine Elhamer
5f6aade92b get_format_from_report(): fix bugs and add a list of dynamic formats 2023-06-25 00:54:55 +01:00
Yacine Elhamer
0c62a5736e add support for determining the format of a sandbox report 2023-06-24 23:51:12 +01:00
Yacine Elhamer
f1406c1ffd scripts/show-features.py: prefix {static,dynamic}_analysis() functions' name with 'print_' 2023-06-23 13:58:34 +01:00
Yacine Elhamer
1cdc3e5232 fix codestyle 2023-06-23 13:48:49 +01:00
Yacine Elhamer
bd9870254e Apply suggestions from code review: use EXTENSIONS_CAPE, and ident 'thread' by one more space 2023-06-23 13:31:35 +01:00
Yacine Elhamer
0442b8c1e1 Apply suggestions from code review: use is_ for booleans
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-23 13:27:20 +01:00
Yacine Elhamer
585876d6af capa/main.py: use "rb" for opening json files
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-23 13:25:37 +01:00
Yacine Elhamer
902d726ea6 capa/main.py: change json import positioning to start of the file 2023-06-22 23:57:03 +01:00
Yacine Elhamer
3f35b426dd Apply suggestions from code review
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-22 21:58:01 +01:00
Yacine Elhamer
761d861888 Update fixtures.py samples path 2023-06-22 16:55:00 +01:00
Yacine Elhamer
9f185ed5c0 remove incompatible bar union syntax 2023-06-22 15:59:23 +01:00
Yacine Elhamer
63b2077335 get_extractor(): set return type to FeatureExtractor, and cast into the appropriate class before each usage 2023-06-22 15:55:24 +01:00
Yacine Elhamer
12d5beec6e add type cast to fix get_extractor() typing issues 2023-06-22 15:51:56 +01:00
Yacine Elhamer
b77e68df19 fix codestyle and typing 2023-06-22 14:17:06 +01:00
Yacine Elhamer
fcdd4fa410 update changelog 2023-06-22 14:03:01 +01:00
Yacine Elhamer
07c48bca68 scripts/show-features.py: add dynamic feature extraction from cape reports 2023-06-22 13:56:54 +01:00
Yacine Elhamer
79ff76d124 main.py: fix bugs for adding the cape extractor/format 2023-06-22 13:55:50 +01:00
Yacine Elhamer
de2ba1ca94 add the cape report format to main and across several other locations 2023-06-22 12:55:39 +01:00
Yacine Elhamer
45002bd51d Revert "scripts/show-features.py: add dynamic feature extraction from cape reports"
This reverts commit 64189a4d08.
2023-06-22 12:29:51 +01:00
Yacine Elhamer
be7ebad956 Revert "tests/fixtures.py: update path forming for the cape sample"
This reverts commit 6712801b01.
2023-06-22 12:18:34 +01:00
Yacine Elhamer
64189a4d08 scripts/show-features.py: add dynamic feature extraction from cape reports 2023-06-22 12:16:31 +01:00
Willi Ballenthin
708cb28ed0 Merge pull request #1546 from yelhamer/cape-extractor
add the CAPE feature extractor
2023-06-21 09:33:26 +02:00
Yacine Elhamer
6712801b01 tests/fixtures.py: update path forming for the cape sample
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-20 20:30:06 +01:00
Yacine Elhamer
f29db693c8 fix git submodules error 2023-06-20 20:25:19 +01:00
Yacine Elhamer
0502bfd95d remove cape report from get_md5_hash() function 2023-06-20 20:24:38 +01:00
Yacine Elhamer
78a3901c61 cape/helpers.py: add a find_process() function for quick-fetching processes from the cape report 2023-06-20 15:59:22 +01:00
Yacine Elhamer
0a4e3008af fixtures.py: update CAPE's feature count and presence tests 2023-06-20 13:51:16 +01:00
Yacine Elhamer
d03ba5394f cape/global_.py: add warning messages if architecture/os/format are unknown 2023-06-20 13:26:25 +01:00
Yacine Elhamer
2262e6c7d0 Merge branch 'test-cape-extractor' into cape-extractor 2023-06-20 13:22:15 +01:00
Yacine Elhamer
31a349b13b cape feature tests: fix feature count function typo 2023-06-20 13:21:52 +01:00
Yacine Elhamer
1ba143ef26 Merge branch 'test-cape-extractor' into cape-extractor 2023-06-20 13:20:49 +01:00
Yacine Elhamer
1532ce1bab add tests for extracting argument values 2023-06-20 13:20:33 +01:00
Yacine Elhamer
fa9b920b71 cape/thread.py: do not extract return values, and extract argument values as Strings 2023-06-20 13:17:53 +01:00
Yacine Elhamer
40b2d5f724 add a remote origin to submodule, and switch to that branch 2023-06-20 12:40:47 +01:00
Yacine Elhamer
0623a5a8de point capa-testfiles submodule towards dynamic-feautre-extractor branch 2023-06-20 12:13:57 +01:00
Yacine Elhamer
cfa1d08e7e update testfiles submodule to point at dev branch 2023-06-20 11:28:40 +01:00
Yacine Elhamer
6196814672 cape/file.py: fix KeyError bug 2023-06-20 10:51:18 +01:00
Yacine Elhamer
f5af2bf393 Merge branch 'test-cape-extractor' into cape-extractor 2023-06-20 10:47:56 +01:00
Yacine Elhamer
374fb033c1 add support for gzip compressed cape samples, and fix QakBot sample path 2023-06-20 10:29:52 +01:00
Yacine Elhamer
4db80e75a4 add mode and encoding parameters to open() 2023-06-20 10:13:06 +01:00
Yacine Elhamer
8547277958 tests/fixtures.py bugfix: remove redundant lambda function
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-20 10:10:42 +01:00
Yacine Elhamer
ec3366b0e5 Update tests/fixtures.py
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-20 10:09:27 +01:00
Yacine Elhamer
48bd04b387 tests/fixtures.py: return direct extractor with no intermediate variable
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-20 10:09:00 +01:00
Yacine Elhamer
41a481252c Update CHANGELOG.md
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-20 10:08:12 +01:00
Yacine Elhamer
a7cf3b5b10 features/insn.py: revert added strace-based API feature 2023-06-20 10:04:37 +01:00
Yacine Elhamer
ba63188f27 cape/file.py: fix bug in call to helpers.generate_symbols()
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-20 10:02:57 +01:00
Yacine Elhamer
9cc34cb70f cape/file.py: fix imports ordering and format 2023-06-20 00:19:55 +01:00
Yacine Elhamer
b9a4d72b42 cape/file.py: add usage of helpers.generate_symbols() 2023-06-20 00:12:21 +01:00
Yacine Elhamer
8eef210547 update changelog 2023-06-19 23:57:51 +01:00
Yacine Elhamer
ef999ed954 rules/__init__.py: remove redundant HBI features 2023-06-19 23:56:10 +01:00
Yacine Elhamer
33de609560 Revert "removed redundant HBI features"
This reverts commit c88f859dae.
2023-06-19 23:55:22 +01:00
Yacine Elhamer
624151c3f7 Revert "update changelog"
This reverts commit 49b77d5477.
2023-06-19 23:55:12 +01:00
Yacine Elhamer
c88f859dae removed redundant HBI features 2023-06-19 23:55:06 +01:00
Yacine Elhamer
49b77d5477 update changelog 2023-06-19 23:49:19 +01:00
Yacine Elhamer
d4c4a17eb7 bugfixes and add cape sample tests 2023-06-19 23:42:27 +01:00
Yacine Elhamer
3c8abab574 fix bugs and refactor code 2023-06-19 23:40:09 +01:00
Yacine Elhamer
38596f8d0e add features for the QakBot sample 2023-06-19 19:32:56 +01:00
Yacine Elhamer
4acdca090d bug fixes 2023-06-19 17:14:59 +01:00
Yacine Elhamer
f02178852b update changelog 2023-06-19 17:01:05 +01:00
Yacine Elhamer
98e7acddf4 fix codestyle issues 2023-06-19 16:59:27 +01:00
Yacine Elhamer
9458e851c0 update test sample's path 2023-06-19 16:46:24 +01:00
Yacine Elhamer
a04512d7b8 add unit tests for the cape feature extractor 2023-06-19 16:43:54 +01:00
Yacine Elhamer
d6fa832d83 cape: move get_processes() method to file scope 2023-06-19 13:50:46 +01:00
Yacine Elhamer
dbad921fa5 code style changes 2023-06-15 13:21:17 +01:00
Yacine Elhamer
e1535dd574 remove Registry, Filename, and mutex features 2023-06-15 13:17:07 +01:00
Yacine Elhamer
22640eb900 cape/file.py: remove FunctionName feature extraction for imported functions 2023-06-15 12:44:57 +01:00
Yacine Elhamer
7e51e03043 cape/file.py: remove String, Filename, and Mutex features 2023-06-15 12:43:39 +01:00
Yacine Elhamer
865616284f cape/thread.py: remove yielding argument features
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-15 12:33:22 +01:00
Yacine Elhamer
0cf728b7e1 global_.py: update typo in yielded OS name
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-15 12:28:08 +01:00
Willi Ballenthin
a2d563b081 Merge branch 'dynamic-feature-extraction' into cape-extractor 2023-06-15 12:43:55 +02:00
Willi Ballenthin
8119aa6933 ci: do tests on dynamic-feature-extraction branch 2023-06-15 12:17:02 +02:00
Willi Ballenthin
6b953363d1 Update capa/features/extractors/base_extractor.py 2023-06-15 11:40:33 +02:00
Willi Ballenthin
139b240250 Update capa/features/extractors/base_extractor.py 2023-06-15 11:40:32 +02:00
Willi Ballenthin
36b5dff1f0 Update capa/features/extractors/base_extractor.py 2023-06-15 11:40:32 +02:00
Yacine Elhamer
7ae07d4de5 remove redundant types
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-15 11:40:32 +02:00
Yacine Elhamer
59ef52a271 remove default implementation
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-15 11:40:31 +02:00
Yacine Elhamer
34a1b22a38 remove ppid member from ProcessHandle
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-15 11:40:31 +02:00
Yacine Elhamer
b4f01fa6c2 add ppid documentation to the dynamic extractor interface 2023-06-15 11:40:30 +02:00
Yacine Elhamer
2d6d16dcd0 add parent process id to the process handle 2023-06-15 11:40:30 +02:00
Yacine Elhamer
1ccae4fef2 remove from_trace() and submit_sample() methods 2023-06-15 11:40:29 +02:00
Yacine Elhamer
ee30acab32 get_threads(): fix mypy typing
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-15 11:40:29 +02:00
Yacine Elhamer
5189bef325 fix bad comment
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-15 11:40:28 +02:00
Yacine Elhamer
17597580f4 add abstract DynamicExtractor class 2023-06-15 11:40:28 +02:00
Yacine Elhamer
f97f9e8646 Merge branch 'dynamic-features' into cape-extractor 2023-06-14 23:07:39 +01:00
Yacine Elhamer
91f1d41324 extract registry keys, files, and mutexes from the sample 2023-06-14 22:57:41 +01:00
Yacine Elhamer
d9d9d98ea0 update the Registry, Filename, and Mutex classes 2023-06-14 22:45:12 +01:00
Willi Ballenthin
e7115c7316 Update capa/features/extractors/base_extractor.py 2023-06-14 22:43:37 +01:00
Willi Ballenthin
6c58e26f14 Update capa/features/extractors/base_extractor.py 2023-06-14 22:43:37 +01:00
Willi Ballenthin
dc371580a5 Update capa/features/extractors/base_extractor.py 2023-06-14 22:43:37 +01:00
Yacine Elhamer
2a047073e9 remove redundant types
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-14 22:43:37 +01:00
Stephen Eckels
6e3b1bc240 explorer: optimize cache and extractor interface (#1470)
* Optimize cache and extractor interface

* Update changelog

* Run linter formatters

* Implement review feedback

* Move rulegen extractor construction to tab change

* Change rulegen cache construction behavior

* Adjust return values for CR, format

* Fix mypy errors

* Format

* Fix merge

---------

Co-authored-by: Stephen Eckels <stephen.eckels@mandiant.com>
2023-06-14 22:43:37 +01:00
Capa Bot
51faaae1d0 Sync capa rules submodule 2023-06-14 22:43:37 +01:00
Capa Bot
f55804ef06 Sync capa rules submodule 2023-06-14 22:43:37 +01:00
Xusheng
e671e1c87c Add a test that asserts on the binja version 2023-06-14 22:43:37 +01:00
Xusheng
a7aa817dce Update the stack string detection with BN's builtin outlining of constant expressions 2023-06-14 22:43:37 +01:00
Capa Bot
dcce4db6d5 Sync capa rules submodule 2023-06-14 22:43:37 +01:00
Yacine Elhamer
64c4f0f1aa remove default implementation
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-14 22:43:37 +01:00
Yacine Elhamer
a8f928200b remove ppid member from ProcessHandle
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-14 22:43:37 +01:00
Yacine Elhamer
58d42b09d9 add ppid documentation to the dynamic extractor interface 2023-06-14 22:43:37 +01:00
Yacine Elhamer
0cd481b149 remove redundant comments
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-14 22:42:25 +01:00
Yacine Elhamer
a66c55ca14 add the initial version of the cape extractor 2023-06-14 22:34:11 +01:00
Yacine Elhamer
18715dbe2e fix typo bug 2023-06-14 21:47:40 +01:00
Willi Ballenthin
23dee61389 Merge branch 'dynamic-feature-extraction' into cape-extractor 2023-06-14 12:41:08 +02:00
Willi Ballenthin
23dc3f29cd Merge pull request #1528 from yelhamer/dynamic-extractor
add a Dynamic extractor interface
2023-06-14 11:00:06 +02:00
Willi Ballenthin
4c701f4b6c Update capa/features/extractors/base_extractor.py 2023-06-14 10:59:07 +02:00
Willi Ballenthin
7a94f524b4 Update capa/features/extractors/base_extractor.py 2023-06-14 10:58:59 +02:00
Willi Ballenthin
23deb41436 Update capa/features/extractors/base_extractor.py 2023-06-14 10:58:50 +02:00
Yacine Elhamer
7198ebefc9 remove redundant types
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-14 09:58:33 +01:00
Willi Ballenthin
32cb57532e Merge branch 'dynamic-feature-extraction' into dynamic-extractor 2023-06-14 10:54:44 +02:00
Yacine Elhamer
edcfece993 remove default implementation
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-14 09:33:24 +01:00
Yacine Elhamer
baf209f3cc remove ppid member from ProcessHandle
Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com>
2023-06-14 09:33:07 +01:00
Yacine Elhamer
ece47c9ed5 add ppid documentation to the dynamic extractor interface 2023-06-14 09:05:53 +01:00
Yacine Elhamer
3d40ed968a Merge branch 'dynamic-features' into cape-extractor 2023-06-13 23:04:44 +01:00
Yacine Elhamer
10f56de5e8 Merge branch 'dynamic-extractor' into dynamic-features 2023-06-13 23:03:33 +01:00
Yacine Elhamer
5ee4fc2cd5 add parent process id to the process handle 2023-06-13 23:02:00 +01:00
Yacine Elhamer
a7917a0f3d add cape's thread features' extraction module 2023-06-13 22:56:15 +01:00
Yacine Elhamer
0274cf3ec7 add cape's global features' extraction module 2023-06-13 22:55:42 +01:00
Yacine Elhamer
3aa7c96902 add cape extractor class 2023-06-13 22:54:52 +01:00
Yacine Elhamer
ffa1851bbf Merge branch 'dynamic-features' into cape-extractor 2023-06-13 14:26:34 +01:00
Yacine Elhamer
45c3345bbc Merge branch 'dynamic-extractor' into dynamic-features 2023-06-13 14:26:14 +01:00
Yacine Elhamer
a6ca3aaa66 remove from_trace() and submit_sample() methods 2023-06-13 14:23:50 +01:00
Yacine Elhamer
5a10b612a1 add a Mutex feature 2023-06-12 00:06:53 +01:00
Yacine Elhamer
632b3ff07c add a Filename feature 2023-06-12 00:06:05 +01:00
Yacine Elhamer
efe1d1c0ac add a Registry feature 2023-06-12 00:05:20 +01:00
Yacine Elhamer
86e2f83a7d extend the API feature to support an strace-like argument style 2023-06-11 23:19:24 +01:00
Yacine Elhamer
a2b3a38f86 add the cape extractor's file hierarchy 2023-06-10 20:06:57 +01:00
Yacine Elhamer
f243749d38 get_threads(): fix mypy typing
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-09 09:03:49 +00:00
Yacine Elhamer
dac103c621 fix bad comment
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2023-06-09 09:03:09 +00:00
Yacine Elhamer
35e53e9691 add abstract DynamicExtractor class 2023-06-08 23:15:29 +00:00
107 changed files with 39355 additions and 1588 deletions

View File

@@ -18,7 +18,7 @@ a = Analysis(
# this gets invoked from the directory of the spec file,
# i.e. ./.github/pyinstaller
("../../rules", "rules"),
("../../sigs", "sigs"),
("../../capa/sigs", "sigs"),
("../../cache", "cache"),
# capa.render.default uses tabulate that depends on wcwidth.
# it seems wcwidth uses a json file `version.json`
@@ -79,7 +79,7 @@ exe = EXE(
name="capa",
icon="logo.ico",
debug=False,
strip=None,
strip=False,
upx=True,
console=True,
)

View File

@@ -11,34 +11,41 @@ permissions:
jobs:
build:
name: PyInstaller for ${{ matrix.os }}
name: PyInstaller for ${{ matrix.os }} / Py ${{ matrix.python_version }}
runs-on: ${{ matrix.os }}
strategy:
# set to false for debugging
fail-fast: true
matrix:
# using Python 3.8 to support running across multiple operating systems including Windows 7
include:
- os: ubuntu-20.04
# use old linux so that the shared library versioning is more portable
artifact_name: capa
asset_name: linux
python_version: 3.8
- os: ubuntu-20.04
artifact_name: capa
asset_name: linux-py311
python_version: 3.11
- os: windows-2019
artifact_name: capa.exe
asset_name: windows
python_version: 3.8
- os: macos-11
# use older macOS for assumed better portability
artifact_name: capa
asset_name: macos
python_version: 3.8
steps:
- name: Checkout capa
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
submodules: true
# using Python 3.8 to support running across multiple operating systems including Windows 7
- name: Set up Python 3.8
- name: Set up Python ${{ matrix.python_version }}
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
with:
python-version: 3.8
python-version: ${{ matrix.python_version }}
- if: matrix.os == 'ubuntu-20.04'
run: sudo apt-get install -y libyaml-dev
- name: Upgrade pip, setuptools
@@ -55,13 +62,17 @@ jobs:
run: dist/capa "tests/data/499c2a85f6e8142c3f48d4251c9c7cd6.raw32"
- name: Does it run (ELF)?
run: dist/capa "tests/data/7351f8a40c5450557b24622417fc478d.elf_"
- name: Does it run (CAPE)?
run: |
7z e "tests/data/dynamic/cape/v2.2/d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json.gz"
dist/capa "d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json"
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ${{ matrix.asset_name }}
path: dist/${{ matrix.artifact_name }}
test_run:
name: Test run on ${{ matrix.os }}
name: Test run on ${{ matrix.os }} / ${{ matrix.asset_name }}
runs-on: ${{ matrix.os }}
needs: [build]
strategy:
@@ -71,6 +82,9 @@ jobs:
- os: ubuntu-22.04
artifact_name: capa
asset_name: linux
- os: ubuntu-22.04
artifact_name: capa
asset_name: linux-py311
- os: windows-2022
artifact_name: capa.exe
asset_name: windows
@@ -96,6 +110,8 @@ jobs:
include:
- asset_name: linux
artifact_name: capa
- asset_name: linux-py311
artifact_name: capa
- asset_name: windows
artifact_name: capa.exe
- asset_name: macos

21
.github/workflows/pip-audit.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: PIP audit
on:
schedule:
- cron: '0 8 * * 1'
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
matrix:
python-version: ["3.11"]
steps:
- name: Check out repository code
uses: actions/checkout@v4
- uses: pypa/gh-action-pip-audit@v1.0.8
with:
inputs: .

View File

@@ -39,13 +39,13 @@ jobs:
- name: Lint with ruff
run: pre-commit run ruff
- name: Lint with isort
run: pre-commit run isort
run: pre-commit run isort --show-diff-on-failure
- name: Lint with black
run: pre-commit run black
run: pre-commit run black --show-diff-on-failure
- name: Lint with flake8
run: pre-commit run flake8
run: pre-commit run flake8 --hook-stage manual
- name: Check types with mypy
run: pre-commit run mypy
run: pre-commit run mypy --hook-stage manual
rule_linter:
runs-on: ubuntu-20.04
@@ -95,6 +95,10 @@ jobs:
run: sudo apt-get install -y libyaml-dev
- name: Install capa
run: pip install -e .[dev]
- name: Run tests (fast)
# this set of tests runs about 80% of the cases in 20% of the time,
# and should catch most errors quickly.
run: pre-commit run pytest-fast --all-files --hook-stage manual
- name: Run tests
run: pytest -v tests/
@@ -103,7 +107,7 @@ jobs:
env:
BN_SERIAL: ${{ secrets.BN_SERIAL }}
runs-on: ubuntu-20.04
needs: [code_style, rule_linter]
needs: [tests]
strategy:
fail-fast: false
matrix:
@@ -143,7 +147,7 @@ jobs:
ghidra-tests:
name: Ghidra tests for ${{ matrix.python-version }}
runs-on: ubuntu-20.04
needs: [code_style, rule_linter]
needs: [tests]
strategy:
fail-fast: false
matrix:
@@ -197,4 +201,4 @@ jobs:
cat ../output.log
exit_code=$(cat ../output.log | grep exit | awk '{print $NF}')
exit $exit_code

2
.gitmodules vendored
View File

@@ -1,6 +1,8 @@
[submodule "rules"]
path = rules
url = ../capa-rules.git
branch = dynamic-syntax
[submodule "tests/data"]
path = tests/data
url = ../capa-testfiles.git
branch = dynamic-feature-extractor

View File

@@ -25,7 +25,7 @@ repos:
hooks:
- id: isort
name: isort
stages: [commit, push]
stages: [commit, push, manual]
language: system
entry: isort
args:
@@ -45,7 +45,7 @@ repos:
hooks:
- id: black
name: black
stages: [commit, push]
stages: [commit, push, manual]
language: system
entry: black
args:
@@ -62,7 +62,7 @@ repos:
hooks:
- id: ruff
name: ruff
stages: [commit, push]
stages: [commit, push, manual]
language: system
entry: ruff
args:
@@ -79,7 +79,7 @@ repos:
hooks:
- id: flake8
name: flake8
stages: [commit, push]
stages: [push, manual]
language: system
entry: flake8
args:
@@ -97,7 +97,7 @@ repos:
hooks:
- id: mypy
name: mypy
stages: [commit, push]
stages: [push, manual]
language: system
entry: mypy
args:
@@ -109,3 +109,21 @@ repos:
- "tests/"
always_run: true
pass_filenames: false
- repo: local
hooks:
- id: pytest-fast
name: pytest (fast)
stages: [manual]
language: system
entry: pytest
args:
- "tests/"
- "--ignore=tests/test_binja_features.py"
- "--ignore=tests/test_ghidra_features.py"
- "--ignore=tests/test_ida_features.py"
- "--ignore=tests/test_viv_features.py"
- "--ignore=tests/test_main.py"
- "--ignore=tests/test_scripts.py"
always_run: true
pass_filenames: false

View File

@@ -3,27 +3,79 @@
## master (unreleased)
### New Features
- ghidra: add Ghidra feature extractor and supporting code #1770 @colton-gabertan
- ghidra: add entry script helping users run capa against a loaded Ghidra database #1767 @mike-hunhoff
- add Ghidra backend #1770 #1767 @colton-gabertan @mike-hunhoff
- add dynamic analysis via CAPE sandbox reports #48 #1535 @yelhamer
- add call scope #771 @yelhamer
- add thread scope #1517 @yelhamer
- add process scope #1517 @yelhamer
- rules: change `meta.scope` to `meta.scopes` @yelhamer
- protobuf: add `Metadata.flavor` @williballenthin
- binja: add support for forwarded exports #1646 @xusheng6
- binja: add support for symtab names #1504 @xusheng6
- add com class/interface features #322 @Aayush-goel-04
- dotnet: emit enclosing class information for nested classes #1780 #1913 @bkojusner @mike-hunhoff
### Breaking Changes
### New Rules (1)
- remove the `SCOPE_*` constants in favor of the `Scope` enum #1764 @williballenthin
- protobuf: deprecate `RuleMetadata.scope` in favor of `RuleMetadata.scopes` @williballenthin
- protobuf: deprecate `Metadata.analysis` in favor of `Metadata.analysis2` that is dynamic analysis aware @williballenthin
- update freeze format to v3, adding support for dynamic analysis @williballenthin
- extractor: ignore DLL name for api features #1815 @mr-tz
### New Rules (39)
- nursery/get-ntoskrnl-base-address @mr-tz
- host-interaction/network/connectivity/set-tcp-connection-state @johnk3r
- nursery/capture-process-snapshot-data @mr-tz
- collection/network/capture-packets-using-sharppcap jakub.jozwiak@mandiant.com
- nursery/communicate-with-kernel-module-via-netlink-socket-on-linux michael.hunhoff@mandiant.com
- nursery/get-current-pid-on-linux michael.hunhoff@mandiant.com
- nursery/get-file-system-information-on-linux michael.hunhoff@mandiant.com
- nursery/get-password-database-entry-on-linux michael.hunhoff@mandiant.com
- nursery/mark-thread-detached-on-linux michael.hunhoff@mandiant.com
- nursery/persist-via-gnome-autostart-on-linux michael.hunhoff@mandiant.com
- nursery/set-thread-name-on-linux michael.hunhoff@mandiant.com
- load-code/dotnet/load-windows-common-language-runtime michael.hunhoff@mandiant.com blas.kojusner@mandiant.com jakub.jozwiak@mandiant.com
- nursery/log-keystrokes-via-input-method-manager @mr-tz
- nursery/encrypt-data-using-rc4-via-systemfunction032 richard.weiss@mandiant.com
- nursery/add-value-to-global-atom-table @mr-tz
- nursery/enumerate-processes-that-use-resource @Ana06
- host-interaction/process/inject/allocate-or-change-rwx-memory @mr-tz
- lib/allocate-or-change-rw-memory 0x534a@mailbox.org @mr-tz
- lib/change-memory-protection @mr-tz
- anti-analysis/anti-av/patch-antimalware-scan-interface-function jakub.jozwiak@mandiant.com
- executable/dotnet-singlefile/bundled-with-dotnet-single-file-deployment sara.rincon@mandiant.com
- internal/limitation/file/internal-dotnet-single-file-deployment-limitation sara.rincon@mandiant.com
- data-manipulation/encoding/encode-data-using-add-xor-sub-operations jakub.jozwiak@mandiant.com
- nursery/access-camera-in-dotnet-on-android michael.hunhoff@mandiant.com
- nursery/capture-microphone-audio-in-dotnet-on-android michael.hunhoff@mandiant.com
- nursery/capture-screenshot-in-dotnet-on-android michael.hunhoff@mandiant.com
- nursery/check-for-incoming-call-in-dotnet-on-android michael.hunhoff@mandiant.com
- nursery/check-for-outgoing-call-in-dotnet-on-android michael.hunhoff@mandiant.com
- nursery/compiled-with-xamarin michael.hunhoff@mandiant.com
- nursery/get-os-version-in-dotnet-on-android michael.hunhoff@mandiant.com
- data-manipulation/compression/create-cabinet-on-windows michael.hunhoff@mandiant.com jakub.jozwiak@mandiant.com
- data-manipulation/compression/extract-cabinet-on-windows jakub.jozwiak@mandiant.com
- lib/create-file-decompression-interface-context-on-windows jakub.jozwiak@mandiant.com
- nursery/enumerate-files-in-dotnet moritz.raabe@mandiant.com anushka.virgaonkar@mandiant.com
- nursery/get-mac-address-in-dotnet moritz.raabe@mandiant.com michael.hunhoff@mandiant.com echernofsky@google.com
- nursery/get-current-process-command-line william.ballenthin@mandiant.com
- nursery/get-current-process-file-path william.ballenthin@mandiant.com
- nursery/hook-routines-via-dlsym-rtld_next william.ballenthin@mandiant.com
-
### Bug Fixes
- ghidra: fix ints_to_bytes performance #1761 @mike-hunhoff
- ghidra: fix `ints_to_bytes` performance #1761 @mike-hunhoff
- binja: improve function call site detection @xusheng6
- binja: use binaryninja.load to open files @xusheng6
- binja: use `binaryninja.load` to open files @xusheng6
- binja: bump binja version to 3.5 #1789 @xusheng6
- elf: better detect ELF OS via GCC .ident directives #1928 @williballenthin
### capa explorer IDA Pro plugin
### Development
- update ATT&CK/MBC data for linting #1932 @mr-tz
### Raw diffs
- [capa v6.1.0...master](https://github.com/mandiant/capa/compare/v6.1.0...master)

129
README.md
View File

@@ -2,13 +2,13 @@
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flare-capa)](https://pypi.org/project/flare-capa)
[![Last release](https://img.shields.io/github/v/release/mandiant/capa)](https://github.com/mandiant/capa/releases)
[![Number of rules](https://img.shields.io/badge/rules-832-blue.svg)](https://github.com/mandiant/capa-rules)
[![Number of rules](https://img.shields.io/badge/rules-864-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, .NET module, or shellcode file and it tells you what it thinks the program can do.
You run it against a PE, ELF, .NET module, shellcode file, or a sandbox report and it tells you what it thinks the program can do.
For example, it might suggest that the file is a backdoor, is capable of installing services, or relies on HTTP to communicate.
Check out:
@@ -125,6 +125,96 @@ function @ 0x4011C0
...
```
Additionally, capa also supports analyzing [CAPE](https://github.com/kevoreilly/CAPEv2) sandbox reports for dynamic capabilty extraction.
In order to use this, you first submit your sample to CAPE for analysis, and then run capa against the generated report (JSON).
Here's an example of running capa against a packed binary, and then running capa against the CAPE report of that binary:
```yaml
$ capa 05be49819139a3fdcdbddbdefd298398779521f3d68daa25275cc77508e42310.exe
WARNING:capa.capabilities.common:--------------------------------------------------------------------------------
WARNING:capa.capabilities.common: This sample appears to be packed.
WARNING:capa.capabilities.common:
WARNING:capa.capabilities.common: Packed samples have often been obfuscated to hide their logic.
WARNING:capa.capabilities.common: capa cannot handle obfuscation well using static analysis. This means the results may be misleading or incomplete.
WARNING:capa.capabilities.common: If possible, you should try to unpack this input file before analyzing it with capa.
WARNING:capa.capabilities.common: Alternatively, run the sample in a supported sandbox and invoke capa against the report to obtain dynamic analysis results.
WARNING:capa.capabilities.common:
WARNING:capa.capabilities.common: Identified via rule: (internal) packer file limitation
WARNING:capa.capabilities.common:
WARNING:capa.capabilities.common: Use -v or -vv if you really want to see the capabilities identified by capa.
WARNING:capa.capabilities.common:--------------------------------------------------------------------------------
$ capa 05be49819139a3fdcdbddbdefd298398779521f3d68daa25275cc77508e42310.json
┍━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┑
│ ATT&CK Tactic │ ATT&CK Technique │
┝━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┥
│ CREDENTIAL ACCESS │ Credentials from Password Stores T1555 │
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
│ DEFENSE EVASION │ File and Directory Permissions Modification T1222 │
│ │ Modify Registry T1112 │
│ │ Obfuscated Files or Information T1027 │
│ │ Virtualization/Sandbox Evasion::User Activity Based Checks T1497.002 │
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
│ DISCOVERY │ Account Discovery T1087 │
│ │ Application Window Discovery T1010 │
│ │ File and Directory Discovery T1083 │
│ │ Query Registry T1012 │
│ │ System Information Discovery T1082 │
│ │ System Location Discovery::System Language Discovery T1614.001 │
│ │ System Owner/User Discovery T1033 │
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
│ EXECUTION │ System Services::Service Execution T1569.002 │
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
│ PERSISTENCE │ Boot or Logon Autostart Execution::Registry Run Keys / Startup Folder T1547.001 │
│ │ Boot or Logon Autostart Execution::Winlogon Helper DLL T1547.004 │
│ │ Create or Modify System Process::Windows Service T1543.003 │
┕━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙
┍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┑
│ Capability │ Namespace │
┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┥
│ check for unmoving mouse cursor (3 matches) │ anti-analysis/anti-vm/vm-detection │
│ gather bitkinex information │ collection/file-managers │
│ gather classicftp information │ collection/file-managers │
│ gather filezilla information │ collection/file-managers │
│ gather total-commander information │ collection/file-managers │
│ gather ultrafxp information │ collection/file-managers │
│ resolve DNS (23 matches) │ communication/dns │
│ initialize Winsock library (7 matches) │ communication/socket │
│ act as TCP client (3 matches) │ communication/tcp/client │
│ create new key via CryptAcquireContext │ data-manipulation/encryption │
│ encrypt or decrypt via WinCrypt │ data-manipulation/encryption │
│ hash data via WinCrypt │ data-manipulation/hashing │
│ initialize hashing via WinCrypt │ data-manipulation/hashing │
│ hash data with MD5 │ data-manipulation/hashing/md5 │
│ generate random numbers via WinAPI │ data-manipulation/prng │
│ extract resource via kernel32 functions (2 matches) │ executable/resource │
│ interact with driver via control codes (2 matches) │ host-interaction/driver │
│ get Program Files directory (18 matches) │ host-interaction/file-system │
│ get common file path (575 matches) │ host-interaction/file-system │
│ create directory (2 matches) │ host-interaction/file-system/create │
│ delete file │ host-interaction/file-system/delete │
│ get file attributes (122 matches) │ host-interaction/file-system/meta │
│ set file attributes (8 matches) │ host-interaction/file-system/meta │
│ move file │ host-interaction/file-system/move │
│ find taskbar (3 matches) │ host-interaction/gui/taskbar/find │
│ get keyboard layout (12 matches) │ host-interaction/hardware/keyboard │
│ get disk size │ host-interaction/hardware/storage │
│ get hostname (4 matches) │ host-interaction/os/hostname │
│ allocate or change RWX memory (3 matches) │ host-interaction/process/inject │
│ query or enumerate registry key (3 matches) │ host-interaction/registry │
│ query or enumerate registry value (8 matches) │ host-interaction/registry │
│ delete registry key │ host-interaction/registry/delete │
│ start service │ host-interaction/service/start │
│ get session user name │ host-interaction/session │
│ persist via Run registry key │ persistence/registry/run │
│ persist via Winlogon Helper DLL registry key │ persistence/registry/winlogon-helper │
│ persist via Windows service (2 matches) │ persistence/service │
┕━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙
```
capa uses a collection of rules to identify capabilities within a program.
These rules are easy to write, even for those new to reverse engineering.
By authoring rules, you can extend the capabilities that capa recognizes.
@@ -135,31 +225,30 @@ Here's an example rule used by capa:
```yaml
rule:
meta:
name: hash data with CRC32
namespace: data-manipulation/checksum/crc32
name: create TCP socket
namespace: communication/socket/tcp
authors:
- moritz.raabe@mandiant.com
scope: function
- william.ballenthin@mandiant.com
- joakim@intezer.com
- anushka.virgaonkar@mandiant.com
scopes:
static: basic block
dynamic: call
mbc:
- Data::Checksum::CRC32 [C0032.001]
- Communication::Socket Communication::Create TCP Socket [C0001.011]
examples:
- 2D3EDC218A90F03089CC01715A9F047F:0x403CBD
- 7D28CB106CB54876B2A5C111724A07CD:0x402350 # RtlComputeCrc32
- 7EFF498DE13CC734262F87E6B3EF38AB:0x100084A6
- Practical Malware Analysis Lab 01-01.dll_:0x10001010
features:
- or:
- and:
- mnemonic: shr
- number: 6 = IPPROTO_TCP
- number: 1 = SOCK_STREAM
- number: 2 = AF_INET
- 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
- api: ws2_32.socket
- api: ws2_32.WSASocket
- api: socket
- property/read: System.Net.Sockets.TcpClient::Client
```
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.

View File

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
import itertools
import collections
from typing import Any, Tuple
from capa.rules import Scope, RuleSet
from capa.engine import FeatureSet, MatchResults
from capa.features.address import NO_ADDRESS
from capa.features.extractors.base_extractor import FeatureExtractor, StaticFeatureExtractor, DynamicFeatureExtractor
logger = logging.getLogger(__name__)
def find_file_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, function_features: FeatureSet):
file_features: FeatureSet = collections.defaultdict(set)
for feature, va in itertools.chain(extractor.extract_file_features(), extractor.extract_global_features()):
# not all file features may have virtual addresses.
# if not, then at least ensure the feature shows up in the index.
# the set of addresses will still be empty.
if va:
file_features[feature].add(va)
else:
if feature not in file_features:
file_features[feature] = set()
logger.debug("analyzed file and extracted %d features", len(file_features))
file_features.update(function_features)
_, matches = ruleset.match(Scope.FILE, file_features, NO_ADDRESS)
return matches, len(file_features)
def has_file_limitation(rules: RuleSet, capabilities: MatchResults, is_standalone=True) -> bool:
file_limitation_rules = list(filter(lambda r: r.is_file_limitation_rule(), rules.rules.values()))
for file_limitation_rule in file_limitation_rules:
if file_limitation_rule.name not in capabilities:
continue
logger.warning("-" * 80)
for line in file_limitation_rule.meta.get("description", "").split("\n"):
logger.warning(" %s", line)
logger.warning(" Identified via rule: %s", file_limitation_rule.name)
if is_standalone:
logger.warning(" ")
logger.warning(" Use -v or -vv if you really want to see the capabilities identified by capa.")
logger.warning("-" * 80)
# bail on first file limitation
return True
return False
def find_capabilities(
ruleset: RuleSet, extractor: FeatureExtractor, disable_progress=None, **kwargs
) -> Tuple[MatchResults, Any]:
from capa.capabilities.static import find_static_capabilities
from capa.capabilities.dynamic import find_dynamic_capabilities
if isinstance(extractor, StaticFeatureExtractor):
# for the time being, extractors are either static or dynamic.
# Remove this assertion once that has changed
assert not isinstance(extractor, DynamicFeatureExtractor)
return find_static_capabilities(ruleset, extractor, disable_progress=disable_progress, **kwargs)
if isinstance(extractor, DynamicFeatureExtractor):
return find_dynamic_capabilities(ruleset, extractor, disable_progress=disable_progress, **kwargs)
raise ValueError(f"unexpected extractor type: {extractor.__class__.__name__}")

View File

@@ -0,0 +1,198 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
import itertools
import collections
from typing import Any, Tuple
import tqdm
import capa.perf
import capa.features.freeze as frz
import capa.render.result_document as rdoc
from capa.rules import Scope, RuleSet
from capa.engine import FeatureSet, MatchResults
from capa.helpers import redirecting_print_to_tqdm
from capa.capabilities.common import find_file_capabilities
from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle, DynamicFeatureExtractor
logger = logging.getLogger(__name__)
def find_call_capabilities(
ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle
) -> Tuple[FeatureSet, MatchResults]:
"""
find matches for the given rules for the given call.
returns: tuple containing (features for call, match results for call)
"""
# all features found for the call.
features: FeatureSet = collections.defaultdict(set)
for feature, addr in itertools.chain(
extractor.extract_call_features(ph, th, ch), extractor.extract_global_features()
):
features[feature].add(addr)
# matches found at this thread.
_, matches = ruleset.match(Scope.CALL, features, ch.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_thread_capabilities(
ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle
) -> Tuple[FeatureSet, MatchResults, MatchResults]:
"""
find matches for the given rules within the given thread.
returns: tuple containing (features for thread, match results for thread, match results for calls)
"""
# all features found within this thread,
# includes features found within calls.
features: FeatureSet = collections.defaultdict(set)
# matches found at the call scope.
# might be found at different calls, thats ok.
call_matches: MatchResults = collections.defaultdict(list)
for ch in extractor.get_calls(ph, th):
ifeatures, imatches = find_call_capabilities(ruleset, extractor, ph, th, ch)
for feature, vas in ifeatures.items():
features[feature].update(vas)
for rule_name, res in imatches.items():
call_matches[rule_name].extend(res)
for feature, va in itertools.chain(extractor.extract_thread_features(ph, th), extractor.extract_global_features()):
features[feature].add(va)
# matches found within this thread.
_, matches = ruleset.match(Scope.THREAD, features, th.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, call_matches
def find_process_capabilities(
ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle
) -> Tuple[MatchResults, MatchResults, MatchResults, int]:
"""
find matches for the given rules within the given process.
returns: tuple containing (match results for process, match results for threads, match results for calls, number of features)
"""
# all features found within this process,
# includes features found within threads (and calls).
process_features: FeatureSet = collections.defaultdict(set)
# matches found at the basic threads.
# might be found at different threads, thats ok.
thread_matches: MatchResults = collections.defaultdict(list)
# matches found at the call scope.
# might be found at different calls, thats ok.
call_matches: MatchResults = collections.defaultdict(list)
for th in extractor.get_threads(ph):
features, tmatches, cmatches = find_thread_capabilities(ruleset, extractor, ph, th)
for feature, vas in features.items():
process_features[feature].update(vas)
for rule_name, res in tmatches.items():
thread_matches[rule_name].extend(res)
for rule_name, res in cmatches.items():
call_matches[rule_name].extend(res)
for feature, va in itertools.chain(extractor.extract_process_features(ph), extractor.extract_global_features()):
process_features[feature].add(va)
_, process_matches = ruleset.match(Scope.PROCESS, process_features, ph.address)
return process_matches, thread_matches, call_matches, len(process_features)
def find_dynamic_capabilities(
ruleset: RuleSet, extractor: DynamicFeatureExtractor, disable_progress=None
) -> Tuple[MatchResults, Any]:
all_process_matches: MatchResults = collections.defaultdict(list)
all_thread_matches: MatchResults = collections.defaultdict(list)
all_call_matches: MatchResults = collections.defaultdict(list)
feature_counts = rdoc.DynamicFeatureCounts(file=0, processes=())
assert isinstance(extractor, DynamicFeatureExtractor)
with redirecting_print_to_tqdm(disable_progress):
with tqdm.contrib.logging.logging_redirect_tqdm():
pbar = tqdm.tqdm
if disable_progress:
# do not use tqdm to avoid unnecessary side effects when caller intends
# to disable progress completely
def pbar(s, *args, **kwargs):
return s
processes = list(extractor.get_processes())
pb = pbar(processes, desc="matching", unit=" processes", leave=False)
for p in pb:
process_matches, thread_matches, call_matches, feature_count = find_process_capabilities(
ruleset, extractor, p
)
feature_counts.processes += (
rdoc.ProcessFeatureCount(address=frz.Address.from_capa(p.address), count=feature_count),
)
logger.debug("analyzed %s and extracted %d features", p.address, feature_count)
for rule_name, res in process_matches.items():
all_process_matches[rule_name].extend(res)
for rule_name, res in thread_matches.items():
all_thread_matches[rule_name].extend(res)
for rule_name, res in call_matches.items():
all_call_matches[rule_name].extend(res)
# collection of features that captures the rule matches within process and thread scopes.
# mapping from feature (matched rule) to set of addresses at which it matched.
process_and_lower_features: FeatureSet = collections.defaultdict(set)
for rule_name, results in itertools.chain(
all_process_matches.items(), all_thread_matches.items(), all_call_matches.items()
):
locations = {p[0] for p in results}
rule = ruleset[rule_name]
capa.engine.index_rule_matches(process_and_lower_features, rule, locations)
all_file_matches, feature_count = find_file_capabilities(ruleset, extractor, process_and_lower_features)
feature_counts.file = feature_count
matches = dict(
itertools.chain(
# 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_thread_matches.items(),
all_process_matches.items(),
all_call_matches.items(),
all_file_matches.items(),
)
)
meta = {
"feature_counts": feature_counts,
}
return matches, meta

233
capa/capabilities/static.py Normal file
View File

@@ -0,0 +1,233 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import time
import logging
import itertools
import collections
from typing import Any, Tuple
import tqdm.contrib.logging
import capa.perf
import capa.features.freeze as frz
import capa.render.result_document as rdoc
from capa.rules import Scope, RuleSet
from capa.engine import FeatureSet, MatchResults
from capa.helpers import redirecting_print_to_tqdm
from capa.capabilities.common import find_file_capabilities
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, StaticFeatureExtractor
logger = logging.getLogger(__name__)
def find_instruction_capabilities(
ruleset: RuleSet, extractor: StaticFeatureExtractor, 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: FeatureSet = collections.defaultdict(set)
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: StaticFeatureExtractor, 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: FeatureSet = collections.defaultdict(set)
# matches found at the instruction scope.
# might be found at different instructions, thats ok.
insn_matches: MatchResults = collections.defaultdict(list)
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: StaticFeatureExtractor, 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: FeatureSet = collections.defaultdict(set)
# matches found at the basic block scope.
# might be found at different basic blocks, thats ok.
bb_matches: MatchResults = collections.defaultdict(list)
# matches found at the instruction scope.
# might be found at different instructions, thats ok.
insn_matches: MatchResults = collections.defaultdict(list)
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)
_, function_matches = ruleset.match(Scope.FUNCTION, function_features, fh.address)
return function_matches, bb_matches, insn_matches, len(function_features)
def find_static_capabilities(
ruleset: RuleSet, extractor: StaticFeatureExtractor, disable_progress=None
) -> Tuple[MatchResults, Any]:
all_function_matches: MatchResults = collections.defaultdict(list)
all_bb_matches: MatchResults = collections.defaultdict(list)
all_insn_matches: MatchResults = collections.defaultdict(list)
feature_counts = rdoc.StaticFeatureCounts(file=0, functions=())
library_functions: Tuple[rdoc.LibraryFunction, ...] = ()
assert isinstance(extractor, StaticFeatureExtractor)
with redirecting_print_to_tqdm(disable_progress):
with tqdm.contrib.logging.logging_redirect_tqdm():
pbar = tqdm.tqdm
if capa.helpers.is_runtime_ghidra():
# Ghidrathon interpreter cannot properly handle
# the TMonitor thread that is created via a monitor_interval
# > 0
pbar.monitor_interval = 0
if disable_progress:
# do not use tqdm to avoid unnecessary side effects when caller intends
# to disable progress completely
def pbar(s, *args, **kwargs):
return s
functions = list(extractor.get_functions())
n_funcs = len(functions)
pb = pbar(functions, desc="matching", unit=" functions", postfix="skipped 0 library functions", leave=False)
for f in pb:
t0 = time.time()
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)
library_functions += (
rdoc.LibraryFunction(address=frz.Address.from_capa(f.address), name=function_name),
)
n_libs = len(library_functions)
percentage = round(100 * (n_libs / n_funcs))
if isinstance(pb, tqdm.tqdm):
pb.set_postfix_str(f"skipped {n_libs} library functions ({percentage}%)")
continue
function_matches, bb_matches, insn_matches, feature_count = find_code_capabilities(
ruleset, extractor, f
)
feature_counts.functions += (
rdoc.FunctionFeatureCount(address=frz.Address.from_capa(f.address), count=feature_count),
)
t1 = time.time()
match_count = sum(len(res) for res in function_matches.values())
match_count += sum(len(res) for res in bb_matches.values())
match_count += sum(len(res) for res in insn_matches.values())
logger.debug(
"analyzed function 0x%x and extracted %d features, %d matches in %0.02fs",
f.address,
feature_count,
match_count,
t1 - t0,
)
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, 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(), all_insn_matches.items()
):
locations = {p[0] for p in results}
rule = ruleset[rule_name]
capa.engine.index_rule_matches(function_and_lower_features, rule, locations)
all_file_matches, feature_count = find_file_capabilities(ruleset, extractor, function_and_lower_features)
feature_counts.file = feature_count
matches = dict(
itertools.chain(
# 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(),
)
)
meta = {
"feature_counts": feature_counts,
"library_functions": library_functions,
}
return matches, meta

View File

@@ -304,7 +304,7 @@ def match(rules: List["capa.rules.Rule"], features: FeatureSet, addr: Address) -
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
results: MatchResults = collections.defaultdict(list)
# copy features so that we can modify it
# without affecting the caller (keep this function pure)

View File

@@ -19,3 +19,7 @@ class UnsupportedArchError(ValueError):
class UnsupportedOSError(ValueError):
pass
class EmptyReportError(ValueError):
pass

View File

@@ -43,6 +43,79 @@ class AbsoluteVirtualAddress(int, Address):
return int.__hash__(self)
class ProcessAddress(Address):
"""an address of a process in a dynamic execution trace"""
def __init__(self, pid: int, ppid: int = 0):
assert ppid >= 0
assert pid > 0
self.ppid = ppid
self.pid = pid
def __repr__(self):
return "process(%s%s)" % (
f"ppid: {self.ppid}, " if self.ppid > 0 else "",
f"pid: {self.pid}",
)
def __hash__(self):
return hash((self.ppid, self.pid))
def __eq__(self, other):
assert isinstance(other, ProcessAddress)
return (self.ppid, self.pid) == (other.ppid, other.pid)
def __lt__(self, other):
assert isinstance(other, ProcessAddress)
return (self.ppid, self.pid) < (other.ppid, other.pid)
class ThreadAddress(Address):
"""addresses a thread in a dynamic execution trace"""
def __init__(self, process: ProcessAddress, tid: int):
assert tid >= 0
self.process = process
self.tid = tid
def __repr__(self):
return f"{self.process}, thread(tid: {self.tid})"
def __hash__(self):
return hash((self.process, self.tid))
def __eq__(self, other):
assert isinstance(other, ThreadAddress)
return (self.process, self.tid) == (other.process, other.tid)
def __lt__(self, other):
assert isinstance(other, ThreadAddress)
return (self.process, self.tid) < (other.process, other.tid)
class DynamicCallAddress(Address):
"""addesses a call in a dynamic execution trace"""
def __init__(self, thread: ThreadAddress, id: int):
assert id >= 0
self.thread = thread
self.id = id
def __repr__(self):
return f"{self.thread}, call(id: {self.id})"
def __hash__(self):
return hash((self.thread, self.id))
def __eq__(self, other):
assert isinstance(other, DynamicCallAddress)
return (self.thread, self.id) == (other.thread, other.id)
def __lt__(self, other):
assert isinstance(other, DynamicCallAddress)
return (self.thread, self.id) < (other.thread, other.id)
class RelativeVirtualAddress(int, Address):
"""a memory address relative to a base address"""

View File

@@ -0,0 +1,36 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from enum import Enum
from typing import Dict, List
from capa.helpers import assert_never
class ComType(Enum):
CLASS = "class"
INTERFACE = "interface"
COM_PREFIXES = {
ComType.CLASS: "CLSID_",
ComType.INTERFACE: "IID_",
}
def load_com_database(com_type: ComType) -> Dict[str, List[str]]:
# lazy load these python files since they are so large.
# that is, don't load them unless a COM feature is being handled.
import capa.features.com.classes
import capa.features.com.interfaces
if com_type == ComType.CLASS:
return capa.features.com.classes.COM_CLASSES
elif com_type == ComType.INTERFACE:
return capa.features.com.interfaces.COM_INTERFACES
else:
assert_never(com_type)

3696
capa/features/com/classes.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -457,6 +457,17 @@ VALID_FORMAT = (FORMAT_PE, FORMAT_ELF, FORMAT_DOTNET)
FORMAT_AUTO = "auto"
FORMAT_SC32 = "sc32"
FORMAT_SC64 = "sc64"
FORMAT_CAPE = "cape"
STATIC_FORMATS = {
FORMAT_SC32,
FORMAT_SC64,
FORMAT_PE,
FORMAT_ELF,
FORMAT_DOTNET,
}
DYNAMIC_FORMATS = {
FORMAT_CAPE,
}
FORMAT_FREEZE = "freeze"
FORMAT_RESULT = "result"
FORMAT_UNKNOWN = "unknown"

View File

@@ -7,13 +7,18 @@
# See the License for the specific language governing permissions and limitations under the License.
import abc
import hashlib
import dataclasses
from typing import Any, Dict, Tuple, Union, Iterator
from dataclasses import dataclass
# TODO(williballenthin): use typing.TypeAlias directly when Python 3.9 is deprecated
# https://github.com/mandiant/capa/issues/1699
from typing_extensions import TypeAlias
import capa.features.address
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.address import Address, ThreadAddress, ProcessAddress, DynamicCallAddress, AbsoluteVirtualAddress
# feature extractors may reference functions, BBs, insns by opaque handle values.
# you can use the `.address` property to get and render the address of the feature.
@@ -22,6 +27,24 @@ from capa.features.address import Address, AbsoluteVirtualAddress
# the feature extractor from which they were created.
@dataclass
class SampleHashes:
md5: str
sha1: str
sha256: str
@classmethod
def from_bytes(cls, buf: bytes) -> "SampleHashes":
md5 = hashlib.md5()
sha1 = hashlib.sha1()
sha256 = hashlib.sha256()
md5.update(buf)
sha1.update(buf)
sha256.update(buf)
return cls(md5=md5.hexdigest(), sha1=sha1.hexdigest(), sha256=sha256.hexdigest())
@dataclass
class FunctionHandle:
"""reference to a function recognized by a feature extractor.
@@ -63,16 +86,18 @@ class InsnHandle:
inner: Any
class FeatureExtractor:
class StaticFeatureExtractor:
"""
FeatureExtractor defines the interface for fetching features from a sample.
StaticFeatureExtractor defines the interface for fetching features from a
sample without running it; extractors that rely on the execution trace of
a sample must implement the other sibling class, DynamicFeatureExtracor.
There may be multiple backends that support fetching features for capa.
For example, we use vivisect by default, but also want to support saving
and restoring features from a JSON file.
When we restore the features, we'd like to use exactly the same matching logic
to find matching rules.
Therefore, we can define a FeatureExtractor that provides features from the
Therefore, we can define a StaticFeatureExtractor that provides features from the
serialized JSON file and do matching without a binary analysis pass.
Also, this provides a way to hook in an IDA backend.
@@ -81,13 +106,14 @@ class FeatureExtractor:
__metaclass__ = abc.ABCMeta
def __init__(self):
def __init__(self, hashes: SampleHashes):
#
# note: a subclass should define ctor parameters for its own use.
# for example, the Vivisect feature extract might require the vw and/or path.
# this base class doesn't know what to do with that info, though.
#
super().__init__()
self._sample_hashes = hashes
@abc.abstractmethod
def get_base_address(self) -> Union[AbsoluteVirtualAddress, capa.features.address._NoAddress]:
@@ -100,6 +126,12 @@ class FeatureExtractor:
"""
raise NotImplementedError()
def get_sample_hashes(self) -> SampleHashes:
"""
fetch the hashes for the sample contained within the extractor.
"""
return self._sample_hashes
@abc.abstractmethod
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
"""
@@ -262,3 +294,177 @@ class FeatureExtractor:
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@dataclass
class ProcessHandle:
"""
reference to a process extracted by the sandbox.
Attributes:
address: process's address (pid)
inner: sandbox-specific data
"""
address: ProcessAddress
inner: Any
@dataclass
class ThreadHandle:
"""
reference to a thread extracted by the sandbox.
Attributes:
address: thread's address (tid)
inner: sandbox-specific data
"""
address: ThreadAddress
inner: Any
@dataclass
class CallHandle:
"""
reference to an api call extracted by the sandbox.
Attributes:
address: call's address, such as event index or id
inner: sandbox-specific data
"""
address: DynamicCallAddress
inner: Any
class DynamicFeatureExtractor:
"""
DynamicFeatureExtractor defines the interface for fetching features from a
sandbox' analysis of a sample; extractors that rely on statically analyzing
a sample must implement the sibling extractor, StaticFeatureExtractor.
Features are grouped mainly into threads that alongside their meta-features are also grouped into
processes (that also have their own features). Other scopes (such as function and file) may also apply
for a specific sandbox.
This class is not instantiated directly; it is the base class for other implementations.
"""
__metaclass__ = abc.ABCMeta
def __init__(self, hashes: SampleHashes):
#
# note: a subclass should define ctor parameters for its own use.
# for example, the Vivisect feature extract might require the vw and/or path.
# this base class doesn't know what to do with that info, though.
#
super().__init__()
self._sample_hashes = hashes
def get_sample_hashes(self) -> SampleHashes:
"""
fetch the hashes for the sample contained within the extractor.
"""
return self._sample_hashes
@abc.abstractmethod
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
"""
extract features found at every scope ("global").
example::
extractor = CapeFeatureExtractor.from_report(json.loads(buf))
for feature, addr in extractor.get_global_features():
print(addr, feature)
yields:
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@abc.abstractmethod
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
"""
extract file-scope features.
example::
extractor = CapeFeatureExtractor.from_report(json.loads(buf))
for feature, addr in extractor.get_file_features():
print(addr, feature)
yields:
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@abc.abstractmethod
def get_processes(self) -> Iterator[ProcessHandle]:
"""
Enumerate processes in the trace.
"""
raise NotImplementedError()
@abc.abstractmethod
def extract_process_features(self, ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
"""
Yields all the features of a process. These include:
- file features of the process' image
"""
raise NotImplementedError()
@abc.abstractmethod
def get_process_name(self, ph: ProcessHandle) -> str:
"""
Returns the human-readable name for the given process,
such as the filename.
"""
raise NotImplementedError()
@abc.abstractmethod
def get_threads(self, ph: ProcessHandle) -> Iterator[ThreadHandle]:
"""
Enumerate threads in the given process.
"""
raise NotImplementedError()
@abc.abstractmethod
def extract_thread_features(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[Tuple[Feature, Address]]:
"""
Yields all the features of a thread. These include:
- sequenced api traces
"""
raise NotImplementedError()
@abc.abstractmethod
def get_calls(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[CallHandle]:
"""
Enumerate calls in the given thread
"""
raise NotImplementedError()
@abc.abstractmethod
def extract_call_features(
self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
Yields all features of a call. These include:
- api name
- bytes/strings/numbers extracted from arguments
"""
raise NotImplementedError()
@abc.abstractmethod
def get_call_name(self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> str:
"""
Returns the human-readable name for the given call,
such as as rendered API log entry, like:
Foo(1, "two", b"\x00\x11") -> -1
"""
raise NotImplementedError()
FeatureExtractor: TypeAlias = Union[StaticFeatureExtractor, DynamicFeatureExtractor]

View File

@@ -17,12 +17,18 @@ import capa.features.extractors.binja.function
import capa.features.extractors.binja.basicblock
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
from capa.features.extractors.base_extractor import (
BBHandle,
InsnHandle,
SampleHashes,
FunctionHandle,
StaticFeatureExtractor,
)
class BinjaFeatureExtractor(FeatureExtractor):
class BinjaFeatureExtractor(StaticFeatureExtractor):
def __init__(self, bv: binja.BinaryView):
super().__init__()
super().__init__(hashes=SampleHashes.from_bytes(bv.file.raw.read(0, len(bv.file.raw))))
self.bv = bv
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.binja.file.extract_file_format(self.bv))

View File

@@ -115,13 +115,13 @@ def extract_file_import_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address
for sym in bv.get_symbols_of_type(SymbolType.ImportAddressSymbol):
lib_name = str(sym.namespace)
addr = AbsoluteVirtualAddress(sym.address)
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym.short_name):
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym.short_name, include_dll=True):
yield Import(name), addr
ordinal = sym.ordinal
if ordinal != 0 and (lib_name != ""):
ordinal_name = f"#{ordinal}"
for name in capa.features.extractors.helpers.generate_symbols(lib_name, ordinal_name):
for name in capa.features.extractors.helpers.generate_symbols(lib_name, ordinal_name, include_dll=True):
yield Import(name), addr

View File

@@ -0,0 +1,62 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
from typing import Tuple, Iterator
from capa.helpers import assert_never
from capa.features.insn import API, Number
from capa.features.common import String, Feature
from capa.features.address import Address
from capa.features.extractors.cape.models import Call
from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle
logger = logging.getLogger(__name__)
def extract_call_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[Tuple[Feature, Address]]:
"""
this method extracts the given call's features (such as API name and arguments),
and returns them as API, Number, and String features.
args:
ph: process handle (for defining the extraction scope)
th: thread handle (for defining the extraction scope)
ch: call handle (for defining the extraction scope)
yields:
Feature, address; where Feature is either: API, Number, or String.
"""
call: Call = ch.inner
# list similar to disassembly: arguments right-to-left, call
for arg in reversed(call.arguments):
value = arg.value
if isinstance(value, list) and len(value) == 0:
# unsure why CAPE captures arguments as empty lists?
continue
elif isinstance(value, str):
yield String(value), ch.address
elif isinstance(value, int):
yield Number(value), ch.address
else:
assert_never(value)
yield API(call.api), ch.address
def extract_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[Tuple[Feature, Address]]:
for handler in CALL_HANDLERS:
for feature, addr in handler(ph, th, ch):
yield feature, addr
CALL_HANDLERS = (extract_call_features,)

View File

@@ -0,0 +1,145 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
from typing import Dict, Tuple, Union, Iterator
import capa.features.extractors.cape.call
import capa.features.extractors.cape.file
import capa.features.extractors.cape.thread
import capa.features.extractors.cape.global_
import capa.features.extractors.cape.process
from capa.exceptions import EmptyReportError, UnsupportedFormatError
from capa.features.common import Feature, Characteristic
from capa.features.address import NO_ADDRESS, Address, AbsoluteVirtualAddress, _NoAddress
from capa.features.extractors.cape.models import Call, Static, Process, CapeReport
from capa.features.extractors.base_extractor import (
CallHandle,
SampleHashes,
ThreadHandle,
ProcessHandle,
DynamicFeatureExtractor,
)
logger = logging.getLogger(__name__)
TESTED_VERSIONS = {"2.2-CAPE", "2.4-CAPE"}
class CapeExtractor(DynamicFeatureExtractor):
def __init__(self, report: CapeReport):
super().__init__(
hashes=SampleHashes(
md5=report.target.file.md5.lower(),
sha1=report.target.file.sha1.lower(),
sha256=report.target.file.sha256.lower(),
)
)
self.report: CapeReport = report
# pre-compute these because we'll yield them at *every* scope.
self.global_features = list(capa.features.extractors.cape.global_.extract_features(self.report))
def get_base_address(self) -> Union[AbsoluteVirtualAddress, _NoAddress, None]:
# value according to the PE header, the actual trace may use a different imagebase
assert self.report.static is not None and self.report.static.pe is not None
return AbsoluteVirtualAddress(self.report.static.pe.imagebase)
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
yield from self.global_features
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.cape.file.extract_features(self.report)
def get_processes(self) -> Iterator[ProcessHandle]:
yield from capa.features.extractors.cape.file.get_processes(self.report)
def extract_process_features(self, ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.cape.process.extract_features(ph)
def get_process_name(self, ph) -> str:
process: Process = ph.inner
return process.process_name
def get_threads(self, ph: ProcessHandle) -> Iterator[ThreadHandle]:
yield from capa.features.extractors.cape.process.get_threads(ph)
def extract_thread_features(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[Tuple[Feature, Address]]:
if False:
# force this routine to be a generator,
# but we don't actually have any elements to generate.
yield Characteristic("never"), NO_ADDRESS
return
def get_calls(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[CallHandle]:
yield from capa.features.extractors.cape.thread.get_calls(ph, th)
def extract_call_features(
self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle
) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.cape.call.extract_features(ph, th, ch)
def get_call_name(self, ph, th, ch) -> str:
call: Call = ch.inner
parts = []
parts.append(call.api)
parts.append("(")
for argument in call.arguments:
parts.append(argument.name)
parts.append("=")
if argument.pretty_value:
parts.append(argument.pretty_value)
else:
if isinstance(argument.value, int):
parts.append(hex(argument.value))
elif isinstance(argument.value, str):
parts.append('"')
parts.append(argument.value)
parts.append('"')
elif isinstance(argument.value, list):
pass
else:
capa.helpers.assert_never(argument.value)
parts.append(", ")
if call.arguments:
# remove the trailing comma
parts.pop()
parts.append(")")
parts.append(" -> ")
if call.pretty_return:
parts.append(call.pretty_return)
else:
parts.append(hex(call.return_))
return "".join(parts)
@classmethod
def from_report(cls, report: Dict) -> "CapeExtractor":
cr = CapeReport.model_validate(report)
if cr.info.version not in TESTED_VERSIONS:
logger.warning("CAPE version '%s' not tested/supported yet", cr.info.version)
# observed in 2.4-CAPE reports from capesandbox.com
if cr.static is None and cr.target.file.pe is not None:
cr.static = Static()
cr.static.pe = cr.target.file.pe
if cr.static is None:
raise UnsupportedFormatError("CAPE report missing static analysis")
if cr.static.pe is None:
raise UnsupportedFormatError("CAPE report missing PE analysis")
if len(cr.behavior.processes) == 0:
raise EmptyReportError("CAPE did not capture any processes")
return cls(cr)

View File

@@ -0,0 +1,132 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
from typing import Tuple, Iterator
from capa.features.file import Export, Import, Section
from capa.features.common import String, Feature
from capa.features.address import NO_ADDRESS, Address, ProcessAddress, AbsoluteVirtualAddress
from capa.features.extractors.helpers import generate_symbols
from capa.features.extractors.cape.models import CapeReport
from capa.features.extractors.base_extractor import ProcessHandle
logger = logging.getLogger(__name__)
def get_processes(report: CapeReport) -> Iterator[ProcessHandle]:
"""
get all the created processes for a sample
"""
seen_processes = {}
for process in report.behavior.processes:
addr = ProcessAddress(pid=process.process_id, ppid=process.parent_id)
yield ProcessHandle(address=addr, inner=process)
# check for pid and ppid reuse
if addr not in seen_processes:
seen_processes[addr] = [process]
else:
logger.warning(
"pid and ppid reuse detected between process %s and process%s: %s",
process,
"es" if len(seen_processes[addr]) > 1 else "",
seen_processes[addr],
)
seen_processes[addr].append(process)
def extract_import_names(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
"""
extract imported function names
"""
assert report.static is not None and report.static.pe is not None
imports = report.static.pe.imports
if isinstance(imports, dict):
imports = list(imports.values())
assert isinstance(imports, list)
for library in imports:
for function in library.imports:
if not function.name:
continue
for name in generate_symbols(library.dll, function.name, include_dll=True):
yield Import(name), AbsoluteVirtualAddress(function.address)
def extract_export_names(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
assert report.static is not None and report.static.pe is not None
for function in report.static.pe.exports:
yield Export(function.name), AbsoluteVirtualAddress(function.address)
def extract_section_names(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
assert report.static is not None and report.static.pe is not None
for section in report.static.pe.sections:
yield Section(section.name), AbsoluteVirtualAddress(section.virtual_address)
def extract_file_strings(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
if report.strings is not None:
for string in report.strings:
yield String(string), NO_ADDRESS
def extract_used_regkeys(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
for regkey in report.behavior.summary.keys:
yield String(regkey), NO_ADDRESS
def extract_used_files(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
for file in report.behavior.summary.files:
yield String(file), NO_ADDRESS
def extract_used_mutexes(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
for mutex in report.behavior.summary.mutexes:
yield String(mutex), NO_ADDRESS
def extract_used_commands(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
for cmd in report.behavior.summary.executed_commands:
yield String(cmd), NO_ADDRESS
def extract_used_apis(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
for symbol in report.behavior.summary.resolved_apis:
yield String(symbol), NO_ADDRESS
def extract_used_services(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
for svc in report.behavior.summary.created_services:
yield String(svc), NO_ADDRESS
for svc in report.behavior.summary.started_services:
yield String(svc), NO_ADDRESS
def extract_features(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
for handler in FILE_HANDLERS:
for feature, addr in handler(report):
yield feature, addr
FILE_HANDLERS = (
extract_import_names,
extract_export_names,
extract_section_names,
extract_file_strings,
extract_used_regkeys,
extract_used_files,
extract_used_mutexes,
extract_used_commands,
extract_used_apis,
extract_used_services,
)

View File

@@ -0,0 +1,93 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
from typing import Tuple, Iterator
from capa.features.common import (
OS,
OS_ANY,
OS_LINUX,
ARCH_I386,
FORMAT_PE,
ARCH_AMD64,
FORMAT_ELF,
OS_WINDOWS,
Arch,
Format,
Feature,
)
from capa.features.address import NO_ADDRESS, Address
from capa.features.extractors.cape.models import CapeReport
logger = logging.getLogger(__name__)
def extract_arch(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
if "Intel 80386" in report.target.file.type:
yield Arch(ARCH_I386), NO_ADDRESS
elif "x86-64" in report.target.file.type:
yield Arch(ARCH_AMD64), NO_ADDRESS
else:
logger.warning("unrecognized Architecture: %s", report.target.file.type)
raise ValueError(
f"unrecognized Architecture from the CAPE report; output of file command: {report.target.file.type}"
)
def extract_format(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
if "PE" in report.target.file.type:
yield Format(FORMAT_PE), NO_ADDRESS
elif "ELF" in report.target.file.type:
yield Format(FORMAT_ELF), NO_ADDRESS
else:
logger.warning("unknown file format, file command output: %s", report.target.file.type)
raise ValueError(
"unrecognized file format from the CAPE report; output of file command: {report.target.file.type}"
)
def extract_os(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
# this variable contains the output of the file command
file_output = report.target.file.type
if "windows" in file_output.lower():
yield OS(OS_WINDOWS), NO_ADDRESS
elif "elf" in file_output.lower():
# operating systems recognized by the file command: https://github.com/file/file/blob/master/src/readelf.c#L609
if "Linux" in file_output:
yield OS(OS_LINUX), NO_ADDRESS
elif "Hurd" in file_output:
yield OS("hurd"), NO_ADDRESS
elif "Solaris" in file_output:
yield OS("solaris"), NO_ADDRESS
elif "kFreeBSD" in file_output:
yield OS("freebsd"), NO_ADDRESS
elif "kNetBSD" in file_output:
yield OS("netbsd"), NO_ADDRESS
else:
# if the operating system information is missing from the cape report, it's likely a bug
logger.warning("unrecognized OS: %s", file_output)
raise ValueError("unrecognized OS from the CAPE report; output of file command: {file_output}")
else:
# the sample is shellcode
logger.debug("unsupported file format, file command output: %s", file_output)
yield OS(OS_ANY), NO_ADDRESS
def extract_features(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
for global_handler in GLOBAL_HANDLER:
for feature, addr in global_handler(report):
yield feature, addr
GLOBAL_HANDLER = (
extract_format,
extract_os,
extract_arch,
)

View File

@@ -0,0 +1,29 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from typing import Any, Dict, List
from capa.features.extractors.base_extractor import ProcessHandle
def find_process(processes: List[Dict[str, Any]], ph: ProcessHandle) -> Dict[str, Any]:
"""
find a specific process identified by a process handler.
args:
processes: a list of processes extracted by CAPE
ph: handle of the sought process
return:
a CAPE-defined dictionary for the sought process' information
"""
for process in processes:
if ph.address.ppid == process["parent_id"] and ph.address.pid == process["process_id"]:
return process
return {}

View File

@@ -0,0 +1,446 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import binascii
from typing import Any, Dict, List, Union, Literal, Optional
from pydantic import Field, BaseModel, ConfigDict
from typing_extensions import Annotated, TypeAlias
from pydantic.functional_validators import BeforeValidator
def validate_hex_int(value):
if isinstance(value, str):
return int(value, 16) if value.startswith("0x") else int(value, 10)
else:
return value
def validate_hex_bytes(value):
return binascii.unhexlify(value) if isinstance(value, str) else value
HexInt = Annotated[int, BeforeValidator(validate_hex_int)]
HexBytes = Annotated[bytes, BeforeValidator(validate_hex_bytes)]
# a model that *cannot* have extra fields
# if they do, pydantic raises an exception.
# use this for models we rely upon and cannot change.
#
# for things that may be extended and we don't care,
# use FlexibleModel.
class ExactModel(BaseModel):
model_config = ConfigDict(extra="forbid")
# a model that can have extra fields that we ignore.
# use this if we don't want to raise an exception for extra
# data fields that we didn't expect.
class FlexibleModel(BaseModel):
pass
# use this type to indicate that we won't model this data.
# because its not relevant to our use in capa.
#
# while its nice to have full coverage of the data shape,
# it can easily change and break our parsing.
# so we really only want to describe what we'll use.
Skip: TypeAlias = Optional[Any]
# mark fields that we haven't seen yet and need to model.
# pydantic should raise an error when encountering data
# in a field with this type.
# then we can update the model with the discovered shape.
TODO: TypeAlias = None
ListTODO: TypeAlias = List[None]
DictTODO: TypeAlias = ExactModel
EmptyDict: TypeAlias = BaseModel
EmptyList: TypeAlias = List[Any]
class Info(FlexibleModel):
version: str
class ImportedSymbol(ExactModel):
address: HexInt
name: Optional[str] = None
class ImportedDll(ExactModel):
dll: str
imports: List[ImportedSymbol]
class DirectoryEntry(ExactModel):
name: str
virtual_address: HexInt
size: HexInt
class Section(ExactModel):
name: str
raw_address: HexInt
virtual_address: HexInt
virtual_size: HexInt
size_of_data: HexInt
characteristics: str
characteristics_raw: HexInt
entropy: float
class Resource(ExactModel):
name: str
language: Optional[str] = None
sublanguage: str
filetype: Optional[str]
offset: HexInt
size: HexInt
entropy: float
class DigitalSigner(FlexibleModel):
md5_fingerprint: str
not_after: str
not_before: str
serial_number: str
sha1_fingerprint: str
sha256_fingerprint: str
issuer_commonName: Optional[str] = None
issuer_countryName: Optional[str] = None
issuer_localityName: Optional[str] = None
issuer_organizationName: Optional[str] = None
issuer_stateOrProvinceName: Optional[str] = None
subject_commonName: Optional[str] = None
subject_countryName: Optional[str] = None
subject_localityName: Optional[str] = None
subject_organizationName: Optional[str] = None
subject_stateOrProvinceName: Optional[str] = None
extensions_authorityInfoAccess_caIssuers: Optional[str] = None
extensions_authorityKeyIdentifier: Optional[str] = None
extensions_cRLDistributionPoints_0: Optional[str] = None
extensions_certificatePolicies_0: Optional[str] = None
extensions_subjectAltName_0: Optional[str] = None
extensions_subjectKeyIdentifier: Optional[str] = None
class AuxSigner(ExactModel):
name: str
issued_to: str = Field(alias="Issued to")
issued_by: str = Field(alias="Issued by")
expires: str = Field(alias="Expires")
sha1_hash: str = Field(alias="SHA1 hash")
class Signer(ExactModel):
aux_sha1: Optional[str] = None
aux_timestamp: Optional[str] = None
aux_valid: Optional[bool] = None
aux_error: Optional[bool] = None
aux_error_desc: Optional[str] = None
aux_signers: Optional[List[AuxSigner]] = None
class Overlay(ExactModel):
offset: HexInt
size: HexInt
class KV(ExactModel):
name: str
value: str
class ExportedSymbol(ExactModel):
address: HexInt
name: str
ordinal: int
class PE(ExactModel):
peid_signatures: TODO
imagebase: HexInt
entrypoint: HexInt
reported_checksum: HexInt
actual_checksum: HexInt
osversion: str
pdbpath: Optional[str] = None
timestamp: str
# List[ImportedDll], or Dict[basename(dll), ImportedDll]
imports: Union[List[ImportedDll], Dict[str, ImportedDll]]
imported_dll_count: Optional[int] = None
imphash: str
exported_dll_name: Optional[str] = None
exports: List[ExportedSymbol]
dirents: List[DirectoryEntry]
sections: List[Section]
ep_bytes: Optional[HexBytes] = None
overlay: Optional[Overlay] = None
resources: List[Resource]
versioninfo: List[KV]
# base64 encoded data
icon: Optional[str] = None
# MD5-like hash
icon_hash: Optional[str] = None
# MD5-like hash
icon_fuzzy: Optional[str] = None
# short hex string
icon_dhash: Optional[str] = None
digital_signers: List[DigitalSigner]
guest_signers: Signer
# TODO(mr-tz): target.file.dotnet, target.file.extracted_files, target.file.extracted_files_tool,
# target.file.extracted_files_time
# https://github.com/mandiant/capa/issues/1814
class File(FlexibleModel):
type: str
cape_type_code: Optional[int] = None
cape_type: Optional[str] = None
pid: Optional[Union[int, Literal[""]]] = None
name: Union[List[str], str]
path: str
guest_paths: Union[List[str], str, None]
timestamp: Optional[str] = None
#
# hashes
#
crc32: str
md5: str
sha1: str
sha256: str
sha512: str
sha3_384: str
ssdeep: str
# unsure why this would ever be "False"
tlsh: Optional[Union[str, bool]] = None
rh_hash: Optional[str] = None
#
# other metadata, static analysis
#
size: int
pe: Optional[PE] = None
ep_bytes: Optional[HexBytes] = None
entrypoint: Optional[int] = None
data: Optional[str] = None
strings: Optional[List[str]] = None
#
# detections (skip)
#
yara: Skip = None
cape_yara: Skip = None
clamav: Skip = None
virustotal: Skip = None
class ProcessFile(File):
#
# like a File, but also has dynamic analysis results
#
pid: Optional[int] = None
process_path: Optional[str] = None
process_name: Optional[str] = None
module_path: Optional[str] = None
virtual_address: Optional[HexInt] = None
target_pid: Optional[Union[int, str]] = None
target_path: Optional[str] = None
target_process: Optional[str] = None
class Argument(ExactModel):
name: str
# unsure why empty list is provided here
value: Union[HexInt, int, str, EmptyList]
pretty_value: Optional[str] = None
class Call(ExactModel):
timestamp: str
thread_id: int
category: str
api: str
arguments: List[Argument]
status: bool
return_: HexInt = Field(alias="return")
pretty_return: Optional[str] = None
repeated: int
# virtual addresses
caller: HexInt
parentcaller: HexInt
# index into calls array
id: int
class Process(ExactModel):
process_id: int
process_name: str
parent_id: int
module_path: str
first_seen: str
calls: List[Call]
threads: List[int]
environ: Dict[str, str]
class ProcessTree(ExactModel):
name: str
pid: int
parent_id: int
module_path: str
threads: List[int]
environ: Dict[str, str]
children: List["ProcessTree"]
class Summary(ExactModel):
files: List[str]
read_files: List[str]
write_files: List[str]
delete_files: List[str]
keys: List[str]
read_keys: List[str]
write_keys: List[str]
delete_keys: List[str]
executed_commands: List[str]
resolved_apis: List[str]
mutexes: List[str]
created_services: List[str]
started_services: List[str]
class EncryptedBuffer(ExactModel):
process_name: str
pid: int
api_call: str
buffer: str
buffer_size: Optional[int] = None
crypt_key: Optional[Union[HexInt, str]] = None
class Behavior(ExactModel):
summary: Summary
# list of processes, of threads, of calls
processes: List[Process]
# tree of processes
processtree: List[ProcessTree]
anomaly: List[str]
encryptedbuffers: List[EncryptedBuffer]
# these are small objects that describe atomic events,
# like file move, registery access.
# we'll detect the same with our API call analyis.
enhanced: Skip = None
class Target(ExactModel):
category: str
file: File
pe: Optional[PE] = None
class Static(ExactModel):
pe: Optional[PE] = None
flare_capa: Skip = None
class Cape(ExactModel):
payloads: List[ProcessFile]
configs: Skip = None
# flexible because there may be more sorts of analysis
# but we only care about the ones described here.
class CapeReport(FlexibleModel):
# the input file, I think
target: Target
# info about the processing job, like machine and distributed metadata.
info: Info
#
# static analysis results
#
static: Optional[Static] = None
strings: Optional[List[str]] = None
#
# dynamic analysis results
#
# post-processed results: process tree, anomalies, etc
behavior: Behavior
# post-processed results: payloads and extracted configs
CAPE: Optional[Cape] = None
dropped: Optional[List[File]] = None
procdump: Optional[List[ProcessFile]] = None
procmemory: ListTODO
# =========================================================================
# information we won't use in capa
#
#
# NBIs and HBIs
# these are super interesting, but they don't enable use to detect behaviors.
# they take a lot of code to model and details to maintain.
#
# if we come up with a future use for this, go ahead and re-enable!
#
network: Skip = None
suricata: Skip = None
curtain: Skip = None
sysmon: Skip = None
url_analysis: Skip = None
# screenshot hash values
deduplicated_shots: Skip = None
# k-v pairs describing the time it took to run each stage.
statistics: Skip = None
# k-v pairs of ATT&CK ID to signature name or similar.
ttps: Skip = None
# debug log messages
debug: Skip = None
# various signature matches
# we could potentially extend capa to use this info one day,
# though it would be quite sandbox-specific,
# and more detection-oriented than capability detection.
signatures: Skip = None
malfamily_tag: Optional[str] = None
malscore: float
detections: Skip = None
detections2pid: Optional[Dict[int, List[str]]] = None
# AV detections for the sample.
virustotal: Skip = None
@classmethod
def from_buf(cls, buf: bytes) -> "CapeReport":
return cls.model_validate_json(buf)

View File

@@ -0,0 +1,48 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
from typing import List, Tuple, Iterator
from capa.features.common import String, Feature
from capa.features.address import Address, ThreadAddress
from capa.features.extractors.cape.models import Process
from capa.features.extractors.base_extractor import ThreadHandle, ProcessHandle
logger = logging.getLogger(__name__)
def get_threads(ph: ProcessHandle) -> Iterator[ThreadHandle]:
"""
get the threads associated with a given process
"""
process: Process = ph.inner
threads: List[int] = process.threads
for thread in threads:
address: ThreadAddress = ThreadAddress(process=ph.address, tid=thread)
yield ThreadHandle(address=address, inner={})
def extract_environ_strings(ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract strings from a process' provided environment variables.
"""
process: Process = ph.inner
for value in (value for value in process.environ.values() if value):
yield String(value), ph.address
def extract_features(ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
for handler in PROCESS_HANDLERS:
for feature, addr in handler(ph):
yield feature, addr
PROCESS_HANDLERS = (extract_environ_strings,)

View File

@@ -0,0 +1,32 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
from typing import Iterator
from capa.features.address import DynamicCallAddress
from capa.features.extractors.helpers import generate_symbols
from capa.features.extractors.cape.models import Process
from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle
logger = logging.getLogger(__name__)
def get_calls(ph: ProcessHandle, th: ThreadHandle) -> Iterator[CallHandle]:
process: Process = ph.inner
tid = th.address.tid
for call_index, call in enumerate(process.calls):
if call.thread_id != tid:
continue
for symbol in generate_symbols("", call.api):
call.api = symbol
addr = DynamicCallAddress(thread=th.address, id=call_index)
yield CallHandle(address=addr, inner=call)

View File

@@ -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 io
import re
import logging
import binascii
import contextlib
@@ -41,6 +42,7 @@ logger = logging.getLogger(__name__)
MATCH_PE = b"MZ"
MATCH_ELF = b"\x7fELF"
MATCH_RESULT = b'{"meta":'
MATCH_JSON_OBJECT = b'{"'
def extract_file_strings(buf, **kwargs) -> Iterator[Tuple[String, Address]]:
@@ -63,6 +65,11 @@ def extract_format(buf) -> Iterator[Tuple[Feature, Address]]:
yield Format(FORMAT_FREEZE), NO_ADDRESS
elif buf.startswith(MATCH_RESULT):
yield Format(FORMAT_RESULT), NO_ADDRESS
elif re.sub(rb"\s", b"", buf[:20]).startswith(MATCH_JSON_OBJECT):
# potential start of JSON object data without whitespace
# we don't know what it is exactly, but may support it (e.g. a dynamic CAPE sandbox report)
# skip verdict here and let subsequent code analyze this further
return
else:
# we likely end up here:
# 1. handling a file format (e.g. macho)

View File

@@ -22,7 +22,13 @@ import capa.features.extractors.dnfile.function
from capa.features.common import Feature
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
from capa.features.extractors.base_extractor import (
BBHandle,
InsnHandle,
SampleHashes,
FunctionHandle,
StaticFeatureExtractor,
)
from capa.features.extractors.dnfile.helpers import (
get_dotnet_types,
get_dotnet_fields,
@@ -68,10 +74,10 @@ class DnFileFeatureExtractorCache:
return self.types.get(token)
class DnfileFeatureExtractor(FeatureExtractor):
class DnfileFeatureExtractor(StaticFeatureExtractor):
def __init__(self, path: Path):
super().__init__()
self.pe: dnfile.dnPE = dnfile.dnPE(str(path))
super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes()))
# pre-compute .NET token lookup tables; each .NET method has access to this cache for feature extraction
# most relevant at instruction scope

View File

@@ -131,10 +131,14 @@ def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnType]:
# remove get_/set_ from MemberRef name
member_ref_name = member_ref_name[4:]
typerefnamespace, typerefname = resolve_nested_typeref_name(
member_ref.Class.row_index, member_ref.Class.row, pe
)
yield DnType(
token,
member_ref.Class.row.TypeName,
namespace=member_ref.Class.row.TypeNamespace,
typerefname,
namespace=typerefnamespace,
member=member_ref_name,
access=access,
)
@@ -188,6 +192,8 @@ def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]:
TypeNamespace (index into String heap)
MethodList (index into MethodDef table; it marks the first of a contiguous run of Methods owned by this Type)
"""
nested_class_table = get_dotnet_nested_class_table_index(pe)
accessor_map: Dict[int, str] = {}
for methoddef, methoddef_access in get_dotnet_methoddef_property_accessors(pe):
accessor_map[methoddef] = methoddef_access
@@ -211,7 +217,9 @@ def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]:
# remove get_/set_
method_name = method_name[4:]
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=method_name, access=access)
typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe)
yield DnType(token, typedefname, namespace=typedefnamespace, member=method_name, access=access)
def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]:
@@ -225,6 +233,8 @@ def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]:
TypeNamespace (index into String heap)
FieldList (index into Field table; it marks the first of a contiguous run of Fields owned by this Type)
"""
nested_class_table = get_dotnet_nested_class_table_index(pe)
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
@@ -235,8 +245,11 @@ def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]:
if field.row is None:
logger.debug("TypeDef[0x%X] FieldList[0x%X] row is None", rid, idx)
continue
typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe)
token: int = calculate_dotnet_token_value(field.table.number, field.row_index)
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=field.row.Name)
yield DnType(token, typedefname, namespace=typedefnamespace, member=field.row.Name)
def get_dotnet_managed_method_bodies(pe: dnfile.dnPE) -> Iterator[Tuple[int, CilMethodBody]]:
@@ -300,19 +313,119 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod]
yield DnUnmanagedMethod(token, module, method)
def get_dotnet_table_row(pe: dnfile.dnPE, table_index: int, row_index: int) -> Optional[dnfile.base.MDTableRow]:
assert pe.net is not None
assert pe.net.mdtables is not None
if row_index - 1 <= 0:
return None
try:
table = pe.net.mdtables.tables.get(table_index, [])
return table[row_index - 1]
except IndexError:
return None
def resolve_nested_typedef_name(
nested_class_table: dict, index: int, typedef: dnfile.mdtable.TypeDefRow, pe: dnfile.dnPE
) -> Tuple[str, Tuple[str, ...]]:
"""Resolves all nested TypeDef class names. Returns the namespace as a str and the nested TypeRef name as a tuple"""
if index in nested_class_table:
typedef_name = []
name = typedef.TypeName
# Append the current typedef name
typedef_name.append(name)
while nested_class_table[index] in nested_class_table:
# Iterate through the typedef table to resolve the nested name
table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, nested_class_table[index])
if table_row is None:
return typedef.TypeNamespace, tuple(typedef_name[::-1])
name = table_row.TypeName
typedef_name.append(name)
index = nested_class_table[index]
# Document the root enclosing details
table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, nested_class_table[index])
if table_row is None:
return typedef.TypeNamespace, tuple(typedef_name[::-1])
enclosing_name = table_row.TypeName
typedef_name.append(enclosing_name)
return table_row.TypeNamespace, tuple(typedef_name[::-1])
else:
return typedef.TypeNamespace, (typedef.TypeName,)
def resolve_nested_typeref_name(
index: int, typeref: dnfile.mdtable.TypeRefRow, pe: dnfile.dnPE
) -> Tuple[str, Tuple[str, ...]]:
"""Resolves all nested TypeRef class names. Returns the namespace as a str and the nested TypeRef name as a tuple"""
# If the ResolutionScope decodes to a typeRef type then it is nested
if isinstance(typeref.ResolutionScope.table, dnfile.mdtable.TypeRef):
typeref_name = []
name = typeref.TypeName
# Not appending the current typeref name to avoid potential duplicate
# Validate index
table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeRef.number, index)
if table_row is None:
return typeref.TypeNamespace, (typeref.TypeName,)
while isinstance(table_row.ResolutionScope.table, dnfile.mdtable.TypeRef):
# Iterate through the typeref table to resolve the nested name
typeref_name.append(name)
name = table_row.TypeName
table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeRef.number, table_row.ResolutionScope.row_index)
if table_row is None:
return typeref.TypeNamespace, tuple(typeref_name[::-1])
# Document the root enclosing details
typeref_name.append(table_row.TypeName)
return table_row.TypeNamespace, tuple(typeref_name[::-1])
else:
return typeref.TypeNamespace, (typeref.TypeName,)
def get_dotnet_nested_class_table_index(pe: dnfile.dnPE) -> Dict[int, int]:
"""Build index for EnclosingClass based off the NestedClass row index in the nestedclass table"""
nested_class_table = {}
# Used to find nested classes in typedef
for _, nestedclass in iter_dotnet_table(pe, dnfile.mdtable.NestedClass.number):
assert isinstance(nestedclass, dnfile.mdtable.NestedClassRow)
nested_class_table[nestedclass.NestedClass.row_index] = nestedclass.EnclosingClass.row_index
return nested_class_table
def get_dotnet_types(pe: dnfile.dnPE) -> Iterator[DnType]:
"""get .NET types from TypeDef and TypeRef tables"""
nested_class_table = get_dotnet_nested_class_table_index(pe)
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe)
typedef_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
yield DnType(typedef_token, typedef.TypeName, namespace=typedef.TypeNamespace)
yield DnType(typedef_token, typedefname, namespace=typedefnamespace)
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
typerefnamespace, typerefname = resolve_nested_typeref_name(typeref.ResolutionScope.row_index, typeref, pe)
typeref_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
yield DnType(typeref_token, typeref.TypeName, namespace=typeref.TypeNamespace)
yield DnType(typeref_token, typerefname, namespace=typerefnamespace)
def calculate_dotnet_token_value(table: int, rid: int) -> int:

View File

@@ -6,15 +6,17 @@
# 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 Optional
from typing import Tuple, Optional
class DnType:
def __init__(self, token: int, class_: str, namespace: str = "", member: str = "", access: Optional[str] = None):
def __init__(
self, token: int, class_: Tuple[str, ...], namespace: str = "", member: str = "", access: Optional[str] = None
):
self.token: int = token
self.access: Optional[str] = access
self.namespace: str = namespace
self.class_: str = class_
self.class_: Tuple[str, ...] = class_
if member == ".ctor":
member = "ctor"
@@ -42,9 +44,13 @@ class DnType:
return str(self)
@staticmethod
def format_name(class_: str, namespace: str = "", member: str = ""):
def format_name(class_: Tuple[str, ...], namespace: str = "", member: str = ""):
if len(class_) > 1:
class_str = "/".join(class_) # Concat items in tuple, separated by a "/"
else:
class_str = "".join(class_) # Convert tuple to str
# like File::OpenRead
name: str = f"{class_}::{member}" if member else class_
name: str = f"{class_str}::{member}" if member else class_str
if namespace:
# like System.IO.File::OpenRead
name = f"{namespace}.{name}"

View File

@@ -1,158 +0,0 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
from typing import Tuple, Iterator
from pathlib import Path
import dnfile
import pefile
from capa.features.common import (
OS,
OS_ANY,
ARCH_ANY,
ARCH_I386,
FORMAT_PE,
ARCH_AMD64,
FORMAT_DOTNET,
Arch,
Format,
Feature,
)
from capa.features.address import NO_ADDRESS, Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import FeatureExtractor
logger = logging.getLogger(__name__)
def extract_file_format(**kwargs) -> Iterator[Tuple[Feature, Address]]:
yield Format(FORMAT_PE), NO_ADDRESS
yield Format(FORMAT_DOTNET), NO_ADDRESS
def extract_file_os(**kwargs) -> Iterator[Tuple[Feature, Address]]:
yield OS(OS_ANY), NO_ADDRESS
def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Feature, Address]]:
# to distinguish in more detail, see https://stackoverflow.com/a/23614024/10548020
# .NET 4.5 added option: any CPU, 32-bit preferred
assert pe.net is not None
assert pe.net.Flags is not None
if pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE:
yield Arch(ARCH_I386), NO_ADDRESS
elif not pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS:
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: Path):
super().__init__()
self.path: Path = path
self.pe: dnfile.dnPE = dnfile.dnPE(str(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
assert self.pe.net is not None
assert self.pe.net.struct is not None
return self.pe.net.struct.EntryPointTokenOrRva
def extract_global_features(self):
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:
assert self.pe is not None
assert self.pe.net is not None
assert self.pe.net.Flags is not None
return not bool(self.pe.net.Flags.CLR_ILONLY)
def get_runtime_version(self) -> Tuple[int, int]:
assert self.pe is not None
assert self.pe.net is not None
assert self.pe.net.struct is not None
return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion
def get_meta_version_string(self) -> str:
assert self.pe.net is not None
assert self.pe.net.metadata is not None
assert self.pe.net.metadata.struct is not None
assert self.pe.net.metadata.struct.Version is not None
vbuf = self.pe.net.metadata.struct.Version
assert isinstance(vbuf, bytes)
return vbuf.rstrip(b"\x00").decode("utf-8")
def get_functions(self):
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
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

@@ -31,15 +31,18 @@ from capa.features.common import (
Characteristic,
)
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress
from capa.features.extractors.base_extractor import FeatureExtractor
from capa.features.extractors.dnfile.types import DnType
from capa.features.extractors.base_extractor import SampleHashes, StaticFeatureExtractor
from capa.features.extractors.dnfile.helpers import (
DnType,
iter_dotnet_table,
is_dotnet_mixed_mode,
get_dotnet_managed_imports,
get_dotnet_managed_methods,
resolve_nested_typedef_name,
resolve_nested_typeref_name,
calculate_dotnet_token_value,
get_dotnet_unmanaged_imports,
get_dotnet_nested_class_table_index,
)
logger = logging.getLogger(__name__)
@@ -57,7 +60,7 @@ def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Impor
for imp in get_dotnet_unmanaged_imports(pe):
# like kernel32.CreateFileA
for name in capa.features.extractors.helpers.generate_symbols(imp.module, imp.method):
for name in capa.features.extractors.helpers.generate_symbols(imp.module, imp.method, include_dll=True):
yield Import(name), DNTokenAddress(imp.token)
@@ -92,19 +95,25 @@ def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple
def extract_file_class_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Class, Address]]:
"""emit class features from TypeRef and TypeDef tables"""
nested_class_table = get_dotnet_nested_class_table_index(pe)
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
# emit internal .NET classes
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe)
token = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
yield Class(DnType.format_name(typedef.TypeName, namespace=typedef.TypeNamespace)), DNTokenAddress(token)
yield Class(DnType.format_name(typedefname, namespace=typedefnamespace)), DNTokenAddress(token)
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
# emit external .NET classes
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
typerefnamespace, typerefname = resolve_nested_typeref_name(typeref.ResolutionScope.row_index, typeref, pe)
token = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
yield Class(DnType.format_name(typeref.TypeName, namespace=typeref.TypeNamespace)), DNTokenAddress(token)
yield Class(DnType.format_name(typerefname, namespace=typerefnamespace)), DNTokenAddress(token)
def extract_file_os(**kwargs) -> Iterator[Tuple[OS, Address]]:
@@ -165,9 +174,9 @@ GLOBAL_HANDLERS = (
)
class DotnetFileFeatureExtractor(FeatureExtractor):
class DotnetFileFeatureExtractor(StaticFeatureExtractor):
def __init__(self, path: Path):
super().__init__()
super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes()))
self.path: Path = path
self.pe: dnfile.dnPE = dnfile.dnPE(str(path))

View File

@@ -108,6 +108,9 @@ class Shdr:
buf,
)
def get_name(self, elf: "ELF") -> str:
return elf.shstrtab.buf[self.name :].partition(b"\x00")[0].decode("ascii")
class ELF:
def __init__(self, f: BinaryIO):
@@ -120,6 +123,7 @@ class ELF:
self.e_phnum: int
self.e_shentsize: int
self.e_shnum: int
self.e_shstrndx: int
self.phbuf: bytes
self.shbuf: bytes
@@ -151,11 +155,15 @@ class ELF:
if self.bitness == 32:
e_phoff, e_shoff = struct.unpack_from(self.endian + "II", self.file_header, 0x1C)
self.e_phentsize, self.e_phnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x2A)
self.e_shentsize, self.e_shnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x2E)
self.e_shentsize, self.e_shnum, self.e_shstrndx = struct.unpack_from(
self.endian + "HHH", self.file_header, 0x2E
)
elif self.bitness == 64:
e_phoff, e_shoff = struct.unpack_from(self.endian + "QQ", self.file_header, 0x20)
self.e_phentsize, self.e_phnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x36)
self.e_shentsize, self.e_shnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x3A)
self.e_shentsize, self.e_shnum, self.e_shstrndx = struct.unpack_from(
self.endian + "HHH", self.file_header, 0x3A
)
else:
raise NotImplementedError()
@@ -365,6 +373,10 @@ class ELF:
except ValueError:
continue
@property
def shstrtab(self) -> Shdr:
return self.parse_section_header(self.e_shstrndx)
@property
def linker(self):
PT_INTERP = 0x3
@@ -816,6 +828,48 @@ def guess_os_from_sh_notes(elf: ELF) -> Optional[OS]:
return None
def guess_os_from_ident_directive(elf: ELF) -> Optional[OS]:
# GCC inserts the GNU version via an .ident directive
# that gets stored in a section named ".comment".
# look at the version and recognize common OSes.
#
# assume the GCC version matches the target OS version,
# which I guess could be wrong during cross-compilation?
# therefore, don't rely on this if possible.
#
# https://stackoverflow.com/q/6263425
# https://gcc.gnu.org/onlinedocs/cpp/Other-Directives.html
SHT_PROGBITS = 0x1
for shdr in elf.section_headers:
if shdr.type != SHT_PROGBITS:
continue
if shdr.get_name(elf) != ".comment":
continue
try:
comment = shdr.buf.decode("utf-8")
except ValueError:
continue
if "GCC:" not in comment:
continue
logger.debug(".ident: %s", comment)
# these values come from our testfiles, like:
# rg -a "GCC: " tests/data/
if "Debian" in comment:
return OS.LINUX
elif "Ubuntu" in comment:
return OS.LINUX
elif "Red Hat" in comment:
return OS.LINUX
return None
def guess_os_from_linker(elf: ELF) -> Optional[OS]:
# search for recognizable dynamic linkers (interpreters)
# for example, on linux, we see file paths like: /lib64/ld-linux-x86-64.so.2
@@ -851,8 +905,10 @@ def guess_os_from_abi_versions_needed(elf: ELF) -> Optional[OS]:
return OS.HURD
else:
# we don't have any good guesses based on versions needed
pass
# in practice, Hurd isn't a common/viable OS,
# so this is almost certain to be Linux,
# so lets just make that guess.
return OS.LINUX
return None
@@ -927,6 +983,13 @@ def detect_elf_os(f) -> str:
logger.warning("Error guessing OS from section header notes: %s", e)
sh_notes_guess = None
try:
ident_guess = guess_os_from_ident_directive(elf)
logger.debug("guess: .ident: %s", ident_guess)
except Exception as e:
logger.warning("Error guessing OS from .ident directive: %s", e)
ident_guess = None
try:
linker_guess = guess_os_from_linker(elf)
logger.debug("guess: linker: %s", linker_guess)
@@ -960,6 +1023,10 @@ def detect_elf_os(f) -> str:
if osabi_guess:
ret = osabi_guess
elif ident_guess:
# we don't trust this too much due to non-cross-compilation assumptions
ret = ident_guess
elif ph_notes_guess:
ret = ph_notes_guess

View File

@@ -17,7 +17,7 @@ import capa.features.extractors.common
from capa.features.file import Export, 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.base_extractor import FeatureExtractor
from capa.features.extractors.base_extractor import SampleHashes, StaticFeatureExtractor
logger = logging.getLogger(__name__)
@@ -154,9 +154,9 @@ GLOBAL_HANDLERS = (
)
class ElfFeatureExtractor(FeatureExtractor):
class ElfFeatureExtractor(StaticFeatureExtractor):
def __init__(self, path: Path):
super().__init__()
super().__init__(SampleHashes.from_bytes(path.read_bytes()))
self.path: Path = path
self.elf = ELFFile(io.BytesIO(path.read_bytes()))

View File

@@ -14,14 +14,32 @@ import capa.features.extractors.ghidra.function
import capa.features.extractors.ghidra.basicblock
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
from capa.features.extractors.base_extractor import (
BBHandle,
InsnHandle,
SampleHashes,
FunctionHandle,
StaticFeatureExtractor,
)
class GhidraFeatureExtractor(FeatureExtractor):
class GhidraFeatureExtractor(StaticFeatureExtractor):
def __init__(self):
super().__init__()
import capa.features.extractors.ghidra.helpers as ghidra_helpers
super().__init__(
SampleHashes(
md5=capa.ghidra.helpers.get_file_md5(),
# ghidra doesn't expose this hash.
# https://ghidra.re/ghidra_docs/api/ghidra/program/model/listing/Program.html
#
# the hashes are stored in the database, not computed on the fly,
# so its probably not trivial to add SHA1.
sha1="",
sha256=capa.ghidra.helpers.get_file_sha256(),
)
)
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.ghidra.file.extract_file_format())
self.global_features.extend(capa.features.extractors.ghidra.global_.extract_os())

View File

@@ -34,7 +34,7 @@ def find_embedded_pe(block_bytez: bytes, mz_xor: List[Tuple[bytes, bytes, int]])
for match in re.finditer(re.escape(mzx), block_bytez):
todo.append((match.start(), mzx, pex, i))
seg_max = len(block_bytez) # type: ignore [name-defined] # noqa: F821
seg_max = len(block_bytez) # noqa: F821
while len(todo):
off, mzx, pex, i = todo.pop()
@@ -112,7 +112,7 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
if "Ordinal_" in fstr[1]:
fstr[1] = f"#{fstr[1].split('_')[1]}"
for name in capa.features.extractors.helpers.generate_symbols(fstr[0][:-4], fstr[1]):
for name in capa.features.extractors.helpers.generate_symbols(fstr[0][:-4], fstr[1], include_dll=True):
yield Import(name), AbsoluteVirtualAddress(addr)
@@ -127,8 +127,10 @@ def extract_file_strings() -> Iterator[Tuple[Feature, Address]]:
"""extract ASCII and UTF-16 LE strings"""
for block in currentProgram().getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821
if block.isInitialized():
p_bytes = capa.features.extractors.ghidra.helpers.get_block_bytes(block)
if not block.isInitialized():
continue
p_bytes = capa.features.extractors.ghidra.helpers.get_block_bytes(block)
for s in capa.features.extractors.strings.extract_ascii_strings(p_bytes):
offset = block.getStart().getOffset() + s.offset

View File

@@ -275,3 +275,27 @@ def dereference_ptr(insn: ghidra.program.database.code.InstructionDB):
return addr
else:
return to_deref
def find_data_references_from_insn(insn, max_depth: int = 10):
"""yield data references from given instruction"""
for reference in insn.getReferencesFrom():
if not reference.getReferenceType().isData():
# only care about data references
continue
to_addr = reference.getToAddress()
for _ in range(max_depth - 1):
data = getDataAt(to_addr) # type: ignore [name-defined] # noqa: F821
if data and data.isPointer():
ptr_value = data.getValue()
if ptr_value is None:
break
to_addr = ptr_value
else:
break
yield to_addr

View File

@@ -23,6 +23,9 @@ from capa.features.extractors.base_extractor import BBHandle, InsnHandle, Functi
SECURITY_COOKIE_BYTES_DELTA = 0x40
OPERAND_TYPE_DYNAMIC_ADDRESS = OperandType.DYNAMIC | OperandType.ADDRESS
def get_imports(ctx: Dict[str, Any]) -> Dict[int, Any]:
"""Populate the import cache for this context"""
if "imports_cache" not in ctx:
@@ -82,7 +85,7 @@ def check_for_api_call(
if not capa.features.extractors.ghidra.helpers.check_addr_for_api(addr_ref, fakes, imports, externs):
return
ref = addr_ref.getOffset()
elif ref_type == OperandType.DYNAMIC | OperandType.ADDRESS or ref_type == OperandType.DYNAMIC:
elif ref_type == OPERAND_TYPE_DYNAMIC_ADDRESS or ref_type == OperandType.DYNAMIC:
return # cannot resolve dynamics statically
else:
# pure address does not need to get dereferenced/ handled
@@ -195,46 +198,39 @@ def extract_insn_offset_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandl
if insn.getMnemonicString().startswith("LEA"):
return
# ignore any stack references
if not capa.features.extractors.ghidra.helpers.is_stack_referenced(insn):
# Ghidra stores operands in 2D arrays if they contain offsets
for i in range(insn.getNumOperands()):
if insn.getOperandType(i) == OperandType.DYNAMIC: # e.g. [esi + 4]
# manual extraction, since the default api calls only work on the 1st dimension of the array
op_objs = insn.getOpObjects(i)
if isinstance(op_objs[-1], ghidra.program.model.scalar.Scalar):
op_off = op_objs[-1].getValue()
yield Offset(op_off), ih.address
yield OperandOffset(i, op_off), ih.address
else:
yield Offset(0), ih.address
yield OperandOffset(i, 0), ih.address
if capa.features.extractors.ghidra.helpers.is_stack_referenced(insn):
# ignore stack references
return
# Ghidra stores operands in 2D arrays if they contain offsets
for i in range(insn.getNumOperands()):
if insn.getOperandType(i) == OperandType.DYNAMIC: # e.g. [esi + 4]
# manual extraction, since the default api calls only work on the 1st dimension of the array
op_objs = insn.getOpObjects(i)
if not op_objs:
continue
if isinstance(op_objs[-1], ghidra.program.model.scalar.Scalar):
op_off = op_objs[-1].getValue()
else:
op_off = 0
yield Offset(op_off), ih.address
yield OperandOffset(i, op_off), ih.address
def extract_insn_bytes_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse referenced byte sequences
example:
push offset iid_004118d4_IShellLinkA ; riid
"""
insn: ghidra.program.database.code.InstructionDB = ih.inner
if capa.features.extractors.ghidra.helpers.is_call_or_jmp(insn):
return
ref = insn.getAddress() # init to insn addr
for i in range(insn.getNumOperands()):
if OperandType.isAddress(insn.getOperandType(i)):
ref = insn.getAddress(i) # pulls pointer if there is one
if ref != insn.getAddress(): # bail out if there's no pointer
ghidra_dat = getDataAt(ref) # type: ignore [name-defined] # noqa: F821
if (
ghidra_dat and not ghidra_dat.hasStringValue() and not ghidra_dat.isPointer()
): # avoid if the data itself is a pointer
extracted_bytes = capa.features.extractors.ghidra.helpers.get_bytes(ref, MAX_BYTES_FEATURE_SIZE)
for addr in capa.features.extractors.ghidra.helpers.find_data_references_from_insn(ih.inner):
data = getDataAt(addr) # type: ignore [name-defined] # noqa: F821
if data and not data.hasStringValue():
extracted_bytes = capa.features.extractors.ghidra.helpers.get_bytes(addr, MAX_BYTES_FEATURE_SIZE)
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
# don't extract byte features for obvious strings
yield Bytes(extracted_bytes), ih.address
@@ -245,24 +241,10 @@ def extract_insn_string_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandl
example:
push offset aAcr ; "ACR > "
"""
insn: ghidra.program.database.code.InstructionDB = ih.inner
dyn_addr = OperandType.DYNAMIC | OperandType.ADDRESS
ref = insn.getAddress()
for i in range(insn.getNumOperands()):
if OperandType.isScalarAsAddress(insn.getOperandType(i)):
ref = insn.getAddress(i)
# strings are also referenced dynamically via pointers & arrays, so we need to deref them
if insn.getOperandType(i) == dyn_addr:
ref = insn.getAddress(i)
dat = getDataAt(ref) # type: ignore [name-defined] # noqa: F821
if dat and dat.isPointer():
ref = dat.getValue()
if ref != insn.getAddress():
ghidra_dat = getDataAt(ref) # type: ignore [name-defined] # noqa: F821
if ghidra_dat and ghidra_dat.hasStringValue():
yield String(ghidra_dat.getValue()), ih.address
for addr in capa.features.extractors.ghidra.helpers.find_data_references_from_insn(ih.inner):
data = getDataAt(addr) # type: ignore [name-defined] # noqa: F821
if data and data.hasStringValue():
yield String(data.getValue()), ih.address
def extract_insn_mnemonic_features(
@@ -359,7 +341,7 @@ def extract_insn_cross_section_cflow(
ref = capa.features.extractors.ghidra.helpers.dereference_ptr(insn)
if capa.features.extractors.ghidra.helpers.check_addr_for_api(ref, fakes, imports, externs):
return
elif ref_type == OperandType.DYNAMIC | OperandType.ADDRESS or ref_type == OperandType.DYNAMIC:
elif ref_type == OPERAND_TYPE_DYNAMIC_ADDRESS or ref_type == OperandType.DYNAMIC:
return # cannot resolve dynamics statically
else:
# pure address does not need to get dereferenced/ handled

View File

@@ -41,38 +41,50 @@ def is_ordinal(symbol: str) -> bool:
return False
def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
def generate_symbols(dll: str, symbol: str, include_dll=False) -> Iterator[str]:
"""
for a given dll and symbol name, generate variants.
we over-generate features to make matching easier.
these include:
- kernel32.CreateFileA
- kernel32.CreateFile
- CreateFileA
- CreateFile
- ws2_32.#1
note that since capa v7 only `import` features and APIs called via ordinal include DLL names:
- kernel32.CreateFileA
- kernel32.CreateFile
- ws2_32.#1
for `api` features dll names are good for documentation but not used during matching
"""
# normalize dll name
dll = dll.lower()
# kernel32.CreateFileA
yield f"{dll}.{symbol}"
# trim extensions observed in dynamic traces
dll = dll[0:-4] if dll.endswith(".dll") else dll
dll = dll[0:-4] if dll.endswith(".drv") else dll
if include_dll or is_ordinal(symbol):
# ws2_32.#1
# kernel32.CreateFileA
yield f"{dll}.{symbol}"
if not is_ordinal(symbol):
# CreateFileA
yield symbol
if is_aw_function(symbol):
# kernel32.CreateFile
yield f"{dll}.{symbol[:-1]}"
if is_aw_function(symbol):
if include_dll:
# kernel32.CreateFile
yield f"{dll}.{symbol[:-1]}"
if not is_ordinal(symbol):
# CreateFile
yield symbol[:-1]
def reformat_forwarded_export_name(forwarded_name: str) -> str:
"""
a forwarded export has a DLL name/path an symbol name.
a forwarded export has a DLL name/path and symbol name.
we want the former to be lowercase, and the latter to be verbatim.
"""

View File

@@ -8,6 +8,7 @@
from typing import List, Tuple, Iterator
import idaapi
import ida_nalt
import capa.ida.helpers
import capa.features.extractors.elf
@@ -18,12 +19,22 @@ import capa.features.extractors.ida.function
import capa.features.extractors.ida.basicblock
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
from capa.features.extractors.base_extractor import (
BBHandle,
InsnHandle,
SampleHashes,
FunctionHandle,
StaticFeatureExtractor,
)
class IdaFeatureExtractor(FeatureExtractor):
class IdaFeatureExtractor(StaticFeatureExtractor):
def __init__(self):
super().__init__()
super().__init__(
hashes=SampleHashes(
md5=ida_nalt.retrieve_input_file_md5(), sha1="(unknown)", sha256=ida_nalt.retrieve_input_file_sha256()
)
)
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.ida.file.extract_file_format())
self.global_features.extend(capa.features.extractors.ida.global_.extract_os())

View File

@@ -110,7 +110,7 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
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]):
for name in capa.features.extractors.helpers.generate_symbols(info[0], info[1], include_dll=True):
yield Import(name), addr
dll = info[0]
symbol = f"#{info[2]}"
@@ -123,7 +123,7 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
else:
continue
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol, include_dll=True):
yield Import(name), addr
for ea, info in capa.features.extractors.ida.helpers.get_file_externs().items():

View File

@@ -5,12 +5,24 @@
# 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 Dict, List, Tuple
from typing import Dict, List, Tuple, Union
from dataclasses import dataclass
from typing_extensions import TypeAlias
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
from capa.features.address import NO_ADDRESS, Address, ThreadAddress, ProcessAddress, DynamicCallAddress
from capa.features.extractors.base_extractor import (
BBHandle,
CallHandle,
InsnHandle,
SampleHashes,
ThreadHandle,
ProcessHandle,
FunctionHandle,
StaticFeatureExtractor,
DynamicFeatureExtractor,
)
@dataclass
@@ -31,7 +43,7 @@ class FunctionFeatures:
@dataclass
class NullFeatureExtractor(FeatureExtractor):
class NullStaticFeatureExtractor(StaticFeatureExtractor):
"""
An extractor that extracts some user-provided features.
@@ -39,6 +51,7 @@ class NullFeatureExtractor(FeatureExtractor):
"""
base_address: Address
sample_hashes: SampleHashes
global_features: List[Feature]
file_features: List[Tuple[Address, Feature]]
functions: Dict[Address, FunctionFeatures]
@@ -46,6 +59,9 @@ class NullFeatureExtractor(FeatureExtractor):
def get_base_address(self):
return self.base_address
def get_sample_hashes(self) -> SampleHashes:
return self.sample_hashes
def extract_global_features(self):
for feature in self.global_features:
yield feature, NO_ADDRESS
@@ -77,3 +93,78 @@ class NullFeatureExtractor(FeatureExtractor):
def extract_insn_features(self, f, bb, insn):
for address, feature in self.functions[f.address].basic_blocks[bb.address].instructions[insn.address].features:
yield feature, address
@dataclass
class CallFeatures:
name: str
features: List[Tuple[Address, Feature]]
@dataclass
class ThreadFeatures:
features: List[Tuple[Address, Feature]]
calls: Dict[Address, CallFeatures]
@dataclass
class ProcessFeatures:
features: List[Tuple[Address, Feature]]
threads: Dict[Address, ThreadFeatures]
name: str
@dataclass
class NullDynamicFeatureExtractor(DynamicFeatureExtractor):
base_address: Address
sample_hashes: SampleHashes
global_features: List[Feature]
file_features: List[Tuple[Address, Feature]]
processes: Dict[Address, ProcessFeatures]
def extract_global_features(self):
for feature in self.global_features:
yield feature, NO_ADDRESS
def get_sample_hashes(self) -> SampleHashes:
return self.sample_hashes
def extract_file_features(self):
for address, feature in self.file_features:
yield feature, address
def get_processes(self):
for address in sorted(self.processes.keys()):
assert isinstance(address, ProcessAddress)
yield ProcessHandle(address=address, inner={})
def extract_process_features(self, ph):
for addr, feature in self.processes[ph.address].features:
yield feature, addr
def get_process_name(self, ph) -> str:
return self.processes[ph.address].name
def get_threads(self, ph):
for address in sorted(self.processes[ph.address].threads.keys()):
assert isinstance(address, ThreadAddress)
yield ThreadHandle(address=address, inner={})
def extract_thread_features(self, ph, th):
for addr, feature in self.processes[ph.address].threads[th.address].features:
yield feature, addr
def get_calls(self, ph, th):
for address in sorted(self.processes[ph.address].threads[th.address].calls.keys()):
assert isinstance(address, DynamicCallAddress)
yield CallHandle(address=address, inner={})
def extract_call_features(self, ph, th, ch):
for address, feature in self.processes[ph.address].threads[th.address].calls[ch.address].features:
yield feature, address
def get_call_name(self, ph, th, ch) -> str:
return self.processes[ph.address].threads[th.address].calls[ch.address].name
NullFeatureExtractor: TypeAlias = Union[NullStaticFeatureExtractor, NullDynamicFeatureExtractor]

View File

@@ -19,7 +19,7 @@ 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
from capa.features.extractors.base_extractor import SampleHashes, StaticFeatureExtractor
logger = logging.getLogger(__name__)
@@ -84,7 +84,7 @@ def extract_file_import_names(pe, **kwargs):
except UnicodeDecodeError:
continue
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
for name in capa.features.extractors.helpers.generate_symbols(modname, impname, include_dll=True):
yield Import(name), AbsoluteVirtualAddress(imp.address)
@@ -185,9 +185,9 @@ GLOBAL_HANDLERS = (
)
class PefileFeatureExtractor(FeatureExtractor):
class PefileFeatureExtractor(StaticFeatureExtractor):
def __init__(self, path: Path):
super().__init__()
super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes()))
self.path: Path = path
self.pe = pefile.PE(str(path))

View File

@@ -140,7 +140,7 @@ def is_printable_ascii(chars: bytes) -> bool:
def is_printable_utf16le(chars: bytes) -> bool:
if all(c == b"\x00" for c in chars[1::2]):
if all(c == 0x0 for c in chars[1::2]):
return is_printable_ascii(chars[::2])
return False

View File

@@ -20,17 +20,23 @@ import capa.features.extractors.viv.function
import capa.features.extractors.viv.basicblock
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
from capa.features.extractors.base_extractor import (
BBHandle,
InsnHandle,
SampleHashes,
FunctionHandle,
StaticFeatureExtractor,
)
logger = logging.getLogger(__name__)
class VivisectFeatureExtractor(FeatureExtractor):
class VivisectFeatureExtractor(StaticFeatureExtractor):
def __init__(self, vw, path: Path, os):
super().__init__()
self.vw = vw
self.path = path
self.buf = path.read_bytes()
super().__init__(hashes=SampleHashes.from_bytes(self.buf))
# pre-compute these because we'll yield them at *every* scope.
self.global_features: List[Tuple[Feature, Address]] = []

View File

@@ -73,7 +73,7 @@ def extract_file_import_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]
impname = "#" + impname[len("ord") :]
addr = AbsoluteVirtualAddress(va)
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
for name in capa.features.extractors.helpers.generate_symbols(modname, impname, include_dll=True):
yield Import(name), addr

View File

@@ -9,13 +9,18 @@ Unless required by applicable law or agreed to in writing, software distributed
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
"""
import json
import zlib
import logging
from enum import Enum
from typing import List, Tuple, Union
from typing import List, Tuple, Union, Literal
from pydantic import Field, BaseModel, ConfigDict
# TODO(williballenthin): use typing.TypeAlias directly in Python 3.10+
# https://github.com/mandiant/capa/issues/1699
from typing_extensions import TypeAlias
import capa.helpers
import capa.version
import capa.features.file
@@ -23,12 +28,20 @@ import capa.features.insn
import capa.features.common
import capa.features.address
import capa.features.basicblock
import capa.features.extractors.base_extractor
import capa.features.extractors.null as null
from capa.helpers import assert_never
from capa.features.freeze.features import Feature, feature_from_capa
from capa.features.extractors.base_extractor import (
SampleHashes,
FeatureExtractor,
StaticFeatureExtractor,
DynamicFeatureExtractor,
)
logger = logging.getLogger(__name__)
CURRENT_VERSION = 3
class HashableModel(BaseModel):
model_config = ConfigDict(frozen=True)
@@ -40,12 +53,15 @@ class AddressType(str, Enum):
FILE = "file"
DN_TOKEN = "dn token"
DN_TOKEN_OFFSET = "dn token offset"
PROCESS = "process"
THREAD = "thread"
CALL = "call"
NO_ADDRESS = "no address"
class Address(HashableModel):
type: AddressType
value: Union[int, Tuple[int, int], None] = None # None default value to support deserialization of NO_ADDRESS
value: Union[int, Tuple[int, ...], None] = None # None default value to support deserialization of NO_ADDRESS
@classmethod
def from_capa(cls, a: capa.features.address.Address) -> "Address":
@@ -64,6 +80,15 @@ class Address(HashableModel):
elif isinstance(a, capa.features.address.DNTokenOffsetAddress):
return cls(type=AddressType.DN_TOKEN_OFFSET, value=(a.token, a.offset))
elif isinstance(a, capa.features.address.ProcessAddress):
return cls(type=AddressType.PROCESS, value=(a.ppid, a.pid))
elif isinstance(a, capa.features.address.ThreadAddress):
return cls(type=AddressType.THREAD, value=(a.process.ppid, a.process.pid, a.tid))
elif isinstance(a, capa.features.address.DynamicCallAddress):
return cls(type=AddressType.CALL, value=(a.thread.process.ppid, a.thread.process.pid, a.thread.tid, a.id))
elif a == capa.features.address.NO_ADDRESS or isinstance(a, capa.features.address._NoAddress):
return cls(type=AddressType.NO_ADDRESS, value=None)
@@ -100,6 +125,33 @@ class Address(HashableModel):
assert isinstance(offset, int)
return capa.features.address.DNTokenOffsetAddress(token, offset)
elif self.type is AddressType.PROCESS:
assert isinstance(self.value, tuple)
ppid, pid = self.value
assert isinstance(ppid, int)
assert isinstance(pid, int)
return capa.features.address.ProcessAddress(ppid=ppid, pid=pid)
elif self.type is AddressType.THREAD:
assert isinstance(self.value, tuple)
ppid, pid, tid = self.value
assert isinstance(ppid, int)
assert isinstance(pid, int)
assert isinstance(tid, int)
return capa.features.address.ThreadAddress(
process=capa.features.address.ProcessAddress(ppid=ppid, pid=pid), tid=tid
)
elif self.type is AddressType.CALL:
assert isinstance(self.value, tuple)
ppid, pid, tid, id_ = self.value
return capa.features.address.DynamicCallAddress(
thread=capa.features.address.ThreadAddress(
process=capa.features.address.ProcessAddress(ppid=ppid, pid=pid), tid=tid
),
id=id_,
)
elif self.type is AddressType.NO_ADDRESS:
return capa.features.address.NO_ADDRESS
@@ -130,6 +182,48 @@ class FileFeature(HashableModel):
feature: Feature
class ProcessFeature(HashableModel):
"""
args:
process: the address of the process to which this feature belongs.
address: the address at which this feature is found.
process != address because, e.g., the feature may be found *within* the scope (process).
"""
process: Address
address: Address
feature: Feature
class ThreadFeature(HashableModel):
"""
args:
thread: the address of the thread to which this feature belongs.
address: the address at which this feature is found.
thread != address because, e.g., the feature may be found *within* the scope (thread).
"""
thread: Address
address: Address
feature: Feature
class CallFeature(HashableModel):
"""
args:
call: the address of the call to which this feature belongs.
address: the address at which this feature is found.
call != address for consistency with Process and Thread.
"""
call: Address
address: Address
feature: Feature
class FunctionFeature(HashableModel):
"""
args:
@@ -167,8 +261,7 @@ class InstructionFeature(HashableModel):
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 because, for consistency with Function and BasicBlock.
"""
instruction: Address
@@ -194,13 +287,42 @@ class FunctionFeatures(BaseModel):
model_config = ConfigDict(populate_by_name=True)
class Features(BaseModel):
class CallFeatures(BaseModel):
address: Address
name: str
features: Tuple[CallFeature, ...]
class ThreadFeatures(BaseModel):
address: Address
features: Tuple[ThreadFeature, ...]
calls: Tuple[CallFeatures, ...]
class ProcessFeatures(BaseModel):
address: Address
name: str
features: Tuple[ProcessFeature, ...]
threads: Tuple[ThreadFeatures, ...]
class StaticFeatures(BaseModel):
global_: Tuple[GlobalFeature, ...] = Field(alias="global")
file: Tuple[FileFeature, ...]
functions: Tuple[FunctionFeatures, ...]
model_config = ConfigDict(populate_by_name=True)
class DynamicFeatures(BaseModel):
global_: Tuple[GlobalFeature, ...] = Field(alias="global")
file: Tuple[FileFeature, ...]
processes: Tuple[ProcessFeatures, ...]
model_config = ConfigDict(populate_by_name=True)
Features: TypeAlias = Union[StaticFeatures, DynamicFeatures]
class Extractor(BaseModel):
name: str
version: str = capa.version.__version__
@@ -208,18 +330,19 @@ class Extractor(BaseModel):
class Freeze(BaseModel):
version: int = 2
version: int = CURRENT_VERSION
base_address: Address = Field(alias="base address")
sample_hashes: SampleHashes
flavor: Literal["static", "dynamic"]
extractor: Extractor
features: Features
model_config = ConfigDict(populate_by_name=True)
def dumps(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -> str:
def dumps_static(extractor: StaticFeatureExtractor) -> str:
"""
serialize the given extractor to a string
"""
global_features: List[GlobalFeature] = []
for feature, _ in extractor.extract_global_features():
global_features.append(
@@ -298,7 +421,7 @@ def dumps(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -
# Mypy is unable to recognise `basic_blocks` as a argument due to alias
)
features = Features(
features = StaticFeatures(
global_=global_features,
file=tuple(file_features),
functions=tuple(function_features),
@@ -306,8 +429,10 @@ def dumps(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -
# Mypy is unable to recognise `global_` as a argument due to alias
freeze = Freeze(
version=2,
version=CURRENT_VERSION,
base_address=Address.from_capa(extractor.get_base_address()),
sample_hashes=extractor.get_sample_hashes(),
flavor="static",
extractor=Extractor(name=extractor.__class__.__name__),
features=features,
) # type: ignore
@@ -316,16 +441,127 @@ def dumps(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -
return freeze.model_dump_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
def dumps_dynamic(extractor: DynamicFeatureExtractor) -> 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),
)
)
process_features: List[ProcessFeatures] = []
for p in extractor.get_processes():
paddr = Address.from_capa(p.address)
pname = extractor.get_process_name(p)
pfeatures = [
ProcessFeature(
process=paddr,
address=Address.from_capa(addr),
feature=feature_from_capa(feature),
)
for feature, addr in extractor.extract_process_features(p)
]
threads = []
for t in extractor.get_threads(p):
taddr = Address.from_capa(t.address)
tfeatures = [
ThreadFeature(
basic_block=taddr,
address=Address.from_capa(addr),
feature=feature_from_capa(feature),
) # type: ignore
# Mypy is unable to recognise `basic_block` as a argument due to alias
for feature, addr in extractor.extract_thread_features(p, t)
]
calls = []
for call in extractor.get_calls(p, t):
caddr = Address.from_capa(call.address)
cname = extractor.get_call_name(p, t, call)
cfeatures = [
CallFeature(
call=caddr,
address=Address.from_capa(addr),
feature=feature_from_capa(feature),
)
for feature, addr in extractor.extract_call_features(p, t, call)
]
calls.append(
CallFeatures(
address=caddr,
name=cname,
features=tuple(cfeatures),
)
)
threads.append(
ThreadFeatures(
address=taddr,
features=tuple(tfeatures),
calls=tuple(calls),
)
)
process_features.append(
ProcessFeatures(
address=paddr,
name=pname,
features=tuple(pfeatures),
threads=tuple(threads),
)
)
features = DynamicFeatures(
global_=global_features,
file=tuple(file_features),
processes=tuple(process_features),
) # type: ignore
# Mypy is unable to recognise `global_` as a argument due to alias
# workaround around mypy issue: https://github.com/python/mypy/issues/1424
get_base_addr = getattr(extractor, "get_base_addr", None)
base_addr = get_base_addr() if get_base_addr else capa.features.address.NO_ADDRESS
freeze = Freeze(
version=CURRENT_VERSION,
base_address=Address.from_capa(base_addr),
sample_hashes=extractor.get_sample_hashes(),
flavor="dynamic",
extractor=Extractor(name=extractor.__class__.__name__),
features=features,
) # type: ignore
# Mypy is unable to recognise `base_address` as a argument due to alias
return freeze.model_dump_json()
def loads_static(s: str) -> StaticFeatureExtractor:
"""deserialize a set of features (as a NullStaticFeatureExtractor) from a string."""
freeze = Freeze.model_validate_json(s)
if freeze.version != 2:
if freeze.version != CURRENT_VERSION:
raise ValueError(f"unsupported freeze format version: {freeze.version}")
return null.NullFeatureExtractor(
assert freeze.flavor == "static"
assert isinstance(freeze.features, StaticFeatures)
return null.NullStaticFeatureExtractor(
base_address=freeze.base_address.to_capa(),
sample_hashes=freeze.sample_hashes,
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={
@@ -349,10 +585,59 @@ def loads(s: str) -> capa.features.extractors.base_extractor.FeatureExtractor:
)
def loads_dynamic(s: str) -> DynamicFeatureExtractor:
"""deserialize a set of features (as a NullDynamicFeatureExtractor) from a string."""
freeze = Freeze.model_validate_json(s)
if freeze.version != CURRENT_VERSION:
raise ValueError(f"unsupported freeze format version: {freeze.version}")
assert freeze.flavor == "dynamic"
assert isinstance(freeze.features, DynamicFeatures)
return null.NullDynamicFeatureExtractor(
base_address=freeze.base_address.to_capa(),
sample_hashes=freeze.sample_hashes,
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],
processes={
p.address.to_capa(): null.ProcessFeatures(
name=p.name,
features=[(fe.address.to_capa(), fe.feature.to_capa()) for fe in p.features],
threads={
t.address.to_capa(): null.ThreadFeatures(
features=[(fe.address.to_capa(), fe.feature.to_capa()) for fe in t.features],
calls={
c.address.to_capa(): null.CallFeatures(
name=c.name,
features=[(fe.address.to_capa(), fe.feature.to_capa()) for fe in c.features],
)
for c in t.calls
},
)
for t in p.threads
},
)
for p in freeze.features.processes
},
)
MAGIC = "capa0000".encode("ascii")
def dump(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -> bytes:
def dumps(extractor: FeatureExtractor) -> str:
"""serialize the given extractor to a string."""
if isinstance(extractor, StaticFeatureExtractor):
doc = dumps_static(extractor)
elif isinstance(extractor, DynamicFeatureExtractor):
doc = dumps_dynamic(extractor)
else:
raise ValueError("Invalid feature extractor")
return doc
def dump(extractor: FeatureExtractor) -> bytes:
"""serialize the given extractor to a byte array."""
return MAGIC + zlib.compress(dumps(extractor).encode("utf-8"))
@@ -361,11 +646,28 @@ def is_freeze(buf: bytes) -> bool:
return buf[: len(MAGIC)] == MAGIC
def load(buf: bytes) -> capa.features.extractors.base_extractor.FeatureExtractor:
def loads(s: str):
doc = json.loads(s)
if doc["version"] != CURRENT_VERSION:
raise ValueError(f"unsupported freeze format version: {doc['version']}")
if doc["flavor"] == "static":
return loads_static(s)
elif doc["flavor"] == "dynamic":
return loads_dynamic(s)
else:
raise ValueError(f"unsupported freeze format flavor: {doc['flavor']}")
def load(buf: bytes):
"""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"))
s = zlib.decompress(buf[len(MAGIC) :]).decode("utf-8")
return loads(s)
def main(argv=None):

View File

@@ -19,6 +19,7 @@ import capa.main
import capa.rules
import capa.ghidra.helpers
import capa.render.default
import capa.capabilities.common
import capa.features.extractors.ghidra.extractor
logger = logging.getLogger("capa_ghidra")
@@ -73,13 +74,13 @@ def run_headless():
meta = capa.ghidra.helpers.collect_metadata([rules_path])
extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor()
capabilities, counts = capa.main.find_capabilities(rules, extractor, False)
capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor, False)
meta.analysis.feature_counts = counts["feature_counts"]
meta.analysis.library_functions = counts["library_functions"]
meta.analysis.layout = capa.main.compute_layout(rules, extractor, capabilities)
if capa.main.has_file_limitation(rules, capabilities, is_standalone=True):
if capa.capabilities.common.has_file_limitation(rules, capabilities, is_standalone=True):
logger.info("capa encountered warnings during analysis")
if args.json:
@@ -123,13 +124,13 @@ def run_ui():
meta = capa.ghidra.helpers.collect_metadata([rules_path])
extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor()
capabilities, counts = capa.main.find_capabilities(rules, extractor, True)
capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor, True)
meta.analysis.feature_counts = counts["feature_counts"]
meta.analysis.library_functions = counts["library_functions"]
meta.analysis.layout = capa.main.compute_layout(rules, extractor, capabilities)
if capa.main.has_file_limitation(rules, capabilities, is_standalone=False):
if capa.capabilities.common.has_file_limitation(rules, capabilities, is_standalone=False):
logger.info("capa encountered warnings during analysis")
if verbose == "vverbose":

View File

@@ -143,17 +143,18 @@ def collect_metadata(rules: List[Path]):
sha256=sha256,
path=currentProgram().getExecutablePath(), # type: ignore [name-defined] # noqa: F821
),
analysis=rdoc.Analysis(
flavor=rdoc.Flavor.STATIC,
analysis=rdoc.StaticAnalysis(
format=currentProgram().getExecutableFormat(), # type: ignore [name-defined] # noqa: F821
arch=arch,
os=os,
extractor="ghidra",
rules=tuple(r.resolve().absolute().as_posix() for r in rules),
base_address=capa.features.freeze.Address.from_capa(currentProgram().getImageBase().getOffset()), # type: ignore [name-defined] # noqa: F821
layout=rdoc.Layout(
layout=rdoc.StaticLayout(
functions=(),
),
feature_counts=rdoc.FeatureCounts(file=0, functions=()),
feature_counts=rdoc.StaticFeatureCounts(file=0, functions=()),
library_functions=(),
),
)

View File

@@ -5,6 +5,7 @@
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import json
import inspect
import logging
import contextlib
@@ -15,10 +16,11 @@ from pathlib import Path
import tqdm
from capa.exceptions import UnsupportedFormatError
from capa.features.common import FORMAT_PE, FORMAT_SC32, FORMAT_SC64, FORMAT_DOTNET, FORMAT_UNKNOWN, Format
from capa.features.common import FORMAT_PE, FORMAT_CAPE, FORMAT_SC32, FORMAT_SC64, FORMAT_DOTNET, FORMAT_UNKNOWN, Format
EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32")
EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64")
EXTENSIONS_DYNAMIC = ("json", "json_")
EXTENSIONS_ELF = "elf_"
logger = logging.getLogger("capa")
@@ -57,14 +59,31 @@ def assert_never(value) -> NoReturn:
assert False, f"Unhandled value: {value} ({type(value).__name__})" # noqa: B011
def get_format_from_extension(sample: Path) -> str:
if sample.name.endswith(EXTENSIONS_SHELLCODE_32):
return FORMAT_SC32
elif sample.name.endswith(EXTENSIONS_SHELLCODE_64):
return FORMAT_SC64
def get_format_from_report(sample: Path) -> str:
report = json.load(sample.open(encoding="utf-8"))
if "CAPE" in report:
return FORMAT_CAPE
if "target" in report and "info" in report and "behavior" in report:
# CAPE report that's missing the "CAPE" key,
# which is not going to be much use, but its correct.
return FORMAT_CAPE
return FORMAT_UNKNOWN
def get_format_from_extension(sample: Path) -> str:
format_ = FORMAT_UNKNOWN
if sample.name.endswith(EXTENSIONS_SHELLCODE_32):
format_ = FORMAT_SC32
elif sample.name.endswith(EXTENSIONS_SHELLCODE_64):
format_ = FORMAT_SC64
elif sample.name.endswith(EXTENSIONS_DYNAMIC):
format_ = get_format_from_report(sample)
return format_
def get_auto_format(path: Path) -> str:
format_ = get_format(path)
if format_ == FORMAT_UNKNOWN:
@@ -77,13 +96,13 @@ def get_auto_format(path: Path) -> str:
def get_format(sample: Path) -> str:
# imported locally to avoid import cycle
from capa.features.extractors.common import extract_format
from capa.features.extractors.dnfile_ import DnfileFeatureExtractor
from capa.features.extractors.dotnetfile import DotnetFileFeatureExtractor
buf = sample.read_bytes()
for feature, _ in extract_format(buf):
if feature == Format(FORMAT_PE):
dnfile_extractor = DnfileFeatureExtractor(sample)
dnfile_extractor = DotnetFileFeatureExtractor(sample)
if dnfile_extractor.is_dotnet_file():
feature = Format(FORMAT_DOTNET)
@@ -128,15 +147,32 @@ def redirecting_print_to_tqdm(disable_progress):
def log_unsupported_format_error():
logger.error("-" * 80)
logger.error(" Input file does not appear to be a PE or ELF file.")
logger.error(" Input file does not appear to be a supported file.")
logger.error(" ")
logger.error(
" capa currently only supports analyzing PE and ELF files (or shellcode, when using --format sc32|sc64)."
)
logger.error(" See all supported file formats via capa's help output (-h).")
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_cape_report_error(error: str):
logger.error("-" * 80)
logger.error("Input file is not a valid CAPE report: %s", error)
logger.error(" ")
logger.error(" capa currently only supports analyzing standard CAPE reports in JSON format.")
logger.error(
" Please make sure your report file is in the standard format and contains both the static and dynamic sections."
)
logger.error("-" * 80)
def log_empty_cape_report_error(error: str):
logger.error("-" * 80)
logger.error(" CAPE report is empty or only contains little useful data: %s", error)
logger.error(" ")
logger.error(" Please make sure the sandbox run captures useful behaviour of your sample.")
logger.error("-" * 80)
def log_unsupported_os_error():
logger.error("-" * 80)
logger.error(" Input file does not appear to target a supported OS.")

View File

@@ -152,14 +152,15 @@ def collect_metadata(rules: List[Path]):
sha256=sha256,
path=idaapi.get_input_file_path(),
),
analysis=rdoc.Analysis(
flavor=rdoc.Flavor.STATIC,
analysis=rdoc.StaticAnalysis(
format=idaapi.get_file_type_name(),
arch=arch,
os=os,
extractor="ida",
rules=tuple(r.resolve().absolute().as_posix() for r in rules),
base_address=capa.features.freeze.Address.from_capa(idaapi.get_imagebase()),
layout=rdoc.Layout(
layout=rdoc.StaticLayout(
functions=(),
# this is updated after capabilities have been collected.
# will look like:
@@ -167,7 +168,7 @@ def collect_metadata(rules: List[Path]):
# "functions": { 0x401000: { "matched_basic_blocks": [ 0x401000, 0x401005, ... ] }, ... }
),
# ignore these for now - not used by IDA plugin.
feature_counts=rdoc.FeatureCounts(file=0, functions=()),
feature_counts=rdoc.StaticFeatureCounts(file=0, functions=()),
library_functions=(),
),
)

View File

@@ -25,6 +25,7 @@ import capa.version
import capa.ida.helpers
import capa.render.json
import capa.features.common
import capa.capabilities.common
import capa.render.result_document
import capa.features.extractors.ida.extractor
from capa.rules import Rule
@@ -768,7 +769,7 @@ class CapaExplorerForm(idaapi.PluginForm):
try:
meta = capa.ida.helpers.collect_metadata([Path(settings.user[CAPA_SETTINGS_RULE_PATH])])
capabilities, counts = capa.main.find_capabilities(
capabilities, counts = capa.capabilities.common.find_capabilities(
ruleset, self.feature_extractor, disable_progress=True
)
@@ -810,7 +811,7 @@ class CapaExplorerForm(idaapi.PluginForm):
capa.ida.helpers.inform_user_ida_ui("capa encountered file type warnings during analysis")
if capa.main.has_file_limitation(ruleset, capabilities, is_standalone=False):
if capa.capabilities.common.has_file_limitation(ruleset, capabilities, is_standalone=False):
capa.ida.helpers.inform_user_ida_ui("capa encountered file limitation warnings during analysis")
except Exception as e:
logger.exception("Failed to check for file limitations (error: %s)", e)
@@ -1192,10 +1193,13 @@ class CapaExplorerForm(idaapi.PluginForm):
return
is_match: bool = False
if self.rulegen_current_function is not None and rule.scope in (
capa.rules.Scope.FUNCTION,
capa.rules.Scope.BASIC_BLOCK,
capa.rules.Scope.INSTRUCTION,
if self.rulegen_current_function is not None and any(
s in rule.scopes
for s in (
capa.rules.Scope.FUNCTION,
capa.rules.Scope.BASIC_BLOCK,
capa.rules.Scope.INSTRUCTION,
)
):
try:
_, func_matches, bb_matches, insn_matches = self.rulegen_feature_cache.find_code_capabilities(
@@ -1205,13 +1209,13 @@ class CapaExplorerForm(idaapi.PluginForm):
self.set_rulegen_status(f"Failed to create function rule matches from rule set ({e})")
return
if rule.scope == capa.rules.Scope.FUNCTION and rule.name in func_matches:
if capa.rules.Scope.FUNCTION in rule.scopes and rule.name in func_matches:
is_match = True
elif rule.scope == capa.rules.Scope.BASIC_BLOCK and rule.name in bb_matches:
elif capa.rules.Scope.BASIC_BLOCK in rule.scopes and rule.name in bb_matches:
is_match = True
elif rule.scope == capa.rules.Scope.INSTRUCTION and rule.name in insn_matches:
elif capa.rules.Scope.INSTRUCTION in rule.scopes and rule.name in insn_matches:
is_match = True
elif rule.scope == capa.rules.Scope.FILE:
elif capa.rules.Scope.FILE in rule.scopes:
try:
_, file_matches = self.rulegen_feature_cache.find_file_capabilities(ruleset)
except Exception as e:

View File

@@ -500,16 +500,16 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
location = location_.to_capa()
parent2: CapaExplorerDataItem
if rule.meta.scope == capa.rules.FILE_SCOPE:
if capa.rules.Scope.FILE in rule.meta.scopes:
parent2 = parent
elif rule.meta.scope == capa.rules.FUNCTION_SCOPE:
elif capa.rules.Scope.FUNCTION in rule.meta.scopes:
parent2 = CapaExplorerFunctionItem(parent, location)
elif rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE:
elif capa.rules.Scope.BASIC_BLOCK in rule.meta.scopes:
parent2 = CapaExplorerBlockItem(parent, location)
elif rule.meta.scope == capa.rules.INSTRUCTION_SCOPE:
elif capa.rules.Scope.INSTRUCTION in rule.meta.scopes:
parent2 = CapaExplorerInstructionItem(parent, location)
else:
raise RuntimeError("unexpected rule scope: " + str(rule.meta.scope))
raise RuntimeError("unexpected rule scope: " + str(rule.meta.scopes.static))
self.render_capa_doc_match(parent2, match, doc)

View File

@@ -11,23 +11,21 @@ See the License for the specific language governing permissions and limitations
import io
import os
import sys
import json
import time
import hashlib
import logging
import argparse
import datetime
import textwrap
import itertools
import contextlib
import collections
from typing import Any, Dict, List, Tuple, Callable, Optional
from types import TracebackType
from typing import Any, Set, Dict, List, Callable, Optional
from pathlib import Path
import halo
import tqdm
import colorama
import tqdm.contrib.logging
from pefile import PEFormatError
from typing_extensions import assert_never
from elftools.common.exceptions import ELFError
import capa.perf
@@ -47,22 +45,28 @@ import capa.render.result_document
import capa.render.result_document as rdoc
import capa.features.extractors.common
import capa.features.extractors.pefile
import capa.features.extractors.dnfile_
import capa.features.extractors.elffile
import capa.features.extractors.dotnetfile
import capa.features.extractors.base_extractor
from capa.rules import Rule, Scope, RuleSet
from capa.engine import FeatureSet, MatchResults
import capa.features.extractors.cape.extractor
from capa.rules import Rule, RuleSet
from capa.engine import MatchResults
from capa.helpers import (
get_format,
get_file_taste,
get_auto_format,
log_unsupported_os_error,
redirecting_print_to_tqdm,
log_unsupported_arch_error,
log_empty_cape_report_error,
log_unsupported_format_error,
log_unsupported_cape_report_error,
)
from capa.exceptions import (
EmptyReportError,
UnsupportedOSError,
UnsupportedArchError,
UnsupportedFormatError,
UnsupportedRuntimeError,
)
from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError, UnsupportedRuntimeError
from capa.features.common import (
OS_AUTO,
OS_LINUX,
@@ -71,14 +75,21 @@ from capa.features.common import (
FORMAT_ELF,
OS_WINDOWS,
FORMAT_AUTO,
FORMAT_CAPE,
FORMAT_SC32,
FORMAT_SC64,
FORMAT_DOTNET,
FORMAT_FREEZE,
FORMAT_RESULT,
)
from capa.features.address import NO_ADDRESS, Address
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
from capa.features.address import Address
from capa.capabilities.common import find_capabilities, has_file_limitation, find_file_capabilities
from capa.features.extractors.base_extractor import (
SampleHashes,
FeatureExtractor,
StaticFeatureExtractor,
DynamicFeatureExtractor,
)
RULES_PATH_DEFAULT_STRING = "(embedded rules)"
SIGNATURES_PATH_DEFAULT_STRING = "(embedded signatures)"
@@ -98,6 +109,9 @@ E_INVALID_FILE_ARCH = 17
E_INVALID_FILE_OS = 18
E_UNSUPPORTED_IDA_VERSION = 19
E_UNSUPPORTED_GHIDRA_VERSION = 20
E_MISSING_CAPE_STATIC_ANALYSIS = 21
E_MISSING_CAPE_DYNAMIC_ANALYSIS = 22
E_EMPTY_REPORT = 23
logger = logging.getLogger("capa")
@@ -120,267 +134,6 @@ def set_vivisect_log_level(level):
logging.getLogger("Elf").setLevel(level)
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
# 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)
_, 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):
file_features = collections.defaultdict(set) # type: FeatureSet
for feature, va in itertools.chain(extractor.extract_file_features(), extractor.extract_global_features()):
# not all file features may have virtual addresses.
# if not, then at least ensure the feature shows up in the index.
# the set of addresses will still be empty.
if va:
file_features[feature].add(va)
else:
if feature not in file_features:
file_features[feature] = set()
logger.debug("analyzed file and extracted %d features", len(file_features))
file_features.update(function_features)
_, 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
feature_counts = rdoc.FeatureCounts(file=0, functions=())
library_functions: Tuple[rdoc.LibraryFunction, ...] = ()
with redirecting_print_to_tqdm(disable_progress):
with tqdm.contrib.logging.logging_redirect_tqdm():
pbar = tqdm.tqdm
if capa.helpers.is_runtime_ghidra():
# Ghidrathon interpreter cannot properly handle
# the TMonitor thread that is created via a monitor_interval
# > 0
pbar.monitor_interval = 0
if disable_progress:
# do not use tqdm to avoid unnecessary side effects when caller intends
# to disable progress completely
def pbar(s, *args, **kwargs):
return s
functions = list(extractor.get_functions())
n_funcs = len(functions)
pb = pbar(functions, desc="matching", unit=" functions", postfix="skipped 0 library functions", leave=False)
for f in pb:
t0 = time.time()
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)
library_functions += (
rdoc.LibraryFunction(address=frz.Address.from_capa(f.address), name=function_name),
)
n_libs = len(library_functions)
percentage = round(100 * (n_libs / n_funcs))
if isinstance(pb, tqdm.tqdm):
pb.set_postfix_str(f"skipped {n_libs} library functions ({percentage}%)")
continue
function_matches, bb_matches, insn_matches, feature_count = find_code_capabilities(
ruleset, extractor, f
)
feature_counts.functions += (
rdoc.FunctionFeatureCount(address=frz.Address.from_capa(f.address), count=feature_count),
)
t1 = time.time()
match_count = sum(len(res) for res in function_matches.values())
match_count += sum(len(res) for res in bb_matches.values())
match_count += sum(len(res) for res in insn_matches.values())
logger.debug(
"analyzed function 0x%x and extracted %d features, %d matches in %0.02fs",
f.address,
feature_count,
match_count,
t1 - t0,
)
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, 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(), all_insn_matches.items()
):
locations = {p[0] for p in results}
rule = ruleset[rule_name]
capa.engine.index_rule_matches(function_and_lower_features, rule, locations)
all_file_matches, feature_count = find_file_capabilities(ruleset, extractor, function_and_lower_features)
feature_counts.file = feature_count
matches = dict(
itertools.chain(
# 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(),
)
)
meta = {
"feature_counts": feature_counts,
"library_functions": library_functions,
}
return matches, meta
def has_rule_with_namespace(rules: RuleSet, capabilities: MatchResults, namespace: str) -> bool:
return any(
rules.rules[rule_name].meta.get("namespace", "").startswith(namespace) for rule_name in capabilities.keys()
)
def is_internal_rule(rule: Rule) -> bool:
return rule.meta.get("namespace", "").startswith("internal/")
def is_file_limitation_rule(rule: Rule) -> bool:
return rule.meta.get("namespace", "") == "internal/limitation/file"
def has_file_limitation(rules: RuleSet, capabilities: MatchResults, is_standalone=True) -> bool:
file_limitation_rules = list(filter(is_file_limitation_rule, rules.rules.values()))
for file_limitation_rule in file_limitation_rules:
if file_limitation_rule.name not in capabilities:
continue
logger.warning("-" * 80)
for line in file_limitation_rule.meta.get("description", "").split("\n"):
logger.warning(" %s", line)
logger.warning(" Identified via rule: %s", file_limitation_rule.name)
if is_standalone:
logger.warning(" ")
logger.warning(" Use -v or -vv if you really want to see the capabilities identified by capa.")
logger.warning("-" * 80)
# bail on first file limitation
return True
return False
def is_supported_format(sample: Path) -> bool:
"""
Return if this is a supported file based on magic header values
@@ -461,7 +214,7 @@ def get_default_signatures() -> List[Path]:
"""
compute a list of file system paths to the default FLIRT signatures.
"""
sigs_path = get_default_root() / "sigs"
sigs_path = get_default_root() / "capa" / "sigs"
logger.debug("signatures path: %s", sigs_path)
ret = []
@@ -532,7 +285,8 @@ def get_extractor(
UnsupportedArchError
UnsupportedOSError
"""
if format_ not in (FORMAT_SC32, FORMAT_SC64):
if format_ not in (FORMAT_SC32, FORMAT_SC64, FORMAT_CAPE):
if not is_supported_format(path):
raise UnsupportedFormatError()
@@ -542,7 +296,13 @@ def get_extractor(
if os_ == OS_AUTO and not is_supported_os(path):
raise UnsupportedOSError()
if format_ == FORMAT_DOTNET:
if format_ == FORMAT_CAPE:
import capa.features.extractors.cape.extractor
report = json.load(Path(path).open(encoding="utf-8"))
return capa.features.extractors.cape.extractor.CapeExtractor.from_report(report)
elif format_ == FORMAT_DOTNET:
import capa.features.extractors.dnfile.extractor
return capa.features.extractors.dnfile.extractor.DnfileFeatureExtractor(path)
@@ -610,11 +370,15 @@ def get_file_extractors(sample: Path, format_: str) -> List[FeatureExtractor]:
elif format_ == FORMAT_DOTNET:
file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(sample))
file_extractors.append(capa.features.extractors.dnfile_.DnfileFeatureExtractor(sample))
file_extractors.append(capa.features.extractors.dotnetfile.DotnetFileFeatureExtractor(sample))
elif format_ == capa.features.extractors.common.FORMAT_ELF:
elif format_ == capa.features.common.FORMAT_ELF:
file_extractors.append(capa.features.extractors.elffile.ElfFeatureExtractor(sample))
elif format_ == FORMAT_CAPE:
report = json.load(Path(sample).open(encoding="utf-8"))
file_extractors.append(capa.features.extractors.cape.extractor.CapeExtractor.from_report(report))
return file_extractors
@@ -696,7 +460,7 @@ def get_rules(
if ruleset is not None:
return ruleset
rules = [] # type: List[Rule]
rules: List[Rule] = []
total_rule_count = len(rule_file_paths)
for i, (path, content) in enumerate(zip(rule_file_paths, rule_contents)):
@@ -711,7 +475,7 @@ def get_rules(
rule.meta["capa/nursery"] = is_nursery_rule_path(path)
rules.append(rule)
logger.debug("loaded rule: '%s' with scope: %s", rule.name, rule.scope)
logger.debug("loaded rule: '%s' with scope: %s", rule.name, rule.scopes)
ruleset = capa.rules.RuleSet(rules)
@@ -746,60 +510,177 @@ def get_signatures(sigs_path: Path) -> List[Path]:
return paths
def collect_metadata(
argv: List[str],
sample_path: Path,
format_: str,
os_: str,
rules_path: List[Path],
extractor: capa.features.extractors.base_extractor.FeatureExtractor,
) -> rdoc.Metadata:
md5 = hashlib.md5()
sha1 = hashlib.sha1()
sha256 = hashlib.sha256()
buf = sample_path.read_bytes()
md5.update(buf)
sha1.update(buf)
sha256.update(buf)
rules = tuple(r.resolve().absolute().as_posix() for r in rules_path)
format_ = get_format(sample_path) if format_ == FORMAT_AUTO else format_
arch = get_arch(sample_path)
os_ = get_os(sample_path) if os_ == OS_AUTO else os_
return rdoc.Metadata(
timestamp=datetime.datetime.now(),
version=capa.version.__version__,
argv=tuple(argv) if argv else None,
sample=rdoc.Sample(
md5=md5.hexdigest(),
sha1=sha1.hexdigest(),
sha256=sha256.hexdigest(),
path=sample_path.resolve().absolute().as_posix(),
),
analysis=rdoc.Analysis(
def get_sample_analysis(format_, arch, os_, extractor, rules_path, counts):
if isinstance(extractor, StaticFeatureExtractor):
return rdoc.StaticAnalysis(
format=format_,
arch=arch,
os=os_,
extractor=extractor.__class__.__name__,
rules=rules,
rules=tuple(rules_path),
base_address=frz.Address.from_capa(extractor.get_base_address()),
layout=rdoc.Layout(
layout=rdoc.StaticLayout(
functions=(),
# this is updated after capabilities have been collected.
# will look like:
#
# "functions": { 0x401000: { "matched_basic_blocks": [ 0x401000, 0x401005, ... ] }, ... }
),
feature_counts=rdoc.FeatureCounts(file=0, functions=()),
library_functions=(),
feature_counts=counts["feature_counts"],
library_functions=counts["library_functions"],
)
elif isinstance(extractor, DynamicFeatureExtractor):
return rdoc.DynamicAnalysis(
format=format_,
arch=arch,
os=os_,
extractor=extractor.__class__.__name__,
rules=tuple(rules_path),
layout=rdoc.DynamicLayout(
processes=(),
),
feature_counts=counts["feature_counts"],
)
else:
raise ValueError("invalid extractor type")
def collect_metadata(
argv: List[str],
sample_path: Path,
format_: str,
os_: str,
rules_path: List[Path],
extractor: FeatureExtractor,
counts: dict,
) -> rdoc.Metadata:
# if it's a binary sample we hash it, if it's a report
# we fetch the hashes from the report
sample_hashes: SampleHashes = extractor.get_sample_hashes()
md5, sha1, sha256 = sample_hashes.md5, sample_hashes.sha1, sample_hashes.sha256
global_feats = list(extractor.extract_global_features())
extractor_format = [f.value for (f, _) in global_feats if isinstance(f, capa.features.common.Format)]
extractor_arch = [f.value for (f, _) in global_feats if isinstance(f, capa.features.common.Arch)]
extractor_os = [f.value for (f, _) in global_feats if isinstance(f, capa.features.common.OS)]
format_ = str(extractor_format[0]) if extractor_format else "unknown" if format_ == FORMAT_AUTO else format_
arch = str(extractor_arch[0]) if extractor_arch else "unknown"
os_ = str(extractor_os[0]) if extractor_os else "unknown" if os_ == OS_AUTO else os_
if isinstance(extractor, StaticFeatureExtractor):
meta_class: type = rdoc.StaticMetadata
elif isinstance(extractor, DynamicFeatureExtractor):
meta_class = rdoc.DynamicMetadata
else:
assert_never(extractor)
rules = tuple(r.resolve().absolute().as_posix() for r in rules_path)
return meta_class(
timestamp=datetime.datetime.now(),
version=capa.version.__version__,
argv=tuple(argv) if argv else None,
sample=rdoc.Sample(
md5=md5,
sha1=sha1,
sha256=sha256,
path=Path(sample_path).resolve().as_posix(),
),
analysis=get_sample_analysis(
format_,
arch,
os_,
extractor,
rules,
counts,
),
)
def compute_layout(rules, extractor, capabilities) -> rdoc.Layout:
def compute_dynamic_layout(rules, extractor: DynamicFeatureExtractor, capabilities: MatchResults) -> rdoc.DynamicLayout:
"""
compute a metadata structure that links threads
to the processes in which they're found.
only collect the threads at which some rule matched.
otherwise, we may pollute the json document with
a large amount of un-referenced data.
"""
assert isinstance(extractor, DynamicFeatureExtractor)
matched_calls: Set[Address] = set()
def result_rec(result: capa.features.common.Result):
for loc in result.locations:
if isinstance(loc, capa.features.address.DynamicCallAddress):
matched_calls.add(loc)
for child in result.children:
result_rec(child)
for matches in capabilities.values():
for _, result in matches:
result_rec(result)
names_by_process: Dict[Address, str] = {}
names_by_call: Dict[Address, str] = {}
matched_processes: Set[Address] = set()
matched_threads: Set[Address] = set()
threads_by_process: Dict[Address, List[Address]] = {}
calls_by_thread: Dict[Address, List[Address]] = {}
for p in extractor.get_processes():
threads_by_process[p.address] = []
for t in extractor.get_threads(p):
calls_by_thread[t.address] = []
for c in extractor.get_calls(p, t):
if c.address in matched_calls:
names_by_call[c.address] = extractor.get_call_name(p, t, c)
calls_by_thread[t.address].append(c.address)
if calls_by_thread[t.address]:
matched_threads.add(t.address)
threads_by_process[p.address].append(t.address)
if threads_by_process[p.address]:
matched_processes.add(p.address)
names_by_process[p.address] = extractor.get_process_name(p)
layout = rdoc.DynamicLayout(
processes=tuple(
rdoc.ProcessLayout(
address=frz.Address.from_capa(p),
name=names_by_process[p],
matched_threads=tuple(
rdoc.ThreadLayout(
address=frz.Address.from_capa(t),
matched_calls=tuple(
rdoc.CallLayout(
address=frz.Address.from_capa(c),
name=names_by_call[c],
)
for c in calls_by_thread[t]
if c in matched_calls
),
)
for t in threads
if t in matched_threads
) # this object is open to extension in the future,
# such as with the function name, etc.
)
for p, threads in threads_by_process.items()
if p in matched_processes
)
)
return layout
def compute_static_layout(rules, extractor: StaticFeatureExtractor, capabilities) -> rdoc.StaticLayout:
"""
compute a metadata structure that links basic blocks
to the functions in which they're found.
@@ -819,12 +700,12 @@ def compute_layout(rules, extractor, capabilities) -> rdoc.Layout:
matched_bbs = set()
for rule_name, matches in capabilities.items():
rule = rules[rule_name]
if rule.meta.get("scope") == capa.rules.BASIC_BLOCK_SCOPE:
if capa.rules.Scope.BASIC_BLOCK in rule.scopes:
for addr, _ in matches:
assert addr in functions_by_bb
matched_bbs.add(addr)
layout = rdoc.Layout(
layout = rdoc.StaticLayout(
functions=tuple(
rdoc.FunctionLayout(
address=frz.Address.from_capa(f),
@@ -841,6 +722,15 @@ def compute_layout(rules, extractor, capabilities) -> rdoc.Layout:
return layout
def compute_layout(rules, extractor, capabilities) -> rdoc.Layout:
if isinstance(extractor, StaticFeatureExtractor):
return compute_static_layout(rules, extractor, capabilities)
elif isinstance(extractor, DynamicFeatureExtractor):
return compute_dynamic_layout(rules, extractor, capabilities)
else:
raise ValueError("extractor must be either a static or dynamic extracotr")
def install_common_args(parser, wanted=None):
"""
register a common set of command line arguments for re-use by main & scripts.
@@ -909,6 +799,7 @@ def install_common_args(parser, wanted=None):
(FORMAT_ELF, "Executable and Linkable Format"),
(FORMAT_SC32, "32-bit shellcode"),
(FORMAT_SC64, "64-bit shellcode"),
(FORMAT_CAPE, "CAPE sandbox report"),
(FORMAT_FREEZE, "features previously frozen by capa"),
]
format_help = ", ".join([f"{f[0]}: {f[1]}" for f in formats])
@@ -1071,7 +962,7 @@ def handle_common_args(args):
)
logger.debug("-" * 80)
sigs_path = get_default_root() / "sigs"
sigs_path = get_default_root() / "capa" / "sigs"
if not sigs_path.exists():
logger.error(
@@ -1087,6 +978,27 @@ def handle_common_args(args):
args.signatures = sigs_path
def simple_message_exception_handler(exctype, value: BaseException, traceback: TracebackType):
"""
prints friendly message on unexpected exceptions to regular users (debug mode shows regular stack trace)
args:
# TODO(aaronatp): Once capa drops support for Python 3.8, move the exctype type annotation to
# the function parameters and remove the "# type: ignore[assignment]" from the relevant place
# in the main function, see (https://github.com/mandiant/capa/issues/1896)
exctype (type[BaseException]): exception class
"""
if exctype is KeyboardInterrupt:
print("KeyboardInterrupt detected, program terminated")
else:
print(
f"Unexpected exception raised: {exctype}. Please run capa in debug mode (-d/--debug) "
+ "to see the stack trace. Please also report your issue on the capa GitHub page so we "
+ "can improve the code! (https://github.com/mandiant/capa/issues)"
)
def main(argv: Optional[List[str]] = None):
if sys.version_info < (3, 8):
raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.8+")
@@ -1129,6 +1041,8 @@ def main(argv: Optional[List[str]] = None):
install_common_args(parser, {"sample", "format", "backend", "os", "signatures", "rules", "tag"})
parser.add_argument("-j", "--json", action="store_true", help="emit JSON instead of text")
args = parser.parse_args(args=argv)
if not args.debug:
sys.excepthook = simple_message_exception_handler # type: ignore[assignment]
ret = handle_common_args(args)
if ret is not None and ret != 0:
return ret
@@ -1165,7 +1079,7 @@ def main(argv: Optional[List[str]] = None):
# 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(list(filter(lambda r: not r.is_subscope_rule(), rules.rules.values()))),
len(list(filter(lambda r: not (r.is_subscope_rule()), rules.rules.values()))),
)
if args.tag:
rules = rules.filter_rules_by_meta(args.tag)
@@ -1204,8 +1118,26 @@ def main(argv: Optional[List[str]] = None):
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
except UnsupportedFormatError as e:
if format_ == FORMAT_CAPE:
log_unsupported_cape_report_error(str(e))
else:
log_unsupported_format_error()
return E_INVALID_FILE_TYPE
except EmptyReportError as e:
if format_ == FORMAT_CAPE:
log_empty_cape_report_error(str(e))
return E_EMPTY_REPORT
else:
log_unsupported_format_error()
return E_INVALID_FILE_TYPE
found_file_limitation = False
for file_extractor in file_extractors:
if isinstance(file_extractor, DynamicFeatureExtractor):
# Dynamic feature extractors can handle packed samples
continue
try:
pure_file_capabilities, _ = find_file_capabilities(rules, file_extractor, {})
except PEFormatError as e:
@@ -1217,7 +1149,8 @@ def main(argv: Optional[List[str]] = None):
# file limitations that rely on non-file scope won't be detected here.
# nor on FunctionName features, because pefile doesn't support this.
if has_file_limitation(rules, pure_file_capabilities):
found_file_limitation = has_file_limitation(rules, pure_file_capabilities)
if found_file_limitation:
# 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):
@@ -1239,7 +1172,7 @@ def main(argv: Optional[List[str]] = None):
if format_ == FORMAT_FREEZE:
# freeze format deserializes directly into an extractor
extractor = frz.load(Path(args.sample).read_bytes())
extractor: FeatureExtractor = frz.load(Path(args.sample).read_bytes())
else:
# all other formats we must create an extractor,
# such as viv, binary ninja, etc. workspaces
@@ -1257,6 +1190,9 @@ def main(argv: Optional[List[str]] = None):
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
# TODO(mr-tz): this should be wrapped and refactored as it's tedious to update everywhere
# see same code and show-features above examples
# https://github.com/mandiant/capa/issues/1813
try:
extractor = get_extractor(
args.sample,
@@ -1267,8 +1203,11 @@ def main(argv: Optional[List[str]] = None):
should_save_workspace,
disable_progress=args.quiet or args.debug,
)
except UnsupportedFormatError:
log_unsupported_format_error()
except UnsupportedFormatError as e:
if format_ == FORMAT_CAPE:
log_unsupported_cape_report_error(str(e))
else:
log_unsupported_format_error()
return E_INVALID_FILE_TYPE
except UnsupportedArchError:
log_unsupported_arch_error()
@@ -1277,16 +1216,13 @@ def main(argv: Optional[List[str]] = None):
log_unsupported_os_error()
return E_INVALID_FILE_OS
meta = collect_metadata(argv, args.sample, args.format, args.os, args.rules, extractor)
capabilities, counts = find_capabilities(rules, extractor, disable_progress=args.quiet)
meta.analysis.feature_counts = counts["feature_counts"]
meta.analysis.library_functions = counts["library_functions"]
meta = collect_metadata(argv, args.sample, args.format, args.os, args.rules, extractor, 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
if isinstance(extractor, StaticFeatureExtractor) and found_file_limitation:
# bail if capa's static feature extractor 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 E_FILE_LIMITATION

View File

@@ -33,6 +33,7 @@ def render_meta(doc: rd.ResultDocument, ostream: StringIO):
(width("md5", 22), width(doc.meta.sample.md5, 82)),
("sha1", doc.meta.sample.sha1),
("sha256", doc.meta.sample.sha256),
("analysis", doc.meta.flavor.value),
("os", doc.meta.analysis.os),
("format", doc.meta.analysis.format),
("arch", doc.meta.analysis.arch),

View File

@@ -38,16 +38,6 @@ from capa.helpers import assert_never
from capa.features.freeze import AddressType
def dict_tuple_to_list_values(d: Dict) -> Dict:
o = {}
for k, v in d.items():
if isinstance(v, tuple):
o[k] = list(v)
else:
o[k] = v
return o
def int_to_pb2(v: int) -> capa_pb2.Integer:
if v < -2_147_483_648:
raise ValueError(f"value underflow: {v}")
@@ -100,6 +90,51 @@ def addr_to_pb2(addr: frz.Address) -> capa_pb2.Address:
token_offset=capa_pb2.Token_Offset(token=int_to_pb2(token), offset=offset),
)
elif addr.type is AddressType.PROCESS:
assert isinstance(addr.value, tuple)
ppid, pid = addr.value
assert isinstance(ppid, int)
assert isinstance(pid, int)
return capa_pb2.Address(
type=capa_pb2.AddressType.ADDRESSTYPE_PROCESS,
ppid_pid=capa_pb2.Ppid_Pid(
ppid=int_to_pb2(ppid),
pid=int_to_pb2(pid),
),
)
elif addr.type is AddressType.THREAD:
assert isinstance(addr.value, tuple)
ppid, pid, tid = addr.value
assert isinstance(ppid, int)
assert isinstance(pid, int)
assert isinstance(tid, int)
return capa_pb2.Address(
type=capa_pb2.AddressType.ADDRESSTYPE_THREAD,
ppid_pid_tid=capa_pb2.Ppid_Pid_Tid(
ppid=int_to_pb2(ppid),
pid=int_to_pb2(pid),
tid=int_to_pb2(tid),
),
)
elif addr.type is AddressType.CALL:
assert isinstance(addr.value, tuple)
ppid, pid, tid, id_ = addr.value
assert isinstance(ppid, int)
assert isinstance(pid, int)
assert isinstance(tid, int)
assert isinstance(id_, int)
return capa_pb2.Address(
type=capa_pb2.AddressType.ADDRESSTYPE_CALL,
ppid_pid_tid_id=capa_pb2.Ppid_Pid_Tid_Id(
ppid=int_to_pb2(ppid),
pid=int_to_pb2(pid),
tid=int_to_pb2(tid),
id=int_to_pb2(id_),
),
)
elif addr.type is AddressType.NO_ADDRESS:
# value == None, so only set type
return capa_pb2.Address(type=capa_pb2.AddressType.ADDRESSTYPE_NO_ADDRESS)
@@ -117,49 +152,129 @@ def scope_to_pb2(scope: capa.rules.Scope) -> capa_pb2.Scope.ValueType:
return capa_pb2.Scope.SCOPE_BASIC_BLOCK
elif scope == capa.rules.Scope.INSTRUCTION:
return capa_pb2.Scope.SCOPE_INSTRUCTION
elif scope == capa.rules.Scope.PROCESS:
return capa_pb2.Scope.SCOPE_PROCESS
elif scope == capa.rules.Scope.THREAD:
return capa_pb2.Scope.SCOPE_THREAD
elif scope == capa.rules.Scope.CALL:
return capa_pb2.Scope.SCOPE_CALL
else:
assert_never(scope)
def metadata_to_pb2(meta: rd.Metadata) -> capa_pb2.Metadata:
return capa_pb2.Metadata(
timestamp=str(meta.timestamp),
version=meta.version,
argv=meta.argv,
sample=google.protobuf.json_format.ParseDict(meta.sample.model_dump(), capa_pb2.Sample()),
analysis=capa_pb2.Analysis(
format=meta.analysis.format,
arch=meta.analysis.arch,
os=meta.analysis.os,
extractor=meta.analysis.extractor,
rules=list(meta.analysis.rules),
base_address=addr_to_pb2(meta.analysis.base_address),
layout=capa_pb2.Layout(
functions=[
capa_pb2.FunctionLayout(
address=addr_to_pb2(f.address),
matched_basic_blocks=[
capa_pb2.BasicBlockLayout(address=addr_to_pb2(bb.address)) for bb in f.matched_basic_blocks
],
)
for f in meta.analysis.layout.functions
]
),
feature_counts=capa_pb2.FeatureCounts(
file=meta.analysis.feature_counts.file,
functions=[
capa_pb2.FunctionFeatureCount(address=addr_to_pb2(f.address), count=f.count)
for f in meta.analysis.feature_counts.functions
],
),
library_functions=[
capa_pb2.LibraryFunction(address=addr_to_pb2(lf.address), name=lf.name)
for lf in meta.analysis.library_functions
def scopes_to_pb2(scopes: capa.rules.Scopes) -> capa_pb2.Scopes:
doc = {}
if scopes.static:
doc["static"] = scope_to_pb2(scopes.static)
if scopes.dynamic:
doc["dynamic"] = scope_to_pb2(scopes.dynamic)
return google.protobuf.json_format.ParseDict(doc, capa_pb2.Scopes())
def flavor_to_pb2(flavor: rd.Flavor) -> capa_pb2.Flavor.ValueType:
if flavor == rd.Flavor.STATIC:
return capa_pb2.Flavor.FLAVOR_STATIC
elif flavor == rd.Flavor.DYNAMIC:
return capa_pb2.Flavor.FLAVOR_DYNAMIC
else:
assert_never(flavor)
def static_analysis_to_pb2(analysis: rd.StaticAnalysis) -> capa_pb2.StaticAnalysis:
return capa_pb2.StaticAnalysis(
format=analysis.format,
arch=analysis.arch,
os=analysis.os,
extractor=analysis.extractor,
rules=list(analysis.rules),
base_address=addr_to_pb2(analysis.base_address),
layout=capa_pb2.StaticLayout(
functions=[
capa_pb2.FunctionLayout(
address=addr_to_pb2(f.address),
matched_basic_blocks=[
capa_pb2.BasicBlockLayout(address=addr_to_pb2(bb.address)) for bb in f.matched_basic_blocks
],
)
for f in analysis.layout.functions
]
),
feature_counts=capa_pb2.StaticFeatureCounts(
file=analysis.feature_counts.file,
functions=[
capa_pb2.FunctionFeatureCount(address=addr_to_pb2(f.address), count=f.count)
for f in analysis.feature_counts.functions
],
),
library_functions=[
capa_pb2.LibraryFunction(address=addr_to_pb2(lf.address), name=lf.name) for lf in analysis.library_functions
],
)
def dynamic_analysis_to_pb2(analysis: rd.DynamicAnalysis) -> capa_pb2.DynamicAnalysis:
return capa_pb2.DynamicAnalysis(
format=analysis.format,
arch=analysis.arch,
os=analysis.os,
extractor=analysis.extractor,
rules=list(analysis.rules),
layout=capa_pb2.DynamicLayout(
processes=[
capa_pb2.ProcessLayout(
address=addr_to_pb2(p.address),
name=p.name,
matched_threads=[
capa_pb2.ThreadLayout(
address=addr_to_pb2(t.address),
matched_calls=[
capa_pb2.CallLayout(
address=addr_to_pb2(c.address),
name=c.name,
)
for c in t.matched_calls
],
)
for t in p.matched_threads
],
)
for p in analysis.layout.processes
]
),
feature_counts=capa_pb2.DynamicFeatureCounts(
file=analysis.feature_counts.file,
processes=[
capa_pb2.ProcessFeatureCount(address=addr_to_pb2(p.address), count=p.count)
for p in analysis.feature_counts.processes
],
),
)
def metadata_to_pb2(meta: rd.Metadata) -> capa_pb2.Metadata:
if isinstance(meta.analysis, rd.StaticAnalysis):
return capa_pb2.Metadata(
timestamp=str(meta.timestamp),
version=meta.version,
argv=meta.argv,
sample=google.protobuf.json_format.ParseDict(meta.sample.model_dump(), capa_pb2.Sample()),
flavor=flavor_to_pb2(meta.flavor),
static_analysis=static_analysis_to_pb2(meta.analysis),
)
elif isinstance(meta.analysis, rd.DynamicAnalysis):
return capa_pb2.Metadata(
timestamp=str(meta.timestamp),
version=meta.version,
argv=meta.argv,
sample=google.protobuf.json_format.ParseDict(meta.sample.model_dump(), capa_pb2.Sample()),
flavor=flavor_to_pb2(meta.flavor),
dynamic_analysis=dynamic_analysis_to_pb2(meta.analysis),
)
else:
assert_never(meta.analysis)
def statement_to_pb2(statement: rd.Statement) -> capa_pb2.StatementNode:
if isinstance(statement, rd.RangeStatement):
return capa_pb2.StatementNode(
@@ -390,15 +505,51 @@ def match_to_pb2(match: rd.Match) -> capa_pb2.Match:
assert_never(match)
def rule_metadata_to_pb2(rule_metadata: rd.RuleMetadata) -> capa_pb2.RuleMetadata:
# after manual type conversions to the RuleMetadata, we can rely on the protobuf json parser
# conversions include tuple -> list and rd.Enum -> proto.enum
meta = dict_tuple_to_list_values(rule_metadata.model_dump())
meta["scope"] = scope_to_pb2(meta["scope"])
meta["attack"] = list(map(dict_tuple_to_list_values, meta.get("attack", [])))
meta["mbc"] = list(map(dict_tuple_to_list_values, meta.get("mbc", [])))
def attack_to_pb2(attack: rd.AttackSpec) -> capa_pb2.AttackSpec:
return capa_pb2.AttackSpec(
parts=list(attack.parts),
tactic=attack.tactic,
technique=attack.technique,
subtechnique=attack.subtechnique,
id=attack.id,
)
return google.protobuf.json_format.ParseDict(meta, capa_pb2.RuleMetadata())
def mbc_to_pb2(mbc: rd.MBCSpec) -> capa_pb2.MBCSpec:
return capa_pb2.MBCSpec(
parts=list(mbc.parts),
objective=mbc.objective,
behavior=mbc.behavior,
method=mbc.method,
id=mbc.id,
)
def maec_to_pb2(maec: rd.MaecMetadata) -> capa_pb2.MaecMetadata:
return capa_pb2.MaecMetadata(
analysis_conclusion=maec.analysis_conclusion or "",
analysis_conclusion_ov=maec.analysis_conclusion_ov or "",
malware_family=maec.malware_family or "",
malware_category=maec.malware_category or "",
malware_category_ov=maec.malware_category_ov or "",
)
def rule_metadata_to_pb2(rule_metadata: rd.RuleMetadata) -> capa_pb2.RuleMetadata:
return capa_pb2.RuleMetadata(
name=rule_metadata.name,
namespace=rule_metadata.namespace or "",
authors=rule_metadata.authors,
attack=[attack_to_pb2(m) for m in rule_metadata.attack],
mbc=[mbc_to_pb2(m) for m in rule_metadata.mbc],
references=rule_metadata.references,
examples=rule_metadata.examples,
description=rule_metadata.description,
lib=rule_metadata.lib,
maec=maec_to_pb2(rule_metadata.maec),
is_subscope_rule=rule_metadata.is_subscope_rule,
scopes=scopes_to_pb2(rule_metadata.scopes),
)
def doc_to_pb2(doc: rd.ResultDocument) -> capa_pb2.ResultDocument:
@@ -459,6 +610,24 @@ def addr_from_pb2(addr: capa_pb2.Address) -> frz.Address:
offset = addr.token_offset.offset
return frz.Address(type=frz.AddressType.DN_TOKEN_OFFSET, value=(token, offset))
elif addr.type == capa_pb2.AddressType.ADDRESSTYPE_PROCESS:
ppid = int_from_pb2(addr.ppid_pid.ppid)
pid = int_from_pb2(addr.ppid_pid.pid)
return frz.Address(type=frz.AddressType.PROCESS, value=(ppid, pid))
elif addr.type == capa_pb2.AddressType.ADDRESSTYPE_THREAD:
ppid = int_from_pb2(addr.ppid_pid_tid.ppid)
pid = int_from_pb2(addr.ppid_pid_tid.pid)
tid = int_from_pb2(addr.ppid_pid_tid.tid)
return frz.Address(type=frz.AddressType.THREAD, value=(ppid, pid, tid))
elif addr.type == capa_pb2.AddressType.ADDRESSTYPE_CALL:
ppid = int_from_pb2(addr.ppid_pid_tid_id.ppid)
pid = int_from_pb2(addr.ppid_pid_tid_id.pid)
tid = int_from_pb2(addr.ppid_pid_tid_id.tid)
id_ = int_from_pb2(addr.ppid_pid_tid_id.id)
return frz.Address(type=frz.AddressType.CALL, value=(ppid, pid, tid, id_))
elif addr.type == capa_pb2.AddressType.ADDRESSTYPE_NO_ADDRESS:
return frz.Address(type=frz.AddressType.NO_ADDRESS, value=None)
@@ -475,63 +644,146 @@ def scope_from_pb2(scope: capa_pb2.Scope.ValueType) -> capa.rules.Scope:
return capa.rules.Scope.BASIC_BLOCK
elif scope == capa_pb2.Scope.SCOPE_INSTRUCTION:
return capa.rules.Scope.INSTRUCTION
elif scope == capa_pb2.Scope.SCOPE_PROCESS:
return capa.rules.Scope.PROCESS
elif scope == capa_pb2.Scope.SCOPE_THREAD:
return capa.rules.Scope.THREAD
elif scope == capa_pb2.Scope.SCOPE_CALL:
return capa.rules.Scope.CALL
else:
assert_never(scope)
def metadata_from_pb2(meta: capa_pb2.Metadata) -> rd.Metadata:
return rd.Metadata(
timestamp=datetime.datetime.fromisoformat(meta.timestamp),
version=meta.version,
argv=tuple(meta.argv) if meta.argv else None,
sample=rd.Sample(
md5=meta.sample.md5,
sha1=meta.sample.sha1,
sha256=meta.sample.sha256,
path=meta.sample.path,
),
analysis=rd.Analysis(
format=meta.analysis.format,
arch=meta.analysis.arch,
os=meta.analysis.os,
extractor=meta.analysis.extractor,
rules=tuple(meta.analysis.rules),
base_address=addr_from_pb2(meta.analysis.base_address),
layout=rd.Layout(
functions=tuple(
[
rd.FunctionLayout(
address=addr_from_pb2(f.address),
matched_basic_blocks=tuple(
[
rd.BasicBlockLayout(address=addr_from_pb2(bb.address))
for bb in f.matched_basic_blocks
]
),
)
for f in meta.analysis.layout.functions
]
)
),
feature_counts=rd.FeatureCounts(
file=meta.analysis.feature_counts.file,
functions=tuple(
[
rd.FunctionFeatureCount(address=addr_from_pb2(f.address), count=f.count)
for f in meta.analysis.feature_counts.functions
]
),
),
library_functions=tuple(
def scopes_from_pb2(scopes: capa_pb2.Scopes) -> capa.rules.Scopes:
return capa.rules.Scopes(
static=scope_from_pb2(scopes.static) if scopes.static else None,
dynamic=scope_from_pb2(scopes.dynamic) if scopes.dynamic else None,
)
def flavor_from_pb2(flavor: capa_pb2.Flavor.ValueType) -> rd.Flavor:
if flavor == capa_pb2.Flavor.FLAVOR_STATIC:
return rd.Flavor.STATIC
elif flavor == capa_pb2.Flavor.FLAVOR_DYNAMIC:
return rd.Flavor.DYNAMIC
else:
assert_never(flavor)
def static_analysis_from_pb2(analysis: capa_pb2.StaticAnalysis) -> rd.StaticAnalysis:
return rd.StaticAnalysis(
format=analysis.format,
arch=analysis.arch,
os=analysis.os,
extractor=analysis.extractor,
rules=tuple(analysis.rules),
base_address=addr_from_pb2(analysis.base_address),
layout=rd.StaticLayout(
functions=tuple(
[
rd.LibraryFunction(address=addr_from_pb2(lf.address), name=lf.name)
for lf in meta.analysis.library_functions
rd.FunctionLayout(
address=addr_from_pb2(f.address),
matched_basic_blocks=tuple(
[rd.BasicBlockLayout(address=addr_from_pb2(bb.address)) for bb in f.matched_basic_blocks]
),
)
for f in analysis.layout.functions
]
)
),
feature_counts=rd.StaticFeatureCounts(
file=analysis.feature_counts.file,
functions=tuple(
[
rd.FunctionFeatureCount(address=addr_from_pb2(f.address), count=f.count)
for f in analysis.feature_counts.functions
]
),
),
library_functions=tuple(
[rd.LibraryFunction(address=addr_from_pb2(lf.address), name=lf.name) for lf in analysis.library_functions]
),
)
def dynamic_analysis_from_pb2(analysis: capa_pb2.DynamicAnalysis) -> rd.DynamicAnalysis:
return rd.DynamicAnalysis(
format=analysis.format,
arch=analysis.arch,
os=analysis.os,
extractor=analysis.extractor,
rules=tuple(analysis.rules),
layout=rd.DynamicLayout(
processes=tuple(
[
rd.ProcessLayout(
address=addr_from_pb2(p.address),
name=p.name,
matched_threads=tuple(
[
rd.ThreadLayout(
address=addr_from_pb2(t.address),
matched_calls=tuple(
[
rd.CallLayout(address=addr_from_pb2(c.address), name=c.name)
for c in t.matched_calls
]
),
)
for t in p.matched_threads
]
),
)
for p in analysis.layout.processes
]
)
),
feature_counts=rd.DynamicFeatureCounts(
file=analysis.feature_counts.file,
processes=tuple(
[
rd.ProcessFeatureCount(address=addr_from_pb2(p.address), count=p.count)
for p in analysis.feature_counts.processes
]
),
),
)
def metadata_from_pb2(meta: capa_pb2.Metadata) -> rd.Metadata:
analysis_type = meta.WhichOneof("analysis2")
if analysis_type == "static_analysis":
return rd.Metadata(
timestamp=datetime.datetime.fromisoformat(meta.timestamp),
version=meta.version,
argv=tuple(meta.argv) if meta.argv else None,
sample=rd.Sample(
md5=meta.sample.md5,
sha1=meta.sample.sha1,
sha256=meta.sample.sha256,
path=meta.sample.path,
),
flavor=flavor_from_pb2(meta.flavor),
analysis=static_analysis_from_pb2(meta.static_analysis),
)
elif analysis_type == "dynamic_analysis":
return rd.Metadata(
timestamp=datetime.datetime.fromisoformat(meta.timestamp),
version=meta.version,
argv=tuple(meta.argv) if meta.argv else None,
sample=rd.Sample(
md5=meta.sample.md5,
sha1=meta.sample.sha1,
sha256=meta.sample.sha256,
path=meta.sample.path,
),
flavor=flavor_from_pb2(meta.flavor),
analysis=dynamic_analysis_from_pb2(meta.dynamic_analysis),
)
else:
assert_never(analysis_type)
def statement_from_pb2(statement: capa_pb2.StatementNode) -> rd.Statement:
type_ = statement.WhichOneof("statement")
@@ -711,7 +963,7 @@ def rule_metadata_from_pb2(pb: capa_pb2.RuleMetadata) -> rd.RuleMetadata:
name=pb.name,
namespace=pb.namespace or None,
authors=tuple(pb.authors),
scope=scope_from_pb2(pb.scope),
scopes=scopes_from_pb2(pb.scopes),
attack=tuple([attack_from_pb2(attack) for attack in pb.attack]),
mbc=tuple([mbc_from_pb2(mbc) for mbc in pb.mbc]),
references=tuple(pb.references),

View File

@@ -11,6 +11,9 @@ message Address {
oneof value {
Integer v = 2;
Token_Offset token_offset = 3;
Ppid_Pid ppid_pid = 4;
Ppid_Pid_Tid ppid_pid_tid = 5;
Ppid_Pid_Tid_Id ppid_pid_tid_id = 6;
};
}
@@ -22,6 +25,9 @@ enum AddressType {
ADDRESSTYPE_DN_TOKEN = 4;
ADDRESSTYPE_DN_TOKEN_OFFSET = 5;
ADDRESSTYPE_NO_ADDRESS = 6;
ADDRESSTYPE_PROCESS = 7;
ADDRESSTYPE_THREAD = 8;
ADDRESSTYPE_CALL = 9;
}
message Analysis {
@@ -82,6 +88,25 @@ message CompoundStatement {
optional string description = 2;
}
message DynamicAnalysis {
string format = 1;
string arch = 2;
string os = 3;
string extractor = 4;
repeated string rules = 5;
DynamicLayout layout = 6;
DynamicFeatureCounts feature_counts = 7;
}
message DynamicFeatureCounts {
uint64 file = 1;
repeated ProcessFeatureCount processes = 2;
}
message DynamicLayout {
repeated ProcessLayout processes = 1;
}
message ExportFeature {
string type = 1;
string export = 2;
@@ -192,12 +217,26 @@ message MatchFeature {
optional string description = 3;
}
enum Flavor {
FLAVOR_UNSPECIFIED = 0;
FLAVOR_STATIC = 1;
FLAVOR_DYNAMIC = 2;
}
message Metadata {
string timestamp = 1; // iso8601 format, like: 2019-01-01T00:00:00Z
string version = 2;
repeated string argv = 3;
Sample sample = 4;
Analysis analysis = 5;
// deprecated in v7.0.
// use analysis2 instead.
Analysis analysis = 5 [deprecated = true];
Flavor flavor = 6;
oneof analysis2 {
// use analysis2 instead of analysis (deprecated in v7.0).
StaticAnalysis static_analysis = 7;
DynamicAnalysis dynamic_analysis = 8;
};
}
message MnemonicFeature {
@@ -244,6 +283,17 @@ message OperandOffsetFeature {
optional string description = 4;
}
message ProcessFeatureCount {
Address address = 1;
uint64 count = 2;
}
message ProcessLayout {
Address address = 1;
repeated ThreadLayout matched_threads = 2;
string name = 3;
}
message PropertyFeature {
string type = 1;
string property_ = 2; // property is a Python top-level decorator name
@@ -281,7 +331,9 @@ message RuleMetadata {
string name = 1;
string namespace = 2;
repeated string authors = 3;
Scope scope = 4;
// deprecated in v7.0.
// use scopes instead.
Scope scope = 4 [deprecated = true];
repeated AttackSpec attack = 5;
repeated MBCSpec mbc = 6;
repeated string references = 7;
@@ -290,6 +342,8 @@ message RuleMetadata {
bool lib = 10;
MaecMetadata maec = 11;
bool is_subscope_rule = 12;
// use scopes over scope (deprecated in v7.0).
Scopes scopes = 13;
}
message Sample {
@@ -305,6 +359,14 @@ enum Scope {
SCOPE_FUNCTION = 2;
SCOPE_BASIC_BLOCK = 3;
SCOPE_INSTRUCTION = 4;
SCOPE_PROCESS = 5;
SCOPE_THREAD = 6;
SCOPE_CALL = 7;
}
message Scopes {
optional Scope static = 1;
optional Scope dynamic = 2;
}
message SectionFeature {
@@ -329,6 +391,27 @@ message StatementNode {
};
}
message StaticAnalysis {
string format = 1;
string arch = 2;
string os = 3;
string extractor = 4;
repeated string rules = 5;
Address base_address = 6;
StaticLayout layout = 7;
StaticFeatureCounts feature_counts = 8;
repeated LibraryFunction library_functions = 9;
}
message StaticFeatureCounts {
uint64 file = 1;
repeated FunctionFeatureCount functions = 2;
}
message StaticLayout {
repeated FunctionLayout functions = 1;
}
message StringFeature {
string type = 1;
string string = 2;
@@ -347,6 +430,16 @@ message SubstringFeature {
optional string description = 3;
}
message CallLayout {
Address address = 1;
string name = 2;
}
message ThreadLayout {
Address address = 1;
repeated CallLayout matched_calls = 2;
}
message Addresses { repeated Address address = 1; }
message Pair_Address_Match {
@@ -359,6 +452,24 @@ message Token_Offset {
uint64 offset = 2; // offset is always >= 0
}
message Ppid_Pid {
Integer ppid = 1;
Integer pid = 2;
}
message Ppid_Pid_Tid {
Integer ppid = 1;
Integer pid = 2;
Integer tid = 3;
}
message Ppid_Pid_Tid_Id {
Integer ppid = 1;
Integer pid = 2;
Integer tid = 3;
Integer id = 4;
}
message Integer { oneof value { uint64 u = 1; sint64 i = 2; } } // unsigned or signed int
message Number { oneof value { uint64 u = 1; sint64 i = 2; double f = 3; } }

File diff suppressed because one or more lines are too long

View File

@@ -31,6 +31,9 @@ class _AddressTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._En
ADDRESSTYPE_DN_TOKEN: _AddressType.ValueType # 4
ADDRESSTYPE_DN_TOKEN_OFFSET: _AddressType.ValueType # 5
ADDRESSTYPE_NO_ADDRESS: _AddressType.ValueType # 6
ADDRESSTYPE_PROCESS: _AddressType.ValueType # 7
ADDRESSTYPE_THREAD: _AddressType.ValueType # 8
ADDRESSTYPE_CALL: _AddressType.ValueType # 9
class AddressType(_AddressType, metaclass=_AddressTypeEnumTypeWrapper): ...
@@ -41,8 +44,28 @@ ADDRESSTYPE_FILE: AddressType.ValueType # 3
ADDRESSTYPE_DN_TOKEN: AddressType.ValueType # 4
ADDRESSTYPE_DN_TOKEN_OFFSET: AddressType.ValueType # 5
ADDRESSTYPE_NO_ADDRESS: AddressType.ValueType # 6
ADDRESSTYPE_PROCESS: AddressType.ValueType # 7
ADDRESSTYPE_THREAD: AddressType.ValueType # 8
ADDRESSTYPE_CALL: AddressType.ValueType # 9
global___AddressType = AddressType
class _Flavor:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FlavorEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_Flavor.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FLAVOR_UNSPECIFIED: _Flavor.ValueType # 0
FLAVOR_STATIC: _Flavor.ValueType # 1
FLAVOR_DYNAMIC: _Flavor.ValueType # 2
class Flavor(_Flavor, metaclass=_FlavorEnumTypeWrapper): ...
FLAVOR_UNSPECIFIED: Flavor.ValueType # 0
FLAVOR_STATIC: Flavor.ValueType # 1
FLAVOR_DYNAMIC: Flavor.ValueType # 2
global___Flavor = Flavor
class _Scope:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
@@ -54,6 +77,9 @@ class _ScopeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumType
SCOPE_FUNCTION: _Scope.ValueType # 2
SCOPE_BASIC_BLOCK: _Scope.ValueType # 3
SCOPE_INSTRUCTION: _Scope.ValueType # 4
SCOPE_PROCESS: _Scope.ValueType # 5
SCOPE_THREAD: _Scope.ValueType # 6
SCOPE_CALL: _Scope.ValueType # 7
class Scope(_Scope, metaclass=_ScopeEnumTypeWrapper): ...
@@ -62,6 +88,9 @@ SCOPE_FILE: Scope.ValueType # 1
SCOPE_FUNCTION: Scope.ValueType # 2
SCOPE_BASIC_BLOCK: Scope.ValueType # 3
SCOPE_INSTRUCTION: Scope.ValueType # 4
SCOPE_PROCESS: Scope.ValueType # 5
SCOPE_THREAD: Scope.ValueType # 6
SCOPE_CALL: Scope.ValueType # 7
global___Scope = Scope
@typing_extensions.final
@@ -94,21 +123,33 @@ class Address(google.protobuf.message.Message):
TYPE_FIELD_NUMBER: builtins.int
V_FIELD_NUMBER: builtins.int
TOKEN_OFFSET_FIELD_NUMBER: builtins.int
PPID_PID_FIELD_NUMBER: builtins.int
PPID_PID_TID_FIELD_NUMBER: builtins.int
PPID_PID_TID_ID_FIELD_NUMBER: builtins.int
type: global___AddressType.ValueType
@property
def v(self) -> global___Integer: ...
@property
def token_offset(self) -> global___Token_Offset: ...
@property
def ppid_pid(self) -> global___Ppid_Pid: ...
@property
def ppid_pid_tid(self) -> global___Ppid_Pid_Tid: ...
@property
def ppid_pid_tid_id(self) -> global___Ppid_Pid_Tid_Id: ...
def __init__(
self,
*,
type: global___AddressType.ValueType = ...,
v: global___Integer | None = ...,
token_offset: global___Token_Offset | None = ...,
ppid_pid: global___Ppid_Pid | None = ...,
ppid_pid_tid: global___Ppid_Pid_Tid | None = ...,
ppid_pid_tid_id: global___Ppid_Pid_Tid_Id | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["token_offset", b"token_offset", "v", b"v", "value", b"value"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["token_offset", b"token_offset", "type", b"type", "v", b"v", "value", b"value"]) -> None: ...
def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["v", "token_offset"] | None: ...
def HasField(self, field_name: typing_extensions.Literal["ppid_pid", b"ppid_pid", "ppid_pid_tid", b"ppid_pid_tid", "ppid_pid_tid_id", b"ppid_pid_tid_id", "token_offset", b"token_offset", "v", b"v", "value", b"value"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["ppid_pid", b"ppid_pid", "ppid_pid_tid", b"ppid_pid_tid", "ppid_pid_tid_id", b"ppid_pid_tid_id", "token_offset", b"token_offset", "type", b"type", "v", b"v", "value", b"value"]) -> None: ...
def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["v", "token_offset", "ppid_pid", "ppid_pid_tid", "ppid_pid_tid_id"] | None: ...
global___Address = Address
@@ -335,6 +376,78 @@ class CompoundStatement(google.protobuf.message.Message):
global___CompoundStatement = CompoundStatement
@typing_extensions.final
class DynamicAnalysis(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
FORMAT_FIELD_NUMBER: builtins.int
ARCH_FIELD_NUMBER: builtins.int
OS_FIELD_NUMBER: builtins.int
EXTRACTOR_FIELD_NUMBER: builtins.int
RULES_FIELD_NUMBER: builtins.int
LAYOUT_FIELD_NUMBER: builtins.int
FEATURE_COUNTS_FIELD_NUMBER: builtins.int
format: builtins.str
arch: builtins.str
os: builtins.str
extractor: builtins.str
@property
def rules(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ...
@property
def layout(self) -> global___DynamicLayout: ...
@property
def feature_counts(self) -> global___DynamicFeatureCounts: ...
def __init__(
self,
*,
format: builtins.str = ...,
arch: builtins.str = ...,
os: builtins.str = ...,
extractor: builtins.str = ...,
rules: collections.abc.Iterable[builtins.str] | None = ...,
layout: global___DynamicLayout | None = ...,
feature_counts: global___DynamicFeatureCounts | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["feature_counts", b"feature_counts", "layout", b"layout"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["arch", b"arch", "extractor", b"extractor", "feature_counts", b"feature_counts", "format", b"format", "layout", b"layout", "os", b"os", "rules", b"rules"]) -> None: ...
global___DynamicAnalysis = DynamicAnalysis
@typing_extensions.final
class DynamicFeatureCounts(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
FILE_FIELD_NUMBER: builtins.int
PROCESSES_FIELD_NUMBER: builtins.int
file: builtins.int
@property
def processes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ProcessFeatureCount]: ...
def __init__(
self,
*,
file: builtins.int = ...,
processes: collections.abc.Iterable[global___ProcessFeatureCount] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["file", b"file", "processes", b"processes"]) -> None: ...
global___DynamicFeatureCounts = DynamicFeatureCounts
@typing_extensions.final
class DynamicLayout(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PROCESSES_FIELD_NUMBER: builtins.int
@property
def processes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ProcessLayout]: ...
def __init__(
self,
*,
processes: collections.abc.Iterable[global___ProcessLayout] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["processes", b"processes"]) -> None: ...
global___DynamicLayout = DynamicLayout
@typing_extensions.final
class ExportFeature(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -776,6 +889,9 @@ class Metadata(google.protobuf.message.Message):
ARGV_FIELD_NUMBER: builtins.int
SAMPLE_FIELD_NUMBER: builtins.int
ANALYSIS_FIELD_NUMBER: builtins.int
FLAVOR_FIELD_NUMBER: builtins.int
STATIC_ANALYSIS_FIELD_NUMBER: builtins.int
DYNAMIC_ANALYSIS_FIELD_NUMBER: builtins.int
timestamp: builtins.str
"""iso8601 format, like: 2019-01-01T00:00:00Z"""
version: builtins.str
@@ -784,7 +900,16 @@ class Metadata(google.protobuf.message.Message):
@property
def sample(self) -> global___Sample: ...
@property
def analysis(self) -> global___Analysis: ...
def analysis(self) -> global___Analysis:
"""deprecated in v7.0.
use analysis2 instead.
"""
flavor: global___Flavor.ValueType
@property
def static_analysis(self) -> global___StaticAnalysis:
"""use analysis2 instead of analysis (deprecated in v7.0)."""
@property
def dynamic_analysis(self) -> global___DynamicAnalysis: ...
def __init__(
self,
*,
@@ -793,9 +918,13 @@ class Metadata(google.protobuf.message.Message):
argv: collections.abc.Iterable[builtins.str] | None = ...,
sample: global___Sample | None = ...,
analysis: global___Analysis | None = ...,
flavor: global___Flavor.ValueType = ...,
static_analysis: global___StaticAnalysis | None = ...,
dynamic_analysis: global___DynamicAnalysis | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["analysis", b"analysis", "sample", b"sample"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["analysis", b"analysis", "argv", b"argv", "sample", b"sample", "timestamp", b"timestamp", "version", b"version"]) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["analysis", b"analysis", "analysis2", b"analysis2", "dynamic_analysis", b"dynamic_analysis", "sample", b"sample", "static_analysis", b"static_analysis"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["analysis", b"analysis", "analysis2", b"analysis2", "argv", b"argv", "dynamic_analysis", b"dynamic_analysis", "flavor", b"flavor", "sample", b"sample", "static_analysis", b"static_analysis", "timestamp", b"timestamp", "version", b"version"]) -> None: ...
def WhichOneof(self, oneof_group: typing_extensions.Literal["analysis2", b"analysis2"]) -> typing_extensions.Literal["static_analysis", "dynamic_analysis"] | None: ...
global___Metadata = Metadata
@@ -973,6 +1102,50 @@ class OperandOffsetFeature(google.protobuf.message.Message):
global___OperandOffsetFeature = OperandOffsetFeature
@typing_extensions.final
class ProcessFeatureCount(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ADDRESS_FIELD_NUMBER: builtins.int
COUNT_FIELD_NUMBER: builtins.int
@property
def address(self) -> global___Address: ...
count: builtins.int
def __init__(
self,
*,
address: global___Address | None = ...,
count: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "count", b"count"]) -> None: ...
global___ProcessFeatureCount = ProcessFeatureCount
@typing_extensions.final
class ProcessLayout(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ADDRESS_FIELD_NUMBER: builtins.int
MATCHED_THREADS_FIELD_NUMBER: builtins.int
NAME_FIELD_NUMBER: builtins.int
@property
def address(self) -> global___Address: ...
@property
def matched_threads(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ThreadLayout]: ...
name: builtins.str
def __init__(
self,
*,
address: global___Address | None = ...,
matched_threads: collections.abc.Iterable[global___ThreadLayout] | None = ...,
name: builtins.str = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "matched_threads", b"matched_threads", "name", b"name"]) -> None: ...
global___ProcessLayout = ProcessLayout
@typing_extensions.final
class PropertyFeature(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -1136,11 +1309,15 @@ class RuleMetadata(google.protobuf.message.Message):
LIB_FIELD_NUMBER: builtins.int
MAEC_FIELD_NUMBER: builtins.int
IS_SUBSCOPE_RULE_FIELD_NUMBER: builtins.int
SCOPES_FIELD_NUMBER: builtins.int
name: builtins.str
namespace: builtins.str
@property
def authors(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ...
scope: global___Scope.ValueType
"""deprecated in v7.0.
use scopes instead.
"""
@property
def attack(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___AttackSpec]: ...
@property
@@ -1154,6 +1331,9 @@ class RuleMetadata(google.protobuf.message.Message):
@property
def maec(self) -> global___MaecMetadata: ...
is_subscope_rule: builtins.bool
@property
def scopes(self) -> global___Scopes:
"""use scopes over scope (deprecated in v7.0)."""
def __init__(
self,
*,
@@ -1169,9 +1349,10 @@ class RuleMetadata(google.protobuf.message.Message):
lib: builtins.bool = ...,
maec: global___MaecMetadata | None = ...,
is_subscope_rule: builtins.bool = ...,
scopes: global___Scopes | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["maec", b"maec"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["attack", b"attack", "authors", b"authors", "description", b"description", "examples", b"examples", "is_subscope_rule", b"is_subscope_rule", "lib", b"lib", "maec", b"maec", "mbc", b"mbc", "name", b"name", "namespace", b"namespace", "references", b"references", "scope", b"scope"]) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["maec", b"maec", "scopes", b"scopes"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["attack", b"attack", "authors", b"authors", "description", b"description", "examples", b"examples", "is_subscope_rule", b"is_subscope_rule", "lib", b"lib", "maec", b"maec", "mbc", b"mbc", "name", b"name", "namespace", b"namespace", "references", b"references", "scope", b"scope", "scopes", b"scopes"]) -> None: ...
global___RuleMetadata = RuleMetadata
@@ -1199,6 +1380,29 @@ class Sample(google.protobuf.message.Message):
global___Sample = Sample
@typing_extensions.final
class Scopes(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
STATIC_FIELD_NUMBER: builtins.int
DYNAMIC_FIELD_NUMBER: builtins.int
static: global___Scope.ValueType
dynamic: global___Scope.ValueType
def __init__(
self,
*,
static: global___Scope.ValueType | None = ...,
dynamic: global___Scope.ValueType | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["_dynamic", b"_dynamic", "_static", b"_static", "dynamic", b"dynamic", "static", b"static"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["_dynamic", b"_dynamic", "_static", b"_static", "dynamic", b"dynamic", "static", b"static"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing_extensions.Literal["_dynamic", b"_dynamic"]) -> typing_extensions.Literal["dynamic"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing_extensions.Literal["_static", b"_static"]) -> typing_extensions.Literal["static"] | None: ...
global___Scopes = Scopes
@typing_extensions.final
class SectionFeature(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -1278,6 +1482,86 @@ class StatementNode(google.protobuf.message.Message):
global___StatementNode = StatementNode
@typing_extensions.final
class StaticAnalysis(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
FORMAT_FIELD_NUMBER: builtins.int
ARCH_FIELD_NUMBER: builtins.int
OS_FIELD_NUMBER: builtins.int
EXTRACTOR_FIELD_NUMBER: builtins.int
RULES_FIELD_NUMBER: builtins.int
BASE_ADDRESS_FIELD_NUMBER: builtins.int
LAYOUT_FIELD_NUMBER: builtins.int
FEATURE_COUNTS_FIELD_NUMBER: builtins.int
LIBRARY_FUNCTIONS_FIELD_NUMBER: builtins.int
format: builtins.str
arch: builtins.str
os: builtins.str
extractor: builtins.str
@property
def rules(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ...
@property
def base_address(self) -> global___Address: ...
@property
def layout(self) -> global___StaticLayout: ...
@property
def feature_counts(self) -> global___StaticFeatureCounts: ...
@property
def library_functions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LibraryFunction]: ...
def __init__(
self,
*,
format: builtins.str = ...,
arch: builtins.str = ...,
os: builtins.str = ...,
extractor: builtins.str = ...,
rules: collections.abc.Iterable[builtins.str] | None = ...,
base_address: global___Address | None = ...,
layout: global___StaticLayout | None = ...,
feature_counts: global___StaticFeatureCounts | None = ...,
library_functions: collections.abc.Iterable[global___LibraryFunction] | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["base_address", b"base_address", "feature_counts", b"feature_counts", "layout", b"layout"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["arch", b"arch", "base_address", b"base_address", "extractor", b"extractor", "feature_counts", b"feature_counts", "format", b"format", "layout", b"layout", "library_functions", b"library_functions", "os", b"os", "rules", b"rules"]) -> None: ...
global___StaticAnalysis = StaticAnalysis
@typing_extensions.final
class StaticFeatureCounts(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
FILE_FIELD_NUMBER: builtins.int
FUNCTIONS_FIELD_NUMBER: builtins.int
file: builtins.int
@property
def functions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___FunctionFeatureCount]: ...
def __init__(
self,
*,
file: builtins.int = ...,
functions: collections.abc.Iterable[global___FunctionFeatureCount] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["file", b"file", "functions", b"functions"]) -> None: ...
global___StaticFeatureCounts = StaticFeatureCounts
@typing_extensions.final
class StaticLayout(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
FUNCTIONS_FIELD_NUMBER: builtins.int
@property
def functions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___FunctionLayout]: ...
def __init__(
self,
*,
functions: collections.abc.Iterable[global___FunctionLayout] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["functions", b"functions"]) -> None: ...
global___StaticLayout = StaticLayout
@typing_extensions.final
class StringFeature(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -1347,6 +1631,47 @@ class SubstringFeature(google.protobuf.message.Message):
global___SubstringFeature = SubstringFeature
@typing_extensions.final
class CallLayout(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ADDRESS_FIELD_NUMBER: builtins.int
NAME_FIELD_NUMBER: builtins.int
@property
def address(self) -> global___Address: ...
name: builtins.str
def __init__(
self,
*,
address: global___Address | None = ...,
name: builtins.str = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "name", b"name"]) -> None: ...
global___CallLayout = CallLayout
@typing_extensions.final
class ThreadLayout(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ADDRESS_FIELD_NUMBER: builtins.int
MATCHED_CALLS_FIELD_NUMBER: builtins.int
@property
def address(self) -> global___Address: ...
@property
def matched_calls(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CallLayout]: ...
def __init__(
self,
*,
address: global___Address | None = ...,
matched_calls: collections.abc.Iterable[global___CallLayout] | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "matched_calls", b"matched_calls"]) -> None: ...
global___ThreadLayout = ThreadLayout
@typing_extensions.final
class Addresses(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -1405,6 +1730,81 @@ class Token_Offset(google.protobuf.message.Message):
global___Token_Offset = Token_Offset
@typing_extensions.final
class Ppid_Pid(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PPID_FIELD_NUMBER: builtins.int
PID_FIELD_NUMBER: builtins.int
@property
def ppid(self) -> global___Integer: ...
@property
def pid(self) -> global___Integer: ...
def __init__(
self,
*,
ppid: global___Integer | None = ...,
pid: global___Integer | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["pid", b"pid", "ppid", b"ppid"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["pid", b"pid", "ppid", b"ppid"]) -> None: ...
global___Ppid_Pid = Ppid_Pid
@typing_extensions.final
class Ppid_Pid_Tid(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PPID_FIELD_NUMBER: builtins.int
PID_FIELD_NUMBER: builtins.int
TID_FIELD_NUMBER: builtins.int
@property
def ppid(self) -> global___Integer: ...
@property
def pid(self) -> global___Integer: ...
@property
def tid(self) -> global___Integer: ...
def __init__(
self,
*,
ppid: global___Integer | None = ...,
pid: global___Integer | None = ...,
tid: global___Integer | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> None: ...
global___Ppid_Pid_Tid = Ppid_Pid_Tid
@typing_extensions.final
class Ppid_Pid_Tid_Id(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PPID_FIELD_NUMBER: builtins.int
PID_FIELD_NUMBER: builtins.int
TID_FIELD_NUMBER: builtins.int
ID_FIELD_NUMBER: builtins.int
@property
def ppid(self) -> global___Integer: ...
@property
def pid(self) -> global___Integer: ...
@property
def tid(self) -> global___Integer: ...
@property
def id(self) -> global___Integer: ...
def __init__(
self,
*,
ppid: global___Integer | None = ...,
pid: global___Integer | None = ...,
tid: global___Integer | None = ...,
id: global___Integer | None = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["id", b"id", "pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["id", b"id", "pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> None: ...
global___Ppid_Pid_Tid_Id = Ppid_Pid_Tid_Id
@typing_extensions.final
class Integer(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

View File

@@ -7,10 +7,12 @@
# See the License for the specific language governing permissions and limitations under the License.
import datetime
import collections
from enum import Enum
from typing import Dict, List, Tuple, Union, Literal, Optional
from pathlib import Path
from pydantic import Field, BaseModel, ConfigDict
from typing_extensions import TypeAlias
import capa.rules
import capa.engine
@@ -47,10 +49,33 @@ class FunctionLayout(Model):
matched_basic_blocks: Tuple[BasicBlockLayout, ...]
class Layout(Model):
class CallLayout(Model):
address: frz.Address
name: str
class ThreadLayout(Model):
address: frz.Address
matched_calls: Tuple[CallLayout, ...]
class ProcessLayout(Model):
address: frz.Address
name: str
matched_threads: Tuple[ThreadLayout, ...]
class StaticLayout(Model):
functions: Tuple[FunctionLayout, ...]
class DynamicLayout(Model):
processes: Tuple[ProcessLayout, ...]
Layout: TypeAlias = Union[StaticLayout, DynamicLayout]
class LibraryFunction(Model):
address: frz.Address
name: str
@@ -61,31 +86,73 @@ class FunctionFeatureCount(Model):
count: int
class FeatureCounts(Model):
class ProcessFeatureCount(Model):
address: frz.Address
count: int
class StaticFeatureCounts(Model):
file: int
functions: Tuple[FunctionFeatureCount, ...]
class Analysis(Model):
class DynamicFeatureCounts(Model):
file: int
processes: Tuple[ProcessFeatureCount, ...]
FeatureCounts: TypeAlias = Union[StaticFeatureCounts, DynamicFeatureCounts]
class StaticAnalysis(Model):
format: str
arch: str
os: str
extractor: str
rules: Tuple[str, ...]
base_address: frz.Address
layout: Layout
feature_counts: FeatureCounts
layout: StaticLayout
feature_counts: StaticFeatureCounts
library_functions: Tuple[LibraryFunction, ...]
class DynamicAnalysis(Model):
format: str
arch: str
os: str
extractor: str
rules: Tuple[str, ...]
layout: DynamicLayout
feature_counts: DynamicFeatureCounts
Analysis: TypeAlias = Union[StaticAnalysis, DynamicAnalysis]
class Flavor(str, Enum):
STATIC = "static"
DYNAMIC = "dynamic"
class Metadata(Model):
timestamp: datetime.datetime
version: str
argv: Optional[Tuple[str, ...]]
sample: Sample
flavor: Flavor
analysis: Analysis
class StaticMetadata(Metadata):
flavor: Flavor = Flavor.STATIC
analysis: StaticAnalysis
class DynamicMetadata(Metadata):
flavor: Flavor = Flavor.DYNAMIC
analysis: DynamicAnalysis
class CompoundStatementType:
AND = "and"
OR = "or"
@@ -155,7 +222,7 @@ def statement_from_capa(node: capa.engine.Statement) -> Statement:
description=node.description,
min=node.min,
max=node.max,
child=frz.feature_from_capa(node.child),
child=frzf.feature_from_capa(node.child),
)
elif isinstance(node, capa.engine.Subscope):
@@ -181,7 +248,7 @@ def node_from_capa(node: Union[capa.engine.Statement, capa.engine.Feature]) -> N
return StatementNode(statement=statement_from_capa(node))
elif isinstance(node, capa.engine.Feature):
return FeatureNode(feature=frz.feature_from_capa(node))
return FeatureNode(feature=frzf.feature_from_capa(node))
else:
assert_never(node)
@@ -308,9 +375,11 @@ class Match(FrozenModel):
# e.g. `contain loop/30c4c78e29bf4d54894fc74f664c62e8` -> `basic block`
#
# note! replace `node`
# subscopes cannot have both a static and dynamic scope set
assert None in (rule.scopes.static, rule.scopes.dynamic)
node = StatementNode(
statement=SubscopeStatement(
scope=rule.meta["scope"],
scope=rule.scopes.static or rule.scopes.dynamic,
)
)
@@ -505,7 +574,7 @@ class RuleMetadata(FrozenModel):
name: str
namespace: Optional[str] = None
authors: Tuple[str, ...]
scope: capa.rules.Scope
scopes: capa.rules.Scopes
attack: Tuple[AttackSpec, ...] = Field(alias="att&ck")
mbc: Tuple[MBCSpec, ...]
references: Tuple[str, ...]
@@ -522,7 +591,7 @@ class RuleMetadata(FrozenModel):
name=rule.meta.get("name"),
namespace=rule.meta.get("namespace"),
authors=rule.meta.get("authors"),
scope=capa.rules.Scope(rule.meta.get("scope")),
scopes=capa.rules.Scopes.from_dict(rule.meta.get("scopes")),
attack=tuple(map(AttackSpec.from_str, rule.meta.get("att&ck", []))),
mbc=tuple(map(MBCSpec.from_str, rule.meta.get("mbc", []))),
references=rule.meta.get("references", []),

View File

@@ -24,6 +24,11 @@ def bold2(s: str) -> str:
return termcolor.colored(s, "green")
def mute(s: str) -> str:
"""draw attention away from the given string"""
return termcolor.colored(s, "dark_grey")
def warn(s: str) -> str:
return termcolor.colored(s, "yellow")

View File

@@ -22,7 +22,7 @@ Unless required by applicable law or agreed to in writing, software distributed
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
"""
import enum
from typing import cast
import tabulate
@@ -54,13 +54,92 @@ def format_address(address: frz.Address) -> str:
assert isinstance(token, int)
assert isinstance(offset, int)
return f"token({capa.helpers.hex(token)})+{capa.helpers.hex(offset)}"
elif address.type == frz.AddressType.PROCESS:
assert isinstance(address.value, tuple)
ppid, pid = address.value
assert isinstance(ppid, int)
assert isinstance(pid, int)
return f"process{{pid:{pid}}}"
elif address.type == frz.AddressType.THREAD:
assert isinstance(address.value, tuple)
ppid, pid, tid = address.value
assert isinstance(ppid, int)
assert isinstance(pid, int)
assert isinstance(tid, int)
return f"process{{pid:{pid},tid:{tid}}}"
elif address.type == frz.AddressType.CALL:
assert isinstance(address.value, tuple)
ppid, pid, tid, id_ = address.value
return f"process{{pid:{pid},tid:{tid},call:{id_}}}"
elif address.type == frz.AddressType.NO_ADDRESS:
return "global"
else:
raise ValueError("unexpected address type")
def render_meta(ostream, doc: rd.ResultDocument):
def _get_process_name(layout: rd.DynamicLayout, addr: frz.Address) -> str:
for p in layout.processes:
if p.address == addr:
return p.name
raise ValueError("name not found for process", addr)
def _get_call_name(layout: rd.DynamicLayout, addr: frz.Address) -> str:
call = addr.to_capa()
assert isinstance(call, capa.features.address.DynamicCallAddress)
thread = frz.Address.from_capa(call.thread)
process = frz.Address.from_capa(call.thread.process)
# danger: O(n**3)
for p in layout.processes:
if p.address == process:
for t in p.matched_threads:
if t.address == thread:
for c in t.matched_calls:
if c.address == addr:
return c.name
raise ValueError("name not found for call", addr)
def render_process(layout: rd.DynamicLayout, addr: frz.Address) -> str:
process = addr.to_capa()
assert isinstance(process, capa.features.address.ProcessAddress)
name = _get_process_name(layout, addr)
return f"{name}{{pid:{process.pid}}}"
def render_thread(layout: rd.DynamicLayout, addr: frz.Address) -> str:
thread = addr.to_capa()
assert isinstance(thread, capa.features.address.ThreadAddress)
name = _get_process_name(layout, frz.Address.from_capa(thread.process))
return f"{name}{{pid:{thread.process.pid},tid:{thread.tid}}}"
def render_call(layout: rd.DynamicLayout, addr: frz.Address) -> str:
call = addr.to_capa()
assert isinstance(call, capa.features.address.DynamicCallAddress)
pname = _get_process_name(layout, frz.Address.from_capa(call.thread.process))
cname = _get_call_name(layout, addr)
fname, _, rest = cname.partition("(")
args, _, rest = rest.rpartition(")")
s = []
s.append(f"{fname}(")
for arg in args.split(", "):
s.append(f" {arg},")
s.append(f"){rest}")
newline = "\n"
return (
f"{pname}{{pid:{call.thread.process.pid},tid:{call.thread.tid},call:{call.id}}}\n{rutils.mute(newline.join(s))}"
)
def render_static_meta(ostream, meta: rd.StaticMetadata):
"""
like:
@@ -73,36 +152,90 @@ def render_meta(ostream, doc: rd.ResultDocument):
os windows
format pe
arch amd64
analysis static
extractor VivisectFeatureExtractor
base address 0x10000000
rules (embedded rules)
function count 42
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", 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)),
("md5", meta.sample.md5),
("sha1", meta.sample.sha1),
("sha256", meta.sample.sha256),
("path", meta.sample.path),
("timestamp", meta.timestamp),
("capa version", meta.version),
("os", meta.analysis.os),
("format", meta.analysis.format),
("arch", meta.analysis.arch),
("analysis", meta.flavor.value),
("extractor", meta.analysis.extractor),
("base address", format_address(meta.analysis.base_address)),
("rules", "\n".join(meta.analysis.rules)),
("function count", len(meta.analysis.feature_counts.functions)),
("library function count", len(meta.analysis.library_functions)),
(
"total feature count",
doc.meta.analysis.feature_counts.file + sum(f.count for f in doc.meta.analysis.feature_counts.functions),
meta.analysis.feature_counts.file + sum(f.count for f in meta.analysis.feature_counts.functions),
),
]
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
def render_dynamic_meta(ostream, meta: rd.DynamicMetadata):
"""
like:
md5 84882c9d43e23d63b82004fae74ebb61
sha1 c6fb3b50d946bec6f391aefa4e54478cf8607211
sha256 5eced7367ed63354b4ed5c556e2363514293f614c2c2eb187273381b2ef5f0f9
path /tmp/packed-report,jspn
timestamp 2023-07-17T10:17:05.796933
capa version 0.0.0
os windows
format pe
arch amd64
extractor CAPEFeatureExtractor
rules (embedded rules)
process count 42
total feature count 1918
"""
rows = [
("md5", meta.sample.md5),
("sha1", meta.sample.sha1),
("sha256", meta.sample.sha256),
("path", meta.sample.path),
("timestamp", meta.timestamp),
("capa version", meta.version),
("os", meta.analysis.os),
("format", meta.analysis.format),
("arch", meta.analysis.arch),
("analysis", meta.flavor.value),
("extractor", meta.analysis.extractor),
("rules", "\n".join(meta.analysis.rules)),
("process count", len(meta.analysis.feature_counts.processes)),
(
"total feature count",
meta.analysis.feature_counts.file + sum(p.count for p in meta.analysis.feature_counts.processes),
),
]
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
def render_meta(osstream, doc: rd.ResultDocument):
if doc.meta.flavor == rd.Flavor.STATIC:
render_static_meta(osstream, cast(rd.StaticMetadata, doc.meta))
elif doc.meta.flavor == rd.Flavor.DYNAMIC:
render_dynamic_meta(osstream, cast(rd.DynamicMetadata, doc.meta))
else:
raise ValueError("invalid meta analysis")
def render_rules(ostream, doc: rd.ResultDocument):
"""
like:
@@ -126,22 +259,55 @@ def render_rules(ostream, doc: rd.ResultDocument):
had_match = True
rows = []
for key in ("namespace", "description", "scope"):
v = getattr(rule.meta, key)
if not v:
continue
if isinstance(v, list) and len(v) == 1:
v = v[0]
ns = rule.meta.namespace
if ns:
rows.append(("namespace", ns))
if isinstance(v, enum.Enum):
v = v.value
desc = rule.meta.description
if desc:
rows.append(("description", desc))
rows.append((key, v))
if doc.meta.flavor == rd.Flavor.STATIC:
scope = rule.meta.scopes.static
elif doc.meta.flavor == rd.Flavor.DYNAMIC:
scope = rule.meta.scopes.dynamic
else:
raise ValueError("invalid meta analysis")
if scope:
rows.append(("scope", scope.value))
if rule.meta.scope != capa.rules.FILE_SCOPE:
if capa.rules.Scope.FILE not in rule.meta.scopes:
locations = [m[0] for m in doc.rules[rule.meta.name].matches]
rows.append(("matches", "\n".join(map(format_address, locations))))
lines = []
if doc.meta.flavor == rd.Flavor.STATIC:
lines = [format_address(loc) for loc in locations]
elif doc.meta.flavor == rd.Flavor.DYNAMIC:
assert rule.meta.scopes.dynamic is not None
assert isinstance(doc.meta.analysis.layout, rd.DynamicLayout)
if rule.meta.scopes.dynamic == capa.rules.Scope.PROCESS:
lines = [render_process(doc.meta.analysis.layout, loc) for loc in locations]
elif rule.meta.scopes.dynamic == capa.rules.Scope.THREAD:
lines = [render_thread(doc.meta.analysis.layout, loc) for loc in locations]
elif rule.meta.scopes.dynamic == capa.rules.Scope.CALL:
# because we're only in verbose mode, we won't show the full call details (name, args, retval)
# we'll only show the details of the thread in which the calls are found.
# so select the thread locations and render those.
thread_locations = set()
for loc in locations:
cloc = loc.to_capa()
assert isinstance(cloc, capa.features.address.DynamicCallAddress)
thread_locations.add(frz.Address.from_capa(cloc.thread))
lines = [render_thread(doc.meta.analysis.layout, loc) for loc in thread_locations]
else:
capa.helpers.assert_never(rule.meta.scopes.dynamic)
else:
capa.helpers.assert_never(doc.meta.flavor)
rows.append(("matches", "\n".join(lines)))
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
ostream.write("\n")

View File

@@ -5,7 +5,8 @@
# 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 textwrap
from typing import Dict, Iterable, Optional
import tabulate
@@ -22,8 +23,29 @@ import capa.features.freeze.features as frzf
from capa.rules import RuleSet
from capa.engine import MatchResults
logger = logging.getLogger(__name__)
def render_locations(ostream, locations: Iterable[frz.Address]):
def hanging_indent(s: str, indent: int) -> str:
"""
indent the given string, except the first line,
such as if the string finishes an existing line.
e.g.,
EXISTINGSTUFFHERE + hanging_indent("xxxx...", 1)
becomes:
EXISTINGSTUFFHERExxxxx
xxxxxx
xxxxxx
"""
prefix = " " * indent
return textwrap.indent(s, prefix=prefix)[len(prefix) :]
def render_locations(ostream, layout: rd.Layout, locations: Iterable[frz.Address], indent: int):
import capa.render.verbose as v
# its possible to have an empty locations array here,
@@ -35,9 +57,23 @@ def render_locations(ostream, locations: Iterable[frz.Address]):
return
ostream.write(" @ ")
location0 = locations[0]
if len(locations) == 1:
ostream.write(v.format_address(locations[0]))
location = locations[0]
if location.type == frz.AddressType.CALL:
assert isinstance(layout, rd.DynamicLayout)
ostream.write(hanging_indent(v.render_call(layout, location), indent + 1))
else:
ostream.write(v.format_address(locations[0]))
elif location0.type == frz.AddressType.CALL and len(locations) > 1:
location = locations[0]
assert isinstance(layout, rd.DynamicLayout)
s = f"{v.render_call(layout, location)}\nand {(len(locations) - 1)} more..."
ostream.write(hanging_indent(s, indent + 1))
elif len(locations) > 4:
# don't display too many locations, because it becomes very noisy.
@@ -52,7 +88,7 @@ def render_locations(ostream, locations: Iterable[frz.Address]):
raise RuntimeError("unreachable")
def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0):
def render_statement(ostream, layout: rd.Layout, match: rd.Match, statement: rd.Statement, indent: int):
ostream.write(" " * indent)
if isinstance(statement, rd.SubscopeStatement):
@@ -114,7 +150,7 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
if statement.description:
ostream.write(f" = {statement.description}")
render_locations(ostream, match.locations)
render_locations(ostream, layout, match.locations, indent)
ostream.writeln("")
else:
@@ -125,7 +161,9 @@ def render_string_value(s: str) -> str:
return f'"{capa.features.common.escape_string(s)}"'
def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
def render_feature(
ostream, layout: rd.Layout, rule: rd.RuleMatches, match: rd.Match, feature: frzf.Feature, indent: int
):
ostream.write(" " * indent)
key = feature.type
@@ -176,8 +214,17 @@ def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
ostream.write(capa.rules.DESCRIPTION_SEPARATOR)
ostream.write(feature.description)
if not isinstance(feature, (frzf.OSFeature, frzf.ArchFeature, frzf.FormatFeature)):
render_locations(ostream, match.locations)
if isinstance(feature, (frzf.OSFeature, frzf.ArchFeature, frzf.FormatFeature)):
# don't show the location of these global features
pass
elif isinstance(layout, rd.DynamicLayout) and rule.meta.scopes.dynamic == capa.rules.Scope.CALL:
# if we're in call scope, then the call will have been rendered at the top
# of the output, so don't re-render it again for each feature.
pass
elif isinstance(feature, (frzf.OSFeature, frzf.ArchFeature, frzf.FormatFeature)):
pass
else:
render_locations(ostream, layout, match.locations, indent)
ostream.write("\n")
else:
# like:
@@ -193,15 +240,19 @@ def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
ostream.write(" " * (indent + 1))
ostream.write("- ")
ostream.write(rutils.bold2(render_string_value(capture)))
render_locations(ostream, locations)
if isinstance(layout, rd.DynamicLayout) and rule.meta.scopes.dynamic == capa.rules.Scope.CALL:
# like above, don't re-render calls when in call scope.
pass
else:
render_locations(ostream, layout, locations, indent=indent)
ostream.write("\n")
def render_node(ostream, match: rd.Match, node: rd.Node, indent=0):
def render_node(ostream, layout: rd.Layout, rule: rd.RuleMatches, match: rd.Match, node: rd.Node, indent: int):
if isinstance(node, rd.StatementNode):
render_statement(ostream, match, node.statement, indent=indent)
render_statement(ostream, layout, match, node.statement, indent=indent)
elif isinstance(node, rd.FeatureNode):
render_feature(ostream, match, node.feature, indent=indent)
render_feature(ostream, layout, rule, match, node.feature, indent=indent)
else:
raise RuntimeError("unexpected node type: " + str(node))
@@ -214,7 +265,7 @@ MODE_SUCCESS = "success"
MODE_FAILURE = "failure"
def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
def render_match(ostream, layout: rd.Layout, rule: rd.RuleMatches, match: rd.Match, indent=0, mode=MODE_SUCCESS):
child_mode = mode
if mode == MODE_SUCCESS:
# display only nodes that evaluated successfully.
@@ -246,10 +297,10 @@ def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
else:
raise RuntimeError("unexpected mode: " + mode)
render_node(ostream, match, match.node, indent=indent)
render_node(ostream, layout, rule, match, match.node, indent=indent)
for child in match.children:
render_match(ostream, child, indent=indent + 1, mode=child_mode)
render_match(ostream, layout, rule, child, indent=indent + 1, mode=child_mode)
def render_rules(ostream, doc: rd.ResultDocument):
@@ -260,7 +311,8 @@ def render_rules(ostream, doc: rd.ResultDocument):
check for OutputDebugString error
namespace anti-analysis/anti-debugging/debugger-detection
author michael.hunhoff@mandiant.com
scope function
static scope: function
dynamic scope: process
mbc Anti-Behavioral Analysis::Detect Debugger::OutputDebugString
function @ 0x10004706
and:
@@ -268,13 +320,20 @@ def render_rules(ostream, doc: rd.ResultDocument):
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()
import capa.render.verbose as v
for bb in finfo.matched_basic_blocks:
bbaddress = bb.address.to_capa()
functions_by_bb[bbaddress] = faddress
functions_by_bb: Dict[capa.features.address.Address, capa.features.address.Address] = {}
if isinstance(doc.meta.analysis, rd.StaticAnalysis):
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
elif isinstance(doc.meta.analysis, rd.DynamicAnalysis):
pass
else:
raise ValueError("invalid analysis field in the document's meta")
had_match = False
@@ -323,7 +382,13 @@ def render_rules(ostream, doc: rd.ResultDocument):
rows.append(("author", ", ".join(rule.meta.authors)))
rows.append(("scope", rule.meta.scope.value))
if doc.meta.flavor == rd.Flavor.STATIC:
assert rule.meta.scopes.static is not None
rows.append(("scope", rule.meta.scopes.static.value))
if doc.meta.flavor == rd.Flavor.DYNAMIC:
assert rule.meta.scopes.dynamic is not None
rows.append(("scope", rule.meta.scopes.dynamic.value))
if rule.meta.attack:
rows.append(("att&ck", ", ".join([rutils.format_parts_id(v) for v in rule.meta.attack])))
@@ -339,7 +404,7 @@ def render_rules(ostream, doc: rd.ResultDocument):
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
if rule.meta.scope == capa.rules.FILE_SCOPE:
if capa.rules.Scope.FILE in rule.meta.scopes:
matches = doc.rules[rule.meta.name].matches
if len(matches) != 1:
# i think there should only ever be one match per file-scope rule,
@@ -347,22 +412,42 @@ def render_rules(ostream, doc: rd.ResultDocument):
# 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(f"unexpected file scope match count: {len(matches)}")
first_address, first_match = matches[0]
render_match(ostream, first_match, indent=0)
_, first_match = matches[0]
render_match(ostream, doc.meta.analysis.layout, rule, first_match, indent=0)
else:
for location, match in sorted(doc.rules[rule.meta.name].matches):
ostream.write(rule.meta.scope)
ostream.write(" @ ")
ostream.write(capa.render.verbose.format_address(location))
if doc.meta.flavor == rd.Flavor.STATIC:
assert rule.meta.scopes.static is not None
ostream.write(rule.meta.scopes.static.value)
ostream.write(" @ ")
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()]))
)
if rule.meta.scopes.static == capa.rules.Scope.BASIC_BLOCK:
func = frz.Address.from_capa(functions_by_bb[location.to_capa()])
ostream.write(f" in function {capa.render.verbose.format_address(func)}")
elif doc.meta.flavor == rd.Flavor.DYNAMIC:
assert rule.meta.scopes.dynamic is not None
assert isinstance(doc.meta.analysis.layout, rd.DynamicLayout)
ostream.write(rule.meta.scopes.dynamic.value)
ostream.write(" @ ")
if rule.meta.scopes.dynamic == capa.rules.Scope.PROCESS:
ostream.write(v.render_process(doc.meta.analysis.layout, location))
elif rule.meta.scopes.dynamic == capa.rules.Scope.THREAD:
ostream.write(v.render_thread(doc.meta.analysis.layout, location))
elif rule.meta.scopes.dynamic == capa.rules.Scope.CALL:
ostream.write(hanging_indent(v.render_call(doc.meta.analysis.layout, location), indent=1))
else:
capa.helpers.assert_never(rule.meta.scopes.dynamic)
else:
capa.helpers.assert_never(doc.meta.flavor)
ostream.write("\n")
render_match(ostream, match, indent=1)
render_match(ostream, doc.meta.analysis.layout, rule, match, indent=1)
if rule.meta.lib:
# only show first match
break

View File

@@ -25,7 +25,8 @@ except ImportError:
# https://github.com/python/mypy/issues/1153
from backports.functools_lru_cache import lru_cache # type: ignore
from typing import Any, Set, Dict, List, Tuple, Union, Iterator
from typing import Any, Set, Dict, List, Tuple, Union, Iterator, Optional
from dataclasses import asdict, dataclass
import yaml
import pydantic
@@ -36,11 +37,13 @@ import capa.perf
import capa.engine as ceng
import capa.features
import capa.optimizer
import capa.features.com
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.com import ComType
from capa.features.common import MAX_BYTES_FEATURE_SIZE, Feature
from capa.features.address import Address
@@ -59,7 +62,7 @@ META_KEYS = (
"authors",
"description",
"lib",
"scope",
"scopes",
"att&ck",
"mbc",
"references",
@@ -74,28 +77,113 @@ HIDDEN_META_KEYS = ("capa/nursery", "capa/path")
class Scope(str, Enum):
FILE = "file"
PROCESS = "process"
THREAD = "thread"
CALL = "call"
FUNCTION = "function"
BASIC_BLOCK = "basic block"
INSTRUCTION = "instruction"
# used only to specify supported features per scope.
# not used to validate rules.
GLOBAL = "global"
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"
@classmethod
def to_yaml(cls, representer, node):
return representer.represent_str(f"{node.value}")
# these literals are used to check if the flavor
# of a rule is correct.
STATIC_SCOPES = {
Scope.FILE,
Scope.GLOBAL,
Scope.FUNCTION,
Scope.BASIC_BLOCK,
Scope.INSTRUCTION,
}
DYNAMIC_SCOPES = {
Scope.FILE,
Scope.GLOBAL,
Scope.PROCESS,
Scope.THREAD,
Scope.CALL,
}
@dataclass
class Scopes:
# when None, the scope is not supported by a rule
static: Optional[Scope] = None
# when None, the scope is not supported by a rule
dynamic: Optional[Scope] = None
def __contains__(self, scope: Scope) -> bool:
return (scope == self.static) or (scope == self.dynamic)
def __repr__(self) -> str:
if self.static and self.dynamic:
return f"static-scope: {self.static}, dynamic-scope: {self.dynamic}"
elif self.static:
return f"static-scope: {self.static}"
elif self.dynamic:
return f"dynamic-scope: {self.dynamic}"
else:
raise ValueError("invalid rules class. at least one scope must be specified")
@classmethod
def from_dict(self, scopes: Dict[str, str]) -> "Scopes":
# make local copy so we don't make changes outside of this routine.
# we'll use the value None to indicate the scope is not supported.
scopes_: Dict[str, Optional[str]] = dict(scopes)
# mark non-specified scopes as invalid
if "static" not in scopes_:
raise InvalidRule("static scope must be provided")
if "dynamic" not in scopes_:
raise InvalidRule("dynamic scope must be provided")
# check the syntax of the meta `scopes` field
if sorted(scopes_) != ["dynamic", "static"]:
raise InvalidRule("scope flavors can be either static or dynamic")
if scopes_["static"] == "unsupported":
scopes_["static"] = None
if scopes_["dynamic"] == "unsupported":
scopes_["dynamic"] = None
# unspecified is used to indicate a rule is yet to be migrated.
# TODO(williballenthin): this scope term should be removed once all rules have been migrated.
# https://github.com/mandiant/capa/issues/1747
if scopes_["static"] == "unspecified":
scopes_["static"] = None
if scopes_["dynamic"] == "unspecified":
scopes_["dynamic"] = None
if (not scopes_["static"]) and (not scopes_["dynamic"]):
raise InvalidRule("invalid scopes value. At least one scope must be specified")
# check that all the specified scopes are valid
if scopes_["static"] and scopes_["static"] not in STATIC_SCOPES:
raise InvalidRule(f"{scopes_['static']} is not a valid static scope")
if scopes_["dynamic"] and scopes_["dynamic"] not in DYNAMIC_SCOPES:
raise InvalidRule(f"{scopes_['dynamic']} is not a valid dynamic scope")
return Scopes(
static=Scope(scopes_["static"]) if scopes_["static"] else None,
dynamic=Scope(scopes_["dynamic"]) if scopes_["dynamic"] else None,
)
SUPPORTED_FEATURES: Dict[str, Set] = {
GLOBAL_SCOPE: {
Scope.GLOBAL: {
# these will be added to other scopes, see below.
capa.features.common.OS,
capa.features.common.Arch,
capa.features.common.Format,
},
FILE_SCOPE: {
Scope.FILE: {
capa.features.common.MatchedRule,
capa.features.file.Export,
capa.features.file.Import,
@@ -108,7 +196,19 @@ SUPPORTED_FEATURES: Dict[str, Set] = {
capa.features.common.Characteristic("mixed mode"),
capa.features.common.Characteristic("forwarded export"),
},
FUNCTION_SCOPE: {
Scope.PROCESS: {
capa.features.common.MatchedRule,
},
Scope.THREAD: set(),
Scope.CALL: {
capa.features.common.MatchedRule,
capa.features.common.Regex,
capa.features.common.String,
capa.features.common.Substring,
capa.features.insn.API,
capa.features.insn.Number,
},
Scope.FUNCTION: {
capa.features.common.MatchedRule,
capa.features.basicblock.BasicBlock,
capa.features.common.Characteristic("calls from"),
@@ -117,13 +217,13 @@ SUPPORTED_FEATURES: Dict[str, Set] = {
capa.features.common.Characteristic("recursive call"),
# plus basic block scope features, see below
},
BASIC_BLOCK_SCOPE: {
Scope.BASIC_BLOCK: {
capa.features.common.MatchedRule,
capa.features.common.Characteristic("tight loop"),
capa.features.common.Characteristic("stack string"),
# plus instruction scope features, see below
},
INSTRUCTION_SCOPE: {
Scope.INSTRUCTION: {
capa.features.common.MatchedRule,
capa.features.insn.API,
capa.features.insn.Property,
@@ -148,15 +248,24 @@ SUPPORTED_FEATURES: Dict[str, Set] = {
}
# 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])
SUPPORTED_FEATURES[Scope.INSTRUCTION].update(SUPPORTED_FEATURES[Scope.GLOBAL])
SUPPORTED_FEATURES[Scope.BASIC_BLOCK].update(SUPPORTED_FEATURES[Scope.GLOBAL])
SUPPORTED_FEATURES[Scope.FUNCTION].update(SUPPORTED_FEATURES[Scope.GLOBAL])
SUPPORTED_FEATURES[Scope.FILE].update(SUPPORTED_FEATURES[Scope.GLOBAL])
SUPPORTED_FEATURES[Scope.PROCESS].update(SUPPORTED_FEATURES[Scope.GLOBAL])
SUPPORTED_FEATURES[Scope.THREAD].update(SUPPORTED_FEATURES[Scope.GLOBAL])
SUPPORTED_FEATURES[Scope.CALL].update(SUPPORTED_FEATURES[Scope.GLOBAL])
# all call scope features are also thread features
SUPPORTED_FEATURES[Scope.THREAD].update(SUPPORTED_FEATURES[Scope.CALL])
# all thread scope features are also process features
SUPPORTED_FEATURES[Scope.PROCESS].update(SUPPORTED_FEATURES[Scope.THREAD])
# all instruction scope features are also basic block features
SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE].update(SUPPORTED_FEATURES[INSTRUCTION_SCOPE])
SUPPORTED_FEATURES[Scope.BASIC_BLOCK].update(SUPPORTED_FEATURES[Scope.INSTRUCTION])
# all basic block scope features are also function scope features
SUPPORTED_FEATURES[FUNCTION_SCOPE].update(SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE])
SUPPORTED_FEATURES[Scope.FUNCTION].update(SUPPORTED_FEATURES[Scope.BASIC_BLOCK])
class InvalidRule(ValueError):
@@ -194,22 +303,66 @@ class InvalidRuleSet(ValueError):
return str(self)
def ensure_feature_valid_for_scope(scope: str, feature: Union[Feature, Statement]):
def ensure_feature_valid_for_scopes(scopes: Scopes, feature: Union[Feature, Statement]):
# construct a dict of all supported features
supported_features: Set = set()
if scopes.static:
supported_features.update(SUPPORTED_FEATURES[scopes.static])
if scopes.dynamic:
supported_features.update(SUPPORTED_FEATURES[scopes.dynamic])
# if the given feature is a characteristic,
# check that is a valid characteristic for the given scope.
if (
isinstance(feature, capa.features.common.Characteristic)
and isinstance(feature.value, str)
and capa.features.common.Characteristic(feature.value) not in SUPPORTED_FEATURES[scope]
and capa.features.common.Characteristic(feature.value) not in supported_features
):
raise InvalidRule(f"feature {feature} not supported for scope {scope}")
raise InvalidRule(f"feature {feature} not supported for scopes {scopes}")
if not isinstance(feature, capa.features.common.Characteristic):
# features of this scope that are not Characteristics will be Type instances.
# check that the given feature is one of these types.
types_for_scope = filter(lambda t: isinstance(t, type), SUPPORTED_FEATURES[scope])
if not isinstance(feature, tuple(types_for_scope)): # type: ignore
raise InvalidRule(f"feature {feature} not supported for scope {scope}")
types_for_scope = filter(lambda t: isinstance(t, type), supported_features)
if not isinstance(feature, tuple(types_for_scope)):
raise InvalidRule(f"feature {feature} not supported for scopes {scopes}")
def translate_com_feature(com_name: str, com_type: ComType) -> ceng.Statement:
com_db = capa.features.com.load_com_database(com_type)
guids: Optional[List[str]] = com_db.get(com_name)
if not guids:
logger.error(" %s doesn't exist in COM %s database", com_name, com_type)
raise InvalidRule(f"'{com_name}' doesn't exist in COM {com_type} database")
com_features: List[Feature] = []
for guid in guids:
hex_chars = guid.replace("-", "")
h = [hex_chars[i : i + 2] for i in range(0, len(hex_chars), 2)]
reordered_hex_pairs = [
h[3],
h[2],
h[1],
h[0],
h[5],
h[4],
h[7],
h[6],
h[8],
h[9],
h[10],
h[11],
h[12],
h[13],
h[14],
h[15],
]
guid_bytes = bytes.fromhex("".join(reordered_hex_pairs))
prefix = capa.features.com.COM_PREFIXES[com_type]
symbol = prefix + com_name
com_features.append(capa.features.common.String(guid, f"{symbol} as GUID string"))
com_features.append(capa.features.common.Bytes(guid_bytes, f"{symbol} as bytes"))
return ceng.Or(com_features)
def parse_int(s: str) -> int:
@@ -417,53 +570,103 @@ def pop_statement_description_entry(d):
return description["description"]
def build_statements(d, scope: str):
def trim_dll_part(api: str) -> str:
# ordinal imports, like ws2_32.#1, keep dll
if ".#" in api:
return api
# kernel32.CreateFileA
if api.count(".") == 1:
if "::" not in api:
# skip System.Convert::FromBase64String
api = api.split(".")[1]
return api
def build_statements(d, scopes: Scopes):
if len(d.keys()) > 2:
raise InvalidRule("too many statements")
key = list(d.keys())[0]
description = pop_statement_description_entry(d[key])
if key == "and":
return ceng.And([build_statements(dd, scope) for dd in d[key]], description=description)
return ceng.And([build_statements(dd, scopes) for dd in d[key]], description=description)
elif key == "or":
return ceng.Or([build_statements(dd, scope) for dd in d[key]], description=description)
return ceng.Or([build_statements(dd, scopes) for dd in d[key]], description=description)
elif key == "not":
if len(d[key]) != 1:
raise InvalidRule("not statement must have exactly one child statement")
return ceng.Not(build_statements(d[key][0], scope), description=description)
return ceng.Not(build_statements(d[key][0], scopes), description=description)
elif key.endswith(" or more"):
count = int(key[: -len("or more")])
return ceng.Some(count, [build_statements(dd, scope) for dd in d[key]], description=description)
return ceng.Some(count, [build_statements(dd, scopes) for dd in d[key]], description=description)
elif key == "optional":
# `optional` is an alias for `0 or more`
# which is useful for documenting behaviors,
# like with `write file`, we might say that `WriteFile` is optionally found alongside `CreateFileA`.
return ceng.Some(0, [build_statements(dd, scope) for dd in d[key]], description=description)
return ceng.Some(0, [build_statements(dd, scopes) for dd in d[key]], description=description)
elif key == "process":
if Scope.FILE not in scopes:
raise InvalidRule("process subscope supported only for file scope")
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(
Scope.PROCESS, build_statements(d[key][0], Scopes(dynamic=Scope.PROCESS)), description=description
)
elif key == "thread":
if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS)):
raise InvalidRule("thread subscope supported only for the process scope")
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(
Scope.THREAD, build_statements(d[key][0], Scopes(dynamic=Scope.THREAD)), description=description
)
elif key == "call":
if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD)):
raise InvalidRule("call subscope supported only for the process and thread scopes")
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(
Scope.CALL, build_statements(d[key][0], Scopes(dynamic=Scope.CALL)), description=description
)
elif key == "function":
if scope != FILE_SCOPE:
if Scope.FILE not in scopes:
raise InvalidRule("function subscope supported only for file scope")
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), description=description)
return ceng.Subscope(
Scope.FUNCTION, build_statements(d[key][0], Scopes(static=Scope.FUNCTION)), description=description
)
elif key == "basic block":
if scope != FUNCTION_SCOPE:
if Scope.FUNCTION not in scopes:
raise InvalidRule("basic block subscope supported only for function scope")
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), description=description)
return ceng.Subscope(
Scope.BASIC_BLOCK, build_statements(d[key][0], Scopes(static=Scope.BASIC_BLOCK)), description=description
)
elif key == "instruction":
if scope not in (FUNCTION_SCOPE, BASIC_BLOCK_SCOPE):
if all(s not in scopes for s in (Scope.FUNCTION, Scope.BASIC_BLOCK)):
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)
statements = build_statements(d[key][0], Scopes(static=Scope.INSTRUCTION))
else:
# for instruction subscopes, we support a shorthand in which the top level AND is implied.
# the following are equivalent:
@@ -477,9 +680,9 @@ def build_statements(d, scope: str):
# - arch: i386
# - mnemonic: cmp
#
statements = ceng.And([build_statements(dd, INSTRUCTION_SCOPE) for dd in d[key]])
statements = ceng.And([build_statements(dd, Scopes(static=Scope.INSTRUCTION)) for dd in d[key]])
return ceng.Subscope(INSTRUCTION_SCOPE, statements, description=description)
return ceng.Subscope(Scope.INSTRUCTION, statements, description=description)
elif key.startswith("count(") and key.endswith(")"):
# e.g.:
@@ -507,6 +710,10 @@ def build_statements(d, scope: str):
# count(number(0x100 = description))
if term != "string":
value, description = parse_description(arg, term)
if term == "api":
value = trim_dll_part(value)
feature = Feature(value, description=description)
else:
# arg is string (which doesn't support inline descriptions), like:
@@ -518,7 +725,7 @@ def build_statements(d, scope: str):
feature = Feature(arg)
else:
feature = Feature()
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
count = d[key]
if isinstance(count, int):
@@ -552,7 +759,7 @@ def build_statements(d, scope: str):
feature = capa.features.insn.OperandNumber(index, value, description=description)
except ValueError as e:
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
return feature
elif key.startswith("operand[") and key.endswith("].offset"):
@@ -568,7 +775,7 @@ def build_statements(d, scope: str):
feature = capa.features.insn.OperandOffset(index, value, description=description)
except ValueError as e:
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
return feature
elif (
@@ -588,17 +795,30 @@ def build_statements(d, scope: str):
feature = capa.features.insn.Property(value, access=access, description=description)
except ValueError as e:
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
return feature
elif key.startswith("com/"):
com_type_name = str(key[len("com/") :])
try:
com_type = ComType(com_type_name)
except ValueError:
raise InvalidRule(f"unexpected COM type: {com_type_name}")
value, description = parse_description(d[key], key, d.get("description"))
return translate_com_feature(value, com_type)
else:
Feature = parse_feature(key)
value, description = parse_description(d[key], key, d.get("description"))
if key == "api":
value = trim_dll_part(value)
try:
feature = Feature(value, description=description)
except ValueError as e:
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
return feature
@@ -611,10 +831,10 @@ def second(s: List[Any]) -> Any:
class Rule:
def __init__(self, name: str, scope: str, statement: Statement, meta, definition=""):
def __init__(self, name: str, scopes: Scopes, statement: Statement, meta, definition=""):
super().__init__()
self.name = name
self.scope = scope
self.scopes = scopes
self.statement = statement
self.meta = meta
self.definition = definition
@@ -623,7 +843,7 @@ class Rule:
return f"Rule(name={self.name})"
def __repr__(self):
return f"Rule(scope={self.scope}, name={self.name})"
return f"Rule(scope={self.scopes}, name={self.name})"
def get_dependencies(self, namespaces):
"""
@@ -681,13 +901,19 @@ class Rule:
# the name is a randomly generated, hopefully unique value.
# ideally, this won't every be rendered to a user.
name = self.name + "/" + uuid.uuid4().hex
if subscope.scope in STATIC_SCOPES:
scopes = Scopes(static=subscope.scope)
elif subscope.scope in DYNAMIC_SCOPES:
scopes = Scopes(dynamic=subscope.scope)
else:
raise InvalidRule(f"scope {subscope.scope} is not a valid subscope")
new_rule = Rule(
name,
subscope.scope,
scopes,
subscope.child,
{
"name": name,
"scope": subscope.scope,
"scopes": asdict(scopes),
# these derived rules are never meant to be inspected separately,
# they are dependencies for the parent rule,
# so mark it as such.
@@ -712,6 +938,9 @@ class Rule:
for child in statement.get_children():
yield from self._extract_subscope_rules_rec(child)
def is_file_limitation_rule(self) -> bool:
return self.meta.get("namespace", "") == "internal/limitation/file"
def is_subscope_rule(self):
return bool(self.meta.get("capa/subscope-rule", False))
@@ -774,9 +1003,21 @@ class Rule:
def from_dict(cls, d: Dict[str, Any], definition: str) -> "Rule":
meta = d["rule"]["meta"]
name = meta["name"]
# if scope is not specified, default to function scope.
# this is probably the mode that rule authors will start with.
scope = meta.get("scope", FUNCTION_SCOPE)
# each rule has two scopes, a static-flavor scope, and a
# dynamic-flavor one. which one is used depends on the analysis type.
if "scope" in meta:
raise InvalidRule(f"legacy rule detected (rule.meta.scope), please update to the new syntax: {name}")
elif "scopes" in meta:
scopes_ = meta.get("scopes")
else:
raise InvalidRule("please specify at least one of this rule's (static/dynamic) scopes")
if not isinstance(scopes_, dict):
raise InvalidRule("the scopes field must contain a dictionary specifying the scopes")
scopes: Scopes = Scopes.from_dict(scopes_)
statements = d["rule"]["features"]
# the rule must start with a single logic node.
@@ -787,16 +1028,13 @@ class Rule:
if isinstance(statements[0], ceng.Subscope):
raise InvalidRule("top level statement may not be a subscope")
if scope not in SUPPORTED_FEATURES.keys():
raise InvalidRule("{:s} is not a supported scope".format(scope))
meta = d["rule"]["meta"]
if not isinstance(meta.get("att&ck", []), list):
raise InvalidRule("ATT&CK mapping must be a list")
if not isinstance(meta.get("mbc", []), list):
raise InvalidRule("MBC mapping must be a list")
return cls(name, scope, build_statements(statements[0], scope), meta, definition)
return cls(name, scopes, build_statements(statements[0], scopes), meta, definition)
@staticmethod
@lru_cache()
@@ -824,7 +1062,7 @@ class Rule:
# leave quotes unchanged.
# manually verified this property exists, even if mypy complains.
y.preserve_quotes = True # type: ignore
y.preserve_quotes = True
# indent lists by two spaces below their parent
#
@@ -836,7 +1074,7 @@ class Rule:
# avoid word wrapping
# manually verified this property exists, even if mypy complains.
y.width = 4096 # type: ignore
y.width = 4096
return y
@@ -895,10 +1133,8 @@ class Rule:
del meta[k]
for k, v in self.meta.items():
meta[k] = v
# the name and scope of the rule instance overrides anything in meta.
meta["name"] = self.name
meta["scope"] = self.scope
def move_to_end(m, k):
# ruamel.yaml uses an ordereddict-like structure to track maps (CommentedMap).
@@ -919,7 +1155,6 @@ class Rule:
if key in META_KEYS:
continue
move_to_end(meta, key)
# save off the existing hidden meta values,
# emit the document,
# and re-add the hidden meta.
@@ -974,12 +1209,11 @@ class Rule:
return doc
def get_rules_with_scope(rules, scope) -> List[Rule]:
def get_rules_with_scope(rules, scope: Scope) -> List[Rule]:
"""
from the given collection of rules, select those with the given scope.
`scope` is one of the capa.rules.*_SCOPE constants.
"""
return [rule for rule in rules if rule.scope == scope]
return [rule for rule in rules if scope in rule.scopes]
def get_rules_and_dependencies(rules: List[Rule], rule_name: str) -> Iterator[Rule]:
@@ -1104,7 +1338,10 @@ class RuleSet:
capa.engine.match(ruleset.file_rules, ...)
"""
def __init__(self, rules: List[Rule]):
def __init__(
self,
rules: List[Rule],
):
super().__init__()
ensure_rules_are_unique(rules)
@@ -1126,15 +1363,23 @@ class RuleSet:
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.file_rules = self._get_rules_for_scope(rules, Scope.FILE)
self.process_rules = self._get_rules_for_scope(rules, Scope.PROCESS)
self.thread_rules = self._get_rules_for_scope(rules, Scope.THREAD)
self.call_rules = self._get_rules_for_scope(rules, Scope.CALL)
self.function_rules = self._get_rules_for_scope(rules, Scope.FUNCTION)
self.basic_block_rules = self._get_rules_for_scope(rules, Scope.BASIC_BLOCK)
self.instruction_rules = self._get_rules_for_scope(rules, Scope.INSTRUCTION)
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_process_rules_by_feature, self._hard_process_rules) = self._index_rules_by_feature(
self.process_rules
)
(self._easy_thread_rules_by_feature, self._hard_thread_rules) = self._index_rules_by_feature(self.thread_rules)
(self._easy_call_rules_by_feature, self._hard_call_rules) = self._index_rules_by_feature(self.call_rules)
(self._easy_function_rules_by_feature, self._hard_function_rules) = self._index_rules_by_feature(
self.function_rules
)
@@ -1380,16 +1625,25 @@ class RuleSet:
except that it may be more performant.
"""
easy_rules_by_feature = {}
if scope is Scope.FILE:
if scope == Scope.FILE:
easy_rules_by_feature = self._easy_file_rules_by_feature
hard_rule_names = self._hard_file_rules
elif scope is Scope.FUNCTION:
elif scope == Scope.PROCESS:
easy_rules_by_feature = self._easy_process_rules_by_feature
hard_rule_names = self._hard_process_rules
elif scope == Scope.THREAD:
easy_rules_by_feature = self._easy_thread_rules_by_feature
hard_rule_names = self._hard_thread_rules
elif scope == Scope.CALL:
easy_rules_by_feature = self._easy_call_rules_by_feature
hard_rule_names = self._hard_call_rules
elif scope == 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:
elif scope == 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:
elif scope == Scope.INSTRUCTION:
easy_rules_by_feature = self._easy_instruction_rules_by_feature
hard_rule_names = self._hard_instruction_rules
else:

View File

@@ -1,4 +1,4 @@
# capa/sigs
# capa FLIRT signatures
This directory contains FLIRT signatures that capa uses to identify library functions.
Typically, capa will ignore library functions, which reduces false positives and improves runtime.

View File

@@ -35,12 +35,6 @@ $ unzip v4.0.0.zip
$ capa -r /path/to/capa-rules suspicious.exe
```
This technique also doesn't set up the default library identification [signatures](https://github.com/mandiant/capa/tree/master/sigs). You can pass the signature directory using the `-s` argument.
For example, to run capa with both a rule path and a signature path:
```console
$ capa -s /path/to/capa-sigs suspicious.exe
```
Alternatively, see Method 3 below.
### 2. Use capa
@@ -105,27 +99,28 @@ To install these development dependencies, run:
We use [pre-commit](https://pre-commit.com/) so that its trivial to run the same linters & configuration locally as in CI.
Run all linters liks:
Run all linters like:
pre-commit run --all-files
pre-commit run --hook-stage=manual --all-files
isort....................................................................Passed
black....................................................................Passed
ruff.....................................................................Passed
flake8...................................................................Passed
mypy.....................................................................Passed
pytest (fast)............................................................Passed
Or run a single linter like:
pre-commit run --all-files isort
pre-commit run --all-files --hook-stage=manual isort
isort....................................................................Passed
Importantly, you can configure pre-commit to run automatically before every commit by running:
pre-commit install --hook-type pre-commit
pre-commit install --hook-type=pre-commit
pre-commit installed at .git/hooks/pre-commit
pre-commit install --hook-type pre-push
pre-commit install --hook-type=pre-push
pre-commit installed at .git/hooks/pre-push
This way you can ensure that you don't commit code style or formatting offenses.

View File

@@ -36,19 +36,19 @@ dependencies = [
"pyyaml==6.0.1",
"tabulate==0.9.0",
"colorama==0.4.6",
"termcolor==2.3.0",
"wcwidth==0.2.6",
"termcolor==2.4.0",
"wcwidth==0.2.13",
"ida-settings==2.1.0",
"viv-utils[flirt]==0.7.9",
"halo==0.0.31",
"networkx==3.1",
"ruamel.yaml==0.17.32",
"ruamel.yaml==0.18.5",
"vivisect==1.1.1",
"pefile==2023.2.7",
"pyelftools==0.30",
"dnfile==0.13.0",
"dnfile==0.14.1",
"dncil==1.0.2",
"pydantic==2.1.1",
"pydantic==2.4.0",
"protobuf==4.23.4",
]
dynamic = ["version"]
@@ -61,26 +61,26 @@ packages = ["capa"]
[project.optional-dependencies]
dev = [
"pre-commit==3.4.0",
"pytest==7.4.2",
"pre-commit==3.5.0",
"pytest==7.4.4",
"pytest-sugar==0.9.7",
"pytest-instafail==0.5.0",
"pytest-cov==4.1.0",
"flake8==6.1.0",
"flake8-bugbear==23.7.10",
"flake8-encodings==0.5.0.post1",
"flake8==7.0.0",
"flake8-bugbear==23.12.2",
"flake8-encodings==0.5.1",
"flake8-comprehensions==3.14.0",
"flake8-logging-format==0.9.0",
"flake8-no-implicit-concat==0.3.4",
"flake8-no-implicit-concat==0.3.5",
"flake8-print==5.0.0",
"flake8-todos==0.3.0",
"flake8-simplify==0.20.0",
"flake8-simplify==0.21.0",
"flake8-use-pathlib==0.3.0",
"flake8-copyright==0.2.4",
"ruff==0.0.291",
"black==23.7.0",
"isort==5.11.4",
"mypy==1.5.1",
"ruff==0.1.13",
"black==23.12.1",
"isort==5.13.2",
"mypy==1.8.0",
"psutil==5.9.2",
"stix2==3.0.1",
"requests==2.31.0",
@@ -89,15 +89,15 @@ dev = [
"types-backports==0.1.3",
"types-colorama==0.4.15.11",
"types-PyYAML==6.0.8",
"types-tabulate==0.9.0.3",
"types-tabulate==0.9.0.20240106",
"types-termcolor==1.1.4",
"types-psutil==5.8.23",
"types_requests==2.31.0.2",
"types_requests==2.31.0.20240106",
"types-protobuf==4.23.0.3",
]
build = [
"pyinstaller==5.10.1",
"setuptools==68.0.0",
"pyinstaller==6.3.0",
"setuptools==69.0.3",
"build==1.0.3"
]

2
rules

Submodule rules updated: 2d615e2386...9161f73a78

View File

@@ -75,6 +75,7 @@ import capa
import capa.main
import capa.rules
import capa.render.json
import capa.capabilities.common
import capa.render.result_document as rd
from capa.features.common import OS_AUTO
@@ -112,7 +113,7 @@ def get_capa_results(args):
extractor = capa.main.get_extractor(
path, format, os_, capa.main.BACKEND_VIV, sigpaths, should_save_workspace, disable_progress=True
)
except capa.main.UnsupportedFormatError:
except capa.exceptions.UnsupportedFormatError:
# i'm 100% sure if multiprocessing will reliably raise exceptions across process boundaries.
# so instead, return an object with explicit success/failure status.
#
@@ -123,7 +124,7 @@ def get_capa_results(args):
"status": "error",
"error": f"input file does not appear to be a PE file: {path}",
}
except capa.main.UnsupportedRuntimeError:
except capa.exceptions.UnsupportedRuntimeError:
return {
"path": path,
"status": "error",
@@ -136,11 +137,9 @@ def get_capa_results(args):
"error": f"unexpected error: {e}",
}
meta = capa.main.collect_metadata([], path, format, os_, [], extractor)
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor, disable_progress=True)
meta.analysis.feature_counts = counts["feature_counts"]
meta.analysis.library_functions = counts["library_functions"]
meta = capa.main.collect_metadata([], path, format, os_, [], extractor, counts)
meta.analysis.layout = capa.main.compute_layout(rules, extractor, capabilities)
doc = rd.ResultDocument.from_capa(meta, rules, capabilities)

View File

@@ -61,7 +61,22 @@ var_names = ["".join(letters) for letters in itertools.product(string.ascii_lowe
# this have to be the internal names used by capa.py which are sometimes different to the ones written out in the rules, e.g. "2 or more" is "Some", count is Range
unsupported = ["characteristic", "mnemonic", "offset", "subscope", "Range"]
unsupported = [
"characteristic",
"mnemonic",
"offset",
"subscope",
"Range",
"os",
"property",
"format",
"class",
"operand[0].number",
"operand[1].number",
"substring",
"arch",
"namespace",
]
# further idea: shorten this list, possible stuff:
# - 2 or more strings: e.g.
# -- https://github.com/mandiant/capa-rules/blob/master/collection/file-managers/gather-direct-ftp-information.yml
@@ -90,8 +105,7 @@ condition_header = """
condition_rule = """
private rule capa_pe_file : CAPA {
meta:
description = "match in PE files. used by all further CAPA rules"
author = "Arnim Rupp"
description = "Match in PE files. Used by other CAPA rules"
condition:
uint16be(0) == 0x4d5a
or uint16be(0) == 0x558b
@@ -566,7 +580,7 @@ def convert_rules(rules, namespaces, cround, make_priv):
logger.info("skipping already converted rule capa: %s - yara rule: %s", rule.name, rule_name)
continue
logger.info("-------------------------- DOING RULE CAPA: %s - yara rule: ", rule.name, rule_name)
logger.info("-------------------------- DOING RULE CAPA: %s - yara rule: %s", rule.name, rule_name)
if "capa/path" in rule.meta:
url = get_rule_url(rule.meta["capa/path"])
else:
@@ -603,7 +617,12 @@ def convert_rules(rules, namespaces, cround, make_priv):
meta_name = meta
# e.g. 'examples:' can be a list
seen_hashes = []
if isinstance(metas[meta], list):
if isinstance(metas[meta], dict):
if meta_name == "scopes":
yara_meta += "\t" + "static scope" + ' = "' + metas[meta]["static"] + '"\n'
yara_meta += "\t" + "dynamic scope" + ' = "' + metas[meta]["dynamic"] + '"\n'
elif isinstance(metas[meta], list):
if meta_name == "examples":
meta_name = "hash"
if meta_name == "att&ck":

View File

@@ -19,6 +19,7 @@ import capa.features
import capa.render.json
import capa.render.utils as rutils
import capa.render.default
import capa.capabilities.common
import capa.render.result_document as rd
import capa.features.freeze.features as frzf
from capa.features.common import OS_AUTO, FORMAT_AUTO
@@ -175,13 +176,10 @@ def capa_details(rules_path: Path, file_path: Path, output_format="dictionary"):
extractor = capa.main.get_extractor(
file_path, FORMAT_AUTO, OS_AUTO, capa.main.BACKEND_VIV, [], False, disable_progress=True
)
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor, disable_progress=True)
# collect metadata (used only to make rendering more complete)
meta = capa.main.collect_metadata([], file_path, FORMAT_AUTO, OS_AUTO, [rules_path], extractor)
meta.analysis.feature_counts = counts["feature_counts"]
meta.analysis.library_functions = counts["library_functions"]
meta = capa.main.collect_metadata([], file_path, FORMAT_AUTO, OS_AUTO, [rules_path], extractor, counts)
meta.analysis.layout = capa.main.compute_layout(rules, extractor, capabilities)
capa_output: Any = False

View File

@@ -90,7 +90,7 @@ def main():
continue
if rule.meta.is_subscope_rule:
continue
if rule.meta.scope != capa.rules.Scope.FUNCTION:
if rule.meta.scopes.static == capa.rules.Scope.FUNCTION:
continue
ns = rule.meta.namespace

View File

@@ -41,6 +41,7 @@ import capa.rules
import capa.engine
import capa.helpers
import capa.features.insn
import capa.capabilities.common
from capa.rules import Rule, RuleSet
from capa.features.common import OS_AUTO, String, Feature, Substring
from capa.render.result_document import RuleMetadata
@@ -151,20 +152,74 @@ class NamespaceDoesntMatchRulePath(Lint):
return rule.meta["namespace"] not in get_normpath(rule.meta["capa/path"])
class MissingScope(Lint):
name = "missing scope"
recommendation = "Add meta.scope so that the scope is explicit (defaults to `function`)"
class MissingScopes(Lint):
name = "missing scopes"
recommendation = (
"Add meta.scopes with both the static (meta.scopes.static) and dynamic (meta.scopes.dynamic) scopes"
)
def check_rule(self, ctx: Context, rule: Rule):
return "scope" not in rule.meta
return "scopes" not in rule.meta
class InvalidScope(Lint):
name = "invalid scope"
recommendation = "Use only file, function, basic block, or instruction rule scopes"
class MissingStaticScope(Lint):
name = "missing static scope"
recommendation = (
"Add a static scope for the rule (file, function, basic block, instruction, or unspecified/unsupported)"
)
def check_rule(self, ctx: Context, rule: Rule):
return rule.meta.get("scope") not in ("file", "function", "basic block", "instruction")
return "static" not in rule.meta.get("scopes")
class MissingDynamicScope(Lint):
name = "missing dynamic scope"
recommendation = "Add a dynamic scope for the rule (file, process, thread, call, or unspecified/unsupported)"
def check_rule(self, ctx: Context, rule: Rule):
return "dynamic" not in rule.meta.get("scopes")
class InvalidStaticScope(Lint):
name = "invalid static scope"
recommendation = (
"For the static scope, use either: file, function, basic block, instruction, or unspecified/unsupported"
)
def check_rule(self, ctx: Context, rule: Rule):
return rule.meta.get("scopes").get("static") not in (
"file",
"function",
"basic block",
"instruction",
"unspecified",
"unsupported",
)
class InvalidDynamicScope(Lint):
name = "invalid static scope"
recommendation = "For the dynamic scope, use either: file, process, thread, call, or unspecified/unsupported"
def check_rule(self, ctx: Context, rule: Rule):
return rule.meta.get("scopes").get("dynamic") not in (
"file",
"process",
"thread",
"call",
"unspecified",
"unsupported",
)
class InvalidScopes(Lint):
name = "invalid scopes"
recommendation = "At least one scope (static or dynamic) must be specified"
def check_rule(self, ctx: Context, rule: Rule):
return (rule.meta.get("scopes").get("static") in ("unspecified", "unsupported")) and (
rule.meta.get("scopes").get("dynamic") in ("unspecified", "unsupported")
)
class MissingAuthors(Lint):
@@ -305,14 +360,14 @@ def get_sample_capabilities(ctx: Context, path: Path) -> Set[str]:
elif nice_path.name.endswith(capa.helpers.EXTENSIONS_SHELLCODE_64):
format_ = "sc64"
else:
format_ = capa.main.get_auto_format(nice_path)
format_ = capa.helpers.get_auto_format(nice_path)
logger.debug("analyzing sample: %s", nice_path)
extractor = capa.main.get_extractor(
nice_path, format_, OS_AUTO, capa.main.BACKEND_VIV, DEFAULT_SIGNATURES, False, disable_progress=True
)
capabilities, _ = capa.main.find_capabilities(ctx.rules, extractor, disable_progress=True)
capabilities, _ = capa.capabilities.common.find_capabilities(ctx.rules, extractor, disable_progress=True)
# mypy doesn't seem to be happy with the MatchResults type alias & set(...keys())?
# so we ignore a few types here.
capabilities = set(capabilities.keys()) # type: ignore
@@ -700,14 +755,18 @@ def lint_name(ctx: Context, rule: Rule):
return run_lints(NAME_LINTS, ctx, rule)
SCOPE_LINTS = (
MissingScope(),
InvalidScope(),
SCOPES_LINTS = (
MissingScopes(),
MissingStaticScope(),
MissingDynamicScope(),
InvalidStaticScope(),
InvalidDynamicScope(),
InvalidScopes(),
)
def lint_scope(ctx: Context, rule: Rule):
return run_lints(SCOPE_LINTS, ctx, rule)
return run_lints(SCOPES_LINTS, ctx, rule)
META_LINTS = (

View File

@@ -43,7 +43,8 @@
"T1598": "Phishing for Information",
"T1598.001": "Phishing for Information::Spearphishing Service",
"T1598.002": "Phishing for Information::Spearphishing Attachment",
"T1598.003": "Phishing for Information::Spearphishing Link"
"T1598.003": "Phishing for Information::Spearphishing Link",
"T1598.004": "Phishing for Information::Spearphishing Voice"
},
"Resource Development": {
"T1583": "Acquire Infrastructure",
@@ -111,7 +112,9 @@
"T1566": "Phishing",
"T1566.001": "Phishing::Spearphishing Attachment",
"T1566.002": "Phishing::Spearphishing Link",
"T1566.003": "Phishing::Spearphishing via Service"
"T1566.003": "Phishing::Spearphishing via Service",
"T1566.004": "Phishing::Spearphishing Voice",
"T1659": "Content Injection"
},
"Execution": {
"T1047": "Windows Management Instrumentation",
@@ -175,6 +178,7 @@
"T1098.003": "Account Manipulation::Additional Cloud Roles",
"T1098.004": "Account Manipulation::SSH Authorized Keys",
"T1098.005": "Account Manipulation::Device Registration",
"T1098.006": "Account Manipulation::Additional Container Cluster Roles",
"T1133": "External Remote Services",
"T1136": "Create Account",
"T1136.001": "Create Account::Local Account",
@@ -264,7 +268,8 @@
"T1574.010": "Hijack Execution Flow::Services File Permissions Weakness",
"T1574.011": "Hijack Execution Flow::Services Registry Permissions Weakness",
"T1574.012": "Hijack Execution Flow::COR_PROFILER",
"T1574.013": "Hijack Execution Flow::KernelCallbackTable"
"T1574.013": "Hijack Execution Flow::KernelCallbackTable",
"T1653": "Power Settings"
},
"Privilege Escalation": {
"T1037": "Boot or Logon Initialization Scripts",
@@ -298,6 +303,13 @@
"T1078.002": "Valid Accounts::Domain Accounts",
"T1078.003": "Valid Accounts::Local Accounts",
"T1078.004": "Valid Accounts::Cloud Accounts",
"T1098": "Account Manipulation",
"T1098.001": "Account Manipulation::Additional Cloud Credentials",
"T1098.002": "Account Manipulation::Additional Email Delegate Permissions",
"T1098.003": "Account Manipulation::Additional Cloud Roles",
"T1098.004": "Account Manipulation::SSH Authorized Keys",
"T1098.005": "Account Manipulation::Device Registration",
"T1098.006": "Account Manipulation::Additional Container Cluster Roles",
"T1134": "Access Token Manipulation",
"T1134.001": "Access Token Manipulation::Token Impersonation/Theft",
"T1134.002": "Access Token Manipulation::Create Process with Token",
@@ -349,6 +361,7 @@
"T1548.002": "Abuse Elevation Control Mechanism::Bypass User Account Control",
"T1548.003": "Abuse Elevation Control Mechanism::Sudo and Sudo Caching",
"T1548.004": "Abuse Elevation Control Mechanism::Elevated Execution with Prompt",
"T1548.005": "Abuse Elevation Control Mechanism::Temporary Elevated Cloud Access",
"T1574": "Hijack Execution Flow",
"T1574.001": "Hijack Execution Flow::DLL Search Order Hijacking",
"T1574.002": "Hijack Execution Flow::DLL Side-Loading",
@@ -379,6 +392,7 @@
"T1027.009": "Obfuscated Files or Information::Embedded Payloads",
"T1027.010": "Obfuscated Files or Information::Command Obfuscation",
"T1027.011": "Obfuscated Files or Information::Fileless Storage",
"T1027.012": "Obfuscated Files or Information::LNK Icon Smuggling",
"T1036": "Masquerading",
"T1036.001": "Masquerading::Invalid Code Signature",
"T1036.002": "Masquerading::Right-to-Left Override",
@@ -388,6 +402,7 @@
"T1036.006": "Masquerading::Space after Filename",
"T1036.007": "Masquerading::Double File Extension",
"T1036.008": "Masquerading::Masquerade File Type",
"T1036.009": "Masquerading::Break Process Trees",
"T1055": "Process Injection",
"T1055.001": "Process Injection::Dynamic-link Library Injection",
"T1055.002": "Process Injection::Portable Executable Injection",
@@ -475,6 +490,7 @@
"T1548.002": "Abuse Elevation Control Mechanism::Bypass User Account Control",
"T1548.003": "Abuse Elevation Control Mechanism::Sudo and Sudo Caching",
"T1548.004": "Abuse Elevation Control Mechanism::Elevated Execution with Prompt",
"T1548.005": "Abuse Elevation Control Mechanism::Temporary Elevated Cloud Access",
"T1550": "Use Alternate Authentication Material",
"T1550.001": "Use Alternate Authentication Material::Application Access Token",
"T1550.002": "Use Alternate Authentication Material::Pass the Hash",
@@ -503,10 +519,11 @@
"T1562.004": "Impair Defenses::Disable or Modify System Firewall",
"T1562.006": "Impair Defenses::Indicator Blocking",
"T1562.007": "Impair Defenses::Disable or Modify Cloud Firewall",
"T1562.008": "Impair Defenses::Disable Cloud Logs",
"T1562.008": "Impair Defenses::Disable or Modify Cloud Logs",
"T1562.009": "Impair Defenses::Safe Mode Boot",
"T1562.010": "Impair Defenses::Downgrade Attack",
"T1562.011": "Impair Defenses::Spoof Security Alerting",
"T1562.012": "Impair Defenses::Disable or Modify Linux Audit System",
"T1564": "Hide Artifacts",
"T1564.001": "Hide Artifacts::Hidden Files and Directories",
"T1564.002": "Hide Artifacts::Hidden Users",
@@ -518,6 +535,7 @@
"T1564.008": "Hide Artifacts::Email Hiding Rules",
"T1564.009": "Hide Artifacts::Resource Forking",
"T1564.010": "Hide Artifacts::Process Argument Spoofing",
"T1564.011": "Hide Artifacts::Ignore Process Interrupts",
"T1574": "Hijack Execution Flow",
"T1574.001": "Hijack Execution Flow::DLL Search Order Hijacking",
"T1574.002": "Hijack Execution Flow::DLL Side-Loading",
@@ -536,6 +554,7 @@
"T1578.002": "Modify Cloud Compute Infrastructure::Create Cloud Instance",
"T1578.003": "Modify Cloud Compute Infrastructure::Delete Cloud Instance",
"T1578.004": "Modify Cloud Compute Infrastructure::Revert Cloud Instance",
"T1578.005": "Modify Cloud Compute Infrastructure::Modify Cloud Compute Configurations",
"T1599": "Network Boundary Bridging",
"T1599.001": "Network Boundary Bridging::Network Address Translation Traversal",
"T1600": "Weaken Encryption",
@@ -548,7 +567,8 @@
"T1612": "Build Image on Host",
"T1620": "Reflective Code Loading",
"T1622": "Debugger Evasion",
"T1647": "Plist File Modification"
"T1647": "Plist File Modification",
"T1656": "Impersonation"
},
"Credential Access": {
"T1003": "OS Credential Dumping",
@@ -591,6 +611,7 @@
"T1555.003": "Credentials from Password Stores::Credentials from Web Browsers",
"T1555.004": "Credentials from Password Stores::Windows Credential Manager",
"T1555.005": "Credentials from Password Stores::Password Managers",
"T1555.006": "Credentials from Password Stores::Cloud Secrets Management Stores",
"T1556": "Modify Authentication Process",
"T1556.001": "Modify Authentication Process::Domain Controller Authentication",
"T1556.002": "Modify Authentication Process::Password Filter DLL",
@@ -621,6 +642,7 @@
"T1012": "Query Registry",
"T1016": "System Network Configuration Discovery",
"T1016.001": "System Network Configuration Discovery::Internet Connection Discovery",
"T1016.002": "System Network Configuration Discovery::Wi-Fi Discovery",
"T1018": "Remote System Discovery",
"T1033": "System Owner/User Discovery",
"T1040": "Network Sniffing",
@@ -659,7 +681,8 @@
"T1615": "Group Policy Discovery",
"T1619": "Cloud Storage Object Discovery",
"T1622": "Debugger Evasion",
"T1652": "Device Driver Discovery"
"T1652": "Device Driver Discovery",
"T1654": "Log Enumeration"
},
"Lateral Movement": {
"T1021": "Remote Services",
@@ -670,6 +693,7 @@
"T1021.005": "Remote Services::VNC",
"T1021.006": "Remote Services::Windows Remote Management",
"T1021.007": "Remote Services::Cloud Services",
"T1021.008": "Remote Services::Direct Cloud VM Connections",
"T1072": "Software Deployment Tools",
"T1080": "Taint Shared Content",
"T1091": "Replication Through Removable Media",
@@ -763,7 +787,8 @@
"T1572": "Protocol Tunneling",
"T1573": "Encrypted Channel",
"T1573.001": "Encrypted Channel::Symmetric Cryptography",
"T1573.002": "Encrypted Channel::Asymmetric Cryptography"
"T1573.002": "Encrypted Channel::Asymmetric Cryptography",
"T1659": "Content Injection"
},
"Exfiltration": {
"T1011": "Exfiltration Over Other Network Medium",
@@ -783,7 +808,8 @@
"T1567": "Exfiltration Over Web Service",
"T1567.001": "Exfiltration Over Web Service::Exfiltration to Code Repository",
"T1567.002": "Exfiltration Over Web Service::Exfiltration to Cloud Storage",
"T1567.003": "Exfiltration Over Web Service::Exfiltration to Text Storage Sites"
"T1567.003": "Exfiltration Over Web Service::Exfiltration to Text Storage Sites",
"T1567.004": "Exfiltration Over Web Service::Exfiltration Over Webhook"
},
"Impact": {
"T1485": "Data Destruction",
@@ -811,7 +837,8 @@
"T1565": "Data Manipulation",
"T1565.001": "Data Manipulation::Stored Data Manipulation",
"T1565.002": "Data Manipulation::Transmitted Data Manipulation",
"T1565.003": "Data Manipulation::Runtime Data Manipulation"
"T1565.003": "Data Manipulation::Runtime Data Manipulation",
"T1657": "Financial Theft"
}
},
"mbc": {

View File

@@ -54,6 +54,7 @@ import capa.helpers
import capa.features
import capa.features.common
import capa.features.freeze
import capa.capabilities.common
logger = logging.getLogger("capa.profile")
@@ -114,7 +115,7 @@ def main(argv=None):
def do_iteration():
capa.perf.reset()
capa.main.find_capabilities(rules, extractor, disable_progress=True)
capa.capabilities.common.find_capabilities(rules, extractor, disable_progress=True)
pbar.update(1)
samples = timeit.repeat(do_iteration, number=args.number, repeat=args.repeat)

View File

@@ -47,7 +47,7 @@ from typing import Dict, List
from pathlib import Path
import requests
from stix2 import Filter, MemoryStore, AttackPattern # type: ignore
from stix2 import Filter, MemoryStore, AttackPattern
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")

View File

@@ -74,10 +74,12 @@ import capa.exceptions
import capa.render.utils as rutils
import capa.render.verbose
import capa.features.freeze
import capa.capabilities.common
import capa.render.result_document as rd
from capa.helpers import get_file_taste
from capa.features.common import FORMAT_AUTO
from capa.features.freeze import Address
from capa.features.extractors.base_extractor import FeatureExtractor, StaticFeatureExtractor
logger = logging.getLogger("capa.show-capabilities-by-function")
@@ -101,6 +103,7 @@ def render_matches_by_function(doc: rd.ResultDocument):
- send HTTP request
- connect to HTTP server
"""
assert isinstance(doc.meta.analysis, rd.StaticAnalysis)
functions_by_bb: Dict[Address, Address] = {}
for finfo in doc.meta.analysis.layout.functions:
faddress = finfo.address
@@ -113,10 +116,10 @@ def render_matches_by_function(doc: rd.ResultDocument):
matches_by_function = collections.defaultdict(set)
for rule in rutils.capability_rules(doc):
if rule.meta.scope == capa.rules.FUNCTION_SCOPE:
if capa.rules.Scope.FUNCTION in rule.meta.scopes:
for addr, _ in rule.matches:
matches_by_function[addr].add(rule.meta.name)
elif rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE:
elif capa.rules.Scope.BASIC_BLOCK in rule.meta.scopes:
for addr, _ in rule.matches:
function = functions_by_bb[addr]
matches_by_function[function].add(rule.meta.name)
@@ -167,7 +170,7 @@ def main(argv=None):
if (args.format == "freeze") or (args.format == FORMAT_AUTO and capa.features.freeze.is_freeze(taste)):
format_ = "freeze"
extractor = capa.features.freeze.load(Path(args.sample).read_bytes())
extractor: FeatureExtractor = capa.features.freeze.load(Path(args.sample).read_bytes())
else:
format_ = args.format
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
@@ -176,6 +179,7 @@ def main(argv=None):
extractor = capa.main.get_extractor(
args.sample, args.format, args.os, args.backend, sig_paths, should_save_workspace
)
assert isinstance(extractor, StaticFeatureExtractor)
except capa.exceptions.UnsupportedFormatError:
capa.helpers.log_unsupported_format_error()
return -1
@@ -183,14 +187,12 @@ def main(argv=None):
capa.helpers.log_unsupported_runtime_error()
return -1
meta = capa.main.collect_metadata(argv, args.sample, format_, args.os, args.rules, extractor)
capabilities, counts = capa.main.find_capabilities(rules, extractor)
capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor)
meta.analysis.feature_counts = counts["feature_counts"]
meta.analysis.library_functions = counts["library_functions"]
meta = capa.main.collect_metadata(argv, args.sample, format_, args.os, args.rules, extractor, counts)
meta.analysis.layout = capa.main.compute_layout(rules, extractor, capabilities)
if capa.main.has_file_limitation(rules, capabilities):
if capa.capabilities.common.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):

View File

@@ -78,13 +78,21 @@ import capa.helpers
import capa.features
import capa.exceptions
import capa.render.verbose as v
import capa.features.common
import capa.features.freeze
import capa.features.address
import capa.features.extractors.pefile
import capa.features.extractors.base_extractor
from capa.helpers import log_unsupported_runtime_error
from capa.features.extractors.base_extractor import FunctionHandle
from capa.helpers import get_auto_format, log_unsupported_runtime_error
from capa.features.insn import API, Number
from capa.features.common import (
FORMAT_AUTO,
FORMAT_CAPE,
FORMAT_FREEZE,
DYNAMIC_FORMATS,
String,
Feature,
is_global_feature,
)
from capa.features.extractors.base_extractor import FunctionHandle, StaticFeatureExtractor, DynamicFeatureExtractor
logger = logging.getLogger("capa.show-features")
@@ -101,6 +109,7 @@ def main(argv=None):
capa.main.install_common_args(parser, wanted={"format", "os", "sample", "signatures", "backend"})
parser.add_argument("-F", "--function", type=str, help="Show features for specific function")
parser.add_argument("-P", "--process", type=str, help="Show features for specific process name")
args = parser.parse_args(args=argv)
capa.main.handle_common_args(args)
@@ -109,7 +118,7 @@ def main(argv=None):
return -1
try:
taste = capa.helpers.get_file_taste(Path(args.sample))
_ = capa.helpers.get_file_taste(Path(args.sample))
except IOError as e:
logger.error("%s", str(e))
return -1
@@ -120,23 +129,38 @@ def main(argv=None):
logger.error("%s", str(e))
return -1
if (args.format == "freeze") or (
args.format == capa.features.common.FORMAT_AUTO and capa.features.freeze.is_freeze(taste)
):
format_ = args.format if args.format != FORMAT_AUTO else get_auto_format(args.sample)
if format_ == FORMAT_FREEZE:
# this should be moved above the previous if clause after implementing
# feature freeze for the dynamic analysis flavor
extractor = capa.features.freeze.load(Path(args.sample).read_bytes())
else:
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
try:
extractor = capa.main.get_extractor(
args.sample, args.format, args.os, args.backend, sig_paths, should_save_workspace
args.sample, format_, args.os, args.backend, sig_paths, should_save_workspace
)
except capa.exceptions.UnsupportedFormatError:
capa.helpers.log_unsupported_format_error()
except capa.exceptions.UnsupportedFormatError as e:
if format_ == FORMAT_CAPE:
capa.helpers.log_unsupported_cape_report_error(str(e))
else:
capa.helpers.log_unsupported_format_error()
return -1
except capa.exceptions.UnsupportedRuntimeError:
log_unsupported_runtime_error()
return -1
if format_ in DYNAMIC_FORMATS:
assert isinstance(extractor, DynamicFeatureExtractor)
print_dynamic_analysis(extractor, args)
else:
assert isinstance(extractor, StaticFeatureExtractor)
print_static_analysis(extractor, args)
return 0
def print_static_analysis(extractor: StaticFeatureExtractor, args):
for feature, addr in extractor.extract_global_features():
print(f"global: {format_address(addr)}: {feature}")
@@ -165,9 +189,112 @@ def main(argv=None):
print(f"{args.function} not a function")
return -1
print_features(function_handles, extractor)
print_static_features(function_handles, extractor)
return 0
def print_dynamic_analysis(extractor: DynamicFeatureExtractor, args):
for feature, addr in extractor.extract_global_features():
print(f"global: {format_address(addr)}: {feature}")
if not args.process:
for feature, addr in extractor.extract_file_features():
print(f"file: {format_address(addr)}: {feature}")
process_handles = tuple(extractor.get_processes())
if args.process:
process_handles = tuple(filter(lambda ph: ph.inner["name"] == args.process, process_handles))
if args.process not in [ph.inner["name"] for ph in args.process]:
print(f"{args.process} not a process")
return -1
print_dynamic_features(process_handles, extractor)
def print_static_features(functions, extractor: StaticFeatureExtractor):
for f in functions:
if extractor.is_library_function(f.address):
function_name = extractor.get_function_name(f.address)
logger.debug("skipping library function %s (%s)", format_address(f.address), function_name)
continue
print(f"func: {format_address(f.address)}")
for feature, addr in extractor.extract_function_features(f):
if is_global_feature(feature):
continue
if f.address != addr:
print(f" func: {format_address(f.address)}: {feature} -> {format_address(addr)}")
else:
print(f" func: {format_address(f.address)}: {feature}")
for bb in extractor.get_basic_blocks(f):
for feature, addr in extractor.extract_basic_block_features(f, bb):
if is_global_feature(feature):
continue
if bb.address != addr:
print(f" bb: {format_address(bb.address)}: {feature} -> {format_address(addr)}")
else:
print(f" bb: {format_address(bb.address)}: {feature}")
for insn in extractor.get_instructions(f, bb):
for feature, addr in extractor.extract_insn_features(f, bb, insn):
if is_global_feature(feature):
continue
try:
if insn.address != addr:
print(
f" insn: {format_address(f.address)}: {format_address(insn.address)}: {feature} -> {format_address(addr)}"
)
else:
print(f" insn: {format_address(insn.address)}: {feature}")
except UnicodeEncodeError:
# may be an issue while piping to less and encountering non-ascii characters
continue
def print_dynamic_features(processes, extractor: DynamicFeatureExtractor):
for p in processes:
print(f"proc: {p.inner.process_name} (ppid={p.address.ppid}, pid={p.address.pid})")
for feature, addr in extractor.extract_process_features(p):
if is_global_feature(feature):
continue
print(f" proc: {p.inner.process_name}: {feature}")
for t in extractor.get_threads(p):
print(f" thread: {t.address.tid}")
for feature, addr in extractor.extract_thread_features(p, t):
if is_global_feature(feature):
continue
if feature != Feature(0):
print(f" {format_address(addr)}: {feature}")
for call in extractor.get_calls(p, t):
apis = []
arguments = []
for feature, addr in extractor.extract_call_features(p, t, call):
if is_global_feature(feature):
continue
if isinstance(feature, API):
assert isinstance(addr, capa.features.address.DynamicCallAddress)
apis.append((addr.id, str(feature.value)))
if isinstance(feature, (Number, String)):
arguments.append(str(feature.value))
if not apis:
print(f" arguments=[{', '.join(arguments)}]")
for cid, api in apis:
print(f" call {cid}: {api}({', '.join(arguments)})")
def ida_main():
@@ -194,7 +321,7 @@ def ida_main():
print(f"{hex(function)} not a function")
return -1
print_features(function_handles, extractor)
print_static_features(function_handles, extractor)
return 0
@@ -209,57 +336,11 @@ def ghidra_main():
function_handles = tuple(extractor.get_functions())
print_features(function_handles, extractor)
print_static_features(function_handles, extractor)
return 0
def print_features(functions, extractor: capa.features.extractors.base_extractor.FeatureExtractor):
for f in functions:
if extractor.is_library_function(f.address):
function_name = extractor.get_function_name(f.address)
logger.debug("skipping library function %s (%s)", format_address(f.address), function_name)
continue
print(f"func: {format_address(f.address)}")
for feature, addr in extractor.extract_function_features(f):
if capa.features.common.is_global_feature(feature):
continue
if f.address != addr:
print(f" func: {format_address(f.address)}: {feature} -> {format_address(addr)}")
else:
print(f" func: {format_address(f.address)}: {feature}")
for bb in extractor.get_basic_blocks(f):
for feature, addr in extractor.extract_basic_block_features(f, bb):
if capa.features.common.is_global_feature(feature):
continue
if bb.address != addr:
print(f" bb: {format_address(bb.address)}: {feature} -> {format_address(addr)}")
else:
print(f" bb: {format_address(bb.address)}: {feature}")
for insn in extractor.get_instructions(f, bb):
for feature, addr in extractor.extract_insn_features(f, bb, insn):
if capa.features.common.is_global_feature(feature):
continue
try:
if insn.address != addr:
print(
f" insn: {format_address(f.address)}: {format_address(insn.address)}: {feature} -> {format_address(addr)}"
)
else:
print(f" insn: {format_address(insn.address)}: {feature}")
except UnicodeEncodeError:
# may be an issue while piping to less and encountering non-ascii characters
continue
if __name__ == "__main__":
if capa.helpers.is_runtime_ida():
ida_main()

View File

@@ -33,7 +33,7 @@ import capa.features.extractors.pefile
import capa.features.extractors.base_extractor
from capa.helpers import log_unsupported_runtime_error
from capa.features.common import Feature
from capa.features.extractors.base_extractor import FunctionHandle
from capa.features.extractors.base_extractor import FunctionHandle, StaticFeatureExtractor
logger = logging.getLogger("show-unused-features")
@@ -52,7 +52,7 @@ def get_rules_feature_set(rules_path) -> Set[Feature]:
def get_file_features(
functions: Tuple[FunctionHandle, ...], extractor: capa.features.extractors.base_extractor.FeatureExtractor
functions: Tuple[FunctionHandle, ...], extractor: capa.features.extractors.base_extractor.StaticFeatureExtractor
) -> typing.Counter[Feature]:
feature_map: typing.Counter[Feature] = Counter()
@@ -145,6 +145,8 @@ def main(argv=None):
log_unsupported_runtime_error()
return -1
assert isinstance(extractor, StaticFeatureExtractor), "only static analysis supported today"
feature_map: typing.Counter[Feature] = Counter()
feature_map.update([feature for feature, _ in extractor.extract_global_features()])

View File

@@ -38,7 +38,14 @@ from capa.features.common import (
FeatureAccess,
)
from capa.features.address import Address
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
from capa.features.extractors.base_extractor import (
BBHandle,
CallHandle,
InsnHandle,
ThreadHandle,
ProcessHandle,
FunctionHandle,
)
from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor
CD = Path(__file__).resolve().parent
@@ -93,9 +100,9 @@ def get_viv_extractor(path: Path):
sigpaths = [
CD / "data" / "sigs" / "test_aulldiv.pat",
CD / "data" / "sigs" / "test_aullrem.pat.gz",
CD.parent / "sigs" / "1_flare_msvc_rtf_32_64.sig",
CD.parent / "sigs" / "2_flare_msvc_atlmfc_32_64.sig",
CD.parent / "sigs" / "3_flare_common_libs.sig",
CD.parent / "capa" / "sigs" / "1_flare_msvc_rtf_32_64.sig",
CD.parent / "capa" / "sigs" / "2_flare_msvc_atlmfc_32_64.sig",
CD.parent / "capa" / "sigs" / "3_flare_common_libs.sig",
]
if "raw32" in path.name:
@@ -134,10 +141,11 @@ def get_pefile_extractor(path: Path):
return extractor
def get_dotnetfile_extractor(path: Path):
import capa.features.extractors.dotnetfile
@lru_cache(maxsize=1)
def get_dnfile_extractor(path: Path):
import capa.features.extractors.dnfile.extractor
extractor = capa.features.extractors.dotnetfile.DotnetFileFeatureExtractor(path)
extractor = capa.features.extractors.dnfile.extractor.DnfileFeatureExtractor(path)
# overload the extractor so that the fixture exposes `extractor.path`
setattr(extractor, "path", path.as_posix())
@@ -146,10 +154,10 @@ def get_dotnetfile_extractor(path: Path):
@lru_cache(maxsize=1)
def get_dnfile_extractor(path: Path):
import capa.features.extractors.dnfile.extractor
def get_dotnetfile_extractor(path: Path):
import capa.features.extractors.dotnetfile
extractor = capa.features.extractors.dnfile.extractor.DnfileFeatureExtractor(path)
extractor = capa.features.extractors.dotnetfile.DotnetFileFeatureExtractor(path)
# overload the extractor so that the fixture exposes `extractor.path`
setattr(extractor, "path", path.as_posix())
@@ -181,6 +189,20 @@ def get_binja_extractor(path: Path):
return extractor
@lru_cache(maxsize=1)
def get_cape_extractor(path):
import gzip
import json
from capa.features.extractors.cape.extractor import CapeExtractor
with gzip.open(path, "r") as compressed_report:
report_json = compressed_report.read()
report = json.loads(report_json)
return CapeExtractor.from_report(report)
@lru_cache(maxsize=1)
def get_ghidra_extractor(path: Path):
import capa.features.extractors.ghidra.extractor
@@ -206,6 +228,36 @@ def extract_file_features(extractor):
return features
def extract_process_features(extractor, ph):
features = collections.defaultdict(set)
for th in extractor.get_threads(ph):
for ch in extractor.get_calls(ph, th):
for feature, va in extractor.extract_call_features(ph, th, ch):
features[feature].add(va)
for feature, va in extractor.extract_thread_features(ph, th):
features[feature].add(va)
for feature, va in extractor.extract_process_features(ph):
features[feature].add(va)
return features
def extract_thread_features(extractor, ph, th):
features = collections.defaultdict(set)
for ch in extractor.get_calls(ph, th):
for feature, va in extractor.extract_call_features(ph, th, ch):
features[feature].add(va)
for feature, va in extractor.extract_thread_features(ph, th):
features[feature].add(va)
return features
def extract_call_features(extractor, ph, th, ch):
features = collections.defaultdict(set)
for feature, addr in extractor.extract_call_features(ph, th, ch):
features[feature].add(addr)
return features
# f may not be hashable (e.g. ida func_t) so cannot @lru_cache this
def extract_function_features(extractor, fh):
features = collections.defaultdict(set)
@@ -267,6 +319,8 @@ def get_data_path_by_name(name) -> Path:
return CD / "data" / "499c2a85f6e8142c3f48d4251c9c7cd6.raw32"
elif name.startswith("9324d"):
return CD / "data" / "9324d1a8ae37a36ae560c37448c9705a.exe_"
elif name.startswith("395eb"):
return CD / "data" / "395eb0ddd99d2c9e37b6d0b73485ee9c.exe_"
elif name.startswith("a1982"):
return CD / "data" / "a198216798ca38f280dc413f8c57f2c2.exe_"
elif name.startswith("a933a"):
@@ -317,10 +371,32 @@ def get_data_path_by_name(name) -> Path:
return CD / "data" / "294b8db1f2702b60fb2e42fdc50c2cee6a5046112da9a5703a548a4fa50477bc.elf_"
elif name.startswith("2bf18d"):
return CD / "data" / "2bf18d0403677378adad9001b1243211.elf_"
elif name.startswith("0000a657"):
return (
CD
/ "data"
/ "dynamic"
/ "cape"
/ "v2.2"
/ "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz"
)
elif name.startswith("d46900"):
return (
CD
/ "data"
/ "dynamic"
/ "cape"
/ "v2.2"
/ "d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json.gz"
)
elif name.startswith("ea2876"):
return CD / "data" / "ea2876e9175410b6f6719f80ee44b9553960758c7d0f7bed73c0fe9a78d8e669.dll_"
elif name.startswith("1038a2"):
return CD / "data" / "1038a23daad86042c66bfe6c9d052d27048de9653bde5750dc0f240c792d9ac8.elf_"
elif name.startswith("nested_typedef"):
return CD / "data" / "dotnet" / "dd9098ff91717f4906afe9dafdfa2f52.exe_"
elif name.startswith("nested_typeref"):
return CD / "data" / "dotnet" / "2c7d60f77812607dec5085973ff76cea.dll_"
else:
raise ValueError(f"unexpected sample fixture: {name}")
@@ -396,6 +472,27 @@ def sample(request):
return resolve_sample(request.param)
def get_process(extractor, ppid: int, pid: int) -> ProcessHandle:
for ph in extractor.get_processes():
if ph.address.ppid == ppid and ph.address.pid == pid:
return ph
raise ValueError("process not found")
def get_thread(extractor, ph: ProcessHandle, tid: int) -> ThreadHandle:
for th in extractor.get_threads(ph):
if th.address.tid == tid:
return th
raise ValueError("thread not found")
def get_call(extractor, ph: ProcessHandle, th: ThreadHandle, cid: int) -> CallHandle:
for ch in extractor.get_calls(ph, th):
if ch.address.id == cid:
return ch
raise ValueError("call not found")
def get_function(extractor, fva: int) -> FunctionHandle:
for fh in extractor.get_functions():
if isinstance(extractor, DnfileFeatureExtractor):
@@ -503,6 +600,63 @@ def resolve_scope(scope):
inner_function.__name__ = scope
return inner_function
elif "call=" in scope:
# like `process=(pid:ppid),thread=tid,call=id`
assert "process=" in scope
assert "thread=" in scope
pspec, _, spec = scope.partition(",")
tspec, _, cspec = spec.partition(",")
pspec = pspec.partition("=")[2][1:-1].split(":")
assert len(pspec) == 2
pid, ppid = map(int, pspec)
tid = int(tspec.partition("=")[2])
cid = int(cspec.partition("=")[2])
def inner_call(extractor):
ph = get_process(extractor, ppid, pid)
th = get_thread(extractor, ph, tid)
ch = get_call(extractor, ph, th, cid)
features = extract_call_features(extractor, ph, th, ch)
for k, vs in extract_global_features(extractor).items():
features[k].update(vs)
return features
inner_call.__name__ = scope
return inner_call
elif "thread=" in scope:
# like `process=(pid:ppid),thread=tid`
assert "process=" in scope
pspec, _, tspec = scope.partition(",")
pspec = pspec.partition("=")[2][1:-1].split(":")
assert len(pspec) == 2
pid, ppid = map(int, pspec)
tid = int(tspec.partition("=")[2])
def inner_thread(extractor):
ph = get_process(extractor, ppid, pid)
th = get_thread(extractor, ph, tid)
features = extract_thread_features(extractor, ph, th)
for k, vs in extract_global_features(extractor).items():
features[k].update(vs)
return features
inner_thread.__name__ = scope
return inner_thread
elif "process=" in scope:
# like `process=(pid:ppid)`
pspec = scope.partition("=")[2][1:-1].split(":")
assert len(pspec) == 2
pid, ppid = map(int, pspec)
def inner_process(extractor):
ph = get_process(extractor, ppid, pid)
features = extract_process_features(extractor, ph)
for k, vs in extract_global_features(extractor).items():
features[k].update(vs)
return features
inner_process.__name__ = scope
return inner_process
else:
raise ValueError("unexpected scope fixture")
@@ -528,6 +682,84 @@ def parametrize(params, values, **kwargs):
return pytest.mark.parametrize(params, values, ids=ids, **kwargs)
DYNAMIC_FEATURE_PRESENCE_TESTS = sorted(
[
# file/string
("0000a657", "file", capa.features.common.String("T_Ba?.BcRJa"), True),
("0000a657", "file", capa.features.common.String("GetNamedPipeClientSessionId"), True),
("0000a657", "file", capa.features.common.String("nope"), False),
# file/sections
("0000a657", "file", capa.features.file.Section(".rdata"), True),
("0000a657", "file", capa.features.file.Section(".nope"), False),
# file/imports
("0000a657", "file", capa.features.file.Import("NdrSimpleTypeUnmarshall"), True),
("0000a657", "file", capa.features.file.Import("Nope"), False),
# file/exports
("0000a657", "file", capa.features.file.Export("Nope"), False),
# process/environment variables
(
"0000a657",
"process=(1180:3052)",
capa.features.common.String("C:\\Users\\comp\\AppData\\Roaming\\Microsoft\\Jxoqwnx\\jxoqwn.exe"),
True,
),
("0000a657", "process=(1180:3052)", capa.features.common.String("nope"), False),
# thread/api calls
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.API("NtQueryValueKey"), True),
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.API("GetActiveWindow"), False),
# thread/number call argument
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.Number(0x000000EC), True),
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.Number(110173), False),
# thread/string call argument
("0000a657", "process=(2852:3052),thread=2804", capa.features.common.String("SetThreadUILanguage"), True),
("0000a657", "process=(2852:3052),thread=2804", capa.features.common.String("nope"), False),
("0000a657", "process=(2852:3052),thread=2804,call=56", capa.features.insn.API("NtQueryValueKey"), True),
("0000a657", "process=(2852:3052),thread=2804,call=1958", capa.features.insn.API("nope"), False),
],
# order tests by (file, item)
# so that our LRU cache is most effective.
key=lambda t: (t[0], t[1]),
)
DYNAMIC_FEATURE_COUNT_TESTS = sorted(
[
# file/string
("0000a657", "file", capa.features.common.String("T_Ba?.BcRJa"), 1),
("0000a657", "file", capa.features.common.String("GetNamedPipeClientSessionId"), 1),
("0000a657", "file", capa.features.common.String("nope"), 0),
# file/sections
("0000a657", "file", capa.features.file.Section(".rdata"), 1),
("0000a657", "file", capa.features.file.Section(".nope"), 0),
# file/imports
("0000a657", "file", capa.features.file.Import("NdrSimpleTypeUnmarshall"), 1),
("0000a657", "file", capa.features.file.Import("Nope"), 0),
# file/exports
("0000a657", "file", capa.features.file.Export("Nope"), 0),
# process/environment variables
(
"0000a657",
"process=(1180:3052)",
capa.features.common.String("C:\\Users\\comp\\AppData\\Roaming\\Microsoft\\Jxoqwnx\\jxoqwn.exe"),
2,
),
("0000a657", "process=(1180:3052)", capa.features.common.String("nope"), 0),
# thread/api calls
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.API("NtQueryValueKey"), 7),
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.API("GetActiveWindow"), 0),
# thread/number call argument
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.Number(0x000000EC), 1),
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.Number(110173), 0),
# thread/string call argument
("0000a657", "process=(2852:3052),thread=2804", capa.features.common.String("SetThreadUILanguage"), 1),
("0000a657", "process=(2852:3052),thread=2804", capa.features.common.String("nope"), 0),
("0000a657", "process=(2852:3052),thread=2804,call=56", capa.features.insn.API("NtQueryValueKey"), 1),
("0000a657", "process=(2852:3052),thread=2804,call=1958", capa.features.insn.API("nope"), 0),
],
# order tests by (file, item)
# so that our LRU cache is most effective.
key=lambda t: (t[0], t[1]),
)
FEATURE_PRESENCE_TESTS = sorted(
[
# file/characteristic("embedded pe")
@@ -552,6 +784,7 @@ FEATURE_PRESENCE_TESTS = sorted(
("mimikatz", "file", capa.features.file.Import("advapi32.CryptSetHashParam"), True),
("mimikatz", "file", capa.features.file.Import("CryptSetHashParam"), True),
("mimikatz", "file", capa.features.file.Import("kernel32.IsWow64Process"), True),
("mimikatz", "file", capa.features.file.Import("IsWow64Process"), True),
("mimikatz", "file", capa.features.file.Import("msvcrt.exit"), True),
("mimikatz", "file", capa.features.file.Import("cabinet.#11"), True),
("mimikatz", "file", capa.features.file.Import("#11"), False),
@@ -632,11 +865,12 @@ FEATURE_PRESENCE_TESTS = sorted(
# .text:004018C0 8D 4B 02 lea ecx, [ebx+2]
("mimikatz", "function=0x401873,bb=0x4018B2,insn=0x4018C0", capa.features.insn.Number(0x2), True),
# insn/api
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptAcquireContextW"), True),
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptAcquireContext"), True),
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptGenKey"), True),
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptImportKey"), True),
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptDestroyKey"), True),
# not extracting dll anymore
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptAcquireContextW"), False),
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptAcquireContext"), False),
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptGenKey"), False),
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptImportKey"), False),
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptDestroyKey"), False),
("mimikatz", "function=0x403BAC", capa.features.insn.API("CryptAcquireContextW"), True),
("mimikatz", "function=0x403BAC", capa.features.insn.API("CryptAcquireContext"), True),
("mimikatz", "function=0x403BAC", capa.features.insn.API("CryptGenKey"), True),
@@ -645,7 +879,8 @@ FEATURE_PRESENCE_TESTS = sorted(
("mimikatz", "function=0x403BAC", capa.features.insn.API("Nope"), False),
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.Nope"), False),
# insn/api: thunk
("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), True),
# not extracting dll anymore
("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), False),
("mimikatz", "function=0x4556E5", capa.features.insn.API("LsaQueryInformationPolicy"), True),
# insn/api: x64
(
@@ -669,10 +904,15 @@ FEATURE_PRESENCE_TESTS = sorted(
("mimikatz", "function=0x40B3C6", capa.features.insn.API("LocalFree"), True),
("c91887...", "function=0x40156F", capa.features.insn.API("CloseClipboard"), True),
# insn/api: resolve indirect calls
("c91887...", "function=0x401A77", capa.features.insn.API("kernel32.CreatePipe"), True),
("c91887...", "function=0x401A77", capa.features.insn.API("kernel32.SetHandleInformation"), True),
("c91887...", "function=0x401A77", capa.features.insn.API("kernel32.CloseHandle"), True),
("c91887...", "function=0x401A77", capa.features.insn.API("kernel32.WriteFile"), True),
# not extracting dll anymore
("c91887...", "function=0x401A77", capa.features.insn.API("kernel32.CreatePipe"), False),
("c91887...", "function=0x401A77", capa.features.insn.API("kernel32.SetHandleInformation"), False),
("c91887...", "function=0x401A77", capa.features.insn.API("kernel32.CloseHandle"), False),
("c91887...", "function=0x401A77", capa.features.insn.API("kernel32.WriteFile"), False),
("c91887...", "function=0x401A77", capa.features.insn.API("CreatePipe"), True),
("c91887...", "function=0x401A77", capa.features.insn.API("SetHandleInformation"), True),
("c91887...", "function=0x401A77", capa.features.insn.API("CloseHandle"), True),
("c91887...", "function=0x401A77", capa.features.insn.API("WriteFile"), True),
# insn/string
("mimikatz", "function=0x40105D", capa.features.common.String("SCardControl"), True),
("mimikatz", "function=0x40105D", capa.features.common.String("SCardTransmit"), True),
@@ -847,7 +1087,8 @@ FEATURE_PRESENCE_TESTS_DOTNET = sorted(
("_1c444", "file", capa.features.file.Import("CreateCompatibleBitmap"), True),
("_1c444", "file", capa.features.file.Import("gdi32::CreateCompatibleBitmap"), False),
("_1c444", "function=0x1F68", capa.features.insn.API("GetWindowDC"), True),
("_1c444", "function=0x1F68", capa.features.insn.API("user32.GetWindowDC"), True),
# not extracting dll anymore
("_1c444", "function=0x1F68", capa.features.insn.API("user32.GetWindowDC"), False),
("_1c444", "function=0x1F68", capa.features.insn.Number(0xCC0020), True),
("_1c444", "token=0x600001D", capa.features.common.Characteristic("calls to"), True),
("_1c444", "token=0x6000018", capa.features.common.Characteristic("calls to"), False),
@@ -1037,6 +1278,114 @@ FEATURE_PRESENCE_TESTS_DOTNET = sorted(
), # MemberRef method
False,
),
(
"nested_typedef",
"file",
capa.features.common.Class("mynamespace.myclass_outer0"),
True,
),
(
"nested_typedef",
"file",
capa.features.common.Class("mynamespace.myclass_outer1"),
True,
),
(
"nested_typedef",
"file",
capa.features.common.Class("mynamespace.myclass_outer0/myclass_inner0_0"),
True,
),
(
"nested_typedef",
"file",
capa.features.common.Class("mynamespace.myclass_outer0/myclass_inner0_1"),
True,
),
(
"nested_typedef",
"file",
capa.features.common.Class("mynamespace.myclass_outer1/myclass_inner1_0"),
True,
),
(
"nested_typedef",
"file",
capa.features.common.Class("mynamespace.myclass_outer1/myclass_inner1_1"),
True,
),
(
"nested_typedef",
"file",
capa.features.common.Class("mynamespace.myclass_outer1/myclass_inner1_0/myclass_inner_inner"),
True,
),
(
"nested_typedef",
"file",
capa.features.common.Class("myclass_inner_inner"),
False,
),
(
"nested_typedef",
"file",
capa.features.common.Class("myclass_inner1_0"),
False,
),
(
"nested_typedef",
"file",
capa.features.common.Class("myclass_inner1_1"),
False,
),
(
"nested_typedef",
"file",
capa.features.common.Class("myclass_inner0_0"),
False,
),
(
"nested_typedef",
"file",
capa.features.common.Class("myclass_inner0_1"),
False,
),
(
"nested_typeref",
"file",
capa.features.file.Import("Android.OS.Build/VERSION::SdkInt"),
True,
),
(
"nested_typeref",
"file",
capa.features.file.Import("Android.Media.Image/Plane::Buffer"),
True,
),
(
"nested_typeref",
"file",
capa.features.file.Import("Android.Provider.Telephony/Sent/Sent::ContentUri"),
True,
),
(
"nested_typeref",
"file",
capa.features.file.Import("Android.OS.Build::SdkInt"),
False,
),
(
"nested_typeref",
"file",
capa.features.file.Import("Plane::Buffer"),
False,
),
(
"nested_typeref",
"file",
capa.features.file.Import("Sent::ContentUri"),
False,
),
],
# order tests by (file, item)
# so that our LRU cache is most effective.
@@ -1121,6 +1470,11 @@ def z9324d_extractor():
return get_extractor(get_data_path_by_name("9324d..."))
@pytest.fixture
def z395eb_extractor():
return get_extractor(get_data_path_by_name("395eb..."))
@pytest.fixture
def pma12_04_extractor():
return get_extractor(get_data_path_by_name("pma12-04"))
@@ -1207,29 +1561,42 @@ def get_result_doc(path: Path):
@pytest.fixture
def pma0101_rd():
# python -m capa.main tests/data/Practical\ Malware\ Analysis\ Lab\ 01-01.dll_ --json > tests/data/rd/Practical\ Malware\ Analysis\ Lab\ 01-01.dll_.json
return get_result_doc(CD / "data" / "rd" / "Practical Malware Analysis Lab 01-01.dll_.json")
@pytest.fixture
def dotnet_1c444e_rd():
# .NET sample
# python -m capa.main tests/data/dotnet/1c444ebeba24dcba8628b7dfe5fec7c6.exe_ --json > tests/data/rd/1c444ebeba24dcba8628b7dfe5fec7c6.exe_.json
return get_result_doc(CD / "data" / "rd" / "1c444ebeba24dcba8628b7dfe5fec7c6.exe_.json")
@pytest.fixture
def a3f3bbc_rd():
# python -m capa.main tests/data/3f3bbcf8fd90bdcdcdc5494314ed4225.exe_ --json > tests/data/rd/3f3bbcf8fd90bdcdcdc5494314ed4225.exe_.json
return get_result_doc(CD / "data" / "rd" / "3f3bbcf8fd90bdcdcdc5494314ed4225.exe_.json")
@pytest.fixture
def al_khaserx86_rd():
# python -m capa.main tests/data/al-khaser_x86.exe_ --json > tests/data/rd/al-khaser_x86.exe_.json
return get_result_doc(CD / "data" / "rd" / "al-khaser_x86.exe_.json")
@pytest.fixture
def al_khaserx64_rd():
# python -m capa.main tests/data/al-khaser_x64.exe_ --json > tests/data/rd/al-khaser_x64.exe_.json
return get_result_doc(CD / "data" / "rd" / "al-khaser_x64.exe_.json")
@pytest.fixture
def a076114_rd():
# python -m capa.main tests/data/0761142efbda6c4b1e801223de723578.dll_ --json > tests/data/rd/0761142efbda6c4b1e801223de723578.dll_.json
return get_result_doc(CD / "data" / "rd" / "0761142efbda6c4b1e801223de723578.dll_.json")
@pytest.fixture
def dynamic_a0000a6_rd():
# python -m capa.main tests/data/dynamic/cape/v2.2/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json --json > tests/data/rd/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json
return get_result_doc(CD / "data" / "rd" / "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json")

309
tests/test_capabilities.py Normal file
View File

@@ -0,0 +1,309 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import textwrap
import capa.capabilities.common
def test_match_across_scopes_file_function(z9324d_extractor):
rules = capa.rules.RuleSet(
[
# this rule should match on a function (0x4073F0)
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: install service
scopes:
static: function
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x4073F0
features:
- and:
- api: advapi32.OpenSCManagerA
- api: advapi32.CreateServiceA
- api: advapi32.StartServiceA
"""
)
),
# this rule should match on a file feature
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: .text section
scopes:
static: file
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a
features:
- section: .text
"""
)
),
# this rule should match on earlier rule matches:
# - install service, with function scope
# - .text section, with file scope
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: .text section and install service
scopes:
static: file
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a
features:
- and:
- match: install service
- match: .text section
"""
)
),
]
)
capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor)
assert "install service" in capabilities
assert ".text section" in capabilities
assert ".text section and install service" in capabilities
def test_match_across_scopes(z9324d_extractor):
rules = capa.rules.RuleSet(
[
# this rule should match on a basic block (including at least 0x403685)
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: tight loop
scopes:
static: basic block
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x403685
features:
- characteristic: tight loop
"""
)
),
# this rule should match on a function (0x403660)
# based on API, as well as prior basic block rule match
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: kill thread loop
scopes:
static: function
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x403660
features:
- and:
- api: kernel32.TerminateThread
- api: kernel32.CloseHandle
- match: tight loop
"""
)
),
# this rule should match on a file feature and a prior function rule match
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: kill thread program
scopes:
static: file
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a
features:
- and:
- section: .text
- match: kill thread loop
"""
)
),
]
)
capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor)
assert "tight loop" in capabilities
assert "kill thread loop" in capabilities
assert "kill thread program" in capabilities
def test_subscope_bb_rules(z9324d_extractor):
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- basic block:
- characteristic: tight loop
"""
)
)
]
)
# tight loop at 0x403685
capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor)
assert "test rule" in capabilities
def test_byte_matching(z9324d_extractor):
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: byte match test
scopes:
static: function
dynamic: process
features:
- and:
- bytes: ED 24 9E F4 52 A9 07 47 55 8E E1 AB 30 8E 23 61
"""
)
)
]
)
capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor)
assert "byte match test" in capabilities
def test_com_feature_matching(z395eb_extractor):
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: initialize IWebBrowser2
scopes:
static: basic block
dynamic: unsupported
features:
- and:
- api: ole32.CoCreateInstance
- com/class: InternetExplorer #bytes: 01 DF 02 00 00 00 00 00 C0 00 00 00 00 00 00 46 = CLSID_InternetExplorer
- com/interface: IWebBrowser2 #bytes: 61 16 0C D3 AF CD D0 11 8A 3E 00 C0 4F C9 E2 6E = IID_IWebBrowser2
"""
)
)
]
)
capabilities, meta = capa.main.find_capabilities(rules, z395eb_extractor)
assert "initialize IWebBrowser2" in capabilities
def test_count_bb(z9324d_extractor):
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: count bb
namespace: test
scopes:
static: function
dynamic: process
features:
- and:
- count(basic blocks): 1 or more
"""
)
)
]
)
capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor)
assert "count bb" in capabilities
def test_instruction_scope(z9324d_extractor):
# .text:004071A4 68 E8 03 00 00 push 3E8h
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: push 1000
namespace: test
scopes:
static: instruction
dynamic: process
features:
- and:
- mnemonic: push
- number: 1000
"""
)
)
]
)
capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor)
assert "push 1000" in capabilities
assert 0x4071A4 in {result[0] for result in capabilities["push 1000"]}
def test_instruction_subscope(z9324d_extractor):
# .text:00406F60 sub_406F60 proc near
# [...]
# .text:004071A4 68 E8 03 00 00 push 3E8h
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: push 1000 on i386
namespace: test
scopes:
static: function
dynamic: process
features:
- and:
- arch: i386
- instruction:
- mnemonic: push
- number: 1000
"""
)
)
]
)
capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor)
assert "push 1000 on i386" in capabilities
assert 0x406F60 in {result[0] for result in capabilities["push 1000 on i386"]}

View File

@@ -0,0 +1,27 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import fixtures
@fixtures.parametrize(
"sample,scope,feature,expected",
fixtures.DYNAMIC_FEATURE_PRESENCE_TESTS,
indirect=["sample", "scope"],
)
def test_cape_features(sample, scope, feature, expected):
fixtures.do_test_feature_presence(fixtures.get_cape_extractor, sample, scope, feature, expected)
@fixtures.parametrize(
"sample,scope,feature,expected",
fixtures.DYNAMIC_FEATURE_COUNT_TESTS,
indirect=["sample", "scope"],
)
def test_cape_feature_counts(sample, scope, feature, expected):
fixtures.do_test_feature_count(fixtures.get_cape_extractor, sample, scope, feature, expected)

72
tests/test_cape_model.py Normal file
View File

@@ -0,0 +1,72 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import gzip
from pathlib import Path
import fixtures
from capa.features.extractors.cape.models import Call, CapeReport
CD = Path(__file__).resolve().parent
CAPE_DIR = CD / "data" / "dynamic" / "cape"
@fixtures.parametrize(
"version,filename",
[
("v2.2", "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz"),
("v2.2", "55dcd38773f4104b95589acc87d93bf8b4a264b4a6d823b73fb6a7ab8144c08b.json.gz"),
("v2.2", "77c961050aa252d6d595ec5120981abf02068c968f4a5be5958d10e87aa6f0e8.json.gz"),
("v2.2", "d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json.gz"),
("v2.4", "36d218f384010cce9f58b8193b7d8cc855d1dff23f80d16e13a883e152d07921.json.gz"),
("v2.4", "41ce492f04accef7931b84b8548a6ca717ffabb9bedc4f624de2d37a5345036c.json.gz"),
("v2.4", "515a6269965ccdf1005008e017ec87fafb97fd2464af1c393ad93b438f6f33fe.json.gz"),
("v2.4", "5d61700feabba201e1ba98df3c8210a3090c8c9f9adbf16cb3d1da3aaa2a9d96.json.gz"),
("v2.4", "5effaf6795932d8b36755f89f99ce7436421ea2bd1ed5bc55476530c1a22009f.json.gz"),
("v2.4", "873275144af88e9b95ea2c59ece39b8ce5a9d7fe09774b683050098ac965054d.json.gz"),
("v2.4", "8b9aaf4fad227cde7a7dabce7ba187b0b923301718d9d40de04bdd15c9b22905.json.gz"),
("v2.4", "b1c4aa078880c579961dc5ec899b2c2e08ae5db80b4263e4ca9607a68e2faef9.json.gz"),
("v2.4", "fb7ade52dc5a1d6128b9c217114a46d0089147610f99f5122face29e429a1e74.json.gz"),
],
)
def test_cape_model_can_load(version: str, filename: str):
path = CAPE_DIR / version / filename
buf = gzip.decompress(path.read_bytes())
report = CapeReport.from_buf(buf)
assert report is not None
def test_cape_model_argument():
call = Call.model_validate_json(
"""
{
"timestamp": "2023-10-20 12:30:14,015",
"thread_id": "2380",
"caller": "0x7797dff8",
"parentcaller": "0x77973486",
"category": "system",
"api": "TestApiCall",
"status": true,
"return": "0x00000000",
"arguments": [
{
"name": "Value Base 10",
"value": "30"
},
{
"name": "Value Base 16",
"value": "0x30"
}
],
"repeated": 19,
"id": 0
}
"""
)
assert call.arguments[0].value == 30
assert call.arguments[1].value == 0x30

View File

@@ -1,33 +0,0 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import fixtures
@fixtures.parametrize(
"sample,scope,feature,expected",
fixtures.FEATURE_PRESENCE_TESTS_DOTNET,
indirect=["sample", "scope"],
)
def test_dnfile_features(sample, scope, feature, expected):
fixtures.do_test_feature_presence(fixtures.get_dnfile_extractor, sample, scope, feature, expected)
@fixtures.parametrize(
"extractor,function,expected",
[
("b9f5b_dotnetfile_extractor", "is_dotnet_file", True),
("b9f5b_dotnetfile_extractor", "is_mixed_mode", False),
("mixed_mode_64_dotnetfile_extractor", "is_mixed_mode", True),
("b9f5b_dotnetfile_extractor", "get_entry_point", 0x6000007),
("b9f5b_dotnetfile_extractor", "get_runtime_version", (2, 5)),
("b9f5b_dotnetfile_extractor", "get_meta_version_string", "v2.0.50727"),
],
)
def test_dnfile_extractor(request, extractor, function, expected):
extractor_function = getattr(request.getfixturevalue(extractor), function)
assert extractor_function() == expected

View File

@@ -0,0 +1,86 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
import pytest
import fixtures
from capa.features.extractors.base_extractor import SampleHashes
logger = logging.getLogger(__name__)
def test_viv_hash_extraction():
assert fixtures.get_viv_extractor(fixtures.get_data_path_by_name("mimikatz")).get_sample_hashes() == SampleHashes(
md5="5f66b82558ca92e54e77f216ef4c066c",
sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38",
sha256="131314a6f6d1d263c75b9909586b3e1bd837036329ace5e69241749e861ac01d",
)
def test_pefile_hash_extraction():
assert fixtures.get_pefile_extractor(
fixtures.get_data_path_by_name("mimikatz")
).get_sample_hashes() == SampleHashes(
md5="5f66b82558ca92e54e77f216ef4c066c",
sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38",
sha256="131314a6f6d1d263c75b9909586b3e1bd837036329ace5e69241749e861ac01d",
)
def test_dnfile_hash_extraction():
assert fixtures.get_dnfile_extractor(fixtures.get_data_path_by_name("b9f5b")).get_sample_hashes() == SampleHashes(
md5="b9f5bd514485fb06da39beff051b9fdc",
sha1="c72a2e50410475a51d897d29ffbbaf2103754d53",
sha256="34acc4c0b61b5ce0b37c3589f97d1f23e6d84011a241e6f85683ee517ce786f1",
)
def test_dotnetfile_hash_extraction():
assert fixtures.get_dotnetfile_extractor(
fixtures.get_data_path_by_name("b9f5b")
).get_sample_hashes() == SampleHashes(
md5="b9f5bd514485fb06da39beff051b9fdc",
sha1="c72a2e50410475a51d897d29ffbbaf2103754d53",
sha256="34acc4c0b61b5ce0b37c3589f97d1f23e6d84011a241e6f85683ee517ce786f1",
)
def test_cape_hash_extraction():
assert fixtures.get_cape_extractor(fixtures.get_data_path_by_name("0000a657")).get_sample_hashes() == SampleHashes(
md5="e2147b5333879f98d515cd9aa905d489",
sha1="ad4d520fb7792b4a5701df973d6bd8a6cbfbb57f",
sha256="0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82",
)
# We need to skip the binja test if we cannot import binaryninja, e.g., in GitHub CI.
binja_present: bool = False
try:
import binaryninja
try:
binaryninja.load(source=b"\x90")
except RuntimeError:
logger.warning("Binary Ninja license is not valid, provide via $BN_LICENSE or license.dat")
else:
binja_present = True
except ImportError:
pass
@pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed")
def test_binja_hash_extraction():
extractor = fixtures.get_binja_extractor(fixtures.get_data_path_by_name("mimikatz"))
hashes = SampleHashes(
md5="5f66b82558ca92e54e77f216ef4c066c",
sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38",
sha256="131314a6f6d1d263c75b9909586b3e1bd837036329ace5e69241749e861ac01d",
)
assert extractor.get_sample_hashes() == hashes

View File

@@ -17,7 +17,9 @@ EXPECTED = textwrap.dedent(
name: test rule
authors:
- user@domain.com
scope: function
scopes:
static: function
dynamic: process
examples:
- foo1234
- bar5678
@@ -41,7 +43,9 @@ def test_rule_reformat_top_level_elements():
name: test rule
authors:
- user@domain.com
scope: function
scopes:
static: function
dynamic: process
examples:
- foo1234
- bar5678
@@ -59,7 +63,9 @@ def test_rule_reformat_indentation():
name: test rule
authors:
- user@domain.com
scope: function
scopes:
static: function
dynamic: process
examples:
- foo1234
- bar5678
@@ -83,7 +89,9 @@ def test_rule_reformat_order():
examples:
- foo1234
- bar5678
scope: function
scopes:
static: function
dynamic: process
name: test rule
features:
- and:
@@ -107,7 +115,9 @@ def test_rule_reformat_meta_update():
examples:
- foo1234
- bar5678
scope: function
scopes:
static: function
dynamic: process
name: AAAA
features:
- and:
@@ -131,7 +141,9 @@ def test_rule_reformat_string_description():
name: test rule
authors:
- user@domain.com
scope: function
scopes:
static: function
dynamic: process
features:
- and:
- string: foo

View File

@@ -0,0 +1,165 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import textwrap
from typing import List
from pathlib import Path
import fixtures
import capa.main
import capa.rules
import capa.helpers
import capa.features.file
import capa.features.insn
import capa.features.common
import capa.features.freeze
import capa.features.basicblock
import capa.features.extractors.null
import capa.features.extractors.base_extractor
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import (
SampleHashes,
ThreadHandle,
ProcessHandle,
ThreadAddress,
ProcessAddress,
DynamicCallAddress,
DynamicFeatureExtractor,
)
EXTRACTOR = capa.features.extractors.null.NullDynamicFeatureExtractor(
base_address=AbsoluteVirtualAddress(0x401000),
sample_hashes=SampleHashes(
md5="6eb7ee7babf913d75df3f86c229df9e7",
sha1="2a082494519acd5130d5120fa48786df7275fdd7",
sha256="0c7d1a34eb9fd55bedbf37ba16e3d5dd8c1dd1d002479cc4af27ef0f82bb4792",
),
global_features=[],
file_features=[
(AbsoluteVirtualAddress(0x402345), capa.features.common.Characteristic("embedded pe")),
],
processes={
ProcessAddress(pid=1): capa.features.extractors.null.ProcessFeatures(
name="explorer.exe",
features=[],
threads={
ThreadAddress(ProcessAddress(pid=1), tid=1): capa.features.extractors.null.ThreadFeatures(
features=[],
calls={
DynamicCallAddress(
thread=ThreadAddress(ProcessAddress(pid=1), tid=1), id=1
): capa.features.extractors.null.CallFeatures(
name="CreateFile(12)",
features=[
(
DynamicCallAddress(thread=ThreadAddress(ProcessAddress(pid=1), tid=1), id=1),
capa.features.insn.API("CreateFile"),
),
(
DynamicCallAddress(thread=ThreadAddress(ProcessAddress(pid=1), tid=1), id=1),
capa.features.insn.Number(12),
),
],
),
DynamicCallAddress(
thread=ThreadAddress(ProcessAddress(pid=1), tid=1), id=2
): capa.features.extractors.null.CallFeatures(
name="WriteFile()",
features=[
(
DynamicCallAddress(thread=ThreadAddress(ProcessAddress(pid=1), tid=1), id=2),
capa.features.insn.API("WriteFile"),
),
],
),
},
),
},
),
},
)
def addresses(s) -> List[Address]:
return sorted(i.address for i in s)
def test_null_feature_extractor():
ph = ProcessHandle(ProcessAddress(pid=1), None)
th = ThreadHandle(ThreadAddress(ProcessAddress(pid=1), tid=1), None)
assert addresses(EXTRACTOR.get_processes()) == [ProcessAddress(pid=1)]
assert addresses(EXTRACTOR.get_threads(ph)) == [ThreadAddress(ProcessAddress(pid=1), tid=1)]
assert addresses(EXTRACTOR.get_calls(ph, th)) == [
DynamicCallAddress(thread=ThreadAddress(ProcessAddress(pid=1), tid=1), id=1),
DynamicCallAddress(thread=ThreadAddress(ProcessAddress(pid=1), tid=1), id=2),
]
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: create file
scopes:
static: basic block
dynamic: call
features:
- and:
- api: CreateFile
"""
)
),
]
)
capabilities, _ = capa.main.find_capabilities(rules, EXTRACTOR)
assert "create file" in capabilities
def compare_extractors(a: DynamicFeatureExtractor, b: DynamicFeatureExtractor):
assert list(a.extract_file_features()) == list(b.extract_file_features())
assert addresses(a.get_processes()) == addresses(b.get_processes())
for p in a.get_processes():
assert addresses(a.get_threads(p)) == addresses(b.get_threads(p))
assert sorted(set(a.extract_process_features(p))) == sorted(set(b.extract_process_features(p)))
for t in a.get_threads(p):
assert addresses(a.get_calls(p, t)) == addresses(b.get_calls(p, t))
assert sorted(set(a.extract_thread_features(p, t))) == sorted(set(b.extract_thread_features(p, t)))
for c in a.get_calls(p, t):
assert sorted(set(a.extract_call_features(p, t, c))) == sorted(set(b.extract_call_features(p, t, c)))
def test_freeze_str_roundtrip():
load = capa.features.freeze.loads
dump = capa.features.freeze.dumps
reanimated = load(dump(EXTRACTOR))
compare_extractors(EXTRACTOR, reanimated)
def test_freeze_bytes_roundtrip():
load = capa.features.freeze.load
dump = capa.features.freeze.dump
reanimated = load(dump(EXTRACTOR))
compare_extractors(EXTRACTOR, reanimated)
def test_freeze_load_sample(tmpdir):
o = tmpdir.mkdir("capa").join("test.frz")
extractor = fixtures.get_cape_extractor(fixtures.get_data_path_by_name("d46900"))
Path(o.strpath).write_bytes(capa.features.freeze.dump(extractor))
null_extractor = capa.features.freeze.load(Path(o.strpath).read_bytes())
compare_extractors(extractor, null_extractor)

View File

@@ -22,10 +22,15 @@ import capa.features.basicblock
import capa.features.extractors.null
import capa.features.extractors.base_extractor
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
from capa.features.extractors.base_extractor import BBHandle, SampleHashes, FunctionHandle
EXTRACTOR = capa.features.extractors.null.NullFeatureExtractor(
EXTRACTOR = capa.features.extractors.null.NullStaticFeatureExtractor(
base_address=AbsoluteVirtualAddress(0x401000),
sample_hashes=SampleHashes(
md5="6eb7ee7babf913d75df3f86c229df9e7",
sha1="2a082494519acd5130d5120fa48786df7275fdd7",
sha256="0c7d1a34eb9fd55bedbf37ba16e3d5dd8c1dd1d002479cc4af27ef0f82bb4792",
),
global_features=[],
file_features=[
(AbsoluteVirtualAddress(0x402345), capa.features.common.Characteristic("embedded pe")),
@@ -83,7 +88,9 @@ def test_null_feature_extractor():
rule:
meta:
name: xor loop
scope: basic block
scopes:
static: basic block
dynamic: process
features:
- and:
- characteristic: tight loop
@@ -119,8 +126,8 @@ def compare_extractors(a, b):
def test_freeze_str_roundtrip():
load = capa.features.freeze.loads
dump = capa.features.freeze.dumps
load = capa.features.freeze.loads_static
dump = capa.features.freeze.dumps_static
reanimated = load(dump(EXTRACTOR))
compare_extractors(EXTRACTOR, reanimated)
@@ -133,7 +140,7 @@ def test_freeze_bytes_roundtrip():
def roundtrip_feature(feature):
assert feature == capa.features.freeze.feature_from_capa(feature).to_capa()
assert feature == capa.features.freeze.features.feature_from_capa(feature).to_capa()
def test_serialize_features():

View File

@@ -20,3 +20,47 @@ def test_all_zeros():
assert helpers.all_zeros(b) is True
assert helpers.all_zeros(c) is False
assert helpers.all_zeros(d) is False
def test_generate_symbols():
assert list(helpers.generate_symbols("name.dll", "api", include_dll=True)) == list(
helpers.generate_symbols("name", "api", include_dll=True)
)
assert list(helpers.generate_symbols("name.dll", "api", include_dll=False)) == list(
helpers.generate_symbols("name", "api", include_dll=False)
)
# A/W import
symbols = list(helpers.generate_symbols("kernel32", "CreateFileA", include_dll=True))
assert len(symbols) == 4
assert "kernel32.CreateFileA" in symbols
assert "kernel32.CreateFile" in symbols
assert "CreateFileA" in symbols
assert "CreateFile" in symbols
# import
symbols = list(helpers.generate_symbols("kernel32", "WriteFile", include_dll=True))
assert len(symbols) == 2
assert "kernel32.WriteFile" in symbols
assert "WriteFile" in symbols
# ordinal import
symbols = list(helpers.generate_symbols("ws2_32", "#1", include_dll=True))
assert len(symbols) == 1
assert "ws2_32.#1" in symbols
# A/W api
symbols = list(helpers.generate_symbols("kernel32", "CreateFileA", include_dll=False))
assert len(symbols) == 2
assert "CreateFileA" in symbols
assert "CreateFile" in symbols
# api
symbols = list(helpers.generate_symbols("kernel32", "WriteFile", include_dll=False))
assert len(symbols) == 1
assert "WriteFile" in symbols
# ordinal api
symbols = list(helpers.generate_symbols("ws2_32", "#1", include_dll=False))
assert len(symbols) == 1
assert "ws2_32.#1" in symbols

View File

@@ -6,8 +6,10 @@
# 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 gzip
import json
import textwrap
from pathlib import Path
import fixtures
@@ -34,7 +36,9 @@ def test_main_single_rule(z9324d_extractor, tmpdir):
rule:
meta:
name: test rule
scope: file
scopes:
static: file
dynamic: file
authors:
- test
features:
@@ -95,7 +99,9 @@ def test_ruleset():
rule:
meta:
name: file rule
scope: file
scopes:
static: file
dynamic: process
features:
- characteristic: embedded pe
"""
@@ -107,7 +113,9 @@ def test_ruleset():
rule:
meta:
name: function rule
scope: function
scopes:
static: function
dynamic: process
features:
- characteristic: tight loop
"""
@@ -119,267 +127,91 @@ def test_ruleset():
rule:
meta:
name: basic block rule
scope: basic block
scopes:
static: basic block
dynamic: process
features:
- characteristic: nzxor
"""
)
),
]
)
assert len(rules.file_rules) == 1
assert len(rules.function_rules) == 1
assert len(rules.basic_block_rules) == 1
def test_match_across_scopes_file_function(z9324d_extractor):
rules = capa.rules.RuleSet(
[
# this rule should match on a function (0x4073F0)
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: install service
scope: function
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x4073F0
name: process rule
scopes:
static: file
dynamic: process
features:
- and:
- api: advapi32.OpenSCManagerA
- api: advapi32.CreateServiceA
- api: advapi32.StartServiceA
- string: "explorer.exe"
"""
)
),
# this rule should match on a file feature
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: .text section
scope: file
examples:
- 9324d1a8ae37a36ae560c37448c9705a
features:
- section: .text
"""
rule:
meta:
name: thread rule
scopes:
static: function
dynamic: thread
features:
- api: RegDeleteKey
"""
)
),
# this rule should match on earlier rule matches:
# - install service, with function scope
# - .text section, with file scope
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: .text section and install service
scope: file
examples:
- 9324d1a8ae37a36ae560c37448c9705a
features:
- and:
- match: install service
- match: .text section
"""
)
),
]
)
capabilities, meta = capa.main.find_capabilities(rules, z9324d_extractor)
assert "install service" in capabilities
assert ".text section" in capabilities
assert ".text section and install service" in capabilities
def test_match_across_scopes(z9324d_extractor):
rules = capa.rules.RuleSet(
[
# this rule should match on a basic block (including at least 0x403685)
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: tight loop
scope: basic block
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x403685
features:
- characteristic: tight loop
"""
)
),
# this rule should match on a function (0x403660)
# based on API, as well as prior basic block rule match
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: kill thread loop
scope: function
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x403660
name: test call subscope
scopes:
static: basic block
dynamic: thread
features:
- and:
- api: kernel32.TerminateThread
- api: kernel32.CloseHandle
- match: tight loop
- string: "explorer.exe"
- call:
- api: HttpOpenRequestW
"""
)
),
# this rule should match on a file feature and a prior function rule match
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: kill thread program
scope: file
examples:
- 9324d1a8ae37a36ae560c37448c9705a
features:
- and:
- section: .text
- match: kill thread loop
"""
)
),
]
)
capabilities, meta = capa.main.find_capabilities(rules, z9324d_extractor)
assert "tight loop" in capabilities
assert "kill thread loop" in capabilities
assert "kill thread program" in capabilities
def test_subscope_bb_rules(z9324d_extractor):
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scope: function
scopes:
static: instruction
dynamic: call
features:
- and:
- basic block:
- characteristic: tight loop
- and:
- or:
- api: socket
- and:
- os: linux
- mnemonic: syscall
- number: 41 = socket()
- number: 6 = IPPROTO_TCP
- number: 1 = SOCK_STREAM
- number: 2 = AF_INET
"""
)
)
),
]
)
# tight loop at 0x403685
capabilities, meta = capa.main.find_capabilities(rules, z9324d_extractor)
assert "test rule" in capabilities
def test_byte_matching(z9324d_extractor):
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: byte match test
scope: function
features:
- and:
- bytes: ED 24 9E F4 52 A9 07 47 55 8E E1 AB 30 8E 23 61
"""
)
)
]
)
capabilities, meta = capa.main.find_capabilities(rules, z9324d_extractor)
assert "byte match test" in capabilities
def test_count_bb(z9324d_extractor):
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: count bb
namespace: test
scope: function
features:
- and:
- count(basic blocks): 1 or more
"""
)
)
]
)
capabilities, meta = capa.main.find_capabilities(rules, z9324d_extractor)
assert "count bb" in capabilities
def test_instruction_scope(z9324d_extractor):
# .text:004071A4 68 E8 03 00 00 push 3E8h
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: push 1000
namespace: test
scope: instruction
features:
- and:
- mnemonic: push
- number: 1000
"""
)
)
]
)
capabilities, meta = capa.main.find_capabilities(rules, z9324d_extractor)
assert "push 1000" in capabilities
assert 0x4071A4 in {result[0] for result in capabilities["push 1000"]}
def test_instruction_subscope(z9324d_extractor):
# .text:00406F60 sub_406F60 proc near
# [...]
# .text:004071A4 68 E8 03 00 00 push 3E8h
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: push 1000 on i386
namespace: test
scope: function
features:
- and:
- arch: i386
- instruction:
- mnemonic: push
- number: 1000
"""
)
)
]
)
capabilities, meta = capa.main.find_capabilities(rules, z9324d_extractor)
assert "push 1000 on i386" in capabilities
assert 0x406F60 in {result[0] for result in capabilities["push 1000 on i386"]}
assert len(rules.file_rules) == 2
assert len(rules.function_rules) == 2
assert len(rules.basic_block_rules) == 2
assert len(rules.instruction_rules) == 1
assert len(rules.process_rules) == 4
assert len(rules.thread_rules) == 2
assert len(rules.call_rules) == 2
def test_fix262(pma16_01_extractor, capsys):
@@ -468,3 +300,59 @@ def test_main_rd():
assert capa.main.main([path, "-j"]) == 0
assert capa.main.main([path, "-q"]) == 0
assert capa.main.main([path]) == 0
def extract_cape_report(tmp_path: Path, gz: Path) -> Path:
report = tmp_path / "report.json"
report.write_bytes(gzip.decompress(gz.read_bytes()))
return report
def test_main_cape1(tmp_path):
path = extract_cape_report(tmp_path, fixtures.get_data_path_by_name("0000a657"))
# TODO(williballenthin): use default rules set
# https://github.com/mandiant/capa/pull/1696
rules = tmp_path / "rules"
rules.mkdir()
(rules / "create-or-open-registry-key.yml").write_text(
textwrap.dedent(
"""
rule:
meta:
name: create or open registry key
authors:
- testing
scopes:
static: instruction
dynamic: call
features:
- or:
- api: advapi32.RegOpenKey
- api: advapi32.RegOpenKeyEx
- api: advapi32.RegCreateKey
- api: advapi32.RegCreateKeyEx
- api: advapi32.RegOpenCurrentUser
- api: advapi32.RegOpenKeyTransacted
- api: advapi32.RegOpenUserClassesRoot
- api: advapi32.RegCreateKeyTransacted
- api: ZwOpenKey
- api: ZwOpenKeyEx
- api: ZwCreateKey
- api: ZwOpenKeyTransacted
- api: ZwOpenKeyTransactedEx
- api: ZwCreateKeyTransacted
- api: NtOpenKey
- api: NtCreateKey
- api: SHRegOpenUSKey
- api: SHRegCreateUSKey
- api: RtlCreateRegistryKey
"""
)
)
assert capa.main.main([str(path), "-r", str(rules)]) == 0
assert capa.main.main([str(path), "-q", "-r", str(rules)]) == 0
assert capa.main.main([str(path), "-j", "-r", str(rules)]) == 0
assert capa.main.main([str(path), "-v", "-r", str(rules)]) == 0
assert capa.main.main([str(path), "-vv", "-r", str(rules)]) == 0

View File

@@ -43,6 +43,9 @@ def test_match_simple():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
namespace: testns1/testns2
features:
- number: 100
@@ -63,6 +66,9 @@ def test_match_range_exact():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): 2
"""
@@ -87,7 +93,10 @@ def test_match_range_range():
"""
rule:
meta:
name: test rule
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): (2, 3)
"""
@@ -117,6 +126,9 @@ def test_match_range_exact_zero():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): 0
"""
@@ -142,7 +154,10 @@ def test_match_range_with_zero():
"""
rule:
meta:
name: test rule
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): (0, 1)
"""
@@ -169,6 +184,9 @@ def test_match_adds_matched_rule_feature():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- number: 100
"""
@@ -187,6 +205,9 @@ def test_match_matched_rules():
rule:
meta:
name: test rule1
scopes:
static: function
dynamic: process
features:
- number: 100
"""
@@ -198,6 +219,9 @@ def test_match_matched_rules():
rule:
meta:
name: test rule2
scopes:
static: function
dynamic: process
features:
- match: test rule1
"""
@@ -232,6 +256,9 @@ def test_match_namespace():
rule:
meta:
name: CreateFile API
scopes:
static: function
dynamic: process
namespace: file/create/CreateFile
features:
- api: CreateFile
@@ -244,6 +271,9 @@ def test_match_namespace():
rule:
meta:
name: WriteFile API
scopes:
static: function
dynamic: process
namespace: file/write
features:
- api: WriteFile
@@ -256,6 +286,9 @@ def test_match_namespace():
rule:
meta:
name: file-create
scopes:
static: function
dynamic: process
features:
- match: file/create
"""
@@ -267,6 +300,9 @@ def test_match_namespace():
rule:
meta:
name: filesystem-any
scopes:
static: function
dynamic: process
features:
- match: file
"""
@@ -304,6 +340,9 @@ def test_match_substring():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- substring: abc
@@ -355,6 +394,9 @@ def test_match_regex():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- string: /.*bbbb.*/
@@ -367,6 +409,9 @@ def test_match_regex():
rule:
meta:
name: rule with implied wildcards
scopes:
static: function
dynamic: process
features:
- and:
- string: /bbbb/
@@ -379,6 +424,9 @@ def test_match_regex():
rule:
meta:
name: rule with anchor
scopes:
static: function
dynamic: process
features:
- and:
- string: /^bbbb/
@@ -425,6 +473,9 @@ def test_match_regex_ignorecase():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- string: /.*bbbb.*/i
@@ -448,6 +499,9 @@ def test_match_regex_complex():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- string: /.*HARDWARE\\Key\\key with spaces\\.*/i
@@ -471,6 +525,9 @@ def test_match_regex_values_always_string():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- string: /123/
@@ -500,6 +557,9 @@ def test_match_not():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
namespace: testns1/testns2
features:
- not:
@@ -518,6 +578,9 @@ def test_match_not_not():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
namespace: testns1/testns2
features:
- not:
@@ -537,6 +600,9 @@ def test_match_operand_number():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- operand[0].number: 0x10
@@ -564,6 +630,9 @@ def test_match_operand_offset():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- operand[0].offset: 0x10
@@ -591,6 +660,9 @@ def test_match_property_access():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- property/read: System.IO.FileInfo::Length
@@ -632,6 +704,9 @@ def test_match_os_any():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- and:

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