Compare commits

..

12 Commits

Author SHA1 Message Date
Ana María Martínez Gómez
c547519ee4 Merge pull request #537 from Ana06/master-py2-1_6_3 2021-04-29 14:13:20 +02:00
Ana Maria Martinez Gomez
b65286a435 changelog: v1.6.3
- Add v1.6.3 to changelog
- Remove capa rules from v1.6.2 (there were no changes). I'll remove the
tag in capa-rules once this is merged.
- Remove master (unreleased) section. This only makes sense in master
and we are only using this branch for backporting bug fixes.
2021-04-29 11:56:50 +02:00
Ana Maria Martinez Gomez
3eef5c8773 version: bump to v1.6.3 2021-04-29 11:56:50 +02:00
Ana Maria Martinez Gomez
f70b046ed4 ci: update isort
Before removing Py2 we were already using isort 3.8.0 in the tests, as
we were requiring isort 5 explicitly:
```
pip install 'isort==5.*' black
```
ce8370931e starts using the setup.py
version, which makes the tests fail.

Note this was not a problem because we were using Py3 for the code
linters.
2021-04-29 11:56:50 +02:00
William Ballenthin
ce8370931e ci: use black/isort dep from setup.py
closes #535
2021-04-29 11:38:14 +02:00
Ana Maria Martinez Gomez
8f58ccc8ae doc: document support IDA versions
Text taken from master (except the Python version).
2021-04-29 11:18:51 +02:00
Willi Ballenthin
92cd6c6726 ida: support 7.6
closes #496
2021-04-29 11:12:36 +02:00
Ana María Martínez Gómez
eea0e1e738 Merge pull request #527 from Ana06/v1-6-2 2021-04-13 17:21:31 +02:00
Ana Maria Martinez Gomez
60834e3ecd changelog: v1.6.2
This release backports a fix to capa 1.6: The Windows binary was built
with Python 3.9 which doesn't support Windows 7.
2021-04-13 12:18:50 +02:00
Ana Maria Martinez Gomez
54f8f6d162 version: bump to v1.6.2 2021-04-13 12:16:19 +02:00
Ana Maria Martinez Gomez
62743e1363 ci: Enable tests for master-py2 branch
Use the master-py branch to backport fixes to capa 1.6 (Python 2
support).
2021-04-13 12:08:30 +02:00
Ana Maria Martinez Gomez
b34d791d05 build: Fix binary for Windows 7
Python 3.9 doesn't support Windows 7. Build with Python 3.8 instead.
2021-04-13 12:06:05 +02:00
182 changed files with 7659 additions and 26571 deletions

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
First off, thanks for taking the time to contribute! First off, thanks for taking the time to contribute!
The following is a set of guidelines for contributing to capa and its packages, which are hosted in the [Mandiant Organization](https://github.com/mandiant) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. The following is a set of guidelines for contributing to capa and its packages, which are hosted in the [FireEye Organization](https://github.com/fireeye) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
#### Table Of Contents #### Table Of Contents
@@ -31,10 +31,10 @@ This project and everyone participating in it is governed by the [Capa Code of C
### Capa and its repositories ### Capa and its repositories
We host the capa project as three GitHub repositories: We host the capa project as three Github repositories:
- [capa](https://github.com/mandiant/capa) - [capa](https://github.com/fireeye/capa)
- [capa-rules](https://github.com/mandiant/capa-rules) - [capa-rules](https://github.com/fireeye/capa-rules)
- [capa-testfiles](https://github.com/mandiant/capa-testfiles) - [capa-testfiles](https://github.com/fireeye/capa-testfiles)
The command line tools, logic engine, and other Python source code are found in the `capa` repository. The command line tools, logic engine, and other Python source code are found in the `capa` repository.
This is the repository to fork when you want to enhance the features, performance, or user interface of capa. This is the repository to fork when you want to enhance the features, performance, or user interface of capa.
@@ -54,7 +54,7 @@ These are files you'll need in order to run the linter (in `--thorough` mode) an
### Design Decisions ### Design Decisions
When we make a significant decision in how we maintain the project and what we can or cannot support, When we make a significant decision in how we maintain the project and what we can or cannot support,
we will document it in the [capa issues tracker](https://github.com/mandiant/capa/issues). we will document it in the [capa issues tracker](https://github.com/fireeye/capa/issues).
This is the best place review our discussions about what/how/why we do things in the project. This is the best place review our discussions about what/how/why we do things in the project.
If you have a question, check to see if it is documented there. If you have a question, check to see if it is documented there.
If it is *not* documented there, or you can't find an answer, please open a issue. If it is *not* documented there, or you can't find an answer, please open a issue.
@@ -78,7 +78,7 @@ Fill out [the required template](./ISSUE_TEMPLATE/bug_report.md),
#### Before Submitting A Bug Report #### Before Submitting A Bug Report
* **Determine [which repository the problem should be reported in](#capa-and-its-repositories)**. * **Determine [which repository the problem should be reported in](#capa-and-its-repositories)**.
* **Perform a [cursory search](https://github.com/mandiant/capa/issues?q=is%3Aissue)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one. * **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
#### How Do I Submit A (Good) Bug Report? #### How Do I Submit A (Good) Bug Report?
@@ -101,7 +101,7 @@ Explain the problem and include additional details to help maintainers reproduce
Provide more context by answering these questions: Provide more context by answering these questions:
* **Did the problem start happening recently** (e.g. after updating to a new version of capa) or was this always a problem? * **Did the problem start happening recently** (e.g. after updating to a new version of capa) or was this always a problem?
* If the problem started happening recently, **can you reproduce the problem in an older version of capa?** What's the most recent version in which the problem doesn't happen? You can download older versions of capa from [the releases page](https://github.com/mandiant/capa/releases). * If the problem started happening recently, **can you reproduce the problem in an older version of capa?** What's the most recent version in which the problem doesn't happen? You can download older versions of capa from [the releases page](https://github.com/fireeye/capa/releases).
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. * **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
* If the problem is related to working with files (e.g. opening and editing files), **does the problem happen for all files and projects or only some?** Does the problem happen only when working with local or remote files (e.g. on network drives), with files of a specific type (e.g. only JavaScript or Python files), with large files or files with very long lines, or with files in a specific encoding? Is there anything else special about the files you are using? * If the problem is related to working with files (e.g. opening and editing files), **does the problem happen for all files and projects or only some?** Does the problem happen only when working with local or remote files (e.g. on network drives), with files of a specific type (e.g. only JavaScript or Python files), with large files or files with very long lines, or with files in a specific encoding? Is there anything else special about the files you are using?
@@ -119,7 +119,7 @@ Before creating enhancement suggestions, please check [this list](#before-submit
#### Before Submitting An Enhancement Suggestion #### Before Submitting An Enhancement Suggestion
* **Determine [which repository the enhancement should be suggested in](#capa-and-its-repositories).** * **Determine [which repository the enhancement should be suggested in](#capa-and-its-repositories).**
* **Perform a [cursory search](https://github.com/mandiant/capa/issues?q=is%3Aissue)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. * **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
#### How Do I Submit A (Good) Enhancement Suggestion? #### How Do I Submit A (Good) Enhancement Suggestion?
@@ -138,15 +138,15 @@ Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com
Unsure where to begin contributing to capa? You can start by looking through these `good-first-issue` and `rule-idea` issues: Unsure where to begin contributing to capa? You can start by looking through these `good-first-issue` and `rule-idea` issues:
* [good-first-issue](https://github.com/mandiant/capa/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two. * [good-first-issue](https://github.com/fireeye/capa/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two.
* [rule-idea](https://github.com/mandiant/capa-rules/issues?q=is%3Aissue+is%3Aopen+label%3A%22rule+idea%22) - issues that describe potential new rule ideas. * [rule-idea](https://github.com/fireeye/capa-rules/issues?q=is%3Aissue+is%3Aopen+label%3A%22rule+idea%22) - issues that describe potential new rule ideas.
Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have. Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have.
#### Local development #### Local development
capa and all its resources can be developed locally. capa and all its resources can be developed locally.
For instructions on how to do this, see the "Method 3" section of the [installation guide](https://github.com/mandiant/capa/blob/master/doc/installation.md). For instructions on how to do this, see the "Method 3" section of the [installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md).
### Pull Requests ### Pull Requests
@@ -159,25 +159,12 @@ The process described here has several goals:
Please follow these steps to have your contribution considered by the maintainers: Please follow these steps to have your contribution considered by the maintainers:
0. Sign the [Contributor License Agreement](#contributor-license-agreement) 1. Follow all instructions in [the template](PULL_REQUEST_TEMPLATE.md)
1. Follow the [styleguides](#styleguides) 2. Follow the [styleguides](#styleguides)
2. Update the CHANGELOG and add tests and documentation. In case they are not needed, indicate it in [the PR template](pull_request_template.md).
3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing? </summary>If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.</details> 3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing? </summary>If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.</details>
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
### Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution,
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Styleguides ## Styleguides
### Git Commit Messages ### Git Commit Messages
@@ -203,8 +190,8 @@ Our CI pipeline will reformat and enforce the Python styleguide.
All (non-nursery) capa rules must: All (non-nursery) capa rules must:
1. pass the [linter](https://github.com/mandiant/capa/blob/master/scripts/lint.py), and 1. pass the [linter](https://github.com/fireeye/capa/blob/master/scripts/lint.py), and
2. be formatted with [capafmt](https://github.com/mandiant/capa/blob/master/scripts/capafmt.py) 2. be formatted with [capafmt](https://github.com/fireeye/capa/blob/master/scripts/capafmt.py)
This ensures that all rules meet the same minimum level of quality and are structured in a consistent way. This ensures that all rules meet the same minimum level of quality and are structured in a consistent way.
Our CI pipeline will reformat and enforce the capa rules styleguide. Our CI pipeline will reformat and enforce the capa rules styleguide.

View File

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

View File

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

41
.github/flake8.ini vendored
View File

@@ -1,41 +0,0 @@
[flake8]
max-line-length = 120
extend-ignore =
# E203: whitespace before ':' (black does this)
E203,
# F401: `foo` imported but unused (prefer ruff)
F401,
# F811 Redefinition of unused `foo` (prefer ruff)
F811,
# E501 line too long (prefer black)
E501,
# B010 Do not call setattr with a constant attribute value
B010,
# G200 Logging statement uses exception in arguments
G200,
# SIM102 Use a single if-statement instead of nested if-statements
# doesn't provide a space for commenting or logical separation of conditions
SIM102,
# SIM114 Use logical or and a single body
# makes logic trees too complex
SIM114,
# SIM117 Use 'with Foo, Bar:' instead of multiple with statements
# makes lines too long
SIM117
per-file-ignores =
# T201 print found.
#
# scripts are meant to print output
scripts/*: T201
# capa.exe is meant to print output
capa/main.py: T201
# IDA tests emit results to output window so need to print
tests/test_ida_features.py: T201
# utility used to find the Binary Ninja API via invoking python.exe
capa/features/extractors/binja/find_binja_api.py: T201
copyright-check = True
copyright-min-file-size = 1
copyright-regexp = Copyright \(C\) 2023 Mandiant, Inc. All Rights Reserved.

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

@@ -1,88 +0,0 @@
[mypy]
[mypy-halo.*]
ignore_missing_imports = True
[mypy-tqdm.*]
ignore_missing_imports = True
[mypy-ruamel.*]
ignore_missing_imports = True
[mypy-networkx.*]
ignore_missing_imports = True
[mypy-pefile.*]
ignore_missing_imports = True
[mypy-viv_utils.*]
ignore_missing_imports = True
[mypy-flirt.*]
ignore_missing_imports = True
[mypy-lief.*]
ignore_missing_imports = True
[mypy-idc.*]
ignore_missing_imports = True
[mypy-vivisect.*]
ignore_missing_imports = True
[mypy-envi.*]
ignore_missing_imports = True
[mypy-PE.*]
ignore_missing_imports = True
[mypy-idaapi.*]
ignore_missing_imports = True
[mypy-idautils.*]
ignore_missing_imports = True
[mypy-ida_auto.*]
ignore_missing_imports = True
[mypy-ida_bytes.*]
ignore_missing_imports = True
[mypy-ida_nalt.*]
ignore_missing_imports = True
[mypy-ida_kernwin.*]
ignore_missing_imports = True
[mypy-ida_settings.*]
ignore_missing_imports = True
[mypy-ida_funcs.*]
ignore_missing_imports = True
[mypy-ida_loader.*]
ignore_missing_imports = True
[mypy-ida_segment.*]
ignore_missing_imports = True
[mypy-PyQt5.*]
ignore_missing_imports = True
[mypy-binaryninja.*]
ignore_missing_imports = True
[mypy-pytest.*]
ignore_missing_imports = True
[mypy-devtools.*]
ignore_missing_imports = True
[mypy-elftools.*]
ignore_missing_imports = True
[mypy-dncil.*]
ignore_missing_imports = True
[mypy-netnode.*]
ignore_missing_imports = True

View File

@@ -1,22 +1,32 @@
<!-- <!--
Thank you for contributing to capa! <3 Thank you for contributing to capa! :heart:
IMPORTANT NOTE
It's most important that you submit your improvements. So even if you don't use this complete template we look forward to collaborating!
Please read capa's CONTRIBUTING guide if you haven't done so already. Please read capa's CONTRIBUTING guide if you haven't done so already.
It contains helpful information about how to contribute to capa. Check https://github.com/mandiant/capa/blob/master/.github/CONTRIBUTING.md It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md
Please describe the changes in this pull request (PR). Include your motivation and context to help us review. PR template based on https://embeddedartistry.com/blog/2017/08/04/a-github-pull-request-template-for-your-projects/
Please mention the issue your PR addresses (if any):
closes #issue_number
--> -->
### Description
### Checklist <!-- Please describe the changes in this PR. Including your motivation and context helps us to review. -->
<!-- CHANGELOG.md has a `master (unreleased)` section. Please add bug fixes, new features, breaking changes and anything else you think is worthwhile mentioning in the release notes to this file. --> closes # (issue)
- [ ] No CHANGELOG update needed
<!-- Tests prove that your fix/work as expected and ensure it doesn't break on the feature. --> ### Type of change
Please update the [CHANGELOG.md](/CHANGELOG.md)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] I have made the corresponding changes to the documentation
### Tests
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] No new tests needed - [ ] No new tests needed
<!-- Please help us keeping capa documentation up-to-date -->
- [ ] No documentation update needed

View File

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

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
from PyInstaller.utils.hooks import copy_metadata from PyInstaller.utils.hooks import copy_metadata
@@ -38,36 +38,39 @@ hiddenimports = [
"vivisect", "vivisect",
"vivisect.analysis", "vivisect.analysis",
"vivisect.analysis.amd64", "vivisect.analysis.amd64",
"vivisect.analysis.amd64",
"vivisect.analysis.amd64.emulation", "vivisect.analysis.amd64.emulation",
"vivisect.analysis.amd64.golang", "vivisect.analysis.amd64.golang",
"vivisect.analysis.crypto", "vivisect.analysis.crypto",
"vivisect.analysis.crypto",
"vivisect.analysis.crypto.constants", "vivisect.analysis.crypto.constants",
"vivisect.analysis.elf", "vivisect.analysis.elf",
"vivisect.analysis.elf",
"vivisect.analysis.elf.elfplt", "vivisect.analysis.elf.elfplt",
"vivisect.analysis.elf.elfplt_late",
"vivisect.analysis.elf.libc_start_main", "vivisect.analysis.elf.libc_start_main",
"vivisect.analysis.generic", "vivisect.analysis.generic",
"vivisect.analysis.generic",
"vivisect.analysis.generic.codeblocks", "vivisect.analysis.generic.codeblocks",
"vivisect.analysis.generic.emucode", "vivisect.analysis.generic.emucode",
"vivisect.analysis.generic.entrypoints", "vivisect.analysis.generic.entrypoints",
"vivisect.analysis.generic.funcentries", "vivisect.analysis.generic.funcentries",
"vivisect.analysis.generic.impapi", "vivisect.analysis.generic.impapi",
"vivisect.analysis.generic.linker",
"vivisect.analysis.generic.mkpointers", "vivisect.analysis.generic.mkpointers",
"vivisect.analysis.generic.noret",
"vivisect.analysis.generic.pointers", "vivisect.analysis.generic.pointers",
"vivisect.analysis.generic.pointertables", "vivisect.analysis.generic.pointertables",
"vivisect.analysis.generic.relocations", "vivisect.analysis.generic.relocations",
"vivisect.analysis.generic.strconst", "vivisect.analysis.generic.strconst",
"vivisect.analysis.generic.switchcase", "vivisect.analysis.generic.switchcase",
"vivisect.analysis.generic.symswitchcase",
"vivisect.analysis.generic.thunks", "vivisect.analysis.generic.thunks",
"vivisect.analysis.generic.noret",
"vivisect.analysis.i386",
"vivisect.analysis.i386", "vivisect.analysis.i386",
"vivisect.analysis.i386.calling", "vivisect.analysis.i386.calling",
"vivisect.analysis.i386.golang", "vivisect.analysis.i386.golang",
"vivisect.analysis.i386.importcalls", "vivisect.analysis.i386.importcalls",
"vivisect.analysis.i386.instrhook", "vivisect.analysis.i386.instrhook",
"vivisect.analysis.i386.thunk_reg", "vivisect.analysis.i386.thunk_bx",
"vivisect.analysis.ms",
"vivisect.analysis.ms", "vivisect.analysis.ms",
"vivisect.analysis.ms.hotpatch", "vivisect.analysis.ms.hotpatch",
"vivisect.analysis.ms.localhints", "vivisect.analysis.ms.localhints",
@@ -78,40 +81,8 @@ hiddenimports = [
"vivisect.impapi.posix.amd64", "vivisect.impapi.posix.amd64",
"vivisect.impapi.posix.i386", "vivisect.impapi.posix.i386",
"vivisect.impapi.windows", "vivisect.impapi.windows",
"vivisect.impapi.windows.advapi_32",
"vivisect.impapi.windows.advapi_64",
"vivisect.impapi.windows.amd64", "vivisect.impapi.windows.amd64",
"vivisect.impapi.windows.gdi_32",
"vivisect.impapi.windows.gdi_64",
"vivisect.impapi.windows.i386", "vivisect.impapi.windows.i386",
"vivisect.impapi.windows.kernel_32",
"vivisect.impapi.windows.kernel_64",
"vivisect.impapi.windows.msvcr100_32",
"vivisect.impapi.windows.msvcr100_64",
"vivisect.impapi.windows.msvcr110_32",
"vivisect.impapi.windows.msvcr110_64",
"vivisect.impapi.windows.msvcr120_32",
"vivisect.impapi.windows.msvcr120_64",
"vivisect.impapi.windows.msvcr71_32",
"vivisect.impapi.windows.msvcr80_32",
"vivisect.impapi.windows.msvcr80_64",
"vivisect.impapi.windows.msvcr90_32",
"vivisect.impapi.windows.msvcr90_64",
"vivisect.impapi.windows.msvcrt_32",
"vivisect.impapi.windows.msvcrt_64",
"vivisect.impapi.windows.ntdll_32",
"vivisect.impapi.windows.ntdll_64",
"vivisect.impapi.windows.ole_32",
"vivisect.impapi.windows.ole_64",
"vivisect.impapi.windows.rpcrt4_32",
"vivisect.impapi.windows.rpcrt4_64",
"vivisect.impapi.windows.shell_32",
"vivisect.impapi.windows.shell_64",
"vivisect.impapi.windows.user_32",
"vivisect.impapi.windows.user_64",
"vivisect.impapi.windows.ws2plus_32",
"vivisect.impapi.windows.ws2plus_64",
"vivisect.impapi.winkern",
"vivisect.impapi.winkern.i386", "vivisect.impapi.winkern.i386",
"vivisect.impapi.winkern.amd64", "vivisect.impapi.winkern.amd64",
"vivisect.parsers.blob", "vivisect.parsers.blob",

View File

@@ -1,40 +1,55 @@
# -*- mode: python -*- # -*- mode: python -*-
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
import os.path import os.path
import subprocess import subprocess
import wcwidth import wcwidth
# when invoking pyinstaller from the project root,
# this gets run from the project root.
with open('./capa/version.py', 'wb') as f:
# git output will look like:
#
# tags/v1.0.0-0-g3af38dc
# ------- tag
# - commits since
# g------- git hash fragment
version = (subprocess.check_output(["git", "describe", "--always", "--tags", "--long"])
.decode("utf-8")
.strip()
.replace("tags/", ""))
f.write(("__version__ = '%s'" % version).encode("utf-8"))
a = Analysis( a = Analysis(
# when invoking pyinstaller from the project root, # when invoking pyinstaller from the project root,
# this gets invoked from the directory of the spec file, # this gets invoked from the directory of the spec file,
# i.e. ./.github/pyinstaller # i.e. ./.github/pyinstaller
["../../capa/main.py"], ['../../capa/main.py'],
pathex=["capa"], pathex=['capa'],
binaries=None, binaries=None,
datas=[ datas=[
# when invoking pyinstaller from the project root, # when invoking pyinstaller from the project root,
# this gets invoked from the directory of the spec file, # this gets invoked from the directory of the spec file,
# i.e. ./.github/pyinstaller # i.e. ./.github/pyinstaller
("../../rules", "rules"), ('../../rules', 'rules'),
("../../sigs", "sigs"),
("../../cache", "cache"),
# capa.render.default uses tabulate that depends on wcwidth. # capa.render.default uses tabulate that depends on wcwidth.
# it seems wcwidth uses a json file `version.json` # it seems wcwidth uses a json file `version.json`
# and this doesn't get picked up by pyinstaller automatically. # and this doesn't get picked up by pyinstaller automatically.
# so we manually embed the wcwidth resources here. # so we manually embed the wcwidth resources here.
# #
# ref: https://stackoverflow.com/a/62278462/87207 # ref: https://stackoverflow.com/a/62278462/87207
(os.path.dirname(wcwidth.__file__), "wcwidth"), (os.path.dirname(wcwidth.__file__), 'wcwidth')
], ],
# when invoking pyinstaller from the project root, # when invoking pyinstaller from the project root,
# this gets run from the project root. # this gets run from the project root.
hookspath=[".github/pyinstaller/hooks"], hookspath=['.github/pyinstaller/hooks'],
runtime_hooks=None, runtime_hooks=None,
excludes=[ excludes=[
# ignore packages that would otherwise be bundled with the .exe. # ignore packages that would otherwise be bundled with the .exe.
# review: build/pyinstaller/xref-pyinstaller.html # review: build/pyinstaller/xref-pyinstaller.html
# we don't do any GUI stuff, so ignore these modules # we don't do any GUI stuff, so ignore these modules
"tkinter", "tkinter",
"_tkinter", "_tkinter",
@@ -44,6 +59,7 @@ a = Analysis(
# since we don't spawn a notebook, we can safely remove these. # since we don't spawn a notebook, we can safely remove these.
"IPython", "IPython",
"ipywidgets", "ipywidgets",
# these are pulled in by networkx # these are pulled in by networkx
# but we don't need to compute the strongly connected components. # but we don't need to compute the strongly connected components.
"numpy", "numpy",
@@ -51,6 +67,7 @@ a = Analysis(
"matplotlib", "matplotlib",
"pandas", "pandas",
"pytest", "pytest",
# deps from viv that we don't use. # deps from viv that we don't use.
# this duplicates the entries in `hook-vivisect`, # this duplicates the entries in `hook-vivisect`,
# but works better this way. # but works better this way.
@@ -60,36 +77,36 @@ a = Analysis(
"PyQt5", "PyQt5",
"qt5", "qt5",
"pyqtwebengine", "pyqtwebengine",
"pyasn1", "pyasn1"
"binaryninja", ])
],
)
a.binaries = a.binaries - TOC([("tcl85.dll", None, None), ("tk85.dll", None, None), ("_tkinter", None, None)]) a.binaries = a.binaries - TOC([
('tcl85.dll', None, None),
('tk85.dll', None, None),
('_tkinter', None, None)])
pyz = PYZ(a.pure, a.zipped_data) pyz = PYZ(a.pure, a.zipped_data)
exe = EXE( exe = EXE(pyz,
pyz, a.scripts,
a.scripts, a.binaries,
a.binaries, a.zipfiles,
a.zipfiles, a.datas,
a.datas, exclude_binaries=False,
exclude_binaries=False, name='capa',
name="capa", icon='logo.ico',
icon="logo.ico", debug=False,
debug=False, strip=None,
strip=None, upx=True,
upx=True, console=True )
console=True,
)
# enable the following to debug the contents of the .exe # enable the following to debug the contents of the .exe
# #
# coll = COLLECT(exe, #coll = COLLECT(exe,
# a.binaries, # a.binaries,
# a.zipfiles, # a.zipfiles,
# a.datas, # a.datas,
# strip=None, # strip=None,
# upx=True, # upx=True,
# name='capa-dat') # name='capa-dat')

43
.github/ruff.toml vendored
View File

@@ -1,43 +0,0 @@
# Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E", "F"]
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# E402 module level import not at top of file
# E722 do not use bare 'except'
# E501 line too long
ignore = ["E402", "E722", "E501"]
line-length = 120
exclude = [
# Exclude a variety of commonly ignored directories.
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
# protobuf generated files
"*_pb2.py",
"*_pb2.pyi"
]

10
.github/tox.ini vendored Normal file
View File

@@ -0,0 +1,10 @@
[pycodestyle]
; E402: module level import not at top of file
; W503: line break before binary operator
; E231 missing whitespace after ',' (emitted by black)
; E203 whitespace before ':' (emitted by black)
ignore = E402,W503,E203,E231
max-line-length = 160
statistics = True
count = True
exclude = .*

View File

@@ -1,96 +1,54 @@
name: build name: build
on: on:
pull_request:
branches: [ master ]
release: release:
types: [edited, published] types: [edited, published]
permissions:
contents: write
jobs: jobs:
build: build:
name: PyInstaller for ${{ matrix.os }} name: PyInstaller for ${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
# set to false for debugging
fail-fast: true
matrix: matrix:
include: include:
- os: ubuntu-20.04 - os: ubuntu-16.04
# use old linux so that the shared library versioning is more portable # use old linux so that the shared library versioning is more portable
artifact_name: capa artifact_name: capa
asset_name: linux asset_name: linux
- os: windows-2019 - os: windows-2019
artifact_name: capa.exe artifact_name: capa.exe
asset_name: windows asset_name: windows
- os: macos-11 - os: macos-10.15
# use older macOS for assumed better portability
artifact_name: capa artifact_name: capa
asset_name: macos asset_name: macos
steps: steps:
- name: Checkout capa - name: Checkout capa
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@v2
with: with:
submodules: true 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 3.8
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.8
- if: matrix.os == 'ubuntu-20.04' - if: matrix.os == 'ubuntu-16.04'
run: sudo apt-get install -y libyaml-dev run: sudo apt-get install -y libyaml-dev
- name: Upgrade pip, setuptools - name: Install PyInstaller
run: python -m pip install --upgrade pip setuptools run: pip install 'pyinstaller==4.2'
- name: Install capa with build requirements - name: Install capa
run: pip install -e .[build] run: pip install -e .
- name: Cache the rule set
run: python ./scripts/cache-ruleset.py ./rules/ ./cache/
- name: Build standalone executable - name: Build standalone executable
run: pyinstaller --log-level DEBUG .github/pyinstaller/pyinstaller.spec run: pyinstaller .github/pyinstaller/pyinstaller.spec
- name: Does it run (PE)? - name: Does it run?
run: dist/capa "tests/data/Practical Malware Analysis Lab 01-01.dll_" run: dist/capa "tests/data/Practical Malware Analysis Lab 01-01.dll_"
- name: Does it run (Shellcode)? - uses: actions/upload-artifact@v2
run: dist/capa "tests/data/499c2a85f6e8142c3f48d4251c9c7cd6.raw32"
- name: Does it run (ELF)?
run: dist/capa "tests/data/7351f8a40c5450557b24622417fc478d.elf_"
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with: with:
name: ${{ matrix.asset_name }} name: ${{ matrix.asset_name }}
path: dist/${{ matrix.artifact_name }} path: dist/${{ matrix.artifact_name }}
test_run: zip:
name: Test run on ${{ matrix.os }} name: zip ${{ matrix.asset_name }}
runs-on: ${{ matrix.os }}
needs: [build]
strategy:
matrix:
include:
# OSs not already tested above
- os: ubuntu-22.04
artifact_name: capa
asset_name: linux
- os: windows-2022
artifact_name: capa.exe
asset_name: windows
steps:
- name: Download ${{ matrix.asset_name }}
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: ${{ matrix.asset_name }}
- name: Set executable flag
if: matrix.os != 'windows-2022'
run: chmod +x ${{ matrix.artifact_name }}
- name: Run capa
run: ./${{ matrix.artifact_name }} -h
zip_and_upload:
# upload zipped binaries to Release page
if: github.event_name == 'release'
name: zip and upload ${{ matrix.asset_name }}
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: [build] needs: build
strategy: strategy:
matrix: matrix:
include: include:
@@ -102,7 +60,7 @@ jobs:
artifact_name: capa artifact_name: capa
steps: steps:
- name: Download ${{ matrix.asset_name }} - name: Download ${{ matrix.asset_name }}
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 uses: actions/download-artifact@v2
with: with:
name: ${{ matrix.asset_name }} name: ${{ matrix.asset_name }}
- name: Set executable flag - name: Set executable flag
@@ -112,7 +70,7 @@ jobs:
- name: Zip ${{ matrix.artifact_name }} into ${{ env.zip_name }} - name: Zip ${{ matrix.artifact_name }} into ${{ env.zip_name }}
run: zip ${{ env.zip_name }} ${{ matrix.artifact_name }} run: zip ${{ env.zip_name }} ${{ matrix.artifact_name }}
- name: Upload ${{ env.zip_name }} to GH Release - name: Upload ${{ env.zip_name }} to GH Release
uses: svenstaro/upload-release-action@2728235f7dc9ff598bd86ce3c274b74f802d2208 # v2 uses: svenstaro/upload-release-action@v2
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN}} repo_token: ${{ secrets.GITHUB_TOKEN}}
file: ${{ env.zip_name }} file: ${{ env.zip_name }}

View File

@@ -1,43 +0,0 @@
name: changelog
on:
# We need pull_request_target instead of pull_request because a write
# repository token is needed to add a review to a PR. DO NOT BUILD
# OR RUN UNTRUSTED CODE FROM PRs IN THIS ACTION
pull_request_target:
types: [opened, edited, synchronize]
permissions: read-all
jobs:
check_changelog:
# no need to check for dependency updates via dependabot
if: github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]'
runs-on: ubuntu-20.04
env:
NO_CHANGELOG: '[x] No CHANGELOG update needed'
steps:
- name: Get changed files
id: files
uses: Ana06/get-changed-files@e0c398b7065a8d84700c471b6afc4116d1ba4e96 # v2.2.0
- name: check changelog updated
id: changelog_updated
env:
PR_BODY: ${{ github.event.pull_request.body }}
FILES: ${{ steps.files.outputs.modified }}
run: |
echo $FILES | grep -qF 'CHANGELOG.md' || echo $PR_BODY | grep -qiF "$NO_CHANGELOG"
- name: Reject pull request if no CHANGELOG update
if: ${{ always() && steps.changelog_updated.outcome == 'failure' }}
uses: Ana06/automatic-pull-request-review@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
event: REQUEST_CHANGES
body: "Please add bug fixes, new features, breaking changes and anything else you think is worthwhile mentioning to the `master (unreleased)` section of CHANGELOG.md. If no CHANGELOG update is needed add the following to the PR description: `${{ env.NO_CHANGELOG }}`"
allow_duplicate: false
- name: Dismiss previous review if CHANGELOG update
uses: Ana06/automatic-pull-request-review@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
event: DISMISS
body: "CHANGELOG updated or no update needed, thanks! :smile:"

View File

@@ -1,41 +1,29 @@
# use PyPI trusted publishing, as described here: # This workflows will upload a Python Package using Twine when a release is created
# https://blog.trailofbits.com/2023/05/23/trusted-publishing-a-new-benchmark-for-packaging-security/ # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: publish to pypi name: publish to pypi
on: on:
release: release:
types: [published] types: [published]
permissions:
contents: write
jobs: jobs:
pypi-publish: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
environment:
name: release
permissions:
id-token: write
steps: steps:
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0 uses: actions/setup-python@v2
with: with:
python-version: '3.8' python-version: '2.7'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -e .[build] pip install setuptools wheel twine
- name: build package - name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: | run: |
python -m build python setup.py sdist bdist_wheel
- name: upload package artifacts twine upload --skip-existing dist/*
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
path: dist/*
- name: publish package
uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 # release/v1
with:
skip-existing: true
verbose: true
print-hash: true

View File

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

View File

@@ -4,29 +4,21 @@ on:
release: release:
types: [published] types: [published]
permissions: read-all
jobs: jobs:
tag: tag:
name: Tag capa rules name: Tag capa rules
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout capa-rules - name: Checkout capa-rules
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@v2
with: with:
repository: mandiant/capa-rules repository: fireeye/capa-rules
token: ${{ secrets.CAPA_TOKEN }} token: ${{ secrets.CAPA_TOKEN }}
- name: Tag capa-rules - name: Tag capa-rules
run: | run: git tag ${{ github.event.release.tag_name }}
# user information is needed to create annotated tags (with a message)
git config user.email 'capa-dev@mandiant.com'
git config user.name 'Capa Bot'
name=${{ github.event.release.tag_name }}
git tag $name -m "https://github.com/mandiant/capa/releases/$name"
# TODO update branch name-major=${name%%.*}
- name: Push tag to capa-rules - name: Push tag to capa-rules
uses: ad-m/github-push-action@0fafdd62b84042d49ec0cb92d9cac7f7ce4ec79e # master uses: ad-m/github-push-action@master
with: with:
repository: mandiant/capa-rules repository: fireeye/capa-rules
github_token: ${{ secrets.CAPA_TOKEN }} github_token: ${{ secrets.CAPA_TOKEN }}
tags: true tags: true

View File

@@ -2,64 +2,40 @@ name: CI
on: on:
push: push:
branches: [ master ] branches: [ master, master-py2 ]
pull_request: pull_request:
branches: [ master ] branches: [ master, master-py2 ]
permissions: read-all
# save workspaces to speed up testing
env:
CAPA_SAVE_WORKSPACE: "True"
jobs: jobs:
changelog_format:
runs-on: ubuntu-20.04
steps:
- name: Checkout capa
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
# The sync GH action in capa-rules relies on a single '- *$' in the CHANGELOG file
- name: Ensure CHANGELOG has '- *$'
run: |
number=$(grep '\- *$' CHANGELOG.md | wc -l)
if [ $number != 1 ]; then exit 1; fi
code_style: code_style:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout capa - name: Checkout capa
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@v2
# use latest available python to take advantage of best performance - name: Set up Python 3.8
- name: Set up Python 3.11 uses: actions/setup-python@v2
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
with: with:
python-version: "3.11" python-version: 3.8
- name: Install dependencies - name: Install dependencies
run: pip install -e .[dev] run: pip install -e .[dev]
- name: Lint with ruff
run: pre-commit run ruff
- name: Lint with isort - name: Lint with isort
run: pre-commit run isort run: isort --profile black --length-sort --line-width 120 -c .
- name: Lint with black - name: Lint with black
run: pre-commit run black run: black -l 120 --check .
- name: Lint with flake8
run: pre-commit run flake8
- name: Check types with mypy
run: pre-commit run mypy
rule_linter: rule_linter:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout capa with submodules - name: Checkout capa with rules submodule
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: true
- name: Set up Python 3.11 - name: Set up Python 3.8
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0 uses: actions/setup-python@v2
with: with:
python-version: "3.11" python-version: 3.8
- name: Install capa - name: Install capa
run: pip install -e .[dev] run: pip install -e .
- name: Run rule linter - name: Run rule linter
run: python scripts/lint.py rules/ run: python scripts/lint.py rules/
@@ -70,72 +46,33 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-20.04, windows-2019, macos-11] os: [ubuntu-20.04, windows-2019, macos-10.15]
# across all operating systems # across all operating systems
python-version: ["3.8", "3.11"] python-version: [3.6, 3.9]
include: include:
# on Ubuntu run these as well # on Ubuntu run these as well
- os: ubuntu-20.04 - os: ubuntu-20.04
python-version: "3.8" python-version: 2.7
- os: ubuntu-20.04 - os: ubuntu-20.04
python-version: "3.9" python-version: 3.7
- os: ubuntu-20.04 - os: ubuntu-20.04
python-version: "3.10" python-version: 3.8
steps: steps:
- name: Checkout capa with submodules - name: Checkout capa with submodules
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: true
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install pyyaml - name: Install pyyaml
if: matrix.os == 'ubuntu-20.04' if: matrix.os == 'ubuntu-20.04'
run: sudo apt-get install -y libyaml-dev run: sudo apt-get install -y libyaml-dev
- name: Install Microsoft Visual C++ 9.0
if: matrix.os == 'windows-2019' && matrix.python-version == '2.7'
run: choco install vcpython27
- name: Install capa - name: Install capa
run: pip install -e .[dev] run: pip install -e .[dev]
- name: Run tests - name: Run tests
run: pytest -v tests/ run: pytest tests/
binja-tests:
name: Binary Ninja tests for ${{ matrix.python-version }}
env:
BN_SERIAL: ${{ secrets.BN_SERIAL }}
runs-on: ubuntu-20.04
needs: [code_style, rule_linter]
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.11"]
steps:
- name: Checkout capa with submodules
# do only run if BN_SERIAL is available, have to do this in every step, see https://github.com/orgs/community/discussions/26726#discussioncomment-3253118
if: ${{ env.BN_SERIAL != 0 }}
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
if: ${{ env.BN_SERIAL != 0 }}
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
with:
python-version: ${{ matrix.python-version }}
- name: Install pyyaml
if: ${{ env.BN_SERIAL != 0 }}
run: sudo apt-get install -y libyaml-dev
- name: Install capa
if: ${{ env.BN_SERIAL != 0 }}
run: pip install -e .[dev]
- name: install Binary Ninja
if: ${{ env.BN_SERIAL != 0 }}
run: |
mkdir ./.github/binja
curl "https://raw.githubusercontent.com/Vector35/binaryninja-api/6812c97/scripts/download_headless.py" -o ./.github/binja/download_headless.py
python ./.github/binja/download_headless.py --serial ${{ env.BN_SERIAL }} --output .github/binja/BinaryNinja-headless.zip
unzip .github/binja/BinaryNinja-headless.zip -d .github/binja/
python .github/binja/binaryninja/scripts/install_api.py --install-on-root --silent
- name: Run tests
if: ${{ env.BN_SERIAL != 0 }}
env:
BN_LICENSE: ${{ secrets.BN_LICENSE }}
run: pytest -v tests/test_binja_features.py # explicitly refer to the binja tests for performance. other tests run above.

20
.gitignore vendored
View File

@@ -108,21 +108,9 @@ venv.bak/
*.viv *.viv
*.idb *.idb
*.i64 *.i64
.vscode
!rules/lib !rules/lib
scripts/perf/*.txt # hooks/ci.sh output
scripts/perf/*.svg isort-output.log
scripts/perf/*.zip black-output.log
rule-linter-output.log
.direnv
.envrc
.DS_Store
*/.DS_Store
Pipfile
Pipfile.lock
/cache/
.github/binja/binaryninja
.github/binja/download_headless.py
.github/binja/BinaryNinja-headless.zip

View File

@@ -1,111 +0,0 @@
# install the pre-commit hooks:
#
# pre-commit install --hook-type pre-commit
# pre-commit installed at .git/hooks/pre-commit
#
# pre-commit install --hook-type pre-push
# pre-commit installed at .git/hooks/pre-push
#
# run all linters liks:
#
# pre-commit run --all-files
# isort....................................................................Passed
# black....................................................................Passed
# ruff.....................................................................Passed
# flake8...................................................................Passed
# mypy.....................................................................Passed
#
# run a single linter like:
#
# pre-commit run --all-files isort
# isort....................................................................Passed
repos:
- repo: local
hooks:
- id: isort
name: isort
stages: [commit, push]
language: system
entry: isort
args:
- "--length-sort"
- "--profile"
- "black"
- "--line-length=120"
- "--skip-glob"
- "*_pb2.py"
- "capa/"
- "scripts/"
- "tests/"
always_run: true
pass_filenames: false
- repo: local
hooks:
- id: black
name: black
stages: [commit, push]
language: system
entry: black
args:
- "--line-length=120"
- "--extend-exclude"
- ".*_pb2.py"
- "capa/"
- "scripts/"
- "tests/"
always_run: true
pass_filenames: false
- repo: local
hooks:
- id: ruff
name: ruff
stages: [commit, push]
language: system
entry: ruff
args:
- "check"
- "--config"
- ".github/ruff.toml"
- "capa/"
- "scripts/"
- "tests/"
always_run: true
pass_filenames: false
- repo: local
hooks:
- id: flake8
name: flake8
stages: [commit, push]
language: system
entry: flake8
args:
- "--config"
- ".github/flake8.ini"
- "--extend-exclude"
- "capa/render/proto/capa_pb2.py"
- "capa/"
- "scripts/"
- "tests/"
always_run: true
pass_filenames: false
- repo: local
hooks:
- id: mypy
name: mypy
stages: [commit, push]
language: system
entry: mypy
args:
- "--check-untyped-defs"
- "--ignore-missing-imports"
- "--config-file=.github/mypy/mypy.ini"
- "capa/"
- "scripts/"
- "tests/"
always_run: true
pass_filenames: false

File diff suppressed because it is too large Load Diff

View File

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

103
README.md
View File

@@ -1,21 +1,17 @@
![capa](https://github.com/mandiant/capa/blob/master/.github/logo.png) ![capa](.github/logo.png)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flare-capa)](https://pypi.org/project/flare-capa) [![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) [![Last release](https://img.shields.io/github/v/release/fireeye/capa)](https://github.com/fireeye/capa/releases)
[![Number of rules](https://img.shields.io/badge/rules-831-blue.svg)](https://github.com/mandiant/capa-rules) [![Number of rules](https://img.shields.io/badge/rules-485-blue.svg)](https://github.com/fireeye/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) [![CI status](https://github.com/fireeye/capa/workflows/CI/badge.svg)](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
[![Downloads](https://img.shields.io/github/downloads/mandiant/capa/total)](https://github.com/mandiant/capa/releases) [![Downloads](https://img.shields.io/github/downloads/fireeye/capa/total)](https://github.com/fireeye/capa/releases)
[![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.txt) [![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.txt)
capa detects capabilities in executable files. 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 file or shellcode 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. For example, it might suggest that the file is a backdoor, is capable of installing services, or relies on HTTP to communicate.
Check out: Check out the overview in our first [capa blog post](https://www.fireeye.com/blog/threat-research/2020/07/capa-automatically-identify-malware-capabilities.html).
- the overview in our first [capa blog post](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities)
- the major version 2.0 updates described in our [second blog post](https://www.mandiant.com/resources/capa-2-better-stronger-faster)
- the major version 3.0 (ELF support) described in the [third blog post](https://www.mandiant.com/resources/elfant-in-the-room-capa-v3)
- the major version 4.0 (.NET support) described in the [fourth blog post](https://www.mandiant.com/resources/blog/capa-v4-casting-wider-net)
``` ```
$ capa.exe suspicious.exe $ capa.exe suspicious.exe
@@ -67,11 +63,18 @@ $ capa.exe suspicious.exe
# download and usage # download and usage
Download stable releases of the standalone capa binaries [here](https://github.com/mandiant/capa/releases). You can run the standalone binaries without installation. capa is a command line tool that should be run from the terminal. Download stable releases of the standalone capa binaries [here](https://github.com/fireeye/capa/releases). You can run the standalone binaries without installation. capa is a command line tool that should be run from the terminal.
To use capa as a library or integrate with another tool, see [doc/installation.md](https://github.com/mandiant/capa/blob/master/doc/installation.md) for further setup instructions. <!--
Alternatively, you can fetch a nightly build of a standalone binary from one of the following links. These are built using the latest development branch.
- Windows 64bit: TODO
- Linux: TODO
- OSX: TODO
-->
For more information about how to use capa, see [doc/usage.md](https://github.com/mandiant/capa/blob/master/doc/usage.md). To use capa as a library or integrate with another tool, see [doc/installation.md](doc/installation.md) for further setup instructions.
For more information about how to use capa, see [doc/usage.md](doc/usage.md).
# example # example
@@ -88,40 +91,31 @@ This is useful for at least two reasons:
- it shows where within the binary an experienced analyst might study with IDA Pro - it shows where within the binary an experienced analyst might study with IDA Pro
``` ```
$ capa.exe suspicious.exe -vv λ capa.exe suspicious.exe -vv
... ...
execute shell command and capture output execute shell command and capture output
namespace c2/shell namespace c2/shell
author matthew.williams@mandiant.com author matthew.williams@fireeye.com
scope function scope function
att&ck Execution::Command and Scripting Interpreter::Windows Command Shell [T1059.003] att&ck Execution::Command and Scripting Interpreter::Windows Command Shell [T1059.003]
references https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa references https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
function @ 0x4011C0 examples Practical Malware Analysis Lab 14-02.exe_:0x4011C0
function @ 0x10003A13
and: and:
match: create a process with modified I/O handles and window @ 0x4011C0 match: create a process with modified I/O handles and window @ 0x10003A13
and: and:
number: 257 = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW @ 0x4012B8
or: or:
number: 68 = StartupInfo.cb (size) @ 0x401282 api: kernel32.CreateProcess @ 0x10003D6D
or: = API functions that accept a pointer to a STARTUPINFO structure number: 0x101 @ 0x10003B03
api: kernel32.CreateProcess @ 0x401343 or:
match: create pipe @ 0x4011C0 number: 0x44 @ 0x10003ADC
optional:
api: kernel32.GetStartupInfo @ 0x10003AE4
match: create pipe @ 0x10003A13
or: or:
api: kernel32.CreatePipe @ 0x40126F, 0x401280 api: kernel32.CreatePipe @ 0x10003ACB
optional:
match: create thread @ 0x40136A, 0x4013BA
or:
and:
os: windows
or:
api: kernel32.CreateThread @ 0x4013D7
or:
and:
os: windows
or:
api: kernel32.CreateThread @ 0x401395
or: or:
string: "cmd.exe" @ 0x4012FD string: cmd.exe /c @ 0x10003AED
... ...
``` ```
@@ -137,49 +131,36 @@ rule:
meta: meta:
name: hash data with CRC32 name: hash data with CRC32
namespace: data-manipulation/checksum/crc32 namespace: data-manipulation/checksum/crc32
authors: author: moritz.raabe@fireeye.com
- moritz.raabe@mandiant.com
scope: function scope: function
mbc:
- Data::Checksum::CRC32 [C0032.001]
examples: examples:
- 2D3EDC218A90F03089CC01715A9F047F:0x403CBD - 2D3EDC218A90F03089CC01715A9F047F:0x403CBD
- 7D28CB106CB54876B2A5C111724A07CD:0x402350 # RtlComputeCrc32 - 7D28CB106CB54876B2A5C111724A07CD:0x402350 # RtlComputeCrc32
- 7EFF498DE13CC734262F87E6B3EF38AB:0x100084A6
features: features:
- or: - or:
- and: - and:
- mnemonic: shr - mnemonic: shr
- or: - number: 0xEDB88320
- 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 - number: 8
- characteristic: nzxor - characteristic: nzxor
- and:
- number: 0x8320
- number: 0xEDB8
- characteristic: nzxor
- api: RtlComputeCrc32 - api: RtlComputeCrc32
``` ```
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard library rules that are distributed with capa. The [github.com/fireeye/capa-rules](https://github.com/fireeye/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
Please learn to write rules and contribute new entries as you find interesting techniques in malware. Please learn to write rules and contribute new entries as you find interesting techniques in malware.
If you use IDA Pro, then you can use the [capa explorer](https://github.com/mandiant/capa/tree/master/capa/ida/plugin) plugin. If you use IDA Pro, then you can use the [capa explorer](capa/ida/plugin/) plugin.
capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted directly from your IDA Pro database. capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted directly from your IDA Pro database.
![capa + IDA Pro integration](https://github.com/mandiant/capa/blob/master/doc/img/explorer_expanded.png) ![capa + IDA Pro integration](doc/img/explorer_expanded.png)
# further information # further information
## capa ## capa
- [Installation](https://github.com/mandiant/capa/blob/master/doc/installation.md) - [doc/installation](doc/installation.md)
- [Usage](https://github.com/mandiant/capa/blob/master/doc/usage.md) - [doc/usage](doc/usage.md)
- [Limitations](https://github.com/mandiant/capa/blob/master/doc/limitations.md) - [doc/limitations](doc/limitations.md)
- [Contributing Guide](https://github.com/mandiant/capa/blob/master/.github/CONTRIBUTING.md) - [Contributing Guide](.github/CONTRIBUTING.md)
## capa rules ## capa rules
- [capa-rules repository](https://github.com/mandiant/capa-rules) - [capa-rules repository](https://github.com/fireeye/capa-rules)
- [capa-rules rule format](https://github.com/mandiant/capa-rules/blob/master/doc/format.md) - [capa-rules rule format](https://github.com/fireeye/capa-rules/blob/master/doc/format.md)
## capa testfiles
The [capa-testfiles repository](https://github.com/mandiant/capa-testfiles) contains the data we use to test capa's code and rules

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -8,29 +8,11 @@
import copy import copy
import collections import collections
from typing import TYPE_CHECKING, Set, Dict, List, Tuple, Union, Mapping, Iterable, Iterator
import capa.perf import capa.features
import capa.features.common
from capa.features.common import Result, Feature
from capa.features.address import Address
if TYPE_CHECKING:
# circular import, otherwise
import capa.rules
# a collection of features and the locations at which they are found. class Statement(object):
#
# used throughout matching as the context in which features are searched:
# to check if a feature exists, do: `Number(0x10) in features`.
# to collect the locations of a feature, do: `features[Number(0x10)]`
#
# aliased here so that the type can be documented and xref'd.
FeatureSet = Dict[Feature, Set[Address]]
class Statement:
""" """
superclass for structural nodes, such as and/or/not. superclass for structural nodes, such as and/or/not.
this exists to provide a default impl for `__str__` and `__repr__`, this exists to provide a default impl for `__str__` and `__repr__`,
@@ -38,194 +20,157 @@ class Statement:
""" """
def __init__(self, description=None): def __init__(self, description=None):
super().__init__() super(Statement, self).__init__()
self.name = self.__class__.__name__ self.name = self.__class__.__name__
self.description = description self.description = description
def __str__(self): def __str__(self):
name = self.name.lower()
children = ",".join(map(str, self.get_children()))
if self.description: if self.description:
return f"{name}({children} = {self.description})" return "%s(%s = %s)" % (self.name.lower(), ",".join(map(str, self.get_children())), self.description)
else: else:
return f"{name}({children})" return "%s(%s)" % (self.name.lower(), ",".join(map(str, self.get_children())))
def __repr__(self): def __repr__(self):
return str(self) return str(self)
def evaluate(self, features: FeatureSet, short_circuit=True) -> Result: def evaluate(self, ctx):
""" """
classes that inherit `Statement` must implement `evaluate` classes that inherit `Statement` must implement `evaluate`
args: args:
short_circuit (bool): if true, then statements like and/or/some may short circuit. ctx (defaultdict[Feature, set[VA]])
returns:
Result
""" """
raise NotImplementedError() raise NotImplementedError()
def get_children(self) -> Iterator[Union["Statement", Feature]]: def get_children(self):
if hasattr(self, "child"): if hasattr(self, "child"):
# this really confuses mypy because the property may not exist yield self.child
# since its defined in the subclasses.
child = self.child # type: ignore
assert isinstance(child, (Statement, Feature))
yield child
if hasattr(self, "children"): if hasattr(self, "children"):
for child in self.children: for child in self.children:
assert isinstance(child, (Statement, Feature))
yield child yield child
def replace_child(self, existing, new): def replace_child(self, existing, new):
if hasattr(self, "child"): if hasattr(self, "child"):
# this really confuses mypy because the property may not exist if self.child is existing:
# since its defined in the subclasses.
if self.child is existing: # type: ignore
self.child = new self.child = new
if hasattr(self, "children"): if hasattr(self, "children"):
children = self.children for i, child in enumerate(self.children):
for i, child in enumerate(children):
if child is existing: if child is existing:
children[i] = new self.children[i] = new
class Result(object):
"""
represents the results of an evaluation of statements against features.
instances of this class should behave like a bool,
e.g. `assert Result(True, ...) == True`
instances track additional metadata about evaluation results.
they contain references to the statement node (e.g. an And statement),
as well as the children Result instances.
we need this so that we can render the tree of expressions and their results.
"""
def __init__(self, success, statement, children, locations=None):
"""
args:
success (bool)
statement (capa.engine.Statement or capa.features.Feature)
children (list[Result])
locations (iterable[VA])
"""
super(Result, self).__init__()
self.success = success
self.statement = statement
self.children = children
self.locations = locations if locations is not None else ()
def __eq__(self, other):
if isinstance(other, bool):
return self.success == other
return False
def __bool__(self):
return self.success
def __nonzero__(self):
return self.success
class And(Statement): class And(Statement):
""" """match if all of the children evaluate to True."""
match if all of the children evaluate to True.
the order of evaluation is dictated by the property
`And.children` (type: List[Statement|Feature]).
a query optimizer may safely manipulate the order of these children.
"""
def __init__(self, children, description=None): def __init__(self, children, description=None):
super().__init__(description=description) super(And, self).__init__(description=description)
self.children = children self.children = children
def evaluate(self, ctx, short_circuit=True): def evaluate(self, ctx):
capa.perf.counters["evaluate.feature"] += 1 results = [child.evaluate(ctx) for child in self.children]
capa.perf.counters["evaluate.feature.and"] += 1 success = all(results)
return Result(success, self, results)
if short_circuit:
results = []
for child in self.children:
result = child.evaluate(ctx, short_circuit=short_circuit)
results.append(result)
if not result:
# short circuit
return Result(False, self, results)
return Result(True, self, results)
else:
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
success = all(results)
return Result(success, self, results)
class Or(Statement): class Or(Statement):
""" """match if any of the children evaluate to True."""
match if any of the children evaluate to True.
the order of evaluation is dictated by the property
`Or.children` (type: List[Statement|Feature]).
a query optimizer may safely manipulate the order of these children.
"""
def __init__(self, children, description=None): def __init__(self, children, description=None):
super().__init__(description=description) super(Or, self).__init__(description=description)
self.children = children self.children = children
def evaluate(self, ctx, short_circuit=True): def evaluate(self, ctx):
capa.perf.counters["evaluate.feature"] += 1 results = [child.evaluate(ctx) for child in self.children]
capa.perf.counters["evaluate.feature.or"] += 1 success = any(results)
return Result(success, self, results)
if short_circuit:
results = []
for child in self.children:
result = child.evaluate(ctx, short_circuit=short_circuit)
results.append(result)
if result:
# short circuit as soon as we hit one match
return Result(True, self, results)
return Result(False, self, results)
else:
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
success = any(results)
return Result(success, self, results)
class Not(Statement): class Not(Statement):
"""match only if the child evaluates to False.""" """match only if the child evaluates to False."""
def __init__(self, child, description=None): def __init__(self, child, description=None):
super().__init__(description=description) super(Not, self).__init__(description=description)
self.child = child self.child = child
def evaluate(self, ctx, short_circuit=True): def evaluate(self, ctx):
capa.perf.counters["evaluate.feature"] += 1 results = [self.child.evaluate(ctx)]
capa.perf.counters["evaluate.feature.not"] += 1
results = [self.child.evaluate(ctx, short_circuit=short_circuit)]
success = not results[0] success = not results[0]
return Result(success, self, results) return Result(success, self, results)
class Some(Statement): class Some(Statement):
""" """match if at least N of the children evaluate to True."""
match if at least N of the children evaluate to True.
the order of evaluation is dictated by the property
`Some.children` (type: List[Statement|Feature]).
a query optimizer may safely manipulate the order of these children.
"""
def __init__(self, count, children, description=None): def __init__(self, count, children, description=None):
super().__init__(description=description) super(Some, self).__init__(description=description)
self.count = count self.count = count
self.children = children self.children = children
def evaluate(self, ctx, short_circuit=True): def evaluate(self, ctx):
capa.perf.counters["evaluate.feature"] += 1 results = [child.evaluate(ctx) for child in self.children]
capa.perf.counters["evaluate.feature.some"] += 1 # note that here we cast the child result as a bool
# because we've overridden `__bool__` above.
if short_circuit: #
results = [] # we can't use `if child is True` because the instance is not True.
satisfied_children_count = 0 success = sum([1 for child in results if bool(child) is True]) >= self.count
for child in self.children: return Result(success, self, results)
result = child.evaluate(ctx, short_circuit=short_circuit)
results.append(result)
if result:
satisfied_children_count += 1
if satisfied_children_count >= self.count:
# short circuit as soon as we hit the threshold
return Result(True, self, results)
return Result(False, self, results)
else:
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
# note that here we cast the child result as a bool
# because we've overridden `__bool__` above.
#
# we can't use `if child is True` because the instance is not True.
success = sum([1 for child in results if bool(child) is True]) >= self.count
return Result(success, self, results)
class Range(Statement): class Range(Statement):
"""match if the child is contained in the ctx set with a count in the given range.""" """match if the child is contained in the ctx set with a count in the given range."""
def __init__(self, child, min=None, max=None, description=None): def __init__(self, child, min=None, max=None, description=None):
super().__init__(description=description) super(Range, self).__init__(description=description)
self.child = child self.child = child
self.min = min if min is not None else 0 self.min = min if min is not None else 0
self.max = max if max is not None else (1 << 64 - 1) self.max = max if max is not None else (1 << 64 - 1)
def evaluate(self, ctx, **kwargs): def evaluate(self, ctx):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.range"] += 1
count = len(ctx.get(self.child, [])) count = len(ctx.get(self.child, []))
if self.min == 0 and count == 0: if self.min == 0 and count == 0:
return Result(True, self, []) return Result(True, self, [])
@@ -234,9 +179,9 @@ class Range(Statement):
def __str__(self): def __str__(self):
if self.max == (1 << 64 - 1): if self.max == (1 << 64 - 1):
return f"range({str(self.child)}, min={self.min}, max=infinity)" return "range(%s, min=%d, max=infinity)" % (str(self.child), self.min)
else: else:
return f"range({str(self.child)}, min={self.min}, max={self.max})" return "range(%s, min=%d, max=%d)" % (str(self.child), self.min, self.max)
class Subscope(Statement): class Subscope(Statement):
@@ -245,66 +190,59 @@ class Subscope(Statement):
the engine should preprocess rules to extract subscope statements into their own rules. the engine should preprocess rules to extract subscope statements into their own rules.
""" """
def __init__(self, scope, child, description=None): def __init__(self, scope, child):
super().__init__(description=description) super(Subscope, self).__init__()
self.scope = scope self.scope = scope
self.child = child self.child = child
def evaluate(self, ctx, **kwargs): def evaluate(self, ctx):
raise ValueError("cannot evaluate a subscope directly!") raise ValueError("cannot evaluate a subscope directly!")
# mapping from rule name to list of: (location of match, result object) def topologically_order_rules(rules):
#
# used throughout matching and rendering to collection the results
# of statement evaluation and their locations.
#
# to check if a rule matched, do: `"TCP client" in matches`.
# to find where a rule matched, do: `map(first, matches["TCP client"])`
# to see how a rule matched, do:
#
# for address, match_details in matches["TCP client"]:
# inspect(match_details)
#
# aliased here so that the type can be documented and xref'd.
MatchResults = Mapping[str, List[Tuple[Address, Result]]]
def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations: Iterable[Address]):
""" """
record into the given featureset that the given rule matched at the given locations. order the given rules such that dependencies show up before dependents.
this means that as we match rules, we can add features for the matches, and these
will be matched by subsequent rules if they follow this order.
naively, this is just adding a MatchedRule feature; assumes that the rule dependency graph is a DAG.
however, we also want to record matches for the rule's namespaces.
updates `features` in-place. doesn't modify the remaining arguments.
""" """
features[capa.features.common.MatchedRule(rule.name)].update(locations) # we evaluate `rules` multiple times, so if its a generator, realize it into a list.
namespace = rule.meta.get("namespace") rules = list(rules)
if namespace: namespaces = capa.rules.index_rules_by_namespace(rules)
while namespace: rules = {rule.name: rule for rule in rules}
features[capa.features.common.MatchedRule(namespace)].update(locations) seen = set([])
namespace, _, _ = namespace.rpartition("/") ret = []
def rec(rule):
if rule.name in seen:
return
for dep in rule.get_dependencies(namespaces):
rec(rules[dep])
ret.append(rule)
seen.add(rule.name)
for rule in rules.values():
rec(rule)
return ret
def match(rules: List["capa.rules.Rule"], features: FeatureSet, addr: Address) -> Tuple[FeatureSet, MatchResults]: def match(rules, features, va):
""" """
match the given rules against the given features, Args:
returning an updated set of features and the matches. rules (List[capa.rules.Rule]): these must already be ordered topologically by dependency.
features (Mapping[capa.features.Feature, int]):
va (int): location of the features
the updated features are just like the input, Returns:
but extended to include the match features (e.g. names of rules that matched). Tuple[List[capa.features.Feature], Dict[str, Tuple[int, capa.engine.Result]]]: two-tuple with entries:
the given feature set is not modified; an updated copy is returned. - list of features used for matching (which may be greater than argument, due to rule match features), and
- mapping from rule name to (location of match, result object)
the given list of rules must be ordered topologically by dependency,
or else `match` statements will not be handled correctly.
this routine should be fairly optimized, but is not guaranteed to be the fastest matcher possible.
it has a particularly convenient signature: (rules, features) -> matches
other strategies can be imagined that match differently; implement these elsewhere.
specifically, this routine does "top down" matching of the given rules against the feature set.
""" """
results = collections.defaultdict(list) # type: MatchResults results = collections.defaultdict(list)
# copy features so that we can modify it # copy features so that we can modify it
# without affecting the caller (keep this function pure) # without affecting the caller (keep this function pure)
@@ -313,22 +251,15 @@ def match(rules: List["capa.rules.Rule"], features: FeatureSet, addr: Address) -
features = collections.defaultdict(set, copy.copy(features)) features = collections.defaultdict(set, copy.copy(features))
for rule in rules: for rule in rules:
res = rule.evaluate(features, short_circuit=True) res = rule.evaluate(features)
if res: if res:
# we first matched the rule with short circuiting enabled. results[rule.name].append((va, res))
# this is much faster than without short circuiting. features[capa.features.MatchedRule(rule.name)].add(va)
# however, we want to collect all results thoroughly,
# so once we've found a match quickly,
# go back and capture results without short circuiting.
res = rule.evaluate(features, short_circuit=False)
# sanity check namespace = rule.meta.get("namespace")
assert bool(res) is True if namespace:
while namespace:
results[rule.name].append((addr, res)) features[capa.features.MatchedRule(namespace)].add(va)
# we need to update the current `features` namespace, _, _ = namespace.rpartition("/")
# because subsequent iterations of this loop may use newly added features,
# such as rule or namespace matches.
index_rule_matches(features, rule, [addr])
return (features, results) return (features, results)

View File

@@ -1,21 +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.
class UnsupportedRuntimeError(RuntimeError):
pass
class UnsupportedFormatError(ValueError):
pass
class UnsupportedArchError(ValueError):
pass
class UnsupportedOSError(ValueError):
pass

View File

@@ -0,0 +1,236 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import re
import sys
import codecs
import logging
import capa.engine
logger = logging.getLogger(__name__)
MAX_BYTES_FEATURE_SIZE = 0x100
# thunks may be chained so we specify a delta to control the depth to which these chains are explored
THUNK_CHAIN_DEPTH_DELTA = 5
# identifiers for supported architectures names that tweak a feature
# for example, offset/x32
ARCH_X32 = "x32"
ARCH_X64 = "x64"
VALID_ARCH = (ARCH_X32, ARCH_X64)
def bytes_to_str(b):
if sys.version_info[0] >= 3:
return str(codecs.encode(b, "hex").decode("utf-8"))
else:
return codecs.encode(b, "hex")
def hex_string(h):
""" render hex string e.g. "0a40b1" as "0A 40 B1" """
return " ".join(h[i : i + 2] for i in range(0, len(h), 2)).upper()
def escape_string(s):
"""escape special characters"""
s = repr(s)
if not s.startswith(('"', "'")):
# u'hello\r\nworld' -> hello\\r\\nworld
s = s[2:-1]
else:
# 'hello\r\nworld' -> hello\\r\\nworld
s = s[1:-1]
s = s.replace("\\'", "'") # repr() may escape "'" in some edge cases, remove
s = s.replace('"', '\\"') # repr() does not escape '"', add
return s
class Feature(object):
def __init__(self, value, arch=None, description=None):
"""
Args:
value (any): the value of the feature, such as the number or string.
arch (str): one of the VALID_ARCH values, or None.
When None, then the feature applies to any architecture.
Modifies the feature name from `feature` to `feature/arch`, like `offset/x32`.
description (str): a human-readable description that explains the feature value.
"""
super(Feature, self).__init__()
if arch is not None:
if arch not in VALID_ARCH:
raise ValueError("arch '%s' must be one of %s" % (arch, VALID_ARCH))
self.name = self.__class__.__name__.lower() + "/" + arch
else:
self.name = self.__class__.__name__.lower()
self.value = value
self.arch = arch
self.description = description
def __hash__(self):
return hash((self.name, self.value, self.arch))
def __eq__(self, other):
return self.name == other.name and self.value == other.value and self.arch == other.arch
def get_value_str(self):
"""
render the value of this feature, for use by `__str__` and friends.
subclasses should override to customize the rendering.
Returns: any
"""
return self.value
def __str__(self):
if self.value is not None:
if self.description:
return "%s(%s = %s)" % (self.name, self.get_value_str(), self.description)
else:
return "%s(%s)" % (self.name, self.get_value_str())
else:
return "%s" % self.name
def __repr__(self):
return str(self)
def evaluate(self, ctx):
return capa.engine.Result(self in ctx, self, [], locations=ctx.get(self, []))
def freeze_serialize(self):
if self.arch is not None:
return (self.__class__.__name__, [self.value, {"arch": self.arch}])
else:
return (self.__class__.__name__, [self.value])
@classmethod
def freeze_deserialize(cls, args):
# as you can see below in code,
# if the last argument is a dictionary,
# consider it to be kwargs passed to the feature constructor.
if len(args) == 1:
return cls(*args)
elif isinstance(args[-1], dict):
kwargs = args[-1]
args = args[:-1]
return cls(*args, **kwargs)
class MatchedRule(Feature):
def __init__(self, value, description=None):
super(MatchedRule, self).__init__(value, description=description)
self.name = "match"
class Characteristic(Feature):
def __init__(self, value, description=None):
super(Characteristic, self).__init__(value, description=description)
class String(Feature):
def __init__(self, value, description=None):
super(String, self).__init__(value, description=description)
class Regex(String):
def __init__(self, value, description=None):
super(Regex, self).__init__(value, description=description)
pat = self.value[len("/") : -len("/")]
flags = re.DOTALL
if value.endswith("/i"):
pat = self.value[len("/") : -len("/i")]
flags |= re.IGNORECASE
try:
self.re = re.compile(pat, flags)
except re.error:
if value.endswith("/i"):
value = value[: -len("i")]
raise ValueError(
"invalid regular expression: %s it should use Python syntax, try it at https://pythex.org" % value
)
def evaluate(self, ctx):
for feature, locations in ctx.items():
if not isinstance(feature, (capa.features.String,)):
continue
# `re.search` finds a match anywhere in the given string
# which implies leading and/or trailing whitespace.
# using this mode cleans is more convenient for rule authors,
# so that they don't have to prefix/suffix their terms like: /.*foo.*/.
if self.re.search(feature.value):
# unlike other features, we cannot return put a reference to `self` directly in a `Result`.
# this is because `self` may match on many strings, so we can't stuff the matched value into it.
# instead, return a new instance that has a reference to both the regex and the matched value.
# see #262.
return capa.engine.Result(True, _MatchedRegex(self, feature.value), [], locations=locations)
return capa.engine.Result(False, _MatchedRegex(self, None), [])
def __str__(self):
return "regex(string =~ %s)" % self.value
class _MatchedRegex(Regex):
"""
this represents a specific instance of a regular expression feature match.
treat it the same as a `Regex` except it has the `match` field that contains the complete string that matched.
note: this type should only ever be constructed by `Regex.evaluate()`. it is not part of the public API.
"""
def __init__(self, regex, match):
"""
args:
regex (Regex): the regex feature that matches
match (string|None): the matching string or None if it doesn't match
"""
super(_MatchedRegex, self).__init__(regex.value, description=regex.description)
# we want this to collide with the name of `Regex` above,
# so that it works nicely with the renderers.
self.name = "regex"
# this may be None if the regex doesn't match
self.match = match
def __str__(self):
return 'regex(string =~ %s, matched = "%s")' % (self.value, self.match)
class StringFactory(object):
def __new__(self, value, description=None):
if value.startswith("/") and (value.endswith("/") or value.endswith("/i")):
return Regex(value, description=description)
return String(value, description=description)
class Bytes(Feature):
def __init__(self, value, description=None):
super(Bytes, self).__init__(value, description=description)
def evaluate(self, ctx):
for feature, locations in ctx.items():
if not isinstance(feature, (capa.features.Bytes,)):
continue
if feature.value.startswith(self.value):
return capa.engine.Result(True, self, [], locations=locations)
return capa.engine.Result(False, self, [])
def get_value_str(self):
return hex_string(bytes_to_str(self.value))
def freeze_serialize(self):
return (self.__class__.__name__, [bytes_to_str(self.value).upper()])
@classmethod
def freeze_deserialize(cls, args):
return cls(*[codecs.decode(x, "hex") for x in args])

View File

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

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,15 +6,22 @@
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 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. # See the License for the specific language governing permissions and limitations under the License.
from capa.features.common import Feature from capa.features import Feature
class BasicBlock(Feature): class BasicBlock(Feature):
def __init__(self, description=None): def __init__(self):
super().__init__(0, description=description) super(BasicBlock, self).__init__(None)
def __str__(self): def __str__(self):
return "basic block" return "basic block"
def get_value_str(self): def get_value_str(self):
return "" return ""
def freeze_serialize(self):
return (self.__class__.__name__, [])
@classmethod
def freeze_deserialize(cls, args):
return cls()

View File

@@ -1,476 +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 re
import abc
import codecs
import typing
import logging
import collections
from typing import TYPE_CHECKING, Set, Dict, List, Union, Optional
if TYPE_CHECKING:
# circular import, otherwise
import capa.engine
import capa.perf
import capa.features
import capa.features.extractors.elf
from capa.features.address import Address
logger = logging.getLogger(__name__)
MAX_BYTES_FEATURE_SIZE = 0x100
# thunks may be chained so we specify a delta to control the depth to which these chains are explored
THUNK_CHAIN_DEPTH_DELTA = 5
class FeatureAccess:
READ = "read"
WRITE = "write"
VALID_FEATURE_ACCESS = (FeatureAccess.READ, FeatureAccess.WRITE)
def bytes_to_str(b: bytes) -> str:
return str(codecs.encode(b, "hex").decode("utf-8"))
def hex_string(h: str) -> str:
"""render hex string e.g. "0a40b1" as "0A 40 B1" """
return " ".join(h[i : i + 2] for i in range(0, len(h), 2)).upper()
def escape_string(s: str) -> str:
"""escape special characters"""
s = repr(s)
if not s.startswith(('"', "'")):
# u'hello\r\nworld' -> hello\\r\\nworld
s = s[2:-1]
else:
# 'hello\r\nworld' -> hello\\r\\nworld
s = s[1:-1]
s = s.replace("\\'", "'") # repr() may escape "'" in some edge cases, remove
s = s.replace('"', '\\"') # repr() does not escape '"', add
return s
class Result:
"""
represents the results of an evaluation of statements against features.
instances of this class should behave like a bool,
e.g. `assert Result(True, ...) == True`
instances track additional metadata about evaluation results.
they contain references to the statement node (e.g. an And statement),
as well as the children Result instances.
we need this so that we can render the tree of expressions and their results.
"""
def __init__(
self,
success: bool,
statement: Union["capa.engine.Statement", "Feature"],
children: List["Result"],
locations: Optional[Set[Address]] = None,
):
super().__init__()
self.success = success
self.statement = statement
self.children = children
self.locations = locations if locations is not None else set()
def __eq__(self, other):
if isinstance(other, bool):
return self.success == other
return False
def __bool__(self):
return self.success
def __nonzero__(self):
return self.success
class Feature(abc.ABC): # noqa: B024
# this is an abstract class, since we don't want anyone to instantiate it directly,
# but it doesn't have any abstract methods.
def __init__(
self,
value: Union[str, int, float, bytes],
description: Optional[str] = None,
):
"""
Args:
value (any): the value of the feature, such as the number or string.
description (str): a human-readable description that explains the feature value.
"""
super().__init__()
self.name = self.__class__.__name__.lower()
self.value = value
self.description = description
def __hash__(self):
return hash((self.name, self.value))
def __eq__(self, other):
return self.name == other.name and self.value == other.value
def __lt__(self, other):
# implementing sorting by serializing to JSON is a huge hack.
# its slow, inelegant, and probably doesn't work intuitively;
# however, we only use it for deterministic output, so it's good enough for now.
# circular import
# we should fix if this wasn't already a huge hack.
import capa.features.freeze.features
return (
capa.features.freeze.features.feature_from_capa(self).model_dump_json()
< capa.features.freeze.features.feature_from_capa(other).model_dump_json()
)
def get_name_str(self) -> str:
"""
render the name of this feature, for use by `__str__` and friends.
subclasses should override to customize the rendering.
"""
return self.name
def get_value_str(self) -> str:
"""
render the value of this feature, for use by `__str__` and friends.
subclasses should override to customize the rendering.
"""
return str(self.value)
def __str__(self):
if self.value is not None:
if self.description:
return f"{self.get_name_str()}({self.get_value_str()} = {self.description})"
else:
return f"{self.get_name_str()}({self.get_value_str()})"
else:
return f"{self.get_name_str()}"
def __repr__(self):
return str(self)
def evaluate(self, ctx: Dict["Feature", Set[Address]], **kwargs) -> Result:
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature." + self.name] += 1
return Result(self in ctx, self, [], locations=ctx.get(self, set()))
class MatchedRule(Feature):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
self.name = "match"
class Characteristic(Feature):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
class String(Feature):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
def get_value_str(self) -> str:
assert isinstance(self.value, str)
return escape_string(self.value)
class Class(Feature):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
class Namespace(Feature):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
class Substring(String):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
self.value = value
def evaluate(self, ctx, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.substring"] += 1
# mapping from string value to list of locations.
# will unique the locations later on.
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
assert isinstance(self.value, str)
for feature, locations in ctx.items():
if not isinstance(feature, (String,)):
continue
if not isinstance(feature.value, str):
# this is a programming error: String should only contain str
raise ValueError("unexpected feature value type")
if self.value in feature.value:
matches[feature.value].update(locations)
if short_circuit:
# we found one matching string, thats sufficient to match.
# don't collect other matching strings in this mode.
break
if matches:
# collect all locations
locations = set()
for locs in matches.values():
locations.update(locs)
# unlike other features, we cannot return put a reference to `self` directly in a `Result`.
# this is because `self` may match on many strings, so we can't stuff the matched value into it.
# instead, return a new instance that has a reference to both the substring and the matched values.
return Result(True, _MatchedSubstring(self, dict(matches)), [], locations=locations)
else:
return Result(False, _MatchedSubstring(self, {}), [])
def get_value_str(self) -> str:
assert isinstance(self.value, str)
return escape_string(self.value)
def __str__(self):
assert isinstance(self.value, str)
return f"substring({escape_string(self.value)})"
class _MatchedSubstring(Substring):
"""
this represents specific match instances of a substring feature.
treat it the same as a `Substring` except it has the `matches` field that contains the complete strings that matched.
note: this type should only ever be constructed by `Substring.evaluate()`. it is not part of the public API.
"""
def __init__(self, substring: Substring, matches: Dict[str, Set[Address]]):
"""
args:
substring: the substring feature that matches.
match: mapping from matching string to its locations.
"""
super().__init__(str(substring.value), description=substring.description)
# we want this to collide with the name of `Substring` above,
# so that it works nicely with the renderers.
self.name = "substring"
# this may be None if the substring doesn't match
self.matches = matches
def __str__(self):
matches = ", ".join(f'"{s}"' for s in (self.matches or {}).keys())
assert isinstance(self.value, str)
return f'substring("{self.value}", matches = {matches})'
class Regex(String):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
self.value = value
pat = self.value[len("/") : -len("/")]
flags = re.DOTALL
if value.endswith("/i"):
pat = self.value[len("/") : -len("/i")]
flags |= re.IGNORECASE
try:
self.re = re.compile(pat, flags)
except re.error as exc:
if value.endswith("/i"):
value = value[: -len("i")]
raise ValueError(
f"invalid regular expression: {value} it should use Python syntax, try it at https://pythex.org"
) from exc
def evaluate(self, ctx, short_circuit=True):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.regex"] += 1
# mapping from string value to list of locations.
# will unique the locations later on.
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
for feature, locations in ctx.items():
if not isinstance(feature, (String,)):
continue
if not isinstance(feature.value, str):
# this is a programming error: String should only contain str
raise ValueError("unexpected feature value type")
# `re.search` finds a match anywhere in the given string
# which implies leading and/or trailing whitespace.
# using this mode cleans is more convenient for rule authors,
# so that they don't have to prefix/suffix their terms like: /.*foo.*/.
if self.re.search(feature.value):
matches[feature.value].update(locations)
if short_circuit:
# we found one matching string, thats sufficient to match.
# don't collect other matching strings in this mode.
break
if matches:
# collect all locations
locations = set()
for locs in matches.values():
locations.update(locs)
# unlike other features, we cannot return put a reference to `self` directly in a `Result`.
# this is because `self` may match on many strings, so we can't stuff the matched value into it.
# instead, return a new instance that has a reference to both the regex and the matched values.
# see #262.
return Result(True, _MatchedRegex(self, dict(matches)), [], locations=locations)
else:
return Result(False, _MatchedRegex(self, {}), [])
def __str__(self):
assert isinstance(self.value, str)
return f"regex(string =~ {self.value})"
class _MatchedRegex(Regex):
"""
this represents specific match instances of a regular expression feature.
treat it the same as a `Regex` except it has the `matches` field that contains the complete strings that matched.
note: this type should only ever be constructed by `Regex.evaluate()`. it is not part of the public API.
"""
def __init__(self, regex: Regex, matches: Dict[str, Set[Address]]):
"""
args:
regex: the regex feature that matches.
matches: mapping from matching string to its locations.
"""
super().__init__(str(regex.value), description=regex.description)
# we want this to collide with the name of `Regex` above,
# so that it works nicely with the renderers.
self.name = "regex"
# this may be None if the regex doesn't match
self.matches = matches
def __str__(self):
matches = ", ".join(f'"{s}"' for s in (self.matches or {}).keys())
assert isinstance(self.value, str)
return f"regex(string =~ {self.value}, matches = {matches})"
class StringFactory:
def __new__(cls, value: str, description=None):
if value.startswith("/") and (value.endswith("/") or value.endswith("/i")):
return Regex(value, description=description)
return String(value, description=description)
class Bytes(Feature):
def __init__(self, value: bytes, description=None):
super().__init__(value, description=description)
self.value = value
def evaluate(self, ctx, **kwargs):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature.bytes"] += 1
assert isinstance(self.value, bytes)
for feature, locations in ctx.items():
if not isinstance(feature, (Bytes,)):
continue
assert isinstance(feature.value, bytes)
if feature.value.startswith(self.value):
return Result(True, self, [], locations=locations)
return Result(False, self, [])
def get_value_str(self):
assert isinstance(self.value, bytes)
return hex_string(bytes_to_str(self.value))
# other candidates here: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
ARCH_I386 = "i386"
ARCH_AMD64 = "amd64"
# dotnet
ARCH_ANY = "any"
VALID_ARCH = (ARCH_I386, ARCH_AMD64, ARCH_ANY)
class Arch(Feature):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
self.name = "arch"
OS_WINDOWS = "windows"
OS_LINUX = "linux"
OS_MACOS = "macos"
# dotnet
OS_ANY = "any"
VALID_OS = {os.value for os in capa.features.extractors.elf.OS}
VALID_OS.update({OS_WINDOWS, OS_LINUX, OS_MACOS, OS_ANY})
# internal only, not to be used in rules
OS_AUTO = "auto"
class OS(Feature):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
self.name = "os"
def evaluate(self, ctx, **kwargs):
capa.perf.counters["evaluate.feature"] += 1
capa.perf.counters["evaluate.feature." + self.name] += 1
for feature, locations in ctx.items():
if not isinstance(feature, (OS,)):
continue
assert isinstance(feature.value, str)
if OS_ANY in (self.value, feature.value) or self.value == feature.value:
return Result(True, self, [], locations=locations)
return Result(False, self, [])
FORMAT_PE = "pe"
FORMAT_ELF = "elf"
FORMAT_DOTNET = "dotnet"
VALID_FORMAT = (FORMAT_PE, FORMAT_ELF, FORMAT_DOTNET)
# internal only, not to be used in rules
FORMAT_AUTO = "auto"
FORMAT_SC32 = "sc32"
FORMAT_SC64 = "sc64"
FORMAT_FREEZE = "freeze"
FORMAT_RESULT = "result"
FORMAT_UNKNOWN = "unknown"
class Format(Feature):
def __init__(self, value: str, description=None):
super().__init__(value, description=description)
self.name = "format"
def is_global_feature(feature):
"""
is this a feature that is extracted at every scope?
today, these are OS and arch features.
"""
return isinstance(feature, (OS, Arch))

View File

@@ -0,0 +1,286 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import abc
class FeatureExtractor(object):
"""
FeatureExtractor defines the interface for fetching features from a sample.
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
serialized JSON file and do matching without a binary analysis pass.
Also, this provides a way to hook in an IDA backend.
This class is not instantiated directly; it is the base class for other implementations.
"""
__metaclass__ = abc.ABCMeta
def __init__(self):
#
# 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(FeatureExtractor, self).__init__()
@abc.abstractmethod
def get_base_address(self):
"""
fetch the preferred load address at which the sample was analyzed.
returns: int
"""
raise NotImplemented
@abc.abstractmethod
def extract_file_features(self):
"""
extract file-scope features.
example::
extractor = VivisectFeatureExtractor(vw, path)
for feature, va in extractor.get_file_features():
print('0x%x: %s', va, feature)
yields:
Tuple[capa.features.Feature, int]: feature and its location
"""
raise NotImplemented
@abc.abstractmethod
def get_functions(self):
"""
enumerate the functions and provide opaque values that will
subsequently be provided to `.extract_function_features()`, etc.
by "opaque value", we mean that this can be any object, as long as it
provides enough context to `.extract_function_features()`.
the opaque value should support casting to int (`__int__`) for the function start address.
yields:
any: the opaque function value.
"""
raise NotImplemented
@abc.abstractmethod
def extract_function_features(self, f):
"""
extract function-scope features.
the arguments are opaque values previously provided by `.get_functions()`, etc.
example::
extractor = VivisectFeatureExtractor(vw, path)
for function in extractor.get_functions():
for feature, va in extractor.extract_function_features(function):
print('0x%x: %s', va, feature)
args:
f [any]: an opaque value previously fetched from `.get_functions()`.
yields:
Tuple[capa.features.Feature, int]: feature and its location
"""
raise NotImplemented
@abc.abstractmethod
def get_basic_blocks(self, f):
"""
enumerate the basic blocks in the given function and provide opaque values that will
subsequently be provided to `.extract_basic_block_features()`, etc.
by "opaque value", we mean that this can be any object, as long as it
provides enough context to `.extract_basic_block_features()`.
the opaque value should support casting to int (`__int__`) for the basic block start address.
yields:
any: the opaque basic block value.
"""
raise NotImplemented
@abc.abstractmethod
def extract_basic_block_features(self, f, bb):
"""
extract basic block-scope features.
the arguments are opaque values previously provided by `.get_functions()`, etc.
example::
extractor = VivisectFeatureExtractor(vw, path)
for function in extractor.get_functions():
for bb in extractor.get_basic_blocks(function):
for feature, va in extractor.extract_basic_block_features(function, bb):
print('0x%x: %s', va, feature)
args:
f [any]: an opaque value previously fetched from `.get_functions()`.
bb [any]: an opaque value previously fetched from `.get_basic_blocks()`.
yields:
Tuple[capa.features.Feature, int]: feature and its location
"""
raise NotImplemented
@abc.abstractmethod
def get_instructions(self, f, bb):
"""
enumerate the instructions in the given basic block and provide opaque values that will
subsequently be provided to `.extract_insn_features()`, etc.
by "opaque value", we mean that this can be any object, as long as it
provides enough context to `.extract_insn_features()`.
the opaque value should support casting to int (`__int__`) for the instruction address.
yields:
any: the opaque function value.
"""
raise NotImplemented
@abc.abstractmethod
def extract_insn_features(self, f, bb, insn):
"""
extract instruction-scope features.
the arguments are opaque values previously provided by `.get_functions()`, etc.
example::
extractor = VivisectFeatureExtractor(vw, path)
for function in extractor.get_functions():
for bb in extractor.get_basic_blocks(function):
for insn in extractor.get_instructions(function, bb):
for feature, va in extractor.extract_insn_features(function, bb, insn):
print('0x%x: %s', va, feature)
args:
f [any]: an opaque value previously fetched from `.get_functions()`.
bb [any]: an opaque value previously fetched from `.get_basic_blocks()`.
insn [any]: an opaque value previously fetched from `.get_instructions()`.
yields:
Tuple[capa.features.Feature, int]: feature and its location
"""
raise NotImplemented
class NullFeatureExtractor(FeatureExtractor):
"""
An extractor that extracts some user-provided features.
The structure of the single parameter is demonstrated in the example below.
This is useful for testing, as we can provide expected values and see if matching works.
Also, this is how we represent features deserialized from a freeze file.
example::
extractor = NullFeatureExtractor({
'base address: 0x401000,
'file features': [
(0x402345, capa.features.Characteristic('embedded pe')),
],
'functions': {
0x401000: {
'features': [
(0x401000, capa.features.Characteristic('nzxor')),
],
'basic blocks': {
0x401000: {
'features': [
(0x401000, capa.features.Characteristic('tight-loop')),
],
'instructions': {
0x401000: {
'features': [
(0x401000, capa.features.Characteristic('nzxor')),
],
},
0x401002: ...
}
},
0x401005: ...
}
},
0x40200: ...
}
)
"""
def __init__(self, features):
super(NullFeatureExtractor, self).__init__()
self.features = features
def get_base_address(self):
return self.features["base address"]
def extract_file_features(self):
for p in self.features.get("file features", []):
va, feature = p
yield feature, va
def get_functions(self):
for va in sorted(self.features["functions"].keys()):
yield va
def extract_function_features(self, f):
for p in self.features.get("functions", {}).get(f, {}).get("features", []): # noqa: E127 line over-indented
va, feature = p
yield feature, va
def get_basic_blocks(self, f):
for va in sorted(
self.features.get("functions", {}) # noqa: E127 line over-indented
.get(f, {})
.get("basic blocks", {})
.keys()
):
yield va
def extract_basic_block_features(self, f, bb):
for p in (
self.features.get("functions", {}) # noqa: E127 line over-indented
.get(f, {})
.get("basic blocks", {})
.get(bb, {})
.get("features", [])
):
va, feature = p
yield feature, va
def get_instructions(self, f, bb):
for va in sorted(
self.features.get("functions", {}) # noqa: E127 line over-indented
.get(f, {})
.get("basic blocks", {})
.get(bb, {})
.get("instructions", {})
.keys()
):
yield va
def extract_insn_features(self, f, bb, insn):
for p in (
self.features.get("functions", {}) # noqa: E127 line over-indented
.get(f, {})
.get("basic blocks", {})
.get(bb, {})
.get("instructions", {})
.get(insn, {})
.get("features", [])
):
va, feature = p
yield feature, va

View File

@@ -1,264 +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 abc
import dataclasses
from typing import Any, Dict, Tuple, Union, Iterator
from dataclasses import dataclass
import capa.features.address
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
# feature extractors may reference functions, BBs, insns by opaque handle values.
# you can use the `.address` property to get and render the address of the feature.
#
# these handles are only consumed by routines on
# the feature extractor from which they were created.
@dataclass
class FunctionHandle:
"""reference to a function recognized by a feature extractor.
Attributes:
address: the address of the function.
inner: extractor-specific data.
ctx: a context object for the extractor.
"""
address: Address
inner: Any
ctx: Dict[str, Any] = dataclasses.field(default_factory=dict)
@dataclass
class BBHandle:
"""reference to a basic block recognized by a feature extractor.
Attributes:
address: the address of the basic block start address.
inner: extractor-specific data.
"""
address: Address
inner: Any
@dataclass
class InsnHandle:
"""reference to a instruction recognized by a feature extractor.
Attributes:
address: the address of the instruction address.
inner: extractor-specific data.
"""
address: Address
inner: Any
class FeatureExtractor:
"""
FeatureExtractor defines the interface for fetching features from a sample.
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
serialized JSON file and do matching without a binary analysis pass.
Also, this provides a way to hook in an IDA backend.
This class is not instantiated directly; it is the base class for other implementations.
"""
__metaclass__ = abc.ABCMeta
def __init__(self):
#
# 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__()
@abc.abstractmethod
def get_base_address(self) -> Union[AbsoluteVirtualAddress, capa.features.address._NoAddress]:
"""
fetch the preferred load address at which the sample was analyzed.
when the base address is `NO_ADDRESS`, then the loader has no concept of a preferred load address.
such as: shellcode, .NET modules, etc.
in these scenarios, RelativeVirtualAddresses aren't used.
"""
raise NotImplementedError()
@abc.abstractmethod
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
"""
extract features found at every scope ("global").
example::
extractor = VivisectFeatureExtractor(vw, path)
for feature, va in extractor.get_global_features():
print('0x%x: %s', va, 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 = VivisectFeatureExtractor(vw, path)
for feature, va in extractor.get_file_features():
print('0x%x: %s', va, feature)
yields:
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@abc.abstractmethod
def get_functions(self) -> Iterator[FunctionHandle]:
"""
enumerate the functions and provide opaque values that will
subsequently be provided to `.extract_function_features()`, etc.
"""
raise NotImplementedError()
def is_library_function(self, addr: Address) -> bool:
"""
is the given address a library function?
the backend may implement its own function matching algorithm, or none at all.
we accept an address here, rather than function object,
to handle addresses identified in instructions.
this information is used to:
- filter out matches in library functions (by default), and
- recognize when to fetch symbol names for called (non-API) functions
args:
addr (Address): the address of a function.
returns:
bool: True if the given address is the start of a library function.
"""
return False
def get_function_name(self, addr: Address) -> str:
"""
fetch any recognized name for the given address.
this is only guaranteed to return a value when the given function is a recognized library function.
we accept a VA here, rather than function object, to handle addresses identified in instructions.
args:
addr (Address): the address of a function.
returns:
str: the function name
raises:
KeyError: when the given function does not have a name.
"""
raise KeyError(addr)
@abc.abstractmethod
def extract_function_features(self, f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract function-scope features.
the arguments are opaque values previously provided by `.get_functions()`, etc.
example::
extractor = VivisectFeatureExtractor(vw, path)
for function in extractor.get_functions():
for feature, address in extractor.extract_function_features(function):
print('0x%x: %s', address, feature)
args:
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
yields:
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@abc.abstractmethod
def get_basic_blocks(self, f: FunctionHandle) -> Iterator[BBHandle]:
"""
enumerate the basic blocks in the given function and provide opaque values that will
subsequently be provided to `.extract_basic_block_features()`, etc.
"""
raise NotImplementedError()
@abc.abstractmethod
def extract_basic_block_features(self, f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""
extract basic block-scope features.
the arguments are opaque values previously provided by `.get_functions()`, etc.
example::
extractor = VivisectFeatureExtractor(vw, path)
for function in extractor.get_functions():
for bb in extractor.get_basic_blocks(function):
for feature, address in extractor.extract_basic_block_features(function, bb):
print('0x%x: %s', address, feature)
args:
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
bb [BBHandle]: an opaque value previously fetched from `.get_basic_blocks()`.
yields:
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()
@abc.abstractmethod
def get_instructions(self, f: FunctionHandle, bb: BBHandle) -> Iterator[InsnHandle]:
"""
enumerate the instructions in the given basic block and provide opaque values that will
subsequently be provided to `.extract_insn_features()`, etc.
"""
raise NotImplementedError()
@abc.abstractmethod
def extract_insn_features(
self, f: FunctionHandle, bb: BBHandle, insn: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
extract instruction-scope features.
the arguments are opaque values previously provided by `.get_functions()`, etc.
example::
extractor = VivisectFeatureExtractor(vw, path)
for function in extractor.get_functions():
for bb in extractor.get_basic_blocks(function):
for insn in extractor.get_instructions(function, bb):
for feature, address in extractor.extract_insn_features(function, bb, insn):
print('0x%x: %s', address, feature)
args:
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
bb [BBHandle]: an opaque value previously fetched from `.get_basic_blocks()`.
insn [InsnHandle]: an opaque value previously fetched from `.get_instructions()`.
yields:
Tuple[Feature, Address]: feature and its location
"""
raise NotImplementedError()

View File

@@ -1,184 +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 string
import struct
from typing import Tuple, Iterator
from binaryninja import Function, Settings
from binaryninja import BasicBlock as BinjaBasicBlock
from binaryninja import (
BinaryView,
SymbolType,
RegisterValueType,
VariableSourceType,
MediumLevelILSetVar,
MediumLevelILOperation,
MediumLevelILBasicBlock,
MediumLevelILInstruction,
)
from capa.features.common import Feature, Characteristic
from capa.features.address import Address
from capa.features.basicblock import BasicBlock
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
use_const_outline: bool = False
settings: Settings = Settings()
if settings.contains("analysis.outlining.builtins") and settings.get_bool("analysis.outlining.builtins"):
use_const_outline = True
def get_printable_len_ascii(s: bytes) -> int:
"""Return string length if all operand bytes are ascii or utf16-le printable"""
count = 0
for c in s:
if c == 0:
return count
if c < 127 and chr(c) in string.printable:
count += 1
return count
def get_printable_len_wide(s: bytes) -> int:
"""Return string length if all operand bytes are ascii or utf16-le printable"""
if all(c == 0x00 for c in s[1::2]):
return get_printable_len_ascii(s[::2])
return 0
def get_stack_string_len(f: Function, il: MediumLevelILInstruction) -> int:
bv: BinaryView = f.view
if il.operation != MediumLevelILOperation.MLIL_CALL:
return 0
target = il.dest
if target.operation not in [MediumLevelILOperation.MLIL_CONST, MediumLevelILOperation.MLIL_CONST_PTR]:
return 0
addr = target.value.value
sym = bv.get_symbol_at(addr)
if not sym or sym.type != SymbolType.LibraryFunctionSymbol:
return 0
if sym.name not in ["__builtin_strncpy", "__builtin_strcpy", "__builtin_wcscpy"]:
return 0
if len(il.params) < 2:
return 0
dest = il.params[0]
if dest.operation in [MediumLevelILOperation.MLIL_ADDRESS_OF, MediumLevelILOperation.MLIL_VAR]:
var = dest.src
else:
return 0
if var.source_type != VariableSourceType.StackVariableSourceType:
return 0
src = il.params[1]
if src.value.type != RegisterValueType.ConstantDataAggregateValue:
return 0
s = f.get_constant_data(RegisterValueType.ConstantDataAggregateValue, src.value.value)
return max(get_printable_len_ascii(bytes(s)), get_printable_len_wide(bytes(s)))
def get_printable_len(il: MediumLevelILSetVar) -> int:
"""Return string length if all operand bytes are ascii or utf16-le printable"""
width = il.dest.type.width
value = il.src.value.value
if width == 1:
chars = struct.pack("<B", value & 0xFF)
elif width == 2:
chars = struct.pack("<H", value & 0xFFFF)
elif width == 4:
chars = struct.pack("<I", value & 0xFFFFFFFF)
elif width == 8:
chars = struct.pack("<Q", value & 0xFFFFFFFFFFFFFFFF)
else:
return 0
def is_printable_ascii(chars_: bytes):
return all(c < 127 and chr(c) in string.printable for c in chars_)
def is_printable_utf16le(chars_: bytes):
if all(c == 0x00 for c in chars_[1::2]):
return is_printable_ascii(chars_[::2])
if is_printable_ascii(chars):
return width
if is_printable_utf16le(chars):
return width // 2
return 0
def is_mov_imm_to_stack(il: MediumLevelILInstruction) -> bool:
"""verify instruction moves immediate onto stack"""
if il.operation != MediumLevelILOperation.MLIL_SET_VAR:
return False
if il.src.operation != MediumLevelILOperation.MLIL_CONST:
return False
if il.dest.source_type != VariableSourceType.StackVariableSourceType:
return False
return True
def bb_contains_stackstring(f: Function, bb: MediumLevelILBasicBlock) -> bool:
"""check basic block for stackstring indicators
true if basic block contains enough moves of constant bytes to the stack
"""
count = 0
for il in bb:
if use_const_outline:
count += get_stack_string_len(f, il)
else:
if is_mov_imm_to_stack(il):
count += get_printable_len(il)
if count > MIN_STACKSTRING_LEN:
return True
return False
def extract_bb_stackstring(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract stackstring indicators from basic block"""
bb: Tuple[BinjaBasicBlock, MediumLevelILBasicBlock] = bbh.inner
if bb[1] is not None and bb_contains_stackstring(fh.inner, bb[1]):
yield Characteristic("stack string"), bbh.address
def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract tight loop indicators from a basic block"""
bb: Tuple[BinjaBasicBlock, MediumLevelILBasicBlock] = bbh.inner
for edge in bb[0].outgoing_edges:
if edge.target.start == bb[0].start:
yield Characteristic("tight loop"), bbh.address
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract basic block features"""
for bb_handler in BASIC_BLOCK_HANDLERS:
for feature, addr in bb_handler(fh, bbh):
yield feature, addr
yield BasicBlock(), bbh.address
BASIC_BLOCK_HANDLERS = (
extract_bb_tight_loop,
extract_bb_stackstring,
)

View File

@@ -1,75 +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.
from typing import List, Tuple, Iterator
import binaryninja as binja
import capa.features.extractors.elf
import capa.features.extractors.binja.file
import capa.features.extractors.binja.insn
import capa.features.extractors.binja.global_
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
class BinjaFeatureExtractor(FeatureExtractor):
def __init__(self, bv: binja.BinaryView):
super().__init__()
self.bv = bv
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.binja.file.extract_file_format(self.bv))
self.global_features.extend(capa.features.extractors.binja.global_.extract_os(self.bv))
self.global_features.extend(capa.features.extractors.binja.global_.extract_arch(self.bv))
def get_base_address(self):
return AbsoluteVirtualAddress(self.bv.start)
def extract_global_features(self):
yield from self.global_features
def extract_file_features(self):
yield from capa.features.extractors.binja.file.extract_features(self.bv)
def get_functions(self) -> Iterator[FunctionHandle]:
for f in self.bv.functions:
yield FunctionHandle(address=AbsoluteVirtualAddress(f.start), inner=f)
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.binja.function.extract_features(fh)
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
f: binja.Function = fh.inner
# Set up a MLIL basic block dict look up to associate the disassembly basic block with its MLIL basic block
mlil_lookup = {}
for mlil_bb in f.mlil.basic_blocks:
mlil_lookup[mlil_bb.source_block.start] = mlil_bb
for bb in f.basic_blocks:
mlil_bb = mlil_lookup.get(bb.start)
yield BBHandle(address=AbsoluteVirtualAddress(bb.start), inner=(bb, mlil_bb))
def extract_basic_block_features(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.binja.basicblock.extract_features(fh, bbh)
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
import capa.features.extractors.binja.helpers as binja_helpers
bb: Tuple[binja.BasicBlock, binja.MediumLevelILBasicBlock] = bbh.inner
addr = bb[0].start
for text, length in bb[0]:
insn = binja_helpers.DisassemblyInstruction(addr, length, text)
yield InsnHandle(address=AbsoluteVirtualAddress(addr), inner=insn)
addr += length
def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle):
yield from capa.features.extractors.binja.insn.extract_features(fh, bbh, ih)

View File

@@ -1,167 +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 struct
from typing import Tuple, Iterator
from binaryninja import Segment, BinaryView, SymbolType, SymbolBinding
import capa.features.extractors.common
import capa.features.extractors.helpers
import capa.features.extractors.strings
from capa.features.file import Export, Import, Section, FunctionName
from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress
from capa.features.extractors.binja.helpers import unmangle_c_name
def check_segment_for_pe(bv: BinaryView, seg: Segment) -> Iterator[Tuple[int, int]]:
"""check segment for embedded PE
adapted for binja from:
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
"""
mz_xor = [
(
capa.features.extractors.helpers.xor_static(b"MZ", i),
capa.features.extractors.helpers.xor_static(b"PE", i),
i,
)
for i in range(256)
]
todo = []
# If this is the first segment of the binary, skip the first bytes. Otherwise, there will always be a matched
# PE at the start of the binaryview.
start = seg.start
if bv.view_type == "PE" and start == bv.start:
start += 1
for mzx, pex, i in mz_xor:
for off, _ in bv.find_all_data(start, seg.end, mzx):
todo.append((off, mzx, pex, i))
while len(todo):
off, mzx, pex, i = todo.pop()
# The MZ header has one field we will check e_lfanew is at 0x3c
e_lfanew = off + 0x3C
if seg.end < (e_lfanew + 4):
continue
newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(bv.read(e_lfanew, 4), i))[0]
peoff = off + newoff
if seg.end < (peoff + 2):
continue
if bv.read(peoff, 2) == pex:
yield off, i
def extract_file_embedded_pe(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
"""extract embedded PE features"""
for seg in bv.segments:
for ea, _ in check_segment_for_pe(bv, seg):
yield Characteristic("embedded pe"), FileOffsetAddress(ea)
def extract_file_export_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
"""extract function exports"""
for sym in bv.get_symbols_of_type(SymbolType.FunctionSymbol):
if sym.binding in [SymbolBinding.GlobalBinding, SymbolBinding.WeakBinding]:
name = sym.short_name
yield Export(name), AbsoluteVirtualAddress(sym.address)
unmangled_name = unmangle_c_name(name)
if name != unmangled_name:
yield Export(unmangled_name), AbsoluteVirtualAddress(sym.address)
def extract_file_import_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
"""extract function imports
1. imports by ordinal:
- modulename.#ordinal
2. imports by name, results in two features to support importname-only
matching:
- modulename.importname
- importname
"""
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):
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):
yield Import(name), addr
def extract_file_section_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
"""extract section names"""
for name, section in bv.sections.items():
yield Section(name), AbsoluteVirtualAddress(section.start)
def extract_file_strings(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
"""extract ASCII and UTF-16 LE strings"""
for s in bv.strings:
yield String(s.value), FileOffsetAddress(s.start)
def extract_file_function_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
"""
extract the names of statically-linked library functions.
"""
for sym_name in bv.symbols:
for sym in bv.symbols[sym_name]:
if sym.type == SymbolType.LibraryFunctionSymbol:
name = sym.short_name
yield FunctionName(name), sym.address
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions.
# extract features for both the mangled and un-mangled representations.
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield FunctionName(name[1:]), sym.address
def extract_file_format(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
view_type = bv.view_type
if view_type in ["PE", "COFF"]:
yield Format(FORMAT_PE), NO_ADDRESS
elif view_type == "ELF":
yield Format(FORMAT_ELF), NO_ADDRESS
elif view_type == "Raw":
# no file type to return when processing a binary file, but we want to continue processing
return
else:
raise NotImplementedError(f"unexpected file format: {view_type}")
def extract_features(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
"""extract file features"""
for file_handler in FILE_HANDLERS:
for feature, addr in file_handler(bv):
yield feature, addr
FILE_HANDLERS = (
extract_file_export_names,
extract_file_import_names,
extract_file_strings,
extract_file_section_names,
extract_file_embedded_pe,
extract_file_function_names,
extract_file_format,
)

View File

@@ -1,35 +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 subprocess
from pathlib import Path
# When the script gets executed as a standalone executable (via PyInstaller), `import binaryninja` does not work because
# we have excluded the binaryninja module in `pyinstaller.spec`. The trick here is to call the system Python and try
# to find out the path of the binaryninja module that has been installed.
# Note, including the binaryninja module in the `pyintaller.spec` would not work, since the binaryninja module tries to
# find the binaryninja core e.g., `libbinaryninjacore.dylib`, using a relative path. And this does not work when the
# binaryninja module is extracted by the PyInstaller.
code = r"""
from pathlib import Path
from importlib import util
spec = util.find_spec('binaryninja')
if spec is not None:
if len(spec.submodule_search_locations) > 0:
path = Path(spec.submodule_search_locations[0])
# encode the path with utf8 then convert to hex, make sure it can be read and restored properly
print(str(path.parent).encode('utf8').hex())
"""
def find_binja_path() -> Path:
raw_output = subprocess.check_output(["python", "-c", code]).decode("ascii").strip()
return Path(bytes.fromhex(raw_output).decode("utf8"))
if __name__ == "__main__":
print(find_binja_path())

View File

@@ -1,68 +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.
from typing import Tuple, Iterator
from binaryninja import Function, BinaryView, LowLevelILOperation
from capa.features.common import Feature, Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors import loops
from capa.features.extractors.base_extractor import FunctionHandle
def extract_function_calls_to(fh: FunctionHandle):
"""extract callers to a function"""
func: Function = fh.inner
for caller in func.caller_sites:
# Everything that is a code reference to the current function is considered a caller, which actually includes
# many other references that are NOT a caller. For example, an instruction `push function_start` will also be
# considered a caller to the function
if caller.llil is not None and caller.llil.operation in [
LowLevelILOperation.LLIL_CALL,
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
LowLevelILOperation.LLIL_JUMP,
LowLevelILOperation.LLIL_TAILCALL,
]:
yield Characteristic("calls to"), AbsoluteVirtualAddress(caller.address)
def extract_function_loop(fh: FunctionHandle):
"""extract loop indicators from a function"""
func: Function = fh.inner
edges = []
# construct control flow graph
for bb in func.basic_blocks:
for edge in bb.outgoing_edges:
edges.append((bb.start, edge.target.start))
if loops.has_loop(edges):
yield Characteristic("loop"), fh.address
def extract_recursive_call(fh: FunctionHandle):
"""extract recursive function call"""
func: Function = fh.inner
bv: BinaryView = func.view
if bv is None:
return
for ref in bv.get_code_refs(func.start):
if ref.function == func:
yield Characteristic("recursive call"), fh.address
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
for func_handler in FUNCTION_HANDLERS:
for feature, addr in func_handler(fh):
yield feature, addr
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call)

View File

@@ -1,60 +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 binaryninja import BinaryView
from capa.features.common import OS, OS_MACOS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature
from capa.features.address import NO_ADDRESS, Address
logger = logging.getLogger(__name__)
def extract_os(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
name = bv.platform.name
if "-" in name:
name = name.split("-")[0]
if name == "windows":
yield OS(OS_WINDOWS), NO_ADDRESS
elif name == "macos":
yield OS(OS_MACOS), NO_ADDRESS
elif name in ["linux", "freebsd", "decree"]:
yield OS(name), NO_ADDRESS
else:
# we likely end up here:
# 1. handling shellcode, or
# 2. handling a new file format (e.g. macho)
#
# for (1) we can't do much - its shellcode and all bets are off.
# we could maybe accept a further CLI argument to specify the OS,
# but i think this would be rarely used.
# rules that rely on OS conditions will fail to match on shellcode.
#
# for (2), this logic will need to be updated as the format is implemented.
logger.debug("unsupported file format: %s, will not guess OS", name)
return
def extract_arch(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
arch = bv.arch.name
if arch == "x86_64":
yield Arch(ARCH_AMD64), NO_ADDRESS
elif arch == "x86":
yield Arch(ARCH_I386), NO_ADDRESS
else:
# we likely end up here:
# 1. handling a new architecture (e.g. aarch64)
#
# for (1), this logic will need to be updated as the format is implemented.
logger.debug("unsupported architecture: %s", arch)
return

View File

@@ -1,53 +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 re
from typing import List, Callable
from dataclasses import dataclass
from binaryninja import LowLevelILInstruction
from binaryninja.architecture import InstructionTextToken
@dataclass
class DisassemblyInstruction:
address: int
length: int
text: List[InstructionTextToken]
LLIL_VISITOR = Callable[[LowLevelILInstruction, LowLevelILInstruction, int], bool]
def visit_llil_exprs(il: LowLevelILInstruction, func: LLIL_VISITOR):
# BN does not really support operand index at the disassembly level, so use the LLIL operand index as a substitute.
# Note, this is NOT always guaranteed to be the same as disassembly operand.
for i, op in enumerate(il.operands):
if isinstance(op, LowLevelILInstruction) and func(op, il, i):
visit_llil_exprs(op, func)
def unmangle_c_name(name: str) -> str:
# https://learn.microsoft.com/en-us/cpp/build/reference/decorated-names?view=msvc-170#FormatC
# Possible variations for BaseThreadInitThunk:
# @BaseThreadInitThunk@12
# _BaseThreadInitThunk
# _BaseThreadInitThunk@12
# It is also possible for a function to have a `Stub` appended to its name:
# _lstrlenWStub@4
# A small optimization to avoid running the regex too many times
# this still increases the unit test execution time from 170s to 200s, should be able to accelerate it
#
# TODO(xusheng): performance optimizations to improve test execution time
# https://github.com/mandiant/capa/issues/1610
if name[0] in ["@", "_"]:
match = re.match(r"^[@|_](.*?)(Stub)?(@\d+)?$", name)
if match:
return match.group(1)
return name

View File

@@ -1,582 +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.
from typing import Any, List, Tuple, Iterator, Optional
from binaryninja import Function
from binaryninja import BasicBlock as BinjaBasicBlock
from binaryninja import (
BinaryView,
ILRegister,
SymbolType,
BinaryReader,
RegisterValueType,
LowLevelILOperation,
LowLevelILInstruction,
)
import capa.features.extractors.helpers
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
from capa.features.common import MAX_BYTES_FEATURE_SIZE, Bytes, String, Feature, Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.binja.helpers import DisassemblyInstruction, visit_llil_exprs
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
# byte range within the first and returning basic blocks, this helps to reduce FP features
SECURITY_COOKIE_BYTES_DELTA = 0x40
# check if a function is a stub function to another function/symbol. The criteria is:
# 1. The function must only have one basic block
# 2. The function must only make one call/jump to another address
# If the function being checked is a stub function, returns the target address. Otherwise, return None.
def is_stub_function(bv: BinaryView, addr: int) -> Optional[int]:
funcs = bv.get_functions_at(addr)
for func in funcs:
if len(func.basic_blocks) != 1:
continue
call_count = 0
call_target = None
for il in func.llil.instructions:
if il.operation in [
LowLevelILOperation.LLIL_CALL,
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
LowLevelILOperation.LLIL_JUMP,
LowLevelILOperation.LLIL_TAILCALL,
]:
call_count += 1
if il.dest.value.type in [
RegisterValueType.ImportedAddressValue,
RegisterValueType.ConstantValue,
RegisterValueType.ConstantPointerValue,
]:
call_target = il.dest.value.value
if call_count == 1 and call_target is not None:
return call_target
return None
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction API features
example:
call dword [0x00473038]
"""
func: Function = fh.inner
bv: BinaryView = func.view
for llil in func.get_llils_at(ih.address):
if llil.operation in [
LowLevelILOperation.LLIL_CALL,
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
LowLevelILOperation.LLIL_JUMP,
LowLevelILOperation.LLIL_TAILCALL,
]:
if llil.dest.value.type not in [
RegisterValueType.ImportedAddressValue,
RegisterValueType.ConstantValue,
RegisterValueType.ConstantPointerValue,
]:
continue
address = llil.dest.value.value
candidate_addrs = [address]
stub_addr = is_stub_function(bv, address)
if stub_addr is not None:
candidate_addrs.append(stub_addr)
for address in candidate_addrs:
sym = func.view.get_symbol_at(address)
if sym is None or sym.type not in [SymbolType.ImportAddressSymbol, SymbolType.ImportedFunctionSymbol]:
continue
sym_name = sym.short_name
lib_name = ""
import_lib = bv.lookup_imported_object_library(sym.address)
if import_lib is not None:
lib_name = import_lib[0].name
if lib_name.endswith(".dll"):
lib_name = lib_name[:-4]
elif lib_name.endswith(".so"):
lib_name = lib_name[:-3]
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym_name):
yield API(name), ih.address
if sym_name.startswith("_"):
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym_name[1:]):
yield API(name), ih.address
def extract_insn_number_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction number features
example:
push 3136B0h ; dwControlCode
"""
func: Function = fh.inner
results: List[Tuple[Any[Number, OperandNumber], Address]] = []
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
if il.operation == LowLevelILOperation.LLIL_LOAD:
return False
if il.operation not in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
return True
for op in parent.operands:
if isinstance(op, ILRegister) and op.name in ["esp", "ebp", "rsp", "rbp", "sp"]:
return False
elif isinstance(op, LowLevelILInstruction) and op.operation == LowLevelILOperation.LLIL_REG:
if op.src.name in ["esp", "ebp", "rsp", "rbp", "sp"]:
return False
raw_value = il.value.value
if parent.operation == LowLevelILOperation.LLIL_SUB:
raw_value = -raw_value
results.append((Number(raw_value), ih.address))
results.append((OperandNumber(index, raw_value), ih.address))
return False
for llil in func.get_llils_at(ih.address):
visit_llil_exprs(llil, llil_checker)
yield from results
def extract_insn_bytes_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""
parse referenced byte sequences
example:
push offset iid_004118d4_IShellLinkA ; riid
"""
func: Function = fh.inner
bv: BinaryView = func.view
candidate_addrs = set()
llil = func.get_llil_at(ih.address)
if llil is None or llil.operation in [LowLevelILOperation.LLIL_CALL, LowLevelILOperation.LLIL_CALL_STACK_ADJUST]:
return
for ref in bv.get_code_refs_from(ih.address):
if ref == ih.address:
continue
if len(bv.get_functions_containing(ref)) > 0:
continue
candidate_addrs.add(ref)
# collect candidate address by enumerating all integers, https://github.com/Vector35/binaryninja-api/issues/3966
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
if il.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
value = il.value.value
if value > 0:
candidate_addrs.add(value)
return False
return True
for llil in func.get_llils_at(ih.address):
visit_llil_exprs(llil, llil_checker)
for addr in candidate_addrs:
extracted_bytes = bv.read(addr, MAX_BYTES_FEATURE_SIZE)
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
if bv.get_string_at(addr) is None:
# don't extract byte features for obvious strings
yield Bytes(extracted_bytes), ih.address
def extract_insn_string_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction string features
example:
push offset aAcr ; "ACR > "
"""
func: Function = fh.inner
bv: BinaryView = func.view
candidate_addrs = set()
# collect candidate address from code refs directly
for ref in bv.get_code_refs_from(ih.address):
if ref == ih.address:
continue
if len(bv.get_functions_containing(ref)) > 0:
continue
candidate_addrs.add(ref)
# collect candidate address by enumerating all integers, https://github.com/Vector35/binaryninja-api/issues/3966
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
if il.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
value = il.value.value
if value > 0:
candidate_addrs.add(value)
return False
return True
for llil in func.get_llils_at(ih.address):
visit_llil_exprs(llil, llil_checker)
# Now we have all the candidate address, check them for string or pointer to string
br = BinaryReader(bv)
for addr in candidate_addrs:
found = bv.get_string_at(addr)
if found:
yield String(found.value), ih.address
br.seek(addr)
pointer = None
if bv.arch.address_size == 4:
pointer = br.read32()
elif bv.arch.address_size == 8:
pointer = br.read64()
if pointer is not None:
found = bv.get_string_at(pointer)
if found:
yield String(found.value), ih.address
def extract_insn_offset_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction structure offset features
example:
.text:0040112F cmp [esi+4], ebx
"""
func: Function = fh.inner
results: List[Tuple[Any[Offset, OperandOffset], Address]] = []
address_size = func.view.arch.address_size * 8
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
# The most common case, read/write dereference to something like `dword [eax+0x28]`
if il.operation in [LowLevelILOperation.LLIL_ADD, LowLevelILOperation.LLIL_SUB]:
left = il.left
right = il.right
# Exclude offsets based on stack/franme pointers
if left.operation == LowLevelILOperation.LLIL_REG and left.src.name in ["esp", "ebp", "rsp", "rbp", "sp"]:
return True
if right.operation != LowLevelILOperation.LLIL_CONST:
return True
raw_value = right.value.value
# If this is not a dereference, then this must be an add and the offset must be in the range \
# [0, MAX_STRUCTURE_SIZE]. For example,
# add eax, 0x10,
# lea ebx, [eax + 1]
if parent.operation not in [LowLevelILOperation.LLIL_LOAD, LowLevelILOperation.LLIL_STORE]:
if il.operation != LowLevelILOperation.LLIL_ADD or (not 0 < raw_value < MAX_STRUCTURE_SIZE):
return False
if address_size > 0:
# BN also encodes the constant value as two's complement, we need to restore its original value
value = capa.features.extractors.helpers.twos_complement(raw_value, address_size)
else:
value = raw_value
results.append((Offset(value), ih.address))
results.append((OperandOffset(index, value), ih.address))
return False
# An edge case: for code like `push dword [esi]`, we need to generate a feature for offset 0x0
elif il.operation in [LowLevelILOperation.LLIL_LOAD, LowLevelILOperation.LLIL_STORE]:
if il.operands[0].operation == LowLevelILOperation.LLIL_REG:
results.append((Offset(0), ih.address))
results.append((OperandOffset(index, 0), ih.address))
return False
return True
for llil in func.get_llils_at(ih.address):
visit_llil_exprs(llil, llil_checker)
yield from results
def is_nzxor_stack_cookie(f: Function, bb: BinjaBasicBlock, llil: LowLevelILInstruction) -> bool:
"""check if nzxor exists within stack cookie delta"""
# TODO(xusheng): use LLIL SSA to do more accurate analysis
# https://github.com/mandiant/capa/issues/1609
reg_names = []
if llil.left.operation == LowLevelILOperation.LLIL_REG:
reg_names.append(llil.left.src.name)
if llil.right.operation == LowLevelILOperation.LLIL_REG:
reg_names.append(llil.right.src.name)
# stack cookie reg should be stack/frame pointer
if not any(reg in ["ebp", "esp", "rbp", "rsp", "sp"] for reg in reg_names):
return False
# expect security cookie init in first basic block within first bytes (instructions)
if len(bb.incoming_edges) == 0 and llil.address < (bb.start + SECURITY_COOKIE_BYTES_DELTA):
return True
# ... or within last bytes (instructions) before a return
if len(bb.outgoing_edges) == 0 and llil.address > (bb.end - SECURITY_COOKIE_BYTES_DELTA):
return True
return False
def extract_insn_nzxor_characteristic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction non-zeroing XOR instruction
ignore expected non-zeroing XORs, e.g. security cookies
"""
func: Function = fh.inner
results = []
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
# If the two operands of the xor instruction are the same, the LLIL will be translated to other instructions,
# e.g., <llil: eax = 0>, (LLIL_SET_REG). So we do not need to check whether the two operands are the same.
if il.operation == LowLevelILOperation.LLIL_XOR:
# Exclude cases related to the stack cookie
if is_nzxor_stack_cookie(fh.inner, bbh.inner[0], il):
return False
results.append((Characteristic("nzxor"), ih.address))
return False
else:
return True
for llil in func.get_llils_at(ih.address):
visit_llil_exprs(llil, llil_checker)
yield from results
def extract_insn_mnemonic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction mnemonic features"""
insn: DisassemblyInstruction = ih.inner
yield Mnemonic(insn.text[0].text), ih.address
def extract_insn_obfs_call_plus_5_characteristic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""
parse call $+5 instruction from the given instruction.
"""
insn: DisassemblyInstruction = ih.inner
if insn.text[0].text == "call" and insn.text[2].text == "$+5" and insn.length == 5:
yield Characteristic("call $+5"), ih.address
def extract_insn_peb_access_characteristic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction peb access
fs:[0x30] on x86, gs:[0x60] on x64
"""
func: Function = fh.inner
results = []
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILOperation, index: int) -> bool:
if il.operation != LowLevelILOperation.LLIL_LOAD:
return True
src = il.src
if src.operation != LowLevelILOperation.LLIL_ADD:
return True
left = src.left
right = src.right
if left.operation != LowLevelILOperation.LLIL_REG:
return True
reg = left.src.name
if right.operation != LowLevelILOperation.LLIL_CONST:
return True
value = right.value.value
if (reg, value) not in (("fsbase", 0x30), ("gsbase", 0x60)):
return True
results.append((Characteristic("peb access"), ih.address))
return False
for llil in func.get_llils_at(ih.address):
visit_llil_exprs(llil, llil_checker)
yield from results
def extract_insn_segment_access_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction fs or gs access"""
func: Function = fh.inner
results = []
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
if il.operation == LowLevelILOperation.LLIL_REG:
reg = il.src.name
if reg == "fsbase":
results.append((Characteristic("fs access"), ih.address))
return False
elif reg == "gsbase":
results.append((Characteristic("gs access"), ih.address))
return False
return False
return True
for llil in func.get_llils_at(ih.address):
visit_llil_exprs(llil, llil_checker)
yield from results
def extract_insn_cross_section_cflow(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""inspect the instruction for a CALL or JMP that crosses section boundaries"""
func: Function = fh.inner
bv: BinaryView = func.view
if bv is None:
return
seg1 = bv.get_segment_at(ih.address)
sections1 = bv.get_sections_at(ih.address)
for ref in bv.get_code_refs_from(ih.address):
if len(bv.get_functions_at(ref)) == 0:
continue
seg2 = bv.get_segment_at(ref)
sections2 = bv.get_sections_at(ref)
if seg1 != seg2 or sections1 != sections2:
yield Characteristic("cross section flow"), ih.address
def extract_function_calls_from(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract functions calls from features
most relevant at the function scope, however, its most efficient to extract at the instruction scope
"""
func: Function = fh.inner
bv: BinaryView = func.view
if bv is None:
return
for il in func.get_llils_at(ih.address):
if il.operation not in [
LowLevelILOperation.LLIL_CALL,
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
LowLevelILOperation.LLIL_TAILCALL,
]:
continue
dest = il.dest
if dest.operation == LowLevelILOperation.LLIL_CONST_PTR:
value = dest.value.value
yield Characteristic("calls from"), AbsoluteVirtualAddress(value)
elif dest.operation == LowLevelILOperation.LLIL_CONST:
yield Characteristic("calls from"), AbsoluteVirtualAddress(dest.value)
elif dest.operation == LowLevelILOperation.LLIL_LOAD:
indirect_src = dest.src
if indirect_src.operation == LowLevelILOperation.LLIL_CONST_PTR:
value = indirect_src.value.value
yield Characteristic("calls from"), AbsoluteVirtualAddress(value)
elif indirect_src.operation == LowLevelILOperation.LLIL_CONST:
yield Characteristic("calls from"), AbsoluteVirtualAddress(indirect_src.value)
elif dest.operation == LowLevelILOperation.LLIL_REG:
if dest.value.type in [
RegisterValueType.ImportedAddressValue,
RegisterValueType.ConstantValue,
RegisterValueType.ConstantPointerValue,
]:
yield Characteristic("calls from"), AbsoluteVirtualAddress(dest.value.value)
def extract_function_indirect_call_characteristic_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""extract indirect function calls (e.g., call eax or call dword ptr [edx+4])
does not include calls like => call ds:dword_ABD4974
most relevant at the function or basic block scope;
however, its most efficient to extract at the instruction scope
"""
func: Function = fh.inner
llil = func.get_llil_at(ih.address)
if llil is None or llil.operation not in [
LowLevelILOperation.LLIL_CALL,
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
LowLevelILOperation.LLIL_TAILCALL,
]:
return
if llil.dest.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
return
if llil.dest.operation == LowLevelILOperation.LLIL_LOAD:
src = llil.dest.src
if src.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
return
yield Characteristic("indirect call"), ih.address
def extract_features(f: FunctionHandle, bbh: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
"""extract instruction features"""
for inst_handler in INSTRUCTION_HANDLERS:
for feature, ea in inst_handler(f, bbh, insn):
yield feature, ea
INSTRUCTION_HANDLERS = (
extract_insn_api_features,
extract_insn_number_features,
extract_insn_bytes_features,
extract_insn_string_features,
extract_insn_offset_features,
extract_insn_nzxor_characteristic_features,
extract_insn_mnemonic_features,
extract_insn_obfs_call_plus_5_characteristic_features,
extract_insn_peb_access_characteristic_features,
extract_insn_cross_section_cflow,
extract_insn_segment_access_features,
extract_function_calls_from,
extract_function_indirect_call_characteristic_features,
)

View File

@@ -1,135 +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 io
import logging
import binascii
import contextlib
from typing import Tuple, Iterator
import pefile
import capa.features
import capa.features.extractors.elf
import capa.features.extractors.pefile
import capa.features.extractors.strings
from capa.features.common import (
OS,
OS_ANY,
OS_AUTO,
ARCH_ANY,
FORMAT_PE,
FORMAT_ELF,
OS_WINDOWS,
FORMAT_FREEZE,
FORMAT_RESULT,
Arch,
Format,
String,
Feature,
)
from capa.features.freeze import is_freeze
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress
logger = logging.getLogger(__name__)
# match strings for formats
MATCH_PE = b"MZ"
MATCH_ELF = b"\x7fELF"
MATCH_RESULT = b'{"meta":'
def extract_file_strings(buf, **kwargs) -> Iterator[Tuple[String, Address]]:
"""
extract ASCII and UTF-16 LE strings from file
"""
for s in capa.features.extractors.strings.extract_ascii_strings(buf):
yield String(s.s), FileOffsetAddress(s.offset)
for s in capa.features.extractors.strings.extract_unicode_strings(buf):
yield String(s.s), FileOffsetAddress(s.offset)
def extract_format(buf) -> Iterator[Tuple[Feature, Address]]:
if buf.startswith(MATCH_PE):
yield Format(FORMAT_PE), NO_ADDRESS
elif buf.startswith(MATCH_ELF):
yield Format(FORMAT_ELF), NO_ADDRESS
elif is_freeze(buf):
yield Format(FORMAT_FREEZE), NO_ADDRESS
elif buf.startswith(MATCH_RESULT):
yield Format(FORMAT_RESULT), NO_ADDRESS
else:
# we likely end up here:
# 1. handling a file format (e.g. macho)
#
# for (1), this logic will need to be updated as the format is implemented.
logger.debug("unsupported file format: %s", binascii.hexlify(buf[:4]).decode("ascii"))
return
def extract_arch(buf) -> Iterator[Tuple[Feature, Address]]:
if buf.startswith(MATCH_PE):
yield from capa.features.extractors.pefile.extract_file_arch(pe=pefile.PE(data=buf))
elif buf.startswith(MATCH_RESULT):
yield Arch(ARCH_ANY), NO_ADDRESS
elif buf.startswith(MATCH_ELF):
with contextlib.closing(io.BytesIO(buf)) as f:
arch = capa.features.extractors.elf.detect_elf_arch(f)
if arch not in capa.features.common.VALID_ARCH:
logger.debug("unsupported arch: %s", arch)
return
yield Arch(arch), NO_ADDRESS
else:
# we likely end up here:
# 1. handling shellcode, or
# 2. handling a new file format (e.g. macho)
#
# for (1) we can't do much - its shellcode and all bets are off.
# we could maybe accept a further CLI argument to specify the arch,
# but i think this would be rarely used.
# rules that rely on arch conditions will fail to match on shellcode.
#
# for (2), this logic will need to be updated as the format is implemented.
logger.debug("unsupported file format: %s, will not guess Arch", binascii.hexlify(buf[:4]).decode("ascii"))
return
def extract_os(buf, os=OS_AUTO) -> Iterator[Tuple[Feature, Address]]:
if os != OS_AUTO:
yield OS(os), NO_ADDRESS
if buf.startswith(MATCH_PE):
yield OS(OS_WINDOWS), NO_ADDRESS
elif buf.startswith(MATCH_RESULT):
yield OS(OS_ANY), NO_ADDRESS
elif buf.startswith(MATCH_ELF):
with contextlib.closing(io.BytesIO(buf)) as f:
os = capa.features.extractors.elf.detect_elf_os(f)
if os not in capa.features.common.VALID_OS:
logger.debug("unsupported os: %s", os)
return
yield OS(os), NO_ADDRESS
else:
# we likely end up here:
# 1. handling shellcode, or
# 2. handling a new file format (e.g. macho)
#
# for (1) we can't do much - its shellcode and all bets are off.
# rules that rely on OS conditions will fail to match on shellcode.
#
# for (2), this logic will need to be updated as the format is implemented.
logger.debug("unsupported file format: %s, will not guess OS", binascii.hexlify(buf[:4]).decode("ascii"))
return

View File

@@ -1,155 +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.
from __future__ import annotations
from typing import Dict, List, Tuple, Union, Iterator, Optional
from pathlib import Path
import dnfile
from dncil.cil.opcode import OpCodes
import capa.features.extractors
import capa.features.extractors.dotnetfile
import capa.features.extractors.dnfile.file
import capa.features.extractors.dnfile.insn
import capa.features.extractors.dnfile.function
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.dnfile.helpers import (
get_dotnet_types,
get_dotnet_fields,
get_dotnet_managed_imports,
get_dotnet_managed_methods,
get_dotnet_unmanaged_imports,
get_dotnet_managed_method_bodies,
)
class DnFileFeatureExtractorCache:
def __init__(self, pe: dnfile.dnPE):
self.imports: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
self.native_imports: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
self.methods: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
self.fields: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
self.types: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
for import_ in get_dotnet_managed_imports(pe):
self.imports[import_.token] = import_
for native_import in get_dotnet_unmanaged_imports(pe):
self.native_imports[native_import.token] = native_import
for method in get_dotnet_managed_methods(pe):
self.methods[method.token] = method
for field in get_dotnet_fields(pe):
self.fields[field.token] = field
for type_ in get_dotnet_types(pe):
self.types[type_.token] = type_
def get_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.imports.get(token)
def get_native_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.native_imports.get(token)
def get_method(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.methods.get(token)
def get_field(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.fields.get(token)
def get_type(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
return self.types.get(token)
class DnfileFeatureExtractor(FeatureExtractor):
def __init__(self, path: Path):
super().__init__()
self.pe: dnfile.dnPE = dnfile.dnPE(str(path))
# pre-compute .NET token lookup tables; each .NET method has access to this cache for feature extraction
# most relevant at instruction scope
self.token_cache: DnFileFeatureExtractorCache = DnFileFeatureExtractorCache(self.pe)
# pre-compute these because we'll yield them at *every* scope.
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_format())
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_os(pe=self.pe))
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe))
def get_base_address(self):
return NO_ADDRESS
def extract_global_features(self):
yield from self.global_features
def extract_file_features(self):
yield from capa.features.extractors.dnfile.file.extract_features(self.pe)
def get_functions(self) -> Iterator[FunctionHandle]:
# create a method lookup table
methods: Dict[Address, FunctionHandle] = {}
for token, method in get_dotnet_managed_method_bodies(self.pe):
fh: FunctionHandle = FunctionHandle(
address=DNTokenAddress(token),
inner=method,
ctx={"pe": self.pe, "calls_from": set(), "calls_to": set(), "cache": self.token_cache},
)
# method tokens should be unique
assert fh.address not in methods.keys()
methods[fh.address] = fh
# calculate unique calls to/from each method
for fh in methods.values():
for insn in fh.inner.instructions:
if insn.opcode not in (
OpCodes.Call,
OpCodes.Callvirt,
OpCodes.Jmp,
OpCodes.Newobj,
):
continue
address: DNTokenAddress = DNTokenAddress(insn.operand.value)
# record call to destination method; note: we only consider MethodDef methods for destinations
dest: Optional[FunctionHandle] = methods.get(address)
if dest is not None:
dest.ctx["calls_to"].add(fh.address)
# record call from source method; note: we record all unique calls from a MethodDef method, not just
# those calls to other MethodDef methods e.g. calls to imported MemberRef methods
fh.ctx["calls_from"].add(address)
yield from methods.values()
def extract_function_features(self, fh) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.dnfile.function.extract_features(fh)
def get_basic_blocks(self, f) -> Iterator[BBHandle]:
# each dotnet method is considered 1 basic block
yield BBHandle(
address=f.address,
inner=f.inner,
)
def extract_basic_block_features(self, fh, bbh):
# we don't support basic block features
yield from []
def get_instructions(self, fh, bbh):
for insn in bbh.inner.instructions:
yield InsnHandle(
address=DNTokenOffsetAddress(bbh.address, insn.offset - (fh.inner.offset + fh.inner.header_size)),
inner=insn,
)
def extract_insn_features(self, fh, bbh, ih) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.dnfile.insn.extract_features(fh, bbh, ih)

View File

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

View File

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

View File

@@ -1,335 +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.
from __future__ import annotations
import logging
from typing import Dict, Tuple, Union, Iterator, Optional
import dnfile
from dncil.cil.body import CilMethodBody
from dncil.cil.error import MethodBodyFormatError
from dncil.clr.token import Token, StringToken, InvalidToken
from dncil.cil.body.reader import CilMethodBodyReaderBase
from capa.features.common import FeatureAccess
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
logger = logging.getLogger(__name__)
class DnfileMethodBodyReader(CilMethodBodyReaderBase):
def __init__(self, pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow):
self.pe: dnfile.dnPE = pe
self.offset: int = self.pe.get_offset_from_rva(row.Rva)
def read(self, n: int) -> bytes:
data: bytes = self.pe.get_data(self.pe.get_rva_from_offset(self.offset), n)
self.offset += n
return data
def tell(self) -> int:
return self.offset
def seek(self, offset: int) -> int:
self.offset = offset
return self.offset
def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Union[dnfile.base.MDTableRow, InvalidToken, str]:
"""map generic token to string or table row"""
assert pe.net is not None
assert pe.net.mdtables is not None
if isinstance(token, StringToken):
user_string: Optional[str] = read_dotnet_user_string(pe, token)
if user_string is None:
return InvalidToken(token.value)
return user_string
table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(token.table)
if table is None:
# table index is not valid
return InvalidToken(token.value)
try:
return table.rows[token.rid - 1]
except IndexError:
# table index is valid but row index is not valid
return InvalidToken(token.value)
def read_dotnet_method_body(pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow) -> Optional[CilMethodBody]:
"""read dotnet method body"""
try:
return CilMethodBody(DnfileMethodBodyReader(pe, row))
except MethodBodyFormatError as e:
logger.debug("failed to parse managed method body @ 0x%08x (%s)", row.Rva, e)
return None
def read_dotnet_user_string(pe: dnfile.dnPE, token: StringToken) -> Optional[str]:
"""read user string from #US stream"""
assert pe.net is not None
if pe.net.user_strings is None:
# stream may not exist (seen in obfuscated .NET)
logger.debug("#US stream does not exist for stream index 0x%08x", token.rid)
return None
try:
user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get_us(token.rid)
except UnicodeDecodeError as e:
logger.debug("failed to decode #US stream index 0x%08x (%s)", token.rid, e)
return None
if user_string is None:
return None
return user_string.value
def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnType]:
"""get managed imports from MemberRef table
see https://www.ntcore.com/files/dotnetformat.htm
10 - MemberRef Table
Each row represents an imported method
Class (index into the TypeRef, ModuleRef, MethodDef, TypeSpec or TypeDef tables)
Name (index into String heap)
01 - TypeRef Table
Each row represents an imported class, its namespace and the assembly which contains it
TypeName (index into String heap)
TypeNamespace (index into String heap)
"""
for rid, member_ref in iter_dotnet_table(pe, dnfile.mdtable.MemberRef.number):
assert isinstance(member_ref, dnfile.mdtable.MemberRefRow)
if not isinstance(member_ref.Class.row, dnfile.mdtable.TypeRefRow):
# only process class imports from TypeRef table
continue
token: int = calculate_dotnet_token_value(dnfile.mdtable.MemberRef.number, rid)
access: Optional[str]
# assume .NET imports starting with get_/set_ are used to access a property
if member_ref.Name.startswith("get_"):
access = FeatureAccess.READ
elif member_ref.Name.startswith("set_"):
access = FeatureAccess.WRITE
else:
access = None
member_ref_name: str = member_ref.Name
if member_ref_name.startswith(("get_", "set_")):
# remove get_/set_ from MemberRef name
member_ref_name = member_ref_name[4:]
yield DnType(
token,
member_ref.Class.row.TypeName,
namespace=member_ref.Class.row.TypeNamespace,
member=member_ref_name,
access=access,
)
def get_dotnet_methoddef_property_accessors(pe: dnfile.dnPE) -> Iterator[Tuple[int, str]]:
"""get MethodDef methods used to access properties
see https://www.ntcore.com/files/dotnetformat.htm
24 - MethodSemantics Table
Links Events and Properties to specific methods. For example one Event can be associated to more methods. A property uses this table to associate get/set methods.
Semantics (a 2-byte bitmask of type MethodSemanticsAttributes)
Method (index into the MethodDef table)
Association (index into the Event or Property table; more precisely, a HasSemantics coded index)
"""
for rid, method_semantics in iter_dotnet_table(pe, dnfile.mdtable.MethodSemantics.number):
assert isinstance(method_semantics, dnfile.mdtable.MethodSemanticsRow)
if method_semantics.Association.row is None:
logger.debug("MethodSemantics[0x%X] Association row is None", rid)
continue
if isinstance(method_semantics.Association.row, dnfile.mdtable.EventRow):
# ignore events
logger.debug("MethodSemantics[0x%X] ignoring Event", rid)
continue
if method_semantics.Method.table is None:
logger.debug("MethodSemantics[0x%X] Method table is None", rid)
continue
token: int = calculate_dotnet_token_value(
method_semantics.Method.table.number, method_semantics.Method.row_index
)
if method_semantics.Semantics.msSetter:
yield token, FeatureAccess.WRITE
elif method_semantics.Semantics.msGetter:
yield token, FeatureAccess.READ
def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]:
"""get managed method names from TypeDef table
see https://www.ntcore.com/files/dotnetformat.htm
02 - TypeDef Table
Each row represents a class in the current assembly.
TypeName (index into String heap)
TypeNamespace (index into String heap)
MethodList (index into MethodDef table; it marks the first of a contiguous run of Methods owned by this Type)
"""
accessor_map: Dict[int, str] = {}
for methoddef, methoddef_access in get_dotnet_methoddef_property_accessors(pe):
accessor_map[methoddef] = methoddef_access
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
for idx, method in enumerate(typedef.MethodList):
if method.table is None:
logger.debug("TypeDef[0x%X] MethodList[0x%X] table is None", rid, idx)
continue
if method.row is None:
logger.debug("TypeDef[0x%X] MethodList[0x%X] row is None", rid, idx)
continue
token: int = calculate_dotnet_token_value(method.table.number, method.row_index)
access: Optional[str] = accessor_map.get(token)
method_name: str = method.row.Name
if method_name.startswith(("get_", "set_")):
# remove get_/set_
method_name = method_name[4:]
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=method_name, access=access)
def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]:
"""get fields from TypeDef table
see https://www.ntcore.com/files/dotnetformat.htm
02 - TypeDef Table
Each row represents a class in the current assembly.
TypeName (index into String heap)
TypeNamespace (index into String heap)
FieldList (index into Field table; it marks the first of a contiguous run of Fields owned by this Type)
"""
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
for idx, field in enumerate(typedef.FieldList):
if field.table is None:
logger.debug("TypeDef[0x%X] FieldList[0x%X] table is None", rid, idx)
continue
if field.row is None:
logger.debug("TypeDef[0x%X] FieldList[0x%X] row is None", rid, idx)
continue
token: int = calculate_dotnet_token_value(field.table.number, field.row_index)
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=field.row.Name)
def get_dotnet_managed_method_bodies(pe: dnfile.dnPE) -> Iterator[Tuple[int, CilMethodBody]]:
"""get managed methods from MethodDef table"""
for rid, method_def in iter_dotnet_table(pe, dnfile.mdtable.MethodDef.number):
assert isinstance(method_def, dnfile.mdtable.MethodDefRow)
if not method_def.ImplFlags.miIL or any((method_def.Flags.mdAbstract, method_def.Flags.mdPinvokeImpl)):
# skip methods that do not have a method body
continue
body: Optional[CilMethodBody] = read_dotnet_method_body(pe, method_def)
if body is None:
logger.debug("MethodDef[0x%X] method body is None", rid)
continue
token: int = calculate_dotnet_token_value(dnfile.mdtable.MethodDef.number, rid)
yield token, body
def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod]:
"""get unmanaged imports from ImplMap table
see https://www.ntcore.com/files/dotnetformat.htm
28 - ImplMap Table
ImplMap table holds information about unmanaged methods that can be reached from managed code, using PInvoke dispatch
MemberForwarded (index into the Field or MethodDef table; more precisely, a MemberForwarded coded index)
ImportName (index into the String heap)
ImportScope (index into the ModuleRef table)
"""
for rid, impl_map in iter_dotnet_table(pe, dnfile.mdtable.ImplMap.number):
assert isinstance(impl_map, dnfile.mdtable.ImplMapRow)
module: str
if impl_map.ImportScope.row is None:
logger.debug("ImplMap[0x%X] ImportScope row is None", rid)
module = ""
else:
module = impl_map.ImportScope.row.Name
method: str = impl_map.ImportName
member_forward_table: int
if impl_map.MemberForwarded.table is None:
logger.debug("ImplMap[0x%X] MemberForwarded table is None", rid)
continue
else:
member_forward_table = impl_map.MemberForwarded.table.number
member_forward_row: int = impl_map.MemberForwarded.row_index
# ECMA says "Each row of the ImplMap table associates a row in the MethodDef table (MemberForwarded) with the
# name of a routine (ImportName) in some unmanaged DLL (ImportScope)"; so we calculate and map the MemberForwarded
# MethodDef table token to help us later record native import method calls made from CIL
token: int = calculate_dotnet_token_value(member_forward_table, member_forward_row)
# like Kernel32.dll
if module and "." in module:
module = module.split(".")[0]
# like kernel32.CreateFileA
yield DnUnmanagedMethod(token, module, method)
def get_dotnet_types(pe: dnfile.dnPE) -> Iterator[DnType]:
"""get .NET types from TypeDef and TypeRef tables"""
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
typedef_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
yield DnType(typedef_token, typedef.TypeName, namespace=typedef.TypeNamespace)
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
typeref_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
yield DnType(typeref_token, typeref.TypeName, namespace=typeref.TypeNamespace)
def calculate_dotnet_token_value(table: int, rid: int) -> int:
return ((table & 0xFF) << Token.TABLE_SHIFT) | (rid & Token.RID_MASK)
def is_dotnet_mixed_mode(pe: dnfile.dnPE) -> bool:
assert pe.net is not None
assert pe.net.Flags is not None
return not bool(pe.net.Flags.CLR_ILONLY)
def iter_dotnet_table(pe: dnfile.dnPE, table_index: int) -> Iterator[Tuple[int, dnfile.base.MDTableRow]]:
assert pe.net is not None
assert pe.net.mdtables is not None
for rid, row in enumerate(pe.net.mdtables.tables.get(table_index, [])):
# .NET tables are 1-indexed
yield rid + 1, row

View File

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

View File

@@ -1,74 +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.
from typing import Optional
class DnType:
def __init__(self, token: int, class_: str, namespace: str = "", member: str = "", access: Optional[str] = None):
self.token: int = token
self.access: Optional[str] = access
self.namespace: str = namespace
self.class_: str = class_
if member == ".ctor":
member = "ctor"
if member == ".cctor":
member = "cctor"
self.member: str = member
def __hash__(self):
return hash((self.token, self.access, self.namespace, self.class_, self.member))
def __eq__(self, other):
return (
self.token == other.token
and self.access == other.access
and self.namespace == other.namespace
and self.class_ == other.class_
and self.member == other.member
)
def __str__(self):
return DnType.format_name(self.class_, namespace=self.namespace, member=self.member)
def __repr__(self):
return str(self)
@staticmethod
def format_name(class_: str, namespace: str = "", member: str = ""):
# like File::OpenRead
name: str = f"{class_}::{member}" if member else class_
if namespace:
# like System.IO.File::OpenRead
name = f"{namespace}.{name}"
return name
class DnUnmanagedMethod:
def __init__(self, token: int, module: str, method: str):
self.token: int = token
self.module: str = module
self.method: str = method
def __hash__(self):
return hash((self.token, self.module, self.method))
def __eq__(self, other):
return self.token == other.token and self.module == other.module and self.method == other.method
def __str__(self):
return DnUnmanagedMethod.format_name(self.module, self.method)
def __repr__(self):
return str(self)
@staticmethod
def format_name(module, method):
return f"{module}.{method}"

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

@@ -1,239 +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
import capa.features.extractors.helpers
from capa.features.file import Import, FunctionName
from capa.features.common import (
OS,
OS_ANY,
ARCH_ANY,
ARCH_I386,
FORMAT_PE,
ARCH_AMD64,
FORMAT_DOTNET,
Arch,
Class,
Format,
String,
Feature,
Namespace,
Characteristic,
)
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress
from capa.features.extractors.base_extractor import FeatureExtractor
from capa.features.extractors.dnfile.helpers import (
DnType,
iter_dotnet_table,
is_dotnet_mixed_mode,
get_dotnet_managed_imports,
get_dotnet_managed_methods,
calculate_dotnet_token_value,
get_dotnet_unmanaged_imports,
)
logger = logging.getLogger(__name__)
def extract_file_format(**kwargs) -> Iterator[Tuple[Format, Address]]:
yield Format(FORMAT_PE), NO_ADDRESS
yield Format(FORMAT_DOTNET), NO_ADDRESS
def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Import, Address]]:
for method in get_dotnet_managed_imports(pe):
# like System.IO.File::OpenRead
yield Import(str(method)), DNTokenAddress(method.token)
for imp in get_dotnet_unmanaged_imports(pe):
# like kernel32.CreateFileA
for name in capa.features.extractors.helpers.generate_symbols(imp.module, imp.method):
yield Import(name), DNTokenAddress(imp.token)
def extract_file_function_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[FunctionName, Address]]:
for method in get_dotnet_managed_methods(pe):
yield FunctionName(str(method)), DNTokenAddress(method.token)
def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Namespace, Address]]:
"""emit namespace features from TypeRef and TypeDef tables"""
# namespaces may be referenced multiple times, so we need to filter
namespaces = set()
for _, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
# emit internal .NET namespaces
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
namespaces.add(typedef.TypeNamespace)
for _, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
# emit external .NET namespaces
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
namespaces.add(typeref.TypeNamespace)
# namespaces may be empty, discard
namespaces.discard("")
for namespace in namespaces:
# namespace do not have an associated token, so we yield 0x0
yield Namespace(namespace), NO_ADDRESS
def extract_file_class_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Class, Address]]:
"""emit class features from TypeRef and TypeDef tables"""
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
# emit internal .NET classes
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
token = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
yield Class(DnType.format_name(typedef.TypeName, namespace=typedef.TypeNamespace)), DNTokenAddress(token)
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
# emit external .NET classes
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
token = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
yield Class(DnType.format_name(typeref.TypeName, namespace=typeref.TypeNamespace)), DNTokenAddress(token)
def extract_file_os(**kwargs) -> Iterator[Tuple[OS, Address]]:
yield OS(OS_ANY), NO_ADDRESS
def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Arch, Address]]:
# to distinguish in more detail, see https://stackoverflow.com/a/23614024/10548020
# .NET 4.5 added option: any CPU, 32-bit preferred
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_strings(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[String, Address]]:
yield from capa.features.extractors.common.extract_file_strings(pe.__data__)
def extract_file_mixed_mode_characteristic_features(
pe: dnfile.dnPE, **kwargs
) -> Iterator[Tuple[Characteristic, Address]]:
if is_dotnet_mixed_mode(pe):
yield Characteristic("mixed mode"), NO_ADDRESS
def extract_file_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
for file_handler in FILE_HANDLERS:
for feature, addr in file_handler(pe=pe): # type: ignore
yield feature, addr
FILE_HANDLERS = (
extract_file_import_names,
extract_file_function_names,
extract_file_strings,
extract_file_format,
extract_file_mixed_mode_characteristic_features,
extract_file_namespace_features,
extract_file_class_features,
)
def extract_global_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
for handler in GLOBAL_HANDLERS:
for feature, va in handler(pe=pe): # type: ignore
yield feature, va
GLOBAL_HANDLERS = (
extract_file_os,
extract_file_arch,
)
class DotnetFileFeatureExtractor(FeatureExtractor):
def __init__(self, path: Path):
super().__init__()
self.path: Path = path
self.pe: dnfile.dnPE = dnfile.dnPE(str(path))
def get_base_address(self):
return NO_ADDRESS
def get_entry_point(self) -> int:
# self.pe.net.Flags.CLT_NATIVE_ENTRYPOINT
# True: native EP: Token
# False: managed EP: RVA
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:
return is_dotnet_mixed_mode(self.pe)
def get_runtime_version(self) -> Tuple[int, int]:
assert self.pe.net is not None
assert self.pe.net.struct is not None
assert self.pe.net.struct.MajorRuntimeVersion is not None
assert self.pe.net.struct.MinorRuntimeVersion is not None
return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion
def get_meta_version_string(self) -> str:
assert self.pe.net is not None
assert self.pe.net.metadata is not None
assert self.pe.net.metadata.struct is not None
assert self.pe.net.metadata.struct.Version is not None
vbuf = self.pe.net.metadata.struct.Version
assert isinstance(vbuf, bytes)
return vbuf.rstrip(b"\x00").decode("utf-8")
def get_functions(self):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def extract_function_features(self, f):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def get_basic_blocks(self, f):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def extract_basic_block_features(self, f, bb):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def get_instructions(self, f, bb):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def extract_insn_features(self, f, bb, insn):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def is_library_function(self, va):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
def get_function_name(self, va):
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")

View File

@@ -1,985 +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 struct
import logging
import itertools
import collections
from enum import Enum
from typing import Set, Dict, List, Tuple, BinaryIO, Iterator, Optional
from dataclasses import dataclass
import Elf # from vivisect
logger = logging.getLogger(__name__)
def align(v, alignment):
remainder = v % alignment
if remainder == 0:
return v
else:
return v + (alignment - remainder)
def read_cstr(buf, offset) -> str:
s = buf[offset:]
s, _, _ = s.partition(b"\x00")
return s.decode("utf-8")
class CorruptElfFile(ValueError):
pass
class OS(str, Enum):
HPUX = "hpux"
NETBSD = "netbsd"
LINUX = "linux"
HURD = "hurd"
_86OPEN = "86open"
SOLARIS = "solaris"
AIX = "aix"
IRIX = "irix"
FREEBSD = "freebsd"
TRU64 = "tru64"
MODESTO = "modesto"
OPENBSD = "openbsd"
OPENVMS = "openvms"
NSK = "nsk"
AROS = "aros"
FENIXOS = "fenixos"
CLOUD = "cloud"
SYLLABLE = "syllable"
NACL = "nacl"
ANDROID = "android"
# via readelf: https://github.com/bminor/binutils-gdb/blob/c0e94211e1ac05049a4ce7c192c9d14d1764eb3e/binutils/readelf.c#L19635-L19658
# and here: https://github.com/bminor/binutils-gdb/blob/34c54daa337da9fadf87d2706d6a590ae1f88f4d/include/elf/common.h#L933-L939
GNU_ABI_TAG = {
0: OS.LINUX,
1: OS.HURD,
2: OS.SOLARIS,
3: OS.FREEBSD,
4: OS.NETBSD,
5: OS.SYLLABLE,
6: OS.NACL,
}
@dataclass
class Phdr:
type: int
offset: int
vaddr: int
paddr: int
filesz: int
buf: bytes
@dataclass
class Shdr:
name: int
type: int
flags: int
addr: int
offset: int
size: int
link: int
entsize: int
buf: bytes
@classmethod
def from_viv(cls, section, buf: bytes) -> "Shdr":
return cls(
section.sh_name,
section.sh_type,
section.sh_flags,
section.sh_addr,
section.sh_offset,
section.sh_size,
section.sh_link,
section.sh_entsize,
buf,
)
class ELF:
def __init__(self, f: BinaryIO):
self.f = f
# these will all be initialized in `_parse()`
self.bitness: int
self.endian: str
self.e_phentsize: int
self.e_phnum: int
self.e_shentsize: int
self.e_shnum: int
self.phbuf: bytes
self.shbuf: bytes
self._parse()
def _parse(self):
self.f.seek(0x0)
self.file_header = self.f.read(0x40)
if not self.file_header.startswith(b"\x7fELF"):
raise CorruptElfFile("missing magic header")
ei_class, ei_data = struct.unpack_from("BB", self.file_header, 4)
logger.debug("ei_class: 0x%02x ei_data: 0x%02x", ei_class, ei_data)
if ei_class == 1:
self.bitness = 32
elif ei_class == 2:
self.bitness = 64
else:
raise CorruptElfFile(f"invalid ei_class: 0x{ei_class:02x}")
if ei_data == 1:
self.endian = "<"
elif ei_data == 2:
self.endian = ">"
else:
raise CorruptElfFile(f"not an ELF file: invalid ei_data: 0x{ei_data:02x}")
if self.bitness == 32:
e_phoff, e_shoff = struct.unpack_from(self.endian + "II", self.file_header, 0x1C)
self.e_phentsize, self.e_phnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x2A)
self.e_shentsize, self.e_shnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x2E)
elif self.bitness == 64:
e_phoff, e_shoff = struct.unpack_from(self.endian + "QQ", self.file_header, 0x20)
self.e_phentsize, self.e_phnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x36)
self.e_shentsize, self.e_shnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x3A)
else:
raise NotImplementedError()
logger.debug("e_phoff: 0x%02x e_phentsize: 0x%02x e_phnum: %d", e_phoff, self.e_phentsize, self.e_phnum)
self.f.seek(e_phoff)
program_header_size = self.e_phnum * self.e_phentsize
self.phbuf = self.f.read(program_header_size)
if len(self.phbuf) != program_header_size:
logger.warning("failed to read program headers")
self.e_phnum = 0
self.f.seek(e_shoff)
section_header_size = self.e_shnum * self.e_shentsize
self.shbuf = self.f.read(section_header_size)
if len(self.shbuf) != section_header_size:
logger.warning("failed to read section headers")
self.e_shnum = 0
OSABI = {
# via pyelftools: https://github.com/eliben/pyelftools/blob/0664de05ed2db3d39041e2d51d19622a8ef4fb0f/elftools/elf/enums.py#L35-L58
# some candidates are commented out because the are not useful values,
# at least when guessing OSes
# 0: "SYSV", # too often used when OS is not SYSV
1: OS.HPUX,
2: OS.NETBSD,
3: OS.LINUX,
4: OS.HURD,
5: OS._86OPEN,
6: OS.SOLARIS,
7: OS.AIX,
8: OS.IRIX,
9: OS.FREEBSD,
10: OS.TRU64,
11: OS.MODESTO,
12: OS.OPENBSD,
13: OS.OPENVMS,
14: OS.NSK,
15: OS.AROS,
16: OS.FENIXOS,
17: OS.CLOUD,
# 53: "SORTFIX", # i can't find any reference to this OS, i dont think it exists
# 64: "ARM_AEABI", # not an OS
# 97: "ARM", # not an OS
# 255: "STANDALONE", # not an OS
}
@property
def ei_osabi(self) -> Optional[OS]:
(ei_osabi,) = struct.unpack_from(self.endian + "B", self.file_header, 7)
return ELF.OSABI.get(ei_osabi)
MACHINE = {
# via https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
1: "M32",
2: "SPARC",
3: "i386",
4: "68K",
5: "88K",
6: "486",
7: "860",
8: "MIPS",
9: "S370",
10: "MIPS_RS3_LE",
11: "RS6000",
15: "PA_RISC",
16: "nCUBE",
17: "VPP500",
18: "SPARC32PLUS",
19: "960",
20: "PPC",
21: "PPC64",
22: "S390",
23: "SPU",
36: "V800",
37: "FR20",
38: "RH32",
39: "RCE",
40: "ARM",
41: "ALPHA",
42: "SH",
43: "SPARCV9",
44: "TRICORE",
45: "ARC",
46: "H8_300",
47: "H8_300H",
48: "H8S",
49: "H8_500",
50: "IA_64",
51: "MIPS_X",
52: "COLDFIRE",
53: "68HC12",
54: "MMA",
55: "PCP",
56: "NCPU",
57: "NDR1",
58: "STARCORE",
59: "ME16",
60: "ST100",
61: "TINYJ",
62: "amd64",
63: "PDSP",
64: "PDP10",
65: "PDP11",
66: "FX66",
67: "ST9PLUS",
68: "ST7",
69: "68HC16",
70: "68HC11",
71: "68HC08",
72: "68HC05",
73: "SVX",
74: "ST19",
75: "VAX",
76: "CRIS",
77: "JAVELIN",
78: "FIREPATH",
79: "ZSP",
80: "MMIX",
81: "HUANY",
82: "PRISM",
83: "AVR",
84: "FR30",
85: "D10V",
86: "D30V",
87: "V850",
88: "M32R",
89: "MN10300",
90: "MN10200",
91: "PJ",
92: "OPENRISC",
93: "ARC_A5",
94: "XTENSA",
95: "VIDEOCORE",
96: "TMM_GPP",
97: "NS32K",
98: "TPC",
99: "SNP1K",
100: "ST200",
}
@property
def e_machine(self) -> Optional[str]:
(e_machine,) = struct.unpack_from(self.endian + "H", self.file_header, 0x12)
return ELF.MACHINE.get(e_machine)
def parse_program_header(self, i) -> Phdr:
phent_offset = i * self.e_phentsize
phent = self.phbuf[phent_offset : phent_offset + self.e_phentsize]
(p_type,) = struct.unpack_from(self.endian + "I", phent, 0x0)
logger.debug("ph:p_type: 0x%04x", p_type)
if self.bitness == 32:
p_offset, p_vaddr, p_paddr, p_filesz = struct.unpack_from(self.endian + "IIII", phent, 0x4)
elif self.bitness == 64:
p_offset, p_vaddr, p_paddr, p_filesz = struct.unpack_from(self.endian + "QQQQ", phent, 0x8)
else:
raise NotImplementedError()
logger.debug("ph:p_offset: 0x%02x p_filesz: 0x%04x", p_offset, p_filesz)
self.f.seek(p_offset)
buf = self.f.read(p_filesz)
if len(buf) != p_filesz:
raise ValueError("failed to read program header content")
return Phdr(p_type, p_offset, p_vaddr, p_paddr, p_filesz, buf)
@property
def program_headers(self):
for i in range(self.e_phnum):
try:
yield self.parse_program_header(i)
except ValueError:
continue
def parse_section_header(self, i) -> Shdr:
shent_offset = i * self.e_shentsize
shent = self.shbuf[shent_offset : shent_offset + self.e_shentsize]
if self.bitness == 32:
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, _, _, sh_entsize = struct.unpack_from(
self.endian + "IIIIIIIIII", shent, 0x0
)
elif self.bitness == 64:
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, _, _, sh_entsize = struct.unpack_from(
self.endian + "IIQQQQIIQQ", shent, 0x0
)
else:
raise NotImplementedError()
logger.debug("sh:sh_offset: 0x%02x sh_size: 0x%04x", sh_offset, sh_size)
self.f.seek(sh_offset)
buf = self.f.read(sh_size)
if len(buf) != sh_size:
raise ValueError("failed to read section header content")
return Shdr(sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, sh_entsize, buf)
@property
def section_headers(self):
for i in range(self.e_shnum):
try:
yield self.parse_section_header(i)
except ValueError:
continue
@property
def linker(self):
PT_INTERP = 0x3
for phdr in self.program_headers:
if phdr.type != PT_INTERP:
continue
return read_cstr(phdr.buf, 0)
@property
def versions_needed(self) -> Dict[str, Set[str]]:
# symbol version requirements are stored in the .gnu.version_r section,
# which has type SHT_GNU_verneed (0x6ffffffe).
#
# this contains a linked list of ElfXX_Verneed structs,
# each referencing a linked list of ElfXX_Vernaux structs.
# strings are stored in the section referenced by the sh_link field of the section header.
# each Verneed struct contains a reference to the name of the library,
# each Vernaux struct contains a reference to the name of a symbol.
SHT_GNU_VERNEED = 0x6FFFFFFE
for shdr in self.section_headers:
if shdr.type != SHT_GNU_VERNEED:
continue
# the linked section contains strings referenced by the verneed structures.
linked_shdr = self.parse_section_header(shdr.link)
versions_needed = collections.defaultdict(set)
# read verneed structures from the start of the section
# until the vn_next link is 0x0.
# each entry describes a shared object that is required by this binary.
vn_offset = 0x0
while True:
# ElfXX_Verneed layout is the same on 32 and 64 bit
vn_version, vn_cnt, vn_file, vn_aux, vn_next = struct.unpack_from(
self.endian + "HHIII", shdr.buf, vn_offset
)
if vn_version != 1:
# unexpected format, don't try to keep parsing
break
# shared object names, like: "libdl.so.2"
so_name = read_cstr(linked_shdr.buf, vn_file)
# read vernaux structures linked from the verneed structure.
# there should be vn_cnt of these.
# each entry describes an ABI name required by the shared object.
vna_offset = vn_offset + vn_aux
for _ in range(vn_cnt):
# ElfXX_Vernaux layout is the same on 32 and 64 bit
_, _, _, vna_name, vna_next = struct.unpack_from(self.endian + "IHHII", shdr.buf, vna_offset)
# ABI names, like: "GLIBC_2.2.5"
abi = read_cstr(linked_shdr.buf, vna_name)
versions_needed[so_name].add(abi)
vna_offset += vna_next
vn_offset += vn_next
if vn_next == 0:
break
return dict(versions_needed)
return {}
@property
def dynamic_entries(self) -> Iterator[Tuple[int, int]]:
"""
read the entries from the dynamic section,
yielding the tag and value for each entry.
"""
DT_NULL = 0x0
PT_DYNAMIC = 0x2
for phdr in self.program_headers:
if phdr.type != PT_DYNAMIC:
continue
offset = 0x0
while True:
if self.bitness == 32:
d_tag, d_val = struct.unpack_from(self.endian + "II", phdr.buf, offset)
offset += 8
elif self.bitness == 64:
d_tag, d_val = struct.unpack_from(self.endian + "QQ", phdr.buf, offset)
offset += 16
else:
raise NotImplementedError()
if d_tag == DT_NULL:
break
yield d_tag, d_val
@property
def strtab(self) -> Optional[bytes]:
"""
fetch the bytes of the string table
referenced by the dynamic section.
"""
DT_STRTAB = 0x5
DT_STRSZ = 0xA
strtab_addr = None
strtab_size = None
for d_tag, d_val in self.dynamic_entries:
if d_tag == DT_STRTAB:
strtab_addr = d_val
break
for d_tag, d_val in self.dynamic_entries:
if d_tag == DT_STRSZ:
strtab_size = d_val
break
if strtab_addr is None:
return None
if strtab_size is None:
return None
strtab_offset = None
for shdr in self.section_headers:
# the section header address should be defined
if shdr.addr and shdr.addr <= strtab_addr < shdr.addr + shdr.size:
strtab_offset = shdr.offset + (strtab_addr - shdr.addr)
break
if strtab_offset is None:
return None
self.f.seek(strtab_offset)
strtab_buf = self.f.read(strtab_size)
if len(strtab_buf) != strtab_size:
return None
return strtab_buf
@property
def needed(self) -> Iterator[str]:
"""
read the names of DT_NEEDED entries from the dynamic section,
which correspond to dependencies on other shared objects,
like: `libpthread.so.0`
"""
DT_NEEDED = 0x1
strtab = self.strtab
if not strtab:
return
for d_tag, d_val in self.dynamic_entries:
if d_tag != DT_NEEDED:
continue
try:
yield read_cstr(strtab, d_val)
except UnicodeDecodeError as e:
logger.warning("failed to read DT_NEEDED entry: %s", str(e))
@property
def symtab(self) -> Optional[Tuple[Shdr, Shdr]]:
"""
fetch the Shdr for the symtab and the associated strtab.
"""
SHT_SYMTAB = 0x2
for shdr in self.section_headers:
if shdr.type != SHT_SYMTAB:
continue
# the linked section contains strings referenced by the symtab structures.
strtab_shdr = self.parse_section_header(shdr.link)
return shdr, strtab_shdr
return None
@dataclass
class ABITag:
os: OS
kmajor: int
kminor: int
kpatch: int
class PHNote:
def __init__(self, endian: str, buf: bytes):
self.endian = endian
self.buf = buf
# these will be initialized in `_parse()`
self.type_: int
self.descsz: int
self.name: str
self._parse()
def _parse(self):
namesz, self.descsz, self.type_ = struct.unpack_from(self.endian + "III", self.buf, 0x0)
name_offset = 0xC
self.desc_offset = name_offset + align(namesz, 0x4)
logger.debug("ph:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, self.descsz, self.type_)
self.name = self.buf[name_offset : name_offset + namesz].partition(b"\x00")[0].decode("ascii")
logger.debug("name: %s", self.name)
@property
def abi_tag(self) -> Optional[ABITag]:
if self.type_ != 1:
# > The type field shall be 1.
# Linux Standard Base Specification 1.2
# ref: https://refspecs.linuxfoundation.org/LSB_1.2.0/gLSB/noteabitag.html
return None
if self.name != "GNU":
return None
if self.descsz < 16:
return None
desc = self.buf[self.desc_offset : self.desc_offset + self.descsz]
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(self.endian + "IIII", desc, 0x0)
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
os = GNU_ABI_TAG.get(abi_tag)
if not os:
return None
logger.debug("abi tag: %s earliest compatible kernel: %d.%d.%d", os, kmajor, kminor, kpatch)
return ABITag(os, kmajor, kminor, kpatch)
class SHNote:
def __init__(self, endian: str, buf: bytes):
self.endian = endian
self.buf = buf
# these will be initialized in `_parse()`
self.type_: int
self.descsz: int
self.name: str
self._parse()
def _parse(self):
namesz, self.descsz, self.type_ = struct.unpack_from(self.endian + "III", self.buf, 0x0)
name_offset = 0xC
self.desc_offset = name_offset + align(namesz, 0x4)
logger.debug("sh:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, self.descsz, self.type_)
name_buf = self.buf[name_offset : name_offset + namesz]
self.name = read_cstr(name_buf, 0x0)
logger.debug("sh:name: %s", self.name)
@property
def abi_tag(self) -> Optional[ABITag]:
if self.name != "GNU":
return None
if self.descsz < 16:
return None
desc = self.buf[self.desc_offset : self.desc_offset + self.descsz]
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(self.endian + "IIII", desc, 0x0)
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
os = GNU_ABI_TAG.get(abi_tag)
if not os:
return None
logger.debug("abi tag: %s earliest compatible kernel: %d.%d.%d", os, kmajor, kminor, kpatch)
return ABITag(os, kmajor, kminor, kpatch)
@dataclass
class Symbol:
name_offset: int
value: int
size: int
info: int
other: int
shndx: int
class SymTab:
def __init__(
self,
endian: str,
bitness: int,
symtab: Shdr,
strtab: Shdr,
) -> None:
self.symbols: List[Symbol] = []
self.symtab = symtab
self.strtab = strtab
self._parse(endian, bitness, symtab.buf)
def _parse(self, endian: str, bitness: int, symtab_buf: bytes) -> None:
"""
return the symbol's information in
the order specified by sys/elf32.h
"""
if self.symtab.entsize == 0:
return
for i in range(int(len(self.symtab.buf) / self.symtab.entsize)):
if bitness == 32:
name_offset, value, size, info, other, shndx = struct.unpack_from(
endian + "IIIBBH", symtab_buf, i * self.symtab.entsize
)
elif bitness == 64:
name_offset, info, other, shndx, value, size = struct.unpack_from(
endian + "IBBHQQ", symtab_buf, i * self.symtab.entsize
)
self.symbols.append(Symbol(name_offset, value, size, info, other, shndx))
def get_name(self, symbol: Symbol) -> str:
"""
fetch a symbol's name from symtab's
associated strings' section (SHT_STRTAB)
"""
if not self.strtab:
raise ValueError("no strings found")
for i in range(symbol.name_offset, self.strtab.size):
if self.strtab.buf[i] == 0:
return self.strtab.buf[symbol.name_offset : i].decode("utf-8")
raise ValueError("symbol name not found")
def get_symbols(self) -> Iterator[Symbol]:
"""
return a tuple: (name, value, size, info, other, shndx)
for each symbol contained in the symbol table
"""
yield from self.symbols
@classmethod
def from_viv(cls, elf: Elf.Elf) -> Optional["SymTab"]:
endian = "<" if elf.getEndian() == 0 else ">"
bitness = elf.bits
SHT_SYMTAB = 0x2
for section in elf.sections:
if section.sh_type == SHT_SYMTAB:
strtab_section = elf.sections[section.sh_link]
sh_symtab = Shdr.from_viv(section, elf.readAtOffset(section.sh_offset, section.sh_size))
sh_strtab = Shdr.from_viv(
strtab_section, elf.readAtOffset(strtab_section.sh_offset, strtab_section.sh_size)
)
try:
return cls(endian, bitness, sh_symtab, sh_strtab)
except NameError:
return None
except Exception:
# all exceptions that could be encountered by
# cls._parse() imply a faulty symbol's table.
raise CorruptElfFile("malformed symbol's table")
def guess_os_from_osabi(elf: ELF) -> Optional[OS]:
return elf.ei_osabi
def guess_os_from_ph_notes(elf: ELF) -> Optional[OS]:
# search for PT_NOTE sections that specify an OS
# for example, on Linux there is a GNU section with minimum kernel version
PT_NOTE = 0x4
for phdr in elf.program_headers:
if phdr.type != PT_NOTE:
continue
note = PHNote(elf.endian, phdr.buf)
if note.type_ != 1:
# > The type field shall be 1.
# Linux Standard Base Specification 1.2
# ref: https://refspecs.linuxfoundation.org/LSB_1.2.0/gLSB/noteabitag.html
continue
if note.name == "Linux":
logger.debug("note owner: %s", "LINUX")
return OS.LINUX
elif note.name == "OpenBSD":
logger.debug("note owner: %s", "OPENBSD")
return OS.OPENBSD
elif note.name == "NetBSD":
logger.debug("note owner: %s", "NETBSD")
return OS.NETBSD
elif note.name == "FreeBSD":
logger.debug("note owner: %s", "FREEBSD")
return OS.FREEBSD
elif note.name == "Android":
logger.debug("note owner: %s", "Android")
# see the following for parsing the structure:
# https://android.googlesource.com/platform/ndk/+/master/parse_elfnote.py
return OS.ANDROID
elif note.name == "GNU":
abi_tag = note.abi_tag
if abi_tag:
return abi_tag.os
else:
# cannot make a guess about the OS, but probably linux or hurd
pass
return None
def guess_os_from_sh_notes(elf: ELF) -> Optional[OS]:
# search for notes stored in sections that aren't visible in program headers.
# e.g. .note.Linux in Linux kernel modules.
SHT_NOTE = 0x7
for shdr in elf.section_headers:
if shdr.type != SHT_NOTE:
continue
note = SHNote(elf.endian, shdr.buf)
if note.name == "Linux":
logger.debug("note owner: %s", "LINUX")
return OS.LINUX
elif note.name == "OpenBSD":
logger.debug("note owner: %s", "OPENBSD")
return OS.OPENBSD
elif note.name == "NetBSD":
logger.debug("note owner: %s", "NETBSD")
return OS.NETBSD
elif note.name == "FreeBSD":
logger.debug("note owner: %s", "FREEBSD")
return OS.FREEBSD
elif note.name == "GNU":
abi_tag = note.abi_tag
if abi_tag:
return abi_tag.os
else:
# cannot make a guess about the OS, but probably linux or hurd
pass
return None
def guess_os_from_linker(elf: ELF) -> Optional[OS]:
# search for recognizable dynamic linkers (interpreters)
# for example, on linux, we see file paths like: /lib64/ld-linux-x86-64.so.2
linker = elf.linker
if linker and "ld-linux" in elf.linker:
return OS.LINUX
return None
def guess_os_from_abi_versions_needed(elf: ELF) -> Optional[OS]:
# then lets look for GLIBC symbol versioning requirements.
# this will let us guess about linux/hurd in some cases.
versions_needed = elf.versions_needed
if any(abi.startswith("GLIBC") for abi in itertools.chain(*versions_needed.values())):
# there are any GLIBC versions needed
if elf.e_machine != "i386":
# GLIBC runs on Linux and Hurd.
# for Hurd, its *only* on i386.
# so if we're not on i386, then we're on Linux.
return OS.LINUX
else:
# we're on i386, so we could be on either Linux or Hurd.
linker = elf.linker
if linker and "ld-linux" in linker:
return OS.LINUX
elif linker and "/ld.so" in linker:
return OS.HURD
else:
# we don't have any good guesses based on versions needed
pass
return None
def guess_os_from_needed_dependencies(elf: ELF) -> Optional[OS]:
for needed in elf.needed:
if needed.startswith("libmachuser.so"):
return OS.HURD
if needed.startswith("libhurduser.so"):
return OS.HURD
if needed.startswith("libandroid.so"):
return OS.ANDROID
return None
def guess_os_from_symtab(elf: ELF) -> Optional[OS]:
shdrs = elf.symtab
if not shdrs:
# executable does not contain a symbol table
# or the symbol's names are stripped
return None
symtab_shdr, strtab_shdr = shdrs
symtab = SymTab(elf.endian, elf.bitness, symtab_shdr, strtab_shdr)
keywords = {
OS.LINUX: [
"linux",
"/linux/",
],
}
for symbol in symtab.get_symbols():
sym_name = symtab.get_name(symbol)
for os, hints in keywords.items():
if any(hint in sym_name for hint in hints):
return os
return None
def detect_elf_os(f) -> str:
"""
f: type Union[BinaryIO, IDAIO]
"""
try:
elf = ELF(f)
except Exception as e:
logger.warning("Error parsing ELF file: %s", e)
return "unknown"
try:
osabi_guess = guess_os_from_osabi(elf)
logger.debug("guess: osabi: %s", osabi_guess)
except Exception as e:
logger.warning("Error guessing OS from OSABI: %s", e)
osabi_guess = None
try:
ph_notes_guess = guess_os_from_ph_notes(elf)
logger.debug("guess: ph notes: %s", ph_notes_guess)
except Exception as e:
logger.warning("Error guessing OS from program header notes: %s", e)
ph_notes_guess = None
try:
sh_notes_guess = guess_os_from_sh_notes(elf)
logger.debug("guess: sh notes: %s", sh_notes_guess)
except Exception as e:
logger.warning("Error guessing OS from section header notes: %s", e)
sh_notes_guess = None
try:
linker_guess = guess_os_from_linker(elf)
logger.debug("guess: linker: %s", linker_guess)
except Exception as e:
logger.warning("Error guessing OS from linker: %s", e)
linker_guess = None
try:
abi_versions_needed_guess = guess_os_from_abi_versions_needed(elf)
logger.debug("guess: ABI versions needed: %s", abi_versions_needed_guess)
except Exception as e:
logger.warning("Error guessing OS from ABI versions needed: %s", e)
abi_versions_needed_guess = None
try:
needed_dependencies_guess = guess_os_from_needed_dependencies(elf)
logger.debug("guess: needed dependencies: %s", needed_dependencies_guess)
except Exception as e:
logger.warning("Error guessing OS from needed dependencies: %s", e)
needed_dependencies_guess = None
try:
symtab_guess = guess_os_from_symtab(elf)
logger.debug("guess: pertinent symbol name: %s", symtab_guess)
except Exception as e:
logger.warning("Error guessing OS from symbol table: %s", e)
symtab_guess = None
ret = None
if osabi_guess:
ret = osabi_guess
elif ph_notes_guess:
ret = ph_notes_guess
elif sh_notes_guess:
ret = sh_notes_guess
elif linker_guess:
ret = linker_guess
elif abi_versions_needed_guess:
ret = abi_versions_needed_guess
elif needed_dependencies_guess:
ret = needed_dependencies_guess
elif symtab_guess:
ret = symtab_guess
return ret.value if ret is not None else "unknown"
def detect_elf_arch(f: BinaryIO) -> str:
return ELF(f).e_machine or "unknown"

View File

@@ -1,203 +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 io
import logging
from typing import Tuple, Iterator
from pathlib import Path
from elftools.elf.elffile import ELFFile, SymbolTableSection
from elftools.elf.relocation import RelocationSection
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
logger = logging.getLogger(__name__)
def extract_file_export_names(elf: ELFFile, **kwargs):
for section in elf.iter_sections():
if not isinstance(section, SymbolTableSection):
continue
if section["sh_entsize"] == 0:
logger.debug("Symbol table '%s' has a sh_entsize of zero!", section.name)
continue
logger.debug("Symbol table '%s' contains %s entries:", section.name, section.num_symbols())
for symbol in section.iter_symbols():
# The following conditions are based on the following article
# http://www.m4b.io/elf/export/binary/analysis/2015/05/25/what-is-an-elf-export.html
if not symbol.name:
continue
if symbol.entry.st_info.type not in ["STT_FUNC", "STT_OBJECT", "STT_IFUNC"]:
continue
if symbol.entry.st_value == 0:
continue
if symbol.entry.st_shndx == "SHN_UNDEF":
continue
yield Export(symbol.name), AbsoluteVirtualAddress(symbol.entry.st_value)
def extract_file_import_names(elf: ELFFile, **kwargs):
# Create a dictionary to store symbol names by their index
symbol_names = {}
# Extract symbol names and store them in the dictionary
for section in elf.iter_sections():
if not isinstance(section, SymbolTableSection):
continue
for _, symbol in enumerate(section.iter_symbols()):
# The following conditions are based on the following article
# http://www.m4b.io/elf/export/binary/analysis/2015/05/25/what-is-an-elf-export.html
if not symbol.name:
continue
if symbol.entry.st_info.type not in ["STT_FUNC", "STT_OBJECT", "STT_IFUNC"]:
continue
if symbol.entry.st_value != 0:
continue
if symbol.entry.st_shndx != "SHN_UNDEF":
continue
if symbol.entry.st_name == 0:
continue
symbol_names[_] = symbol.name
for section in elf.iter_sections():
if not isinstance(section, RelocationSection):
continue
if section["sh_entsize"] == 0:
logger.debug("Symbol table '%s' has a sh_entsize of zero!", section.name)
continue
logger.debug("Symbol table '%s' contains %s entries:", section.name, section.num_relocations())
for relocation in section.iter_relocations():
# Extract the symbol name from the symbol table using the symbol index in the relocation
if relocation["r_info_sym"] not in symbol_names:
continue
yield Import(symbol_names[relocation["r_info_sym"]]), FileOffsetAddress(relocation["r_offset"])
def extract_file_section_names(elf: ELFFile, **kwargs):
for section in elf.iter_sections():
if section.name:
yield Section(section.name), AbsoluteVirtualAddress(section.header.sh_addr)
elif section.is_null():
yield Section("NULL"), AbsoluteVirtualAddress(section.header.sh_addr)
def extract_file_strings(buf, **kwargs):
yield from capa.features.extractors.common.extract_file_strings(buf)
def extract_file_os(elf: ELFFile, buf, **kwargs):
# our current approach does not always get an OS value, e.g. for packed samples
# for file limitation purposes, we're more lax here
try:
os_tuple = next(capa.features.extractors.common.extract_os(buf))
yield os_tuple
except StopIteration:
yield OS("unknown"), NO_ADDRESS
def extract_file_format(**kwargs):
yield Format(FORMAT_ELF), NO_ADDRESS
def extract_file_arch(elf: ELFFile, **kwargs):
arch = elf.get_machine_arch()
if arch == "x86":
yield Arch("i386"), NO_ADDRESS
elif arch == "x64":
yield Arch("amd64"), NO_ADDRESS
else:
logger.warning("unsupported architecture: %s", arch)
def extract_file_features(elf: ELFFile, buf: bytes) -> Iterator[Tuple[Feature, int]]:
for file_handler in FILE_HANDLERS:
for feature, addr in file_handler(elf=elf, buf=buf): # type: ignore
yield feature, addr
FILE_HANDLERS = (
extract_file_export_names,
extract_file_import_names,
extract_file_section_names,
extract_file_strings,
# no library matching
extract_file_format,
)
def extract_global_features(elf: ELFFile, buf: bytes) -> Iterator[Tuple[Feature, int]]:
for global_handler in GLOBAL_HANDLERS:
for feature, addr in global_handler(elf=elf, buf=buf): # type: ignore
yield feature, addr
GLOBAL_HANDLERS = (
extract_file_os,
extract_file_arch,
)
class ElfFeatureExtractor(FeatureExtractor):
def __init__(self, path: Path):
super().__init__()
self.path: Path = path
self.elf = ELFFile(io.BytesIO(path.read_bytes()))
def get_base_address(self):
# virtual address of the first segment with type LOAD
for segment in self.elf.iter_segments():
if segment.header.p_type == "PT_LOAD":
return AbsoluteVirtualAddress(segment.header.p_vaddr)
def extract_global_features(self):
buf = self.path.read_bytes()
for feature, addr in extract_global_features(self.elf, buf):
yield feature, addr
def extract_file_features(self):
buf = self.path.read_bytes()
for feature, addr in extract_file_features(self.elf, buf):
yield feature, addr
def get_functions(self):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
def extract_function_features(self, f):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
def get_basic_blocks(self, f):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
def extract_basic_block_features(self, f, bb):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
def get_instructions(self, f, bb):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
def extract_insn_features(self, f, bb, insn):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
def is_library_function(self, addr):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
def get_function_name(self, addr):
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,18 +6,23 @@
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 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. # See the License for the specific language governing permissions and limitations under the License.
import struct import sys
import builtins import builtins
from typing import Tuple, Iterator
from capa.features.file import Import
from capa.features.insn import API
MIN_STACKSTRING_LEN = 8 MIN_STACKSTRING_LEN = 8
def xor_static(data: bytes, i: int) -> bytes: def xor_static(data, i):
return bytes(c ^ i for c in data) if sys.version_info >= (3, 0):
return bytes(c ^ i for c in data)
else:
return "".join(chr(ord(c) ^ i) for c in data)
def is_aw_function(symbol: str) -> bool: def is_aw_function(symbol):
""" """
is the given function name an A/W function? is the given function name an A/W function?
these are variants of functions that, on Windows, accept either a narrow or wide string. these are variants of functions that, on Windows, accept either a narrow or wide string.
@@ -29,10 +34,11 @@ def is_aw_function(symbol: str) -> bool:
if symbol[-1] not in ("A", "W"): if symbol[-1] not in ("A", "W"):
return False return False
return True # second to last character should be lowercase letter
return "a" <= symbol[-2] <= "z" or "0" <= symbol[-2] <= "9"
def is_ordinal(symbol: str) -> bool: def is_ordinal(symbol):
""" """
is the given symbol an ordinal that is prefixed by "#"? is the given symbol an ordinal that is prefixed by "#"?
""" """
@@ -41,7 +47,7 @@ def is_ordinal(symbol: str) -> bool:
return False return False
def generate_symbols(dll: str, symbol: str) -> Iterator[str]: def generate_symbols(dll, symbol):
""" """
for a given dll and symbol name, generate variants. for a given dll and symbol name, generate variants.
we over-generate features to make matching easier. we over-generate features to make matching easier.
@@ -51,11 +57,8 @@ def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
- CreateFileA - CreateFileA
- CreateFile - CreateFile
""" """
# normalize dll name
dll = dll.lower()
# kernel32.CreateFileA # kernel32.CreateFileA
yield f"{dll}.{symbol}" yield "%s.%s" % (dll, symbol)
if not is_ordinal(symbol): if not is_ordinal(symbol):
# CreateFileA # CreateFileA
@@ -63,35 +66,18 @@ def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
if is_aw_function(symbol): if is_aw_function(symbol):
# kernel32.CreateFile # kernel32.CreateFile
yield f"{dll}.{symbol[:-1]}" yield "%s.%s" % (dll, symbol[:-1])
if not is_ordinal(symbol): if not is_ordinal(symbol):
# CreateFile # CreateFile
yield symbol[:-1] yield symbol[:-1]
def reformat_forwarded_export_name(forwarded_name: str) -> str: def all_zeros(bytez):
"""
a forwarded export has a DLL name/path an symbol name.
we want the former to be lowercase, and the latter to be verbatim.
"""
# use rpartition so we can split on separator between dll and name.
# the dll name can be a full path, like in the case of
# ef64d6d7c34250af8e21a10feb931c9b
# which i assume means the path can have embedded periods.
# so we don't want the first period, we want the last.
forwarded_dll, _, forwarded_symbol = forwarded_name.rpartition(".")
forwarded_dll = forwarded_dll.lower()
return f"{forwarded_dll}.{forwarded_symbol}"
def all_zeros(bytez: bytes) -> bool:
return all(b == 0 for b in builtins.bytes(bytez)) return all(b == 0 for b in builtins.bytes(bytez))
def twos_complement(val: int, bits: int) -> int: def twos_complement(val, bits):
""" """
compute the 2's complement of int value val compute the 2's complement of int value val
@@ -104,48 +90,3 @@ def twos_complement(val: int, bits: int) -> int:
else: else:
# return positive value as is # return positive value as is
return val return val
def carve_pe(pbytes: bytes, offset: int = 0) -> Iterator[Tuple[int, int]]:
"""
Generate (offset, key) tuples of embedded PEs
Based on the version from vivisect:
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
And its IDA adaptation:
capa/features/extractors/ida/file.py
"""
mz_xor = [
(
xor_static(b"MZ", key),
xor_static(b"PE", key),
key,
)
for key in range(256)
]
pblen = len(pbytes)
todo = [(pbytes.find(mzx, offset), mzx, pex, key) for mzx, pex, key in mz_xor]
todo = [(off, mzx, pex, key) for (off, mzx, pex, key) in todo if off != -1]
while len(todo):
off, mzx, pex, key = todo.pop()
# The MZ header has one field we will check
# e_lfanew is at 0x3c
e_lfanew = off + 0x3C
if pblen < (e_lfanew + 4):
continue
newoff = struct.unpack("<I", xor_static(pbytes[e_lfanew : e_lfanew + 4], key))[0]
nextres = pbytes.find(mzx, off + 1)
if nextres != -1:
todo.append((nextres, mzx, pex, key))
peoff = off + newoff
if pblen < (peoff + 2):
continue
if pbytes[peoff : peoff + 2] == pex:
yield (off, key)

View File

@@ -0,0 +1,93 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import sys
import types
import idaapi
import capa.features.extractors.ida.file
import capa.features.extractors.ida.insn
import capa.features.extractors.ida.function
import capa.features.extractors.ida.basicblock
from capa.features.extractors import FeatureExtractor
def get_ea(self):
""" """
if isinstance(self, (idaapi.BasicBlock, idaapi.func_t)):
return self.start_ea
if isinstance(self, idaapi.insn_t):
return self.ea
raise TypeError
def add_ea_int_cast(o):
"""
dynamically add a cast-to-int (`__int__`) method to the given object
that returns the value of the `.ea` property.
this bit of skullduggery lets use cast viv-utils objects as ints.
the correct way of doing this is to update viv-utils (or subclass the objects here).
"""
if sys.version_info[0] >= 3:
setattr(o, "__int__", types.MethodType(get_ea, o))
else:
setattr(o, "__int__", types.MethodType(get_ea, o, type(o)))
return o
class IdaFeatureExtractor(FeatureExtractor):
def __init__(self):
super(IdaFeatureExtractor, self).__init__()
def get_base_address(self):
return idaapi.get_imagebase()
def extract_file_features(self):
for (feature, ea) in capa.features.extractors.ida.file.extract_features():
yield feature, ea
def get_functions(self):
import capa.features.extractors.ida.helpers as ida_helpers
# data structure shared across functions yielded here.
# useful for caching analysis relevant across a single workspace.
ctx = {}
# ignore library functions and thunk functions as identified by IDA
for f in ida_helpers.get_functions(skip_thunks=True, skip_libs=True):
setattr(f, "ctx", ctx)
yield add_ea_int_cast(f)
@staticmethod
def get_function(ea):
f = idaapi.get_func(ea)
setattr(f, "ctx", {})
return add_ea_int_cast(f)
def extract_function_features(self, f):
for (feature, ea) in capa.features.extractors.ida.function.extract_features(f):
yield feature, ea
def get_basic_blocks(self, f):
for bb in capa.features.extractors.ida.helpers.get_function_blocks(f):
yield add_ea_int_cast(bb)
def extract_basic_block_features(self, f, bb):
for (feature, ea) in capa.features.extractors.ida.basicblock.extract_features(f, bb):
yield feature, ea
def get_instructions(self, f, bb):
import capa.features.extractors.ida.helpers as ida_helpers
for insn in ida_helpers.get_instructions_in_range(bb.start_ea, bb.end_ea):
yield add_ea_int_cast(insn)
def extract_insn_features(self, f, bb, insn):
for (feature, ea) in capa.features.extractors.ida.insn.extract_features(f, bb, insn):
yield feature, ea

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,23 +6,25 @@
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 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. # See the License for the specific language governing permissions and limitations under the License.
import sys
import string import string
import struct import struct
from typing import Tuple, Iterator
import idaapi import idaapi
import capa.features.extractors.ida.helpers import capa.features.extractors.ida.helpers
from capa.features.common import Feature, Characteristic from capa.features import Characteristic
from capa.features.address import Address
from capa.features.basicblock import BasicBlock from capa.features.basicblock import BasicBlock
from capa.features.extractors.ida import helpers from capa.features.extractors.ida import helpers
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
def get_printable_len(op: idaapi.op_t) -> int: def get_printable_len(op):
"""Return string length if all operand bytes are ascii or utf16-le printable""" """Return string length if all operand bytes are ascii or utf16-le printable
args:
op (IDA op_t)
"""
op_val = capa.features.extractors.ida.helpers.mask_op_val(op) op_val = capa.features.extractors.ida.helpers.mask_op_val(op)
if op.dtype == idaapi.dt_byte: if op.dtype == idaapi.dt_byte:
@@ -34,14 +36,21 @@ def get_printable_len(op: idaapi.op_t) -> int:
elif op.dtype == idaapi.dt_qword: elif op.dtype == idaapi.dt_qword:
chars = struct.pack("<Q", op_val) chars = struct.pack("<Q", op_val)
else: else:
raise ValueError(f"Unhandled operand data type 0x{op.dtype:x}.") raise ValueError("Unhandled operand data type 0x%x." % op.dtype)
def is_printable_ascii(chars_: bytes): def is_printable_ascii(chars):
return all(c < 127 and chr(c) in string.printable for c in chars_) if sys.version_info[0] >= 3:
return all(c < 127 and chr(c) in string.printable for c in chars)
else:
return all(ord(c) < 127 and c in string.printable for c in chars)
def is_printable_utf16le(chars_: bytes): def is_printable_utf16le(chars):
if all(c == 0x00 for c in chars_[1::2]): if sys.version_info[0] >= 3:
return is_printable_ascii(chars_[::2]) if all(c == 0x00 for c in chars[1::2]):
return is_printable_ascii(chars[::2])
else:
if all(c == "\x00" for c in chars[1::2]):
return is_printable_ascii(chars[::2])
if is_printable_ascii(chars): if is_printable_ascii(chars):
return idaapi.get_dtype_size(op.dtype) return idaapi.get_dtype_size(op.dtype)
@@ -52,8 +61,12 @@ def get_printable_len(op: idaapi.op_t) -> int:
return 0 return 0
def is_mov_imm_to_stack(insn: idaapi.insn_t) -> bool: def is_mov_imm_to_stack(insn):
"""verify instruction moves immediate onto stack""" """verify instruction moves immediate onto stack
args:
insn (IDA insn_t)
"""
if insn.Op2.type != idaapi.o_imm: if insn.Op2.type != idaapi.o_imm:
return False return False
@@ -66,10 +79,14 @@ def is_mov_imm_to_stack(insn: idaapi.insn_t) -> bool:
return True return True
def bb_contains_stackstring(f: idaapi.func_t, bb: idaapi.BasicBlock) -> bool: def bb_contains_stackstring(f, bb):
"""check basic block for stackstring indicators """check basic block for stackstring indicators
true if basic block contains enough moves of constant bytes to the stack true if basic block contains enough moves of constant bytes to the stack
args:
f (IDA func_t)
bb (IDA BasicBlock)
""" """
count = 0 count = 0
for insn in capa.features.extractors.ida.helpers.get_instructions_in_range(bb.start_ea, bb.end_ea): for insn in capa.features.extractors.ida.helpers.get_instructions_in_range(bb.start_ea, bb.end_ea):
@@ -80,27 +97,57 @@ def bb_contains_stackstring(f: idaapi.func_t, bb: idaapi.BasicBlock) -> bool:
return False return False
def extract_bb_stackstring(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]: def extract_bb_stackstring(f, bb):
"""extract stackstring indicators from basic block""" """extract stackstring indicators from basic block
if bb_contains_stackstring(fh.inner, bbh.inner):
yield Characteristic("stack string"), bbh.address args:
f (IDA func_t)
bb (IDA BasicBlock)
"""
if bb_contains_stackstring(f, bb):
yield Characteristic("stack string"), bb.start_ea
def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]: def extract_bb_tight_loop(f, bb):
"""extract tight loop indicators from a basic block""" """extract tight loop indicators from a basic block
if capa.features.extractors.ida.helpers.is_basic_block_tight_loop(bbh.inner):
yield Characteristic("tight loop"), bbh.address args:
f (IDA func_t)
bb (IDA BasicBlock)
"""
if capa.features.extractors.ida.helpers.is_basic_block_tight_loop(bb):
yield Characteristic("tight loop"), bb.start_ea
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]: def extract_features(f, bb):
"""extract basic block features""" """extract basic block features
args:
f (IDA func_t)
bb (IDA BasicBlock)
"""
for bb_handler in BASIC_BLOCK_HANDLERS: for bb_handler in BASIC_BLOCK_HANDLERS:
for feature, addr in bb_handler(fh, bbh): for (feature, ea) in bb_handler(f, bb):
yield feature, addr yield feature, ea
yield BasicBlock(), bbh.address yield BasicBlock(), bb.start_ea
BASIC_BLOCK_HANDLERS = ( BASIC_BLOCK_HANDLERS = (
extract_bb_tight_loop, extract_bb_tight_loop,
extract_bb_stackstring, extract_bb_stackstring,
) )
def main():
features = []
for f in helpers.get_functions(skip_thunks=True, skip_libs=True):
for bb in idaapi.FlowChart(f, flags=idaapi.FC_PREDS):
features.extend(list(extract_features(f, bb)))
import pprint
pprint.pprint(features)
if __name__ == "__main__":
main()

View File

@@ -1,71 +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.
from typing import List, Tuple, Iterator
import idaapi
import capa.ida.helpers
import capa.features.extractors.elf
import capa.features.extractors.ida.file
import capa.features.extractors.ida.insn
import capa.features.extractors.ida.global_
import capa.features.extractors.ida.function
import capa.features.extractors.ida.basicblock
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
class IdaFeatureExtractor(FeatureExtractor):
def __init__(self):
super().__init__()
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.ida.file.extract_file_format())
self.global_features.extend(capa.features.extractors.ida.global_.extract_os())
self.global_features.extend(capa.features.extractors.ida.global_.extract_arch())
def get_base_address(self):
return AbsoluteVirtualAddress(idaapi.get_imagebase())
def extract_global_features(self):
yield from self.global_features
def extract_file_features(self):
yield from capa.features.extractors.ida.file.extract_features()
def get_functions(self) -> Iterator[FunctionHandle]:
import capa.features.extractors.ida.helpers as ida_helpers
# ignore library functions and thunk functions as identified by IDA
yield from ida_helpers.get_functions(skip_thunks=True, skip_libs=True)
@staticmethod
def get_function(ea: int) -> FunctionHandle:
f = idaapi.get_func(ea)
return FunctionHandle(address=AbsoluteVirtualAddress(f.start_ea), inner=f)
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.ida.function.extract_features(fh)
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
import capa.features.extractors.ida.helpers as ida_helpers
for bb in ida_helpers.get_function_blocks(fh.inner):
yield BBHandle(address=AbsoluteVirtualAddress(bb.start_ea), inner=bb)
def extract_basic_block_features(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.ida.basicblock.extract_features(fh, bbh)
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
import capa.features.extractors.ida.helpers as ida_helpers
for insn in ida_helpers.get_instructions_in_range(bbh.inner.start_ea, bbh.inner.end_ea):
yield InsnHandle(address=AbsoluteVirtualAddress(insn.ea), inner=insn)
def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle):
yield from capa.features.extractors.ida.insn.extract_features(fh, bbh, ih)

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -7,29 +7,26 @@
# See the License for the specific language governing permissions and limitations under the License. # See the License for the specific language governing permissions and limitations under the License.
import struct import struct
from typing import Tuple, Iterator
import idc import idc
import idaapi import idaapi
import idautils import idautils
import ida_entry
import capa.features.extractors.common
import capa.features.extractors.helpers import capa.features.extractors.helpers
import capa.features.extractors.strings import capa.features.extractors.strings
import capa.features.extractors.ida.helpers import capa.features.extractors.ida.helpers
from capa.features.file import Export, Import, Section, FunctionName from capa.features import String, Characteristic
from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic from capa.features.file import Export, Import, Section
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress
MAX_OFFSET_PE_AFTER_MZ = 0x200
def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]: def check_segment_for_pe(seg):
"""check segment for embedded PE """check segment for embedded PE
adapted for IDA from: adapted for IDA from:
https://github.com/vivisect/vivisect/blob/91e8419a861f49779f18316f155311967e696836/PE/carve.py#L25 https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
args:
seg (IDA segment_t)
""" """
seg_max = seg.end_ea seg_max = seg.end_ea
mz_xor = [ mz_xor = [
@@ -42,15 +39,14 @@ def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]:
] ]
todo = [] todo = []
for mzx, pex, i in mz_xor: for (mzx, pex, i) in mz_xor:
# find all segment offsets containing XOR'd "MZ" bytes
for off in capa.features.extractors.ida.helpers.find_byte_sequence(seg.start_ea, seg.end_ea, mzx): for off in capa.features.extractors.ida.helpers.find_byte_sequence(seg.start_ea, seg.end_ea, mzx):
todo.append((off, mzx, pex, i)) todo.append((off, mzx, pex, i))
while len(todo): while len(todo):
off, mzx, pex, i = todo.pop() off, mzx, pex, i = todo.pop()
# MZ header has one field we will check e_lfanew is at 0x3c # The MZ header has one field we will check e_lfanew is at 0x3c
e_lfanew = off + 0x3C e_lfanew = off + 0x3C
if seg_max < (e_lfanew + 4): if seg_max < (e_lfanew + 4):
@@ -58,19 +54,18 @@ def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]:
newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(idc.get_bytes(e_lfanew, 4), i))[0] newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(idc.get_bytes(e_lfanew, 4), i))[0]
# assume XOR'd "PE" bytes exist within threshold
if newoff > MAX_OFFSET_PE_AFTER_MZ:
continue
peoff = off + newoff peoff = off + newoff
if seg_max < (peoff + 2): if seg_max < (peoff + 2):
continue continue
if idc.get_bytes(peoff, 2) == pex: if idc.get_bytes(peoff, 2) == pex:
yield off, i yield (off, i)
for nextres in capa.features.extractors.ida.helpers.find_byte_sequence(off + 1, seg.end_ea, mzx):
todo.append((nextres, mzx, pex, i))
def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]: def extract_file_embedded_pe():
"""extract embedded PE features """extract embedded PE features
IDA must load resource sections for this to be complete IDA must load resource sections for this to be complete
@@ -78,23 +73,17 @@ def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]:
- Check 'Load resource sections' when opening binary in IDA manually - Check 'Load resource sections' when opening binary in IDA manually
""" """
for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True): for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True):
for ea, _ in check_segment_for_pe(seg): for (ea, _) in check_segment_for_pe(seg):
yield Characteristic("embedded pe"), FileOffsetAddress(ea) yield Characteristic("embedded pe"), ea
def extract_file_export_names() -> Iterator[Tuple[Feature, Address]]: def extract_file_export_names():
"""extract function exports""" """ extract function exports """
for _, ordinal, ea, name in idautils.Entries(): for (_, _, ea, name) in idautils.Entries():
forwarded_name = ida_entry.get_entry_forwarder(ordinal) yield Export(name), ea
if forwarded_name is None:
yield Export(name), AbsoluteVirtualAddress(ea)
else:
forwarded_name = capa.features.extractors.helpers.reformat_forwarded_export_name(forwarded_name)
yield Export(forwarded_name), AbsoluteVirtualAddress(ea)
yield Characteristic("forwarded export"), AbsoluteVirtualAddress(ea)
def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]: def extract_file_import_names():
"""extract function imports """extract function imports
1. imports by ordinal: 1. imports by ordinal:
@@ -105,32 +94,28 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
- modulename.importname - modulename.importname
- importname - importname
""" """
for ea, info in capa.features.extractors.ida.helpers.get_file_imports().items(): for (ea, info) in capa.features.extractors.ida.helpers.get_file_imports().items():
addr = AbsoluteVirtualAddress(ea)
if info[1] and info[2]: if info[1] and info[2]:
# e.g. in mimikatz: ('cabinet', 'FCIAddFile', 11L) # e.g. in mimikatz: ('cabinet', 'FCIAddFile', 11L)
# extract by name here and by ordinal below # 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]):
yield Import(name), addr yield Import(name), ea
dll = info[0] dll = info[0]
symbol = f"#{info[2]}" symbol = "#%d" % (info[2])
elif info[1]: elif info[1]:
dll = info[0] dll = info[0]
symbol = info[1] symbol = info[1]
elif info[2]: elif info[2]:
dll = info[0] dll = info[0]
symbol = f"#{info[2]}" symbol = "#%d" % (info[2])
else: else:
continue continue
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol): for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield Import(name), addr yield Import(name), ea
for ea, info in capa.features.extractors.ida.helpers.get_file_externs().items():
yield Import(info[1]), AbsoluteVirtualAddress(ea)
def extract_file_section_names() -> Iterator[Tuple[Feature, Address]]: def extract_file_section_names():
"""extract section names """extract section names
IDA must load resource sections for this to be complete IDA must load resource sections for this to be complete
@@ -138,10 +123,10 @@ def extract_file_section_names() -> Iterator[Tuple[Feature, Address]]:
- Check 'Load resource sections' when opening binary in IDA manually - Check 'Load resource sections' when opening binary in IDA manually
""" """
for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True): for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True):
yield Section(idaapi.get_segm_name(seg)), AbsoluteVirtualAddress(seg.start_ea) yield Section(idaapi.get_segm_name(seg)), seg.start_ea
def extract_file_strings() -> Iterator[Tuple[Feature, Address]]: def extract_file_strings():
"""extract ASCII and UTF-16 LE strings """extract ASCII and UTF-16 LE strings
IDA must load resource sections for this to be complete IDA must load resource sections for this to be complete
@@ -151,50 +136,18 @@ def extract_file_strings() -> Iterator[Tuple[Feature, Address]]:
for seg in capa.features.extractors.ida.helpers.get_segments(): for seg in capa.features.extractors.ida.helpers.get_segments():
seg_buff = capa.features.extractors.ida.helpers.get_segment_buffer(seg) seg_buff = capa.features.extractors.ida.helpers.get_segment_buffer(seg)
# differing to common string extractor factor in segment offset here
for s in capa.features.extractors.strings.extract_ascii_strings(seg_buff): for s in capa.features.extractors.strings.extract_ascii_strings(seg_buff):
yield String(s.s), FileOffsetAddress(seg.start_ea + s.offset) yield String(s.s), (seg.start_ea + s.offset)
for s in capa.features.extractors.strings.extract_unicode_strings(seg_buff): for s in capa.features.extractors.strings.extract_unicode_strings(seg_buff):
yield String(s.s), FileOffsetAddress(seg.start_ea + s.offset) yield String(s.s), (seg.start_ea + s.offset)
def extract_file_function_names() -> Iterator[Tuple[Feature, Address]]: def extract_features():
""" """ extract file features """
extract the names of statically-linked library functions.
"""
for ea in idautils.Functions():
addr = AbsoluteVirtualAddress(ea)
if idaapi.get_func(ea).flags & idaapi.FUNC_LIB:
name = idaapi.get_name(ea)
yield FunctionName(name), addr
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions.
# extract features for both the mangled and un-mangled representations.
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield FunctionName(name[1:]), addr
def extract_file_format() -> Iterator[Tuple[Feature, Address]]:
file_info = idaapi.get_inf_structure()
if file_info.filetype in (idaapi.f_PE, idaapi.f_COFF):
yield Format(FORMAT_PE), NO_ADDRESS
elif file_info.filetype == idaapi.f_ELF:
yield Format(FORMAT_ELF), NO_ADDRESS
elif file_info.filetype == idaapi.f_BIN:
# no file type to return when processing a binary file, but we want to continue processing
return
else:
raise NotImplementedError(f"unexpected file format: {file_info.filetype}")
def extract_features() -> Iterator[Tuple[Feature, Address]]:
"""extract file features"""
for file_handler in FILE_HANDLERS: for file_handler in FILE_HANDLERS:
for feature, addr in file_handler(): for feature, va in file_handler():
yield feature, addr yield feature, va
FILE_HANDLERS = ( FILE_HANDLERS = (
@@ -203,6 +156,15 @@ FILE_HANDLERS = (
extract_file_strings, extract_file_strings,
extract_file_section_names, extract_file_section_names,
extract_file_embedded_pe, extract_file_embedded_pe,
extract_file_function_names,
extract_file_format,
) )
def main():
""" """
import pprint
pprint.pprint(list(extract_features()))
if __name__ == "__main__":
main()

View File

@@ -1,31 +1,35 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
from typing import Tuple, Iterator
import idaapi import idaapi
import idautils import idautils
import capa.features.extractors.ida.helpers import capa.features.extractors.ida.helpers
from capa.features.common import Feature, Characteristic from capa.features import Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors import loops from capa.features.extractors import loops
from capa.features.extractors.base_extractor import FunctionHandle
def extract_function_calls_to(fh: FunctionHandle): def extract_function_calls_to(f):
"""extract callers to a function""" """extract callers to a function
for ea in idautils.CodeRefsTo(fh.inner.start_ea, True):
yield Characteristic("calls to"), AbsoluteVirtualAddress(ea) args:
f (IDA func_t)
"""
for ea in idautils.CodeRefsTo(f.start_ea, True):
yield Characteristic("calls to"), ea
def extract_function_loop(fh: FunctionHandle): def extract_function_loop(f):
"""extract loop indicators from a function""" """extract loop indicators from a function
f: idaapi.func_t = fh.inner
args:
f (IDA func_t)
"""
edges = [] edges = []
# construct control flow graph # construct control flow graph
@@ -34,19 +38,43 @@ def extract_function_loop(fh: FunctionHandle):
edges.append((bb.start_ea, succ.start_ea)) edges.append((bb.start_ea, succ.start_ea))
if loops.has_loop(edges): if loops.has_loop(edges):
yield Characteristic("loop"), fh.address yield Characteristic("loop"), f.start_ea
def extract_recursive_call(fh: FunctionHandle): def extract_recursive_call(f):
"""extract recursive function call""" """extract recursive function call
if capa.features.extractors.ida.helpers.is_function_recursive(fh.inner):
yield Characteristic("recursive call"), fh.address args:
f (IDA func_t)
"""
if capa.features.extractors.ida.helpers.is_function_recursive(f):
yield Characteristic("recursive call"), f.start_ea
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]: def extract_features(f):
"""extract function features
arg:
f (IDA func_t)
"""
for func_handler in FUNCTION_HANDLERS: for func_handler in FUNCTION_HANDLERS:
for feature, addr in func_handler(fh): for (feature, ea) in func_handler(f):
yield feature, addr yield feature, ea
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call) FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call)
def main():
""" """
features = []
for f in capa.features.extractors.ida.get_functions(skip_thunks=True, skip_libs=True):
features.extend(list(extract_features(f)))
import pprint
pprint.pprint(features)
if __name__ == "__main__":
main()

View File

@@ -1,65 +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
import contextlib
from typing import Tuple, Iterator
import idaapi
import ida_loader
import capa.ida.helpers
import capa.features.extractors.elf
from capa.features.common import OS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature
from capa.features.address import NO_ADDRESS, Address
logger = logging.getLogger(__name__)
def extract_os() -> Iterator[Tuple[Feature, Address]]:
format_name: str = ida_loader.get_file_type_name()
if "PE" in format_name:
yield OS(OS_WINDOWS), NO_ADDRESS
elif "ELF" in format_name:
with contextlib.closing(capa.ida.helpers.IDAIO()) as f:
os = capa.features.extractors.elf.detect_elf_os(f)
yield OS(os), NO_ADDRESS
else:
# we likely end up here:
# 1. handling shellcode, or
# 2. handling a new file format (e.g. macho)
#
# for (1) we can't do much - its shellcode and all bets are off.
# we could maybe accept a further CLI argument to specify the OS,
# but i think this would be rarely used.
# rules that rely on OS conditions will fail to match on shellcode.
#
# for (2), this logic will need to be updated as the format is implemented.
logger.debug("unsupported file format: %s, will not guess OS", format_name)
return
def extract_arch() -> Iterator[Tuple[Feature, Address]]:
info: idaapi.idainfo = idaapi.get_inf_structure()
if info.procname == "metapc" and info.is_64bit():
yield Arch(ARCH_AMD64), NO_ADDRESS
elif info.procname == "metapc" and info.is_32bit():
yield Arch(ARCH_I386), NO_ADDRESS
elif info.procname == "metapc":
logger.debug("unsupported architecture: non-32-bit nor non-64-bit intel")
return
else:
# we likely end up here:
# 1. handling a new architecture (e.g. aarch64)
#
# for (1), this logic will need to be updated as the format is implemented.
logger.debug("unsupported architecture: %s", info.procname)
return

View File

@@ -1,24 +1,21 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
import functools
from typing import Any, Dict, Tuple, Iterator, Optional import sys
import string
import idc import idc
import idaapi import idaapi
import idautils import idautils
import ida_bytes import ida_bytes
import ida_segment
from capa.features.address import AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import FunctionHandle
def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]: def find_byte_sequence(start, end, seq):
"""yield all ea of a given byte sequence """yield all ea of a given byte sequence
args: args:
@@ -26,33 +23,36 @@ def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
end: max virtual address end: max virtual address
seq: bytes to search e.g. b"\x01\x03" seq: bytes to search e.g. b"\x01\x03"
""" """
seqstr = " ".join([f"{b:02x}" for b in seq]) if sys.version_info[0] >= 3:
seq = " ".join(["%02x" % b for b in seq])
else:
seq = " ".join(["%02x" % ord(b) for b in seq])
while True: while True:
# TODO(mike-hunhoff): find_binary is deprecated. Please use ida_bytes.bin_search() instead. ea = idaapi.find_binary(start, end, seq, 0, idaapi.SEARCH_DOWN)
# https://github.com/mandiant/capa/issues/1606
ea = idaapi.find_binary(start, end, seqstr, 0, idaapi.SEARCH_DOWN)
if ea == idaapi.BADADDR: if ea == idaapi.BADADDR:
break break
start = ea + 1 start = ea + 1
yield ea yield ea
def get_functions( def get_functions(start=None, end=None, skip_thunks=False, skip_libs=False):
start: Optional[int] = None, end: Optional[int] = None, skip_thunks: bool = False, skip_libs: bool = False
) -> Iterator[FunctionHandle]:
"""get functions, range optional """get functions, range optional
args: args:
start: min virtual address start: min virtual address
end: max virtual address end: max virtual address
ret:
yield func_t*
""" """
for ea in idautils.Functions(start=start, end=end): for ea in idautils.Functions(start=start, end=end):
f = idaapi.get_func(ea) f = idaapi.get_func(ea)
if not (skip_thunks and (f.flags & idaapi.FUNC_THUNK) or skip_libs and (f.flags & idaapi.FUNC_LIB)): if not (skip_thunks and (f.flags & idaapi.FUNC_THUNK) or skip_libs and (f.flags & idaapi.FUNC_LIB)):
yield FunctionHandle(address=AbsoluteVirtualAddress(ea), inner=f) yield f
def get_segments(skip_header_segments=False) -> Iterator[idaapi.segment_t]: def get_segments(skip_header_segments=False):
"""get list of segments (sections) in the binary image """get list of segments (sections) in the binary image
args: args:
@@ -64,7 +64,7 @@ def get_segments(skip_header_segments=False) -> Iterator[idaapi.segment_t]:
yield seg yield seg
def get_segment_buffer(seg: idaapi.segment_t) -> bytes: def get_segment_buffer(seg):
"""return bytes stored in a given segment """return bytes stored in a given segment
decrease buffer size until IDA is able to read bytes from the segment decrease buffer size until IDA is able to read bytes from the segment
@@ -82,22 +82,9 @@ def get_segment_buffer(seg: idaapi.segment_t) -> bytes:
return buff if buff else b"" return buff if buff else b""
def inspect_import(imports, library, ea, function, ordinal): def get_file_imports():
if function and function.startswith("__imp_"): """ get file imports """
# handle mangled PE imports imports = {}
function = function[len("__imp_") :]
if function and "@@" in function:
# handle mangled ELF imports, like "fopen@@glibc_2.2.5"
function, _, _ = function.partition("@@")
imports[ea] = (library.lower(), function, ordinal)
return True
def get_file_imports() -> Dict[int, Tuple[str, str, int]]:
"""get file imports"""
imports: Dict[int, Tuple[str, str, int]] = {}
for idx in range(idaapi.get_import_module_qty()): for idx in range(idaapi.get_import_module_qty()):
library = idaapi.get_import_module_name(idx) library = idaapi.get_import_module_name(idx)
@@ -105,38 +92,26 @@ def get_file_imports() -> Dict[int, Tuple[str, str, int]]:
if not library: if not library:
continue continue
# IDA uses section names for the library of ELF imports, like ".dynsym". def inspect_import(ea, function, ordinal):
# These are not useful to us, we may need to expand this list over time if function and function.startswith("__imp_"):
# TODO(williballenthin): find all section names used by IDA # handle mangled names starting
# https://github.com/mandiant/capa/issues/1419 function = function[len("__imp_") :]
if library == ".dynsym": imports[ea] = (library.lower(), function, ordinal)
library = "" return True
cb = functools.partial(inspect_import, imports, library) idaapi.enum_import_names(idx, inspect_import)
idaapi.enum_import_names(idx, cb)
return imports return imports
def get_file_externs() -> Dict[int, Tuple[str, str, int]]: def get_instructions_in_range(start, end):
externs = {}
for seg in get_segments(skip_header_segments=True):
if seg.type != ida_segment.SEG_XTRN:
continue
for ea in idautils.Functions(seg.start_ea, seg.end_ea):
externs[ea] = ("", idaapi.get_func_name(ea), -1)
return externs
def get_instructions_in_range(start: int, end: int) -> Iterator[idaapi.insn_t]:
"""yield instructions in range """yield instructions in range
args: args:
start: virtual address (inclusive) start: virtual address (inclusive)
end: virtual address (exclusive) end: virtual address (exclusive)
yield:
(insn_t*)
""" """
for head in idautils.Heads(start, end): for head in idautils.Heads(start, end):
insn = idautils.DecodeInstruction(head) insn = idautils.DecodeInstruction(head)
@@ -144,8 +119,8 @@ def get_instructions_in_range(start: int, end: int) -> Iterator[idaapi.insn_t]:
yield insn yield insn
def is_operand_equal(op1: idaapi.op_t, op2: idaapi.op_t) -> bool: def is_operand_equal(op1, op2):
"""compare two IDA op_t""" """ compare two IDA op_t """
if op1.flags != op2.flags: if op1.flags != op2.flags:
return False return False
@@ -170,8 +145,8 @@ def is_operand_equal(op1: idaapi.op_t, op2: idaapi.op_t) -> bool:
return True return True
def is_basic_block_equal(bb1: idaapi.BasicBlock, bb2: idaapi.BasicBlock) -> bool: def is_basic_block_equal(bb1, bb2):
"""compare two IDA BasicBlock""" """ compare two IDA BasicBlock """
if bb1.start_ea != bb2.start_ea: if bb1.start_ea != bb2.start_ea:
return False return False
@@ -184,12 +159,12 @@ def is_basic_block_equal(bb1: idaapi.BasicBlock, bb2: idaapi.BasicBlock) -> bool
return True return True
def basic_block_size(bb: idaapi.BasicBlock) -> int: def basic_block_size(bb):
"""calculate size of basic block""" """ calculate size of basic block """
return bb.end_ea - bb.start_ea return bb.end_ea - bb.start_ea
def read_bytes_at(ea: int, count: int) -> bytes: def read_bytes_at(ea, count):
""" """ """ """
# check if byte has a value, see get_wide_byte doc # check if byte has a value, see get_wide_byte doc
if not idc.is_loaded(ea): if not idc.is_loaded(ea):
@@ -202,10 +177,10 @@ def read_bytes_at(ea: int, count: int) -> bytes:
return idc.get_bytes(ea, count) return idc.get_bytes(ea, count)
def find_string_at(ea: int, min_: int = 4) -> str: def find_string_at(ea, min=4):
"""check if ASCII string exists at a given virtual address""" """ check if ASCII string exists at a given virtual address """
found = idaapi.get_strlit_contents(ea, -1, idaapi.STRTYPE_C) found = idaapi.get_strlit_contents(ea, -1, idaapi.STRTYPE_C)
if found and len(found) >= min_: if found and len(found) > min:
try: try:
found = found.decode("ascii") found = found.decode("ascii")
# hacky check for IDA bug; get_strlit_contents also reads Unicode as # hacky check for IDA bug; get_strlit_contents also reads Unicode as
@@ -219,7 +194,7 @@ def find_string_at(ea: int, min_: int = 4) -> str:
return "" return ""
def get_op_phrase_info(op: idaapi.op_t) -> Dict: def get_op_phrase_info(op):
"""parse phrase features from operand """parse phrase features from operand
Pretty much dup of sark's implementation: Pretty much dup of sark's implementation:
@@ -229,8 +204,7 @@ def get_op_phrase_info(op: idaapi.op_t) -> Dict:
return {} return {}
scale = 1 << ((op.specflag2 & 0xC0) >> 6) scale = 1 << ((op.specflag2 & 0xC0) >> 6)
# IDA ea_t may be 32- or 64-bit; we assume displacement can only be 32-bit offset = op.addr
offset = op.addr & 0xFFFFFFFF
if op.specflag1 == 0: if op.specflag1 == 0:
index = None index = None
@@ -257,45 +231,47 @@ def get_op_phrase_info(op: idaapi.op_t) -> Dict:
return {"base": base, "index": index, "scale": scale, "offset": offset} return {"base": base, "index": index, "scale": scale, "offset": offset}
def is_op_write(insn: idaapi.insn_t, op: idaapi.op_t) -> bool: def is_op_write(insn, op):
"""Check if an operand is written to (destination operand)""" """ Check if an operand is written to (destination operand) """
return idaapi.has_cf_chg(insn.get_canon_feature(), op.n) return idaapi.has_cf_chg(insn.get_canon_feature(), op.n)
def is_op_read(insn: idaapi.insn_t, op: idaapi.op_t) -> bool: def is_op_read(insn, op):
"""Check if an operand is read from (source operand)""" """ Check if an operand is read from (source operand) """
return idaapi.has_cf_use(insn.get_canon_feature(), op.n) return idaapi.has_cf_use(insn.get_canon_feature(), op.n)
def is_op_offset(insn: idaapi.insn_t, op: idaapi.op_t) -> bool: def is_op_offset(insn, op):
"""Check is an operand has been marked as an offset (by auto-analysis or manually)""" """ Check is an operand has been marked as an offset (by auto-analysis or manually) """
flags = idaapi.get_flags(insn.ea) flags = idaapi.get_flags(insn.ea)
return ida_bytes.is_off(flags, op.n) return ida_bytes.is_off(flags, op.n)
def is_sp_modified(insn: idaapi.insn_t) -> bool: def is_sp_modified(insn):
"""determine if instruction modifies SP, ESP, RSP""" """ determine if instruction modifies SP, ESP, RSP """
return any( for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,)):
op.reg == idautils.procregs.sp.reg and is_op_write(insn, op) if op.reg == idautils.procregs.sp.reg and is_op_write(insn, op):
for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,)) # register is stack and written
) return True
return False
def is_bp_modified(insn: idaapi.insn_t) -> bool: def is_bp_modified(insn):
"""check if instruction modifies BP, EBP, RBP""" """ check if instruction modifies BP, EBP, RBP """
return any( for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,)):
op.reg == idautils.procregs.bp.reg and is_op_write(insn, op) if op.reg == idautils.procregs.bp.reg and is_op_write(insn, op):
for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,)) # register is base and written
) return True
return False
def is_frame_register(reg: int) -> bool: def is_frame_register(reg):
"""check if register is sp or bp""" """ check if register is sp or bp """
return reg in (idautils.procregs.sp.reg, idautils.procregs.bp.reg) return reg in (idautils.procregs.sp.reg, idautils.procregs.bp.reg)
def get_insn_ops(insn: idaapi.insn_t, target_ops: Optional[Tuple[Any]] = None) -> idaapi.op_t: def get_insn_ops(insn, target_ops=()):
"""yield op_t for instruction, filter on type if specified""" """ yield op_t for instruction, filter on type if specified """
for op in insn.ops: for op in insn.ops:
if op.type == idaapi.o_void: if op.type == idaapi.o_void:
# avoid looping all 6 ops if only subset exists # avoid looping all 6 ops if only subset exists
@@ -305,12 +281,12 @@ def get_insn_ops(insn: idaapi.insn_t, target_ops: Optional[Tuple[Any]] = None) -
yield op yield op
def is_op_stack_var(ea: int, index: int) -> bool: def is_op_stack_var(ea, index):
"""check if operand is a stack variable""" """ check if operand is a stack variable """
return idaapi.is_stkvar(idaapi.get_flags(ea), index) return idaapi.is_stkvar(idaapi.get_flags(ea), index)
def mask_op_val(op: idaapi.op_t) -> int: def mask_op_val(op):
"""mask value by data type """mask value by data type
necessary due to a bug in AMD64 necessary due to a bug in AMD64
@@ -330,15 +306,26 @@ def mask_op_val(op: idaapi.op_t) -> int:
return masks.get(op.dtype, op.value) & op.value return masks.get(op.dtype, op.value) & op.value
def is_function_recursive(f: idaapi.func_t) -> bool: def is_function_recursive(f):
"""check if function is recursive""" """check if function is recursive
return any(f.contains(ref) for ref in idautils.CodeRefsTo(f.start_ea, True))
args:
f (IDA func_t)
"""
for ref in idautils.CodeRefsTo(f.start_ea, True):
if f.contains(ref):
return True
return False
def is_basic_block_tight_loop(bb: idaapi.BasicBlock) -> bool: def is_basic_block_tight_loop(bb):
"""check basic block loops to self """check basic block loops to self
true if last instruction in basic block branches to basic block start true if last instruction in basic block branches to basic block start
args:
f (IDA func_t)
bb (IDA BasicBlock)
""" """
bb_end = idc.prev_head(bb.end_ea) bb_end = idc.prev_head(bb.end_ea)
if bb.start_ea < bb_end: if bb.start_ea < bb_end:
@@ -348,8 +335,8 @@ def is_basic_block_tight_loop(bb: idaapi.BasicBlock) -> bool:
return False return False
def find_data_reference_from_insn(insn: idaapi.insn_t, max_depth: int = 10) -> int: def find_data_reference_from_insn(insn, max_depth=10):
"""search for data reference from instruction, return address of instruction if no reference exists""" """ search for data reference from instruction, return address of instruction if no reference exists """
depth = 0 depth = 0
ea = insn.ea ea = insn.ea
@@ -378,17 +365,19 @@ def find_data_reference_from_insn(insn: idaapi.insn_t, max_depth: int = 10) -> i
return ea return ea
def get_function_blocks(f: idaapi.func_t) -> Iterator[idaapi.BasicBlock]: def get_function_blocks(f):
"""yield basic blocks contained in specified function""" """yield basic blocks contained in specified function
args:
f (IDA func_t)
yield:
block (IDA BasicBlock)
"""
# leverage idaapi.FC_NOEXT flag to ignore useless external blocks referenced by the function # leverage idaapi.FC_NOEXT flag to ignore useless external blocks referenced by the function
yield from idaapi.FlowChart(f, flags=(idaapi.FC_PREDS | idaapi.FC_NOEXT)) for block in idaapi.FlowChart(f, flags=(idaapi.FC_PREDS | idaapi.FC_NOEXT)):
yield block
def is_basic_block_return(bb: idaapi.BasicBlock) -> bool: def is_basic_block_return(bb):
"""check if basic block is return block""" """ check if basic block is return block """
return bb.type == idaapi.fcb_ret return bb.type == idaapi.fcb_ret
def has_sib(oper: idaapi.op_t) -> bool:
# via: https://reverseengineering.stackexchange.com/a/14300
return oper.specflag1 == 1

View File

@@ -1,11 +1,10 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
from typing import Any, Dict, Tuple, Iterator
import idc import idc
import idaapi import idaapi
@@ -13,30 +12,51 @@ import idautils
import capa.features.extractors.helpers import capa.features.extractors.helpers
import capa.features.extractors.ida.helpers import capa.features.extractors.ida.helpers
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset from capa.features import (
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic ARCH_X32,
from capa.features.address import Address, AbsoluteVirtualAddress ARCH_X64,
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle MAX_BYTES_FEATURE_SIZE,
THUNK_CHAIN_DEPTH_DELTA,
Bytes,
String,
Characteristic,
)
from capa.features.insn import API, Number, Offset, Mnemonic
# security cookie checks may perform non-zeroing XORs, these are expected within a certain # security cookie checks may perform non-zeroing XORs, these are expected within a certain
# byte range within the first and returning basic blocks, this helps to reduce FP features # byte range within the first and returning basic blocks, this helps to reduce FP features
SECURITY_COOKIE_BYTES_DELTA = 0x40 SECURITY_COOKIE_BYTES_DELTA = 0x40
def get_imports(ctx: Dict[str, Any]) -> Dict[int, Any]: def get_arch(ctx):
"""
fetch the ARCH_* constant for the currently open workspace.
via Tamir Bahar/@tmr232
https://reverseengineering.stackexchange.com/a/11398/17194
"""
if "arch" not in ctx:
info = idaapi.get_inf_structure()
if info.is_64bit():
ctx["arch"] = ARCH_X64
elif info.is_32bit():
ctx["arch"] = ARCH_X32
else:
raise ValueError("unexpected architecture")
return ctx["arch"]
def get_imports(ctx):
if "imports_cache" not in ctx: if "imports_cache" not in ctx:
ctx["imports_cache"] = capa.features.extractors.ida.helpers.get_file_imports() ctx["imports_cache"] = capa.features.extractors.ida.helpers.get_file_imports()
return ctx["imports_cache"] return ctx["imports_cache"]
def get_externs(ctx: Dict[str, Any]) -> Dict[int, Any]: def check_for_api_call(ctx, insn):
if "externs_cache" not in ctx: """ check instruction for API call """
ctx["externs_cache"] = capa.features.extractors.ida.helpers.get_file_externs() if not insn.get_canon_mnem() in ("call", "jmp"):
return ctx["externs_cache"] return
def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[Any]:
"""check instruction for API call"""
info = () info = ()
ref = insn.ea ref = insn.ea
@@ -52,7 +72,7 @@ def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[A
except IndexError: except IndexError:
break break
info = funcs.get(ref, ()) info = get_imports(ctx).get(ref, ())
if info: if info:
break break
@@ -61,64 +81,37 @@ def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[A
break break
if info: if info:
yield info yield "%s.%s" % (info[0], info[1])
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_insn_api_features(f, bb, insn):
""" """parse instruction API features
parse instruction API features
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
example: example:
call dword [0x00473038] call dword [0x00473038]
""" """
insn: idaapi.insn_t = ih.inner for api in check_for_api_call(f.ctx, insn):
dll, _, symbol = api.rpartition(".")
if insn.get_canon_mnem() not in ("call", "jmp"): for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
return yield API(name), insn.ea
# check calls to imported functions
for api in check_for_api_call(insn, get_imports(fh.ctx)):
# tuple (<module>, <function>, <ordinal>)
for name in capa.features.extractors.helpers.generate_symbols(api[0], api[1]):
yield API(name), ih.address
# check calls to extern functions
for api in check_for_api_call(insn, get_externs(fh.ctx)):
# tuple (<module>, <function>, <ordinal>)
yield API(api[1]), ih.address
# extract IDA/FLIRT recognized API functions
targets = tuple(idautils.CodeRefsFrom(insn.ea, False))
if not targets:
return
target = targets[0]
target_func = idaapi.get_func(target)
if not target_func or target_func.start_ea != target:
# not a function (start)
return
if target_func.flags & idaapi.FUNC_LIB:
name = idaapi.get_name(target_func.start_ea)
yield API(name), ih.address
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions.
# extract features for both the mangled and un-mangled representations.
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield API(name[1:]), ih.address
def extract_insn_number_features( def extract_insn_number_features(f, bb, insn):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle """parse instruction number features
) -> Iterator[Tuple[Feature, Address]]:
""" args:
parse instruction number features f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
example: example:
push 3136B0h ; dwControlCode push 3136B0h ; dwControlCode
""" """
insn: idaapi.insn_t = ih.inner
if idaapi.is_ret_insn(insn): if idaapi.is_ret_insn(insn):
# skip things like: # skip things like:
# .text:0042250E retn 8 # .text:0042250E retn 8
@@ -129,11 +122,7 @@ def extract_insn_number_features(
# .text:00401145 add esp, 0Ch # .text:00401145 add esp, 0Ch
return return
for i, op in enumerate(insn.ops): for op in capa.features.extractors.ida.helpers.get_insn_ops(insn, target_ops=(idaapi.o_imm, idaapi.o_mem)):
if op.type == idaapi.o_void:
break
if op.type not in (idaapi.o_imm, idaapi.o_mem):
continue
# skip things like: # skip things like:
# .text:00401100 shr eax, offset loc_C # .text:00401100 shr eax, offset loc_C
if capa.features.extractors.ida.helpers.is_op_offset(insn, op): if capa.features.extractors.ida.helpers.is_op_offset(insn, op):
@@ -144,27 +133,21 @@ def extract_insn_number_features(
else: else:
const = op.addr const = op.addr
yield Number(const), ih.address yield Number(const), insn.ea
yield OperandNumber(i, const), ih.address yield Number(const, arch=get_arch(f.ctx)), insn.ea
if insn.itype == idaapi.NN_add and 0 < const < MAX_STRUCTURE_SIZE and op.type == idaapi.o_imm:
# for pattern like:
#
# add eax, 0x10
#
# assume 0x10 is also an offset (imagine eax is a pointer).
yield Offset(const), ih.address
yield OperandOffset(i, const), ih.address
def extract_insn_bytes_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_insn_bytes_features(f, bb, insn):
""" """parse referenced byte sequences
parse referenced byte sequences
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
example: example:
push offset iid_004118d4_IShellLinkA ; riid push offset iid_004118d4_IShellLinkA ; riid
""" """
insn: idaapi.insn_t = ih.inner
if idaapi.is_call_insn(insn): if idaapi.is_call_insn(insn):
return return
@@ -172,54 +155,43 @@ def extract_insn_bytes_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandl
if ref != insn.ea: if ref != insn.ea:
extracted_bytes = capa.features.extractors.ida.helpers.read_bytes_at(ref, MAX_BYTES_FEATURE_SIZE) extracted_bytes = capa.features.extractors.ida.helpers.read_bytes_at(ref, MAX_BYTES_FEATURE_SIZE)
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes): if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
if not capa.features.extractors.ida.helpers.find_string_at(ref): yield Bytes(extracted_bytes), insn.ea
# don't extract byte features for obvious strings
yield Bytes(extracted_bytes), ih.address
def extract_insn_string_features( def extract_insn_string_features(f, bb, insn):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle """parse instruction string features
) -> Iterator[Tuple[Feature, Address]]:
""" args:
parse instruction string features f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
example: example:
push offset aAcr ; "ACR > " push offset aAcr ; "ACR > "
""" """
insn: idaapi.insn_t = ih.inner
ref = capa.features.extractors.ida.helpers.find_data_reference_from_insn(insn) ref = capa.features.extractors.ida.helpers.find_data_reference_from_insn(insn)
if ref != insn.ea: if ref != insn.ea:
found = capa.features.extractors.ida.helpers.find_string_at(ref) found = capa.features.extractors.ida.helpers.find_string_at(ref)
if found: if found:
yield String(found), ih.address yield String(found), insn.ea
def extract_insn_offset_features( def extract_insn_offset_features(f, bb, insn):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle """parse instruction structure offset features
) -> Iterator[Tuple[Feature, Address]]:
""" args:
parse instruction structure offset features f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
example: example:
.text:0040112F cmp [esi+4], ebx .text:0040112F cmp [esi+4], ebx
""" """
insn: idaapi.insn_t = ih.inner for op in capa.features.extractors.ida.helpers.get_insn_ops(insn, target_ops=(idaapi.o_phrase, idaapi.o_displ)):
for i, op in enumerate(insn.ops):
if op.type == idaapi.o_void:
break
if op.type not in (idaapi.o_phrase, idaapi.o_displ):
continue
if capa.features.extractors.ida.helpers.is_op_stack_var(insn.ea, op.n): if capa.features.extractors.ida.helpers.is_op_stack_var(insn.ea, op.n):
continue continue
p_info = capa.features.extractors.ida.helpers.get_op_phrase_info(op) p_info = capa.features.extractors.ida.helpers.get_op_phrase_info(op)
op_off = p_info.get("offset", 0)
op_off = p_info.get("offset")
if op_off is None:
continue
if idaapi.is_mapped(op_off): if idaapi.is_mapped(op_off):
# Ignore: # Ignore:
# mov esi, dword_1005B148[esi] # mov esi, dword_1005B148[esi]
@@ -230,32 +202,12 @@ def extract_insn_offset_features(
# https://stackoverflow.com/questions/31853189/x86-64-assembly-why-displacement-not-64-bits # https://stackoverflow.com/questions/31853189/x86-64-assembly-why-displacement-not-64-bits
op_off = capa.features.extractors.helpers.twos_complement(op_off, 32) op_off = capa.features.extractors.helpers.twos_complement(op_off, 32)
yield Offset(op_off), ih.address yield Offset(op_off), insn.ea
yield OperandOffset(i, op_off), ih.address yield Offset(op_off, arch=get_arch(f.ctx)), insn.ea
if (
insn.itype == idaapi.NN_lea
and i == 1
# o_displ is used for both:
# [eax+1]
# [eax+ebx+2]
and op.type == idaapi.o_displ
# but the SIB is only present for [eax+ebx+2]
# which we don't want
and not capa.features.extractors.ida.helpers.has_sib(op)
):
# for pattern like:
#
# lea eax, [ebx + 1]
#
# assume 1 is also an offset (imagine ebx is a zero register).
yield Number(op_off), ih.address
yield OperandNumber(i, op_off), ih.address
def contains_stack_cookie_keywords(s: str) -> bool: def contains_stack_cookie_keywords(s):
""" """check if string contains stack cookie keywords
check if string contains stack cookie keywords
Examples: Examples:
xor ecx, ebp ; StackCookie xor ecx, ebp ; StackCookie
@@ -269,7 +221,7 @@ def contains_stack_cookie_keywords(s: str) -> bool:
return any(keyword in s for keyword in ("stack", "security")) return any(keyword in s for keyword in ("stack", "security"))
def bb_stack_cookie_registers(bb: idaapi.BasicBlock) -> Iterator[int]: def bb_stack_cookie_registers(bb):
"""scan basic block for stack cookie operations """scan basic block for stack cookie operations
yield registers ids that may have been used for stack cookie operations yield registers ids that may have been used for stack cookie operations
@@ -303,8 +255,8 @@ def bb_stack_cookie_registers(bb: idaapi.BasicBlock) -> Iterator[int]:
yield op.reg yield op.reg
def is_nzxor_stack_cookie_delta(f: idaapi.func_t, bb: idaapi.BasicBlock, insn: idaapi.insn_t) -> bool: def is_nzxor_stack_cookie_delta(f, bb, insn):
"""check if nzxor exists within stack cookie delta""" """ check if nzxor exists within stack cookie delta """
# security cookie check should use SP or BP # security cookie check should use SP or BP
if not capa.features.extractors.ida.helpers.is_frame_register(insn.Op2.reg): if not capa.features.extractors.ida.helpers.is_frame_register(insn.Op2.reg):
return False return False
@@ -326,8 +278,8 @@ def is_nzxor_stack_cookie_delta(f: idaapi.func_t, bb: idaapi.BasicBlock, insn: i
return False return False
def is_nzxor_stack_cookie(f: idaapi.func_t, bb: idaapi.BasicBlock, insn: idaapi.insn_t) -> bool: def is_nzxor_stack_cookie(f, bb, insn):
"""check if nzxor is related to stack cookie""" """ check if nzxor is related to stack cookie """
if contains_stack_cookie_keywords(idaapi.get_cmt(insn.ea, False)): if contains_stack_cookie_keywords(idaapi.get_cmt(insn.ea, False)):
# Example: # Example:
# xor ecx, ebp ; StackCookie # xor ecx, ebp ; StackCookie
@@ -343,49 +295,37 @@ def is_nzxor_stack_cookie(f: idaapi.func_t, bb: idaapi.BasicBlock, insn: idaapi.
return False return False
def extract_insn_nzxor_characteristic_features( def extract_insn_nzxor_characteristic_features(f, bb, insn):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle """parse instruction non-zeroing XOR instruction
) -> Iterator[Tuple[Feature, Address]]:
"""
parse instruction non-zeroing XOR instruction
ignore expected non-zeroing XORs, e.g. security cookies
"""
insn: idaapi.insn_t = ih.inner
ignore expected non-zeroing XORs, e.g. security cookies
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
"""
if insn.itype not in (idaapi.NN_xor, idaapi.NN_xorpd, idaapi.NN_xorps, idaapi.NN_pxor): if insn.itype not in (idaapi.NN_xor, idaapi.NN_xorpd, idaapi.NN_xorps, idaapi.NN_pxor):
return return
if capa.features.extractors.ida.helpers.is_operand_equal(insn.Op1, insn.Op2): if capa.features.extractors.ida.helpers.is_operand_equal(insn.Op1, insn.Op2):
return return
if is_nzxor_stack_cookie(fh.inner, bbh.inner, insn): if is_nzxor_stack_cookie(f, bb, insn):
return return
yield Characteristic("nzxor"), ih.address yield Characteristic("nzxor"), insn.ea
def extract_insn_mnemonic_features( def extract_insn_mnemonic_features(f, bb, insn):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle """parse instruction mnemonic features
) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction mnemonic features"""
yield Mnemonic(idc.print_insn_mnem(ih.inner.ea)), ih.address
args:
def extract_insn_obfs_call_plus_5_characteristic_features( f (IDA func_t)
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle bb (IDA BasicBlock)
) -> Iterator[Tuple[Feature, Address]]: insn (IDA insn_t)
""" """
parse call $+5 instruction from the given instruction. yield Mnemonic(insn.get_canon_mnem()), insn.ea
"""
insn: idaapi.insn_t = ih.inner
if not idaapi.is_call_insn(insn):
return
if insn.ea + 5 == idc.get_operand_value(insn.ea, 0):
yield Characteristic("call $+5"), ih.address
def extract_insn_peb_access_characteristic_features( def extract_insn_peb_access_characteristic_features(f, bb, insn):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction peb access """parse instruction peb access
fs:[0x30] on x86, gs:[0x60] on x64 fs:[0x30] on x86, gs:[0x60] on x64
@@ -393,61 +333,51 @@ def extract_insn_peb_access_characteristic_features(
TODO: TODO:
IDA should be able to do this.. IDA should be able to do this..
""" """
insn: idaapi.insn_t = ih.inner
if insn.itype not in (idaapi.NN_push, idaapi.NN_mov): if insn.itype not in (idaapi.NN_push, idaapi.NN_mov):
return return
if all(op.type != idaapi.o_mem for op in insn.ops): if all(map(lambda op: op.type != idaapi.o_mem, insn.ops)):
# try to optimize for only memory references # try to optimize for only memory references
return return
disasm = idc.GetDisasm(insn.ea) disasm = idc.GetDisasm(insn.ea)
if " fs:30h" in disasm or " gs:60h" in disasm: if " fs:30h" in disasm or " gs:60h" in disasm:
# TODO(mike-hunhoff): use proper IDA API for fetching segment access # TODO: replace above with proper IDA
# scanning the disassembly text is a hack. yield Characteristic("peb access"), insn.ea
# https://github.com/mandiant/capa/issues/1605
yield Characteristic("peb access"), ih.address
def extract_insn_segment_access_features( def extract_insn_segment_access_features(f, bb, insn):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""parse instruction fs or gs access """parse instruction fs or gs access
TODO: TODO:
IDA should be able to do this... IDA should be able to do this...
""" """
insn: idaapi.insn_t = ih.inner if all(map(lambda op: op.type != idaapi.o_mem, insn.ops)):
if all(op.type != idaapi.o_mem for op in insn.ops):
# try to optimize for only memory references # try to optimize for only memory references
return return
disasm = idc.GetDisasm(insn.ea) disasm = idc.GetDisasm(insn.ea)
if " fs:" in disasm: if " fs:" in disasm:
# TODO(mike-hunhoff): use proper IDA API for fetching segment access # TODO: replace above with proper IDA
# scanning the disassembly text is a hack. yield Characteristic("fs access"), insn.ea
# https://github.com/mandiant/capa/issues/1605
yield Characteristic("fs access"), ih.address
if " gs:" in disasm: if " gs:" in disasm:
# TODO(mike-hunhoff): use proper IDA API for fetching segment access # TODO: replace above with proper IDA
# scanning the disassembly text is a hack. yield Characteristic("gs access"), insn.ea
# https://github.com/mandiant/capa/issues/1605
yield Characteristic("gs access"), ih.address
def extract_insn_cross_section_cflow( def extract_insn_cross_section_cflow(f, bb, insn):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle """inspect the instruction for a CALL or JMP that crosses section boundaries
) -> Iterator[Tuple[Feature, Address]]:
"""inspect the instruction for a CALL or JMP that crosses section boundaries"""
insn: idaapi.insn_t = ih.inner
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
"""
for ref in idautils.CodeRefsFrom(insn.ea, False): for ref in idautils.CodeRefsFrom(insn.ea, False):
if ref in get_imports(fh.ctx): if ref in get_imports(f.ctx).keys():
# ignore API calls # ignore API calls
continue continue
if not idaapi.getseg(ref): if not idaapi.getseg(ref):
@@ -455,40 +385,50 @@ def extract_insn_cross_section_cflow(
continue continue
if idaapi.getseg(ref) == idaapi.getseg(insn.ea): if idaapi.getseg(ref) == idaapi.getseg(insn.ea):
continue continue
yield Characteristic("cross section flow"), ih.address yield Characteristic("cross section flow"), insn.ea
def extract_function_calls_from(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_function_calls_from(f, bb, insn):
"""extract functions calls from features """extract functions calls from features
most relevant at the function scope, however, its most efficient to extract at the instruction scope most relevant at the function scope, however, its most efficient to extract at the instruction scope
"""
insn: idaapi.insn_t = ih.inner
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
"""
if idaapi.is_call_insn(insn): if idaapi.is_call_insn(insn):
for ref in idautils.CodeRefsFrom(insn.ea, False): for ref in idautils.CodeRefsFrom(insn.ea, False):
yield Characteristic("calls from"), AbsoluteVirtualAddress(ref) yield Characteristic("calls from"), ref
def extract_function_indirect_call_characteristic_features( def extract_function_indirect_call_characteristic_features(f, bb, insn):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
"""extract indirect function calls (e.g., call eax or call dword ptr [edx+4]) """extract indirect function calls (e.g., call eax or call dword ptr [edx+4])
does not include calls like => call ds:dword_ABD4974 does not include calls like => call ds:dword_ABD4974
most relevant at the function or basic block scope; most relevant at the function or basic block scope;
however, its most efficient to extract at the instruction scope however, its most efficient to extract at the instruction scope
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
""" """
insn: idaapi.insn_t = ih.inner
if idaapi.is_call_insn(insn) and idc.get_operand_type(insn.ea, 0) in (idc.o_reg, idc.o_phrase, idc.o_displ): if idaapi.is_call_insn(insn) and idc.get_operand_type(insn.ea, 0) in (idc.o_reg, idc.o_phrase, idc.o_displ):
yield Characteristic("indirect call"), ih.address yield Characteristic("indirect call"), insn.ea
def extract_features(f: FunctionHandle, bbh: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_features(f, bb, insn):
"""extract instruction features""" """extract instruction features
args:
f (IDA func_t)
bb (IDA BasicBlock)
insn (IDA insn_t)
"""
for inst_handler in INSTRUCTION_HANDLERS: for inst_handler in INSTRUCTION_HANDLERS:
for feature, ea in inst_handler(f, bbh, insn): for (feature, ea) in inst_handler(f, bb, insn):
yield feature, ea yield feature, ea
@@ -500,10 +440,26 @@ INSTRUCTION_HANDLERS = (
extract_insn_offset_features, extract_insn_offset_features,
extract_insn_nzxor_characteristic_features, extract_insn_nzxor_characteristic_features,
extract_insn_mnemonic_features, extract_insn_mnemonic_features,
extract_insn_obfs_call_plus_5_characteristic_features,
extract_insn_peb_access_characteristic_features, extract_insn_peb_access_characteristic_features,
extract_insn_cross_section_cflow, extract_insn_cross_section_cflow,
extract_insn_segment_access_features, extract_insn_segment_access_features,
extract_function_calls_from, extract_function_calls_from,
extract_function_indirect_call_characteristic_features, extract_function_indirect_call_characteristic_features,
) )
def main():
""" """
features = []
for f in capa.features.extractors.ida.helpers.get_functions(skip_thunks=True, skip_libs=True):
for bb in idaapi.FlowChart(f, flags=idaapi.FC_PREDS):
for insn in capa.features.extractors.ida.helpers.get_instructions_in_range(bb.start_ea, bb.end_ea):
features.extend(list(extract_features(f, bb, insn)))
import pprint
pprint.pprint(features)
if __name__ == "__main__":
main()

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,7 +6,7 @@
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 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. # See the License for the specific language governing permissions and limitations under the License.
import networkx from networkx import nx
from networkx.algorithms.components import strongly_connected_components from networkx.algorithms.components import strongly_connected_components
@@ -20,6 +20,6 @@ def has_loop(edges, threshold=2):
returns: returns:
bool bool
""" """
g = networkx.DiGraph() g = nx.DiGraph()
g.add_edges_from(edges) g.add_edges_from(edges)
return any(len(comp) >= threshold for comp in strongly_connected_components(g)) return any(len(comp) >= threshold for comp in strongly_connected_components(g))

View File

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

View File

@@ -1,229 +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 pathlib import Path
import pefile
import capa.features.common
import capa.features.extractors
import capa.features.extractors.common
import capa.features.extractors.helpers
import capa.features.extractors.strings
from capa.features.file import Export, Import, Section
from capa.features.common import OS, ARCH_I386, FORMAT_PE, ARCH_AMD64, OS_WINDOWS, Arch, Format, Characteristic
from capa.features.address import NO_ADDRESS, FileOffsetAddress, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import FeatureExtractor
logger = logging.getLogger(__name__)
def extract_file_embedded_pe(buf, **kwargs):
for offset, _ in capa.features.extractors.helpers.carve_pe(buf, 1):
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
def extract_file_export_names(pe, **kwargs):
base_address = pe.OPTIONAL_HEADER.ImageBase
if hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
for export in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if not export.name:
continue
try:
name = export.name.partition(b"\x00")[0].decode("ascii")
except UnicodeDecodeError:
continue
if export.forwarder is None:
va = base_address + export.address
yield Export(name), AbsoluteVirtualAddress(va)
else:
try:
forwarded_name = export.forwarder.partition(b"\x00")[0].decode("ascii")
except UnicodeDecodeError:
continue
forwarded_name = capa.features.extractors.helpers.reformat_forwarded_export_name(forwarded_name)
va = base_address + export.address
yield Export(forwarded_name), AbsoluteVirtualAddress(va)
yield Characteristic("forwarded export"), AbsoluteVirtualAddress(va)
def extract_file_import_names(pe, **kwargs):
"""
extract imported function names
1. imports by ordinal:
- modulename.#ordinal
2. imports by name, results in two features to support importname-only matching:
- modulename.importname
- importname
"""
if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"):
for dll in pe.DIRECTORY_ENTRY_IMPORT:
try:
modname = dll.dll.partition(b"\x00")[0].decode("ascii")
except UnicodeDecodeError:
continue
# strip extension
modname = modname.rpartition(".")[0].lower()
for imp in dll.imports:
if imp.import_by_ordinal:
impname = f"#{imp.ordinal}"
else:
try:
impname = imp.name.partition(b"\x00")[0].decode("ascii")
except UnicodeDecodeError:
continue
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
yield Import(name), AbsoluteVirtualAddress(imp.address)
def extract_file_section_names(pe, **kwargs):
base_address = pe.OPTIONAL_HEADER.ImageBase
for section in pe.sections:
try:
name = section.Name.partition(b"\x00")[0].decode("ascii")
except UnicodeDecodeError:
continue
yield Section(name), AbsoluteVirtualAddress(base_address + section.VirtualAddress)
def extract_file_strings(buf, **kwargs):
yield from capa.features.extractors.common.extract_file_strings(buf)
def extract_file_function_names(**kwargs):
"""
extract the names of statically-linked library functions.
"""
if False:
# using a `yield` here to force this to be a generator, not function.
yield NotImplementedError("pefile doesn't have library matching")
return
def extract_file_os(**kwargs):
# assuming PE -> Windows
# though i suppose they're also used by UEFI
yield OS(OS_WINDOWS), NO_ADDRESS
def extract_file_format(**kwargs):
yield Format(FORMAT_PE), NO_ADDRESS
def extract_file_arch(pe, **kwargs):
if pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_I386"]:
yield Arch(ARCH_I386), NO_ADDRESS
elif pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_AMD64"]:
yield Arch(ARCH_AMD64), NO_ADDRESS
else:
logger.warning("unsupported architecture: %s", pefile.MACHINE_TYPE[pe.FILE_HEADER.Machine])
def extract_file_features(pe, buf):
"""
extract file features from given workspace
args:
pe (pefile.PE): the parsed PE
buf: the raw sample bytes
yields:
Tuple[Feature, VA]: a feature and its location.
"""
for file_handler in FILE_HANDLERS:
# file_handler: type: (pe, bytes) -> Iterable[Tuple[Feature, Address]]
for feature, va in file_handler(pe=pe, buf=buf): # type: ignore
yield feature, va
FILE_HANDLERS = (
extract_file_embedded_pe,
extract_file_export_names,
extract_file_import_names,
extract_file_section_names,
extract_file_strings,
extract_file_function_names,
extract_file_format,
)
def extract_global_features(pe, buf):
"""
extract global features from given workspace
args:
pe (pefile.PE): the parsed PE
buf: the raw sample bytes
yields:
Tuple[Feature, VA]: a feature and its location.
"""
for handler in GLOBAL_HANDLERS:
# file_handler: type: (pe, bytes) -> Iterable[Tuple[Feature, Address]]
for feature, va in handler(pe=pe, buf=buf): # type: ignore
yield feature, va
GLOBAL_HANDLERS = (
extract_file_os,
extract_file_arch,
)
class PefileFeatureExtractor(FeatureExtractor):
def __init__(self, path: Path):
super().__init__()
self.path: Path = path
self.pe = pefile.PE(str(path))
def get_base_address(self):
return AbsoluteVirtualAddress(self.pe.OPTIONAL_HEADER.ImageBase)
def extract_global_features(self):
buf = Path(self.path).read_bytes()
yield from extract_global_features(self.pe, buf)
def extract_file_features(self):
buf = Path(self.path).read_bytes()
yield from extract_file_features(self.pe, buf)
def get_functions(self):
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
def extract_function_features(self, f):
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
def get_basic_blocks(self, f):
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
def extract_basic_block_features(self, f, bb):
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
def get_instructions(self, f, bb):
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
def extract_insn_features(self, f, bb, insn):
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
def is_library_function(self, va):
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
def get_function_name(self, va):
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")

View File

@@ -0,0 +1,52 @@
import sys
import types
from smda.common.SmdaReport import SmdaReport
from smda.common.SmdaInstruction import SmdaInstruction
import capa.features.extractors.smda.file
import capa.features.extractors.smda.insn
import capa.features.extractors.smda.function
import capa.features.extractors.smda.basicblock
from capa.main import UnsupportedRuntimeError
from capa.features.extractors import FeatureExtractor
class SmdaFeatureExtractor(FeatureExtractor):
def __init__(self, smda_report: SmdaReport, path):
super(SmdaFeatureExtractor, self).__init__()
if sys.version_info < (3, 0):
raise UnsupportedRuntimeError("SMDA should only be used with Python 3.")
self.smda_report = smda_report
self.path = path
def get_base_address(self):
return self.smda_report.base_addr
def extract_file_features(self):
for feature, va in capa.features.extractors.smda.file.extract_features(self.smda_report, self.path):
yield feature, va
def get_functions(self):
for function in self.smda_report.getFunctions():
yield function
def extract_function_features(self, f):
for feature, va in capa.features.extractors.smda.function.extract_features(f):
yield feature, va
def get_basic_blocks(self, f):
for bb in f.getBlocks():
yield bb
def extract_basic_block_features(self, f, bb):
for feature, va in capa.features.extractors.smda.basicblock.extract_features(f, bb):
yield feature, va
def get_instructions(self, f, bb):
for smda_ins in bb.getInstructions():
yield smda_ins
def extract_insn_features(self, f, bb, insn):
for feature, va in capa.features.extractors.smda.insn.extract_features(f, bb, insn):
yield feature, va

View File

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

View File

@@ -0,0 +1,139 @@
import struct
# if we have SMDA we definitely have lief
import lief
import capa.features.extractors.helpers
import capa.features.extractors.strings
from capa.features import String, Characteristic
from capa.features.file import Export, Import, Section
def carve(pbytes, offset=0):
"""
Return a list of (offset, size, xor) tuples of embedded PEs
Based on the version from vivisect:
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
And its IDA adaptation:
capa/features/extractors/ida/file.py
"""
mz_xor = [
(
capa.features.extractors.helpers.xor_static(b"MZ", i),
capa.features.extractors.helpers.xor_static(b"PE", i),
i,
)
for i in range(256)
]
pblen = len(pbytes)
todo = [(pbytes.find(mzx, offset), mzx, pex, i) for mzx, pex, i in mz_xor]
todo = [(off, mzx, pex, i) for (off, mzx, pex, i) in todo if off != -1]
while len(todo):
off, mzx, pex, i = todo.pop()
# The MZ header has one field we will check
# e_lfanew is at 0x3c
e_lfanew = off + 0x3C
if pblen < (e_lfanew + 4):
continue
newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(pbytes[e_lfanew : e_lfanew + 4], i))[0]
nextres = pbytes.find(mzx, off + 1)
if nextres != -1:
todo.append((nextres, mzx, pex, i))
peoff = off + newoff
if pblen < (peoff + 2):
continue
if pbytes[peoff : peoff + 2] == pex:
yield (off, i)
def extract_file_embedded_pe(smda_report, file_path):
with open(file_path, "rb") as f:
fbytes = f.read()
for offset, i in carve(fbytes, 1):
yield Characteristic("embedded pe"), offset
def extract_file_export_names(smda_report, file_path):
lief_binary = lief.parse(file_path)
if lief_binary is not None:
for function in lief_binary.exported_functions:
yield Export(function.name), function.address
def extract_file_import_names(smda_report, file_path):
# extract import table info via LIEF
lief_binary = lief.parse(file_path)
if not isinstance(lief_binary, lief.PE.Binary):
return
for imported_library in lief_binary.imports:
library_name = imported_library.name.lower()
library_name = library_name[:-4] if library_name.endswith(".dll") else library_name
for func in imported_library.entries:
if func.name:
va = func.iat_address + smda_report.base_addr
for name in capa.features.extractors.helpers.generate_symbols(library_name, func.name):
yield Import(name), va
elif func.is_ordinal:
for name in capa.features.extractors.helpers.generate_symbols(library_name, "#%s" % func.ordinal):
yield Import(name), va
def extract_file_section_names(smda_report, file_path):
lief_binary = lief.parse(file_path)
if not isinstance(lief_binary, lief.PE.Binary):
return
if lief_binary and lief_binary.sections:
base_address = lief_binary.optional_header.imagebase
for section in lief_binary.sections:
yield Section(section.name), base_address + section.virtual_address
def extract_file_strings(smda_report, file_path):
"""
extract ASCII and UTF-16 LE strings from file
"""
with open(file_path, "rb") as f:
b = f.read()
for s in capa.features.extractors.strings.extract_ascii_strings(b):
yield String(s.s), s.offset
for s in capa.features.extractors.strings.extract_unicode_strings(b):
yield String(s.s), s.offset
def extract_features(smda_report, file_path):
"""
extract file features from given workspace
args:
smda_report (smda.common.SmdaReport): a SmdaReport
file_path: path to the input file
yields:
Tuple[Feature, VA]: a feature and its location.
"""
for file_handler in FILE_HANDLERS:
result = file_handler(smda_report, file_path)
for feature, va in file_handler(smda_report, file_path):
yield feature, va
FILE_HANDLERS = (
extract_file_embedded_pe,
extract_file_export_names,
extract_file_import_names,
extract_file_section_names,
extract_file_strings,
)

View File

@@ -0,0 +1,38 @@
from capa.features import Characteristic
from capa.features.extractors import loops
def extract_function_calls_to(f):
for inref in f.inrefs:
yield Characteristic("calls to"), inref
def extract_function_loop(f):
"""
parse if a function has a loop
"""
edges = []
for bb_from, bb_tos in f.blockrefs.items():
for bb_to in bb_tos:
edges.append((bb_from, bb_to))
if edges and loops.has_loop(edges):
yield Characteristic("loop"), f.offset
def extract_features(f):
"""
extract features from the given function.
args:
f (smda.common.SmdaFunction): the function from which to extract features
yields:
Feature, set[VA]: the features and their location found in this function.
"""
for func_handler in FUNCTION_HANDLERS:
for feature, va in func_handler(f):
yield feature, va
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)

View File

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

View File

@@ -1,6 +1,6 @@
# strings code from FLOSS, https://github.com/mandiant/flare-floss # strings code from FLOSS, https://github.com/fireeye/flare-floss
# #
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -9,7 +9,6 @@
# See the License for the specific language governing permissions and limitations under the License. # See the License for the specific language governing permissions and limitations under the License.
import re import re
import contextlib
from collections import namedtuple from collections import namedtuple
ASCII_BYTE = r" !\"#\$%&\'\(\)\*\+,-\./0123456789:;<=>\?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\[\]\^_`abcdefghijklmnopqrstuvwxyz\{\|\}\\\~\t".encode( ASCII_BYTE = r" !\"#\$%&\'\(\)\*\+,-\./0123456789:;<=>\?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\[\]\^_`abcdefghijklmnopqrstuvwxyz\{\|\}\\\~\t".encode(
@@ -82,5 +81,24 @@ def extract_unicode_strings(buf, n=4):
reg = b"((?:[%s]\x00){%d,})" % (ASCII_BYTE, n) reg = b"((?:[%s]\x00){%d,})" % (ASCII_BYTE, n)
r = re.compile(reg) r = re.compile(reg)
for match in r.finditer(buf): for match in r.finditer(buf):
with contextlib.suppress(UnicodeDecodeError): try:
yield String(match.group().decode("utf-16"), match.start()) yield String(match.group().decode("utf-16"), match.start())
except UnicodeDecodeError:
pass
def main():
import sys
with open(sys.argv[1], "rb") as f:
b = f.read()
for s in extract_ascii_strings(b):
print("0x{:x}: {:s}".format(s.offset, s.s))
for s in extract_unicode_strings(b):
print("0x{:x}: {:s}".format(s.offset, s.s))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,81 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import types
import viv_utils
import capa.features.extractors
import capa.features.extractors.viv.file
import capa.features.extractors.viv.insn
import capa.features.extractors.viv.function
import capa.features.extractors.viv.basicblock
from capa.features.extractors import FeatureExtractor
__all__ = ["file", "function", "basicblock", "insn"]
def get_va(self):
try:
# vivisect type
return self.va
except AttributeError:
pass
raise TypeError()
def add_va_int_cast(o):
"""
dynamically add a cast-to-int (`__int__`) method to the given object
that returns the value of the `.va` property.
this bit of skullduggery lets use cast viv-utils objects as ints.
the correct way of doing this is to update viv-utils (or subclass the objects here).
"""
setattr(o, "__int__", types.MethodType(get_va, o))
return o
class VivisectFeatureExtractor(FeatureExtractor):
def __init__(self, vw, path):
super(VivisectFeatureExtractor, self).__init__()
self.vw = vw
self.path = path
def get_base_address(self):
# assume there is only one file loaded into the vw
return list(self.vw.filemeta.values())[0]["imagebase"]
def extract_file_features(self):
for feature, va in capa.features.extractors.viv.file.extract_features(self.vw, self.path):
yield feature, va
def get_functions(self):
for va in sorted(self.vw.getFunctions()):
yield add_va_int_cast(viv_utils.Function(self.vw, va))
def extract_function_features(self, f):
for feature, va in capa.features.extractors.viv.function.extract_features(f):
yield feature, va
def get_basic_blocks(self, f):
for bb in f.basic_blocks:
yield add_va_int_cast(bb)
def extract_basic_block_features(self, f, bb):
for feature, va in capa.features.extractors.viv.basicblock.extract_features(f, bb):
yield feature, va
def get_instructions(self, f, bb):
for insn in bb.instructions:
yield add_va_int_cast(insn)
def extract_insn_features(self, f, bb, insn):
for feature, va in capa.features.extractors.viv.insn.extract_features(f, bb, insn):
yield feature, va

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -8,30 +8,27 @@
import string import string
import struct import struct
from typing import Tuple, Iterator
import envi import envi
import envi.archs.i386.disasm import vivisect.const
from capa.features.common import Feature, Characteristic from capa.features import Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.basicblock import BasicBlock from capa.features.basicblock import BasicBlock
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
def interface_extract_basic_block_XXX(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]: def interface_extract_basic_block_XXX(f, bb):
""" """
parse features from the given basic block. parse features from the given basic block.
args: args:
f: the function to process. f (viv_utils.Function): the function to process.
bb: the basic block to process. bb (viv_utils.BasicBlock): the basic block to process.
yields: yields:
(Feature, Address): the feature and the address at which its found. (Feature, int): the feature and the address at which its found.
""" """
raise NotImplementedError yield NotImplementedError("feature"), NotImplementedError("virtual address")
def _bb_has_tight_loop(f, bb): def _bb_has_tight_loop(f, bb):
@@ -40,17 +37,17 @@ def _bb_has_tight_loop(f, bb):
""" """
if len(bb.instructions) > 0: if len(bb.instructions) > 0:
for bva, bflags in bb.instructions[-1].getBranches(): for bva, bflags in bb.instructions[-1].getBranches():
if bflags & envi.BR_COND: if bflags & vivisect.envi.BR_COND:
if bva == bb.va: if bva == bb.va:
return True return True
return False return False
def extract_bb_tight_loop(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]: def extract_bb_tight_loop(f, bb):
"""check basic block for tight loop indicators""" """ check basic block for tight loop indicators """
if _bb_has_tight_loop(f, bb.inner): if _bb_has_tight_loop(f, bb):
yield Characteristic("tight loop"), bb.address yield Characteristic("tight loop"), bb.va
def _bb_has_stackstring(f, bb): def _bb_has_stackstring(f, bb):
@@ -70,13 +67,13 @@ def _bb_has_stackstring(f, bb):
return False return False
def extract_stackstring(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]: def extract_stackstring(f, bb):
"""check basic block for stackstring indicators""" """ check basic block for stackstring indicators """
if _bb_has_stackstring(f, bb.inner): if _bb_has_stackstring(f, bb):
yield Characteristic("stack string"), bb.address yield Characteristic("stack string"), bb.va
def is_mov_imm_to_stack(instr: envi.archs.i386.disasm.i386Opcode) -> bool: def is_mov_imm_to_stack(instr):
""" """
Return if instruction moves immediate onto stack Return if instruction moves immediate onto stack
""" """
@@ -92,6 +89,7 @@ def is_mov_imm_to_stack(instr: envi.archs.i386.disasm.i386Opcode) -> bool:
if not src.isImmed(): if not src.isImmed():
return False return False
# TODO what about 64-bit operands?
if not isinstance(dst, envi.archs.i386.disasm.i386SibOper) and not isinstance( if not isinstance(dst, envi.archs.i386.disasm.i386SibOper) and not isinstance(
dst, envi.archs.i386.disasm.i386RegMemOper dst, envi.archs.i386.disasm.i386RegMemOper
): ):
@@ -107,7 +105,7 @@ def is_mov_imm_to_stack(instr: envi.archs.i386.disasm.i386Opcode) -> bool:
return True return True
def get_printable_len(oper: envi.archs.i386.disasm.i386ImmOper) -> int: def get_printable_len(oper):
""" """
Return string length if all operand bytes are ascii or utf16-le printable Return string length if all operand bytes are ascii or utf16-le printable
""" """
@@ -119,18 +117,14 @@ def get_printable_len(oper: envi.archs.i386.disasm.i386ImmOper) -> int:
chars = struct.pack("<I", oper.imm) chars = struct.pack("<I", oper.imm)
elif oper.tsize == 8: elif oper.tsize == 8:
chars = struct.pack("<Q", oper.imm) chars = struct.pack("<Q", oper.imm)
else:
raise ValueError(f"unexpected oper.tsize: {oper.tsize}")
if is_printable_ascii(chars): if is_printable_ascii(chars):
return oper.tsize return oper.tsize
elif is_printable_utf16le(chars): if is_printable_utf16le(chars):
return oper.tsize / 2 return oper.tsize / 2
else: return 0
return 0
def is_printable_ascii(chars: bytes) -> bool: def is_printable_ascii(chars):
try: try:
chars_str = chars.decode("ascii") chars_str = chars.decode("ascii")
except UnicodeDecodeError: except UnicodeDecodeError:
@@ -139,13 +133,12 @@ def is_printable_ascii(chars: bytes) -> bool:
return all(c in string.printable for c in chars_str) return all(c in string.printable for c in chars_str)
def is_printable_utf16le(chars: bytes) -> bool: def is_printable_utf16le(chars):
if all(c == b"\x00" for c in chars[1::2]): if all(c == b"\x00" for c in chars[1::2]):
return is_printable_ascii(chars[::2]) return is_printable_ascii(chars[::2])
return False
def extract_features(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]: def extract_features(f, bb):
""" """
extract features from the given basic block. extract features from the given basic block.
@@ -154,12 +147,12 @@ def extract_features(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature,
bb (viv_utils.BasicBlock): the basic block to process. bb (viv_utils.BasicBlock): the basic block to process.
yields: yields:
Tuple[Feature, int]: the features and their location found in this basic block. Feature, set[VA]: the features and their location found in this basic block.
""" """
yield BasicBlock(), AbsoluteVirtualAddress(bb.inner.va) yield BasicBlock(), bb.va
for bb_handler in BASIC_BLOCK_HANDLERS: for bb_handler in BASIC_BLOCK_HANDLERS:
for feature, addr in bb_handler(f, bb): for feature, va in bb_handler(f, bb):
yield feature, addr yield feature, va
BASIC_BLOCK_HANDLERS = ( BASIC_BLOCK_HANDLERS = (

View File

@@ -1,83 +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 Any, Dict, List, Tuple, Iterator
from pathlib import Path
import viv_utils
import viv_utils.flirt
import capa.features.extractors.common
import capa.features.extractors.viv.file
import capa.features.extractors.viv.insn
import capa.features.extractors.viv.global_
import capa.features.extractors.viv.function
import capa.features.extractors.viv.basicblock
from capa.features.common import Feature
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
logger = logging.getLogger(__name__)
class VivisectFeatureExtractor(FeatureExtractor):
def __init__(self, vw, path: Path, os):
super().__init__()
self.vw = vw
self.path = path
self.buf = path.read_bytes()
# pre-compute these because we'll yield them at *every* scope.
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.viv.file.extract_file_format(self.buf))
self.global_features.extend(capa.features.extractors.common.extract_os(self.buf, os))
self.global_features.extend(capa.features.extractors.viv.global_.extract_arch(self.vw))
def get_base_address(self):
# assume there is only one file loaded into the vw
return AbsoluteVirtualAddress(list(self.vw.filemeta.values())[0]["imagebase"])
def extract_global_features(self):
yield from self.global_features
def extract_file_features(self):
yield from capa.features.extractors.viv.file.extract_features(self.vw, self.buf)
def get_functions(self) -> Iterator[FunctionHandle]:
cache: Dict[str, Any] = {}
for va in sorted(self.vw.getFunctions()):
yield FunctionHandle(
address=AbsoluteVirtualAddress(va), inner=viv_utils.Function(self.vw, va), ctx={"cache": cache}
)
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.viv.function.extract_features(fh)
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
f: viv_utils.Function = fh.inner
for bb in f.basic_blocks:
yield BBHandle(address=AbsoluteVirtualAddress(bb.va), inner=bb)
def extract_basic_block_features(self, fh: FunctionHandle, bbh) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.viv.basicblock.extract_features(fh, bbh)
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
bb: viv_utils.BasicBlock = bbh.inner
for insn in bb.instructions:
yield InsnHandle(address=AbsoluteVirtualAddress(insn.va), inner=insn)
def extract_insn_features(
self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
yield from capa.features.extractors.viv.insn.extract_features(fh, bbh, ih)
def is_library_function(self, addr):
return viv_utils.flirt.is_library_function(self.vw, addr)
def get_function_name(self, addr):
return viv_utils.get_function_name(self.vw, addr)

View File

@@ -1,62 +1,33 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
from typing import Tuple, Iterator
import PE.carve as pe_carve # vivisect PE import PE.carve as pe_carve # vivisect PE
import vivisect
import viv_utils
import viv_utils.flirt
import capa.features.insn
import capa.features.extractors.common
import capa.features.extractors.helpers import capa.features.extractors.helpers
import capa.features.extractors.strings import capa.features.extractors.strings
from capa.features.file import Export, Import, Section, FunctionName from capa.features import String, Characteristic
from capa.features.common import Feature, Characteristic from capa.features.file import Export, Import, Section
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
def extract_file_embedded_pe(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]: def extract_file_embedded_pe(vw, file_path):
for offset, _ in pe_carve.carve(buf, 1): with open(file_path, "rb") as f:
yield Characteristic("embedded pe"), FileOffsetAddress(offset) fbytes = f.read()
for offset, i in pe_carve.carve(fbytes, 1):
yield Characteristic("embedded pe"), offset
def get_first_vw_filename(vw: vivisect.VivWorkspace): def extract_file_export_names(vw, file_path):
# vivisect associates metadata with each file that its loaded into the workspace. for va, etype, name, _ in vw.getExports():
# capa only loads a single file into each workspace. yield Export(name), va
# so to access the metadata for the file in question, we can just take the first one.
# otherwise, we'd have to pass around the module name of the file we're analyzing,
# which is a pain.
#
# so this is a simplifying assumption.
return next(iter(vw.filemeta.keys()))
def extract_file_export_names(vw: vivisect.VivWorkspace, **kwargs) -> Iterator[Tuple[Feature, Address]]: def extract_file_import_names(vw, file_path):
for va, _, name, _ in vw.getExports():
yield Export(name), AbsoluteVirtualAddress(va)
if vw.getMeta("Format") == "pe":
pe = vw.parsedbin
baseaddr = pe.IMAGE_NT_HEADERS.OptionalHeader.ImageBase
for rva, _, forwarded_name in vw.getFileMeta(get_first_vw_filename(vw), "forwarders"):
try:
forwarded_name = forwarded_name.partition(b"\x00")[0].decode("ascii")
except UnicodeDecodeError:
continue
forwarded_name = capa.features.extractors.helpers.reformat_forwarded_export_name(forwarded_name)
va = baseaddr + rva
yield Export(forwarded_name), AbsoluteVirtualAddress(va)
yield Characteristic("forwarded export"), AbsoluteVirtualAddress(va)
def extract_file_import_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
""" """
extract imported function names extract imported function names
1. imports by ordinal: 1. imports by ordinal:
@@ -67,17 +38,16 @@ def extract_file_import_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]
""" """
for va, _, _, tinfo in vw.getImports(): for va, _, _, tinfo in vw.getImports():
# vivisect source: tinfo = "%s.%s" % (libname, impname) # vivisect source: tinfo = "%s.%s" % (libname, impname)
modname, impname = tinfo.split(".", 1) modname, impname = tinfo.split(".")
if is_viv_ord_impname(impname): if is_viv_ord_impname(impname):
# replace ord prefix with # # replace ord prefix with #
impname = "#" + impname[len("ord") :] impname = "#%s" % 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):
yield Import(name), addr yield Import(name), va
def is_viv_ord_impname(impname: str) -> bool: def is_viv_ord_impname(impname):
""" """
return if import name matches vivisect's ordinal naming scheme `'ord%d' % ord` return if import name matches vivisect's ordinal naming scheme `'ord%d' % ord`
""" """
@@ -91,51 +61,40 @@ def is_viv_ord_impname(impname: str) -> bool:
return True return True
def extract_file_section_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]: def extract_file_section_names(vw, file_path):
for va, _, segname, _ in vw.getSegments(): for va, _, segname, _ in vw.getSegments():
yield Section(segname), AbsoluteVirtualAddress(va) yield Section(segname), va
def extract_file_strings(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]: def extract_file_strings(vw, file_path):
yield from capa.features.extractors.common.extract_file_strings(buf)
def extract_file_function_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
""" """
extract the names of statically-linked library functions. extract ASCII and UTF-16 LE strings from file
""" """
for va in sorted(vw.getFunctions()): with open(file_path, "rb") as f:
addr = AbsoluteVirtualAddress(va) b = f.read()
if viv_utils.flirt.is_library_function(vw, va):
name = viv_utils.get_function_name(vw, va) for s in capa.features.extractors.strings.extract_ascii_strings(b):
yield FunctionName(name), addr yield String(s.s), s.offset
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions. for s in capa.features.extractors.strings.extract_unicode_strings(b):
# extract features for both the mangled and un-mangled representations. yield String(s.s), s.offset
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield FunctionName(name[1:]), addr
def extract_file_format(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]: def extract_features(vw, file_path):
yield from capa.features.extractors.common.extract_format(buf)
def extract_features(vw, buf: bytes) -> Iterator[Tuple[Feature, Address]]:
""" """
extract file features from given workspace extract file features from given workspace
args: args:
vw (vivisect.VivWorkspace): the vivisect workspace vw (vivisect.VivWorkspace): the vivisect workspace
buf: the raw input file bytes file_path: path to the input file
yields: yields:
Tuple[Feature, Address]: a feature and its location. Tuple[Feature, VA]: a feature and its location.
""" """
for file_handler in FILE_HANDLERS: for file_handler in FILE_HANDLERS:
for feature, addr in file_handler(vw=vw, buf=buf): # type: ignore for feature, va in file_handler(vw, file_path):
yield feature, addr yield feature, va
FILE_HANDLERS = ( FILE_HANDLERS = (
@@ -144,6 +103,4 @@ FILE_HANDLERS = (
extract_file_import_names, extract_file_import_names,
extract_file_section_names, extract_file_section_names,
extract_file_strings, extract_file_strings,
extract_file_function_names,
extract_file_format,
) )

View File

@@ -1,110 +1,70 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
from typing import Tuple, Iterator
import envi
import viv_utils
import vivisect.const import vivisect.const
from capa.features.file import FunctionName from capa.features import Characteristic
from capa.features.common import Feature, Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors import loops from capa.features.extractors import loops
from capa.features.extractors.elf import SymTab
from capa.features.extractors.base_extractor import FunctionHandle
def interface_extract_function_XXX(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]: def interface_extract_function_XXX(f):
""" """
parse features from the given function. parse features from the given function.
args: args:
f: the function to process. f (viv_utils.Function): the function to process.
yields: yields:
(Feature, Address): the feature and the address at which its found. (Feature, int): the feature and the address at which its found.
""" """
raise NotImplementedError yield NotImplementedError("feature"), NotImplementedError("virtual address")
def extract_function_symtab_names(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]: def extract_function_calls_to(f):
if fh.inner.vw.metadata["Format"] == "elf":
# the file's symbol table gets added to the metadata of the vivisect workspace.
# this is in order to eliminate the computational overhead of refetching symtab each time.
if "symtab" not in fh.ctx["cache"]:
try:
fh.ctx["cache"]["symtab"] = SymTab.from_viv(fh.inner.vw.parsedbin)
except Exception:
fh.ctx["cache"]["symtab"] = None
symtab = fh.ctx["cache"]["symtab"]
if symtab:
for symbol in symtab.get_symbols():
sym_name = symtab.get_name(symbol)
sym_value = symbol.value
sym_info = symbol.info
STT_FUNC = 0x2
if sym_value == fh.address and sym_info & STT_FUNC != 0:
yield FunctionName(sym_name), fh.address
def extract_function_calls_to(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
f: viv_utils.Function = fhandle.inner
for src, _, _, _ in f.vw.getXrefsTo(f.va, rtype=vivisect.const.REF_CODE): for src, _, _, _ in f.vw.getXrefsTo(f.va, rtype=vivisect.const.REF_CODE):
yield Characteristic("calls to"), AbsoluteVirtualAddress(src) yield Characteristic("calls to"), src
def extract_function_loop(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Address]]: def extract_function_loop(f):
""" """
parse if a function has a loop parse if a function has a loop
""" """
f: viv_utils.Function = fhandle.inner
edges = [] edges = []
for bb in f.basic_blocks: for bb in f.basic_blocks:
if len(bb.instructions) > 0: if len(bb.instructions) > 0:
for bva, bflags in bb.instructions[-1].getBranches(): for bva, bflags in bb.instructions[-1].getBranches():
if bva is None:
# vivisect may be unable to recover the call target, e.g. on dynamic calls like `call esi`
# for this bva is None, and we don't want to add it for loop detection, ref: vivisect#574
continue
# vivisect does not set branch flags for non-conditional jmp so add explicit check # vivisect does not set branch flags for non-conditional jmp so add explicit check
if ( if (
bflags & envi.BR_COND bflags & vivisect.envi.BR_COND
or bflags & envi.BR_FALL or bflags & vivisect.envi.BR_FALL
or bflags & envi.BR_TABLE or bflags & vivisect.envi.BR_TABLE
or bb.instructions[-1].mnem == "jmp" or bb.instructions[-1].mnem == "jmp"
): ):
edges.append((bb.va, bva)) edges.append((bb.va, bva))
if edges and loops.has_loop(edges): if edges and loops.has_loop(edges):
yield Characteristic("loop"), fhandle.address yield Characteristic("loop"), f.va
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]: def extract_features(f):
""" """
extract features from the given function. extract features from the given function.
args: args:
fh: the function handle from which to extract features f (viv_utils.Function): the function from which to extract features
yields: yields:
Tuple[Feature, int]: the features and their location found in this function. Feature, set[VA]: the features and their location found in this function.
""" """
for func_handler in FUNCTION_HANDLERS: for func_handler in FUNCTION_HANDLERS:
for feature, addr in func_handler(fh): for feature, va in func_handler(f):
yield feature, addr yield feature, va
FUNCTION_HANDLERS = ( FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)
extract_function_symtab_names,
extract_function_calls_to,
extract_function_loop,
)

View File

@@ -1,31 +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 capa.features.common import ARCH_I386, ARCH_AMD64, Arch, Feature
from capa.features.address import NO_ADDRESS, Address
logger = logging.getLogger(__name__)
def extract_arch(vw) -> Iterator[Tuple[Feature, Address]]:
arch = vw.getMeta("Architecture")
if arch == "amd64":
yield Arch(ARCH_AMD64), NO_ADDRESS
elif arch == "i386":
yield Arch(ARCH_I386), NO_ADDRESS
else:
# we likely end up here:
# 1. handling a new architecture (e.g. aarch64)
#
# for (1), this logic will need to be updated as the format is implemented.
logger.debug("unsupported architecture: %s", vw.arch.__class__.__name__)
return

View File

@@ -1,17 +1,14 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
from typing import Optional
from vivisect import VivWorkspace
from vivisect.const import XR_TO, REF_CODE from vivisect.const import XR_TO, REF_CODE
def get_coderef_from(vw: VivWorkspace, va: int) -> Optional[int]: def get_coderef_from(vw, va):
""" """
return first code `tova` whose origin is the specified va return first code `tova` whose origin is the specified va
return None if no code reference is found return None if no code reference is found

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -7,13 +7,11 @@
# See the License for the specific language governing permissions and limitations under the License. # See the License for the specific language governing permissions and limitations under the License.
import collections import collections
from typing import Set, List, Deque, Tuple, Optional
import envi import envi
import vivisect.const import vivisect.const
import envi.archs.i386.disasm import envi.archs.i386.disasm
import envi.archs.amd64.disasm import envi.archs.amd64.disasm
from vivisect import VivWorkspace
# pull out consts for lookup performance # pull out consts for lookup performance
i386RegOper = envi.archs.i386.disasm.i386RegOper i386RegOper = envi.archs.i386.disasm.i386RegOper
@@ -28,7 +26,7 @@ FAR_BRANCH_MASK = envi.BR_PROC | envi.BR_DEREF | envi.BR_ARCH
DESTRUCTIVE_MNEMONICS = ("mov", "lea", "pop", "xor") DESTRUCTIVE_MNEMONICS = ("mov", "lea", "pop", "xor")
def get_previous_instructions(vw: VivWorkspace, va: int) -> List[int]: def get_previous_instructions(vw, va):
""" """
collect the instructions that flow to the given address, local to the current function. collect the instructions that flow to the given address, local to the current function.
@@ -42,24 +40,22 @@ def get_previous_instructions(vw: VivWorkspace, va: int) -> List[int]:
ret = [] ret = []
# find the immediate prior instruction. # find the immediate prior instruction.
# ensure that it falls through to this one. # ensure that it fallsthrough to this one.
loc = vw.getPrevLocation(va, adjacent=True) loc = vw.getPrevLocation(va, adjacent=True)
if loc is not None: if loc is not None:
ploc = vw.getPrevLocation(va, adjacent=True) # from vivisect.const:
if ploc is not None: # location: (L_VA, L_SIZE, L_LTYPE, L_TINFO)
# from vivisect.const: (pva, _, ptype, pinfo) = vw.getPrevLocation(va, adjacent=True)
# location: (L_VA, L_SIZE, L_LTYPE, L_TINFO)
(pva, _, ptype, pinfo) = ploc
if ptype == LOC_OP and not (pinfo & IF_NOFALL): if ptype == LOC_OP and not (pinfo & IF_NOFALL):
ret.append(pva) ret.append(pva)
# find any code refs, e.g. jmp, to this location. # find any code refs, e.g. jmp, to this location.
# ignore any calls. # ignore any calls.
# #
# from vivisect.const: # from vivisect.const:
# xref: (XR_FROM, XR_TO, XR_RTYPE, XR_RFLAG) # xref: (XR_FROM, XR_TO, XR_RTYPE, XR_RFLAG)
for xfrom, _, _, xflag in vw.getXrefsTo(va, REF_CODE): for (xfrom, _, _, xflag) in vw.getXrefsTo(va, REF_CODE):
if (xflag & FAR_BRANCH_MASK) != 0: if (xflag & FAR_BRANCH_MASK) != 0:
continue continue
ret.append(xfrom) ret.append(xfrom)
@@ -71,7 +67,7 @@ class NotFoundError(Exception):
pass pass
def find_definition(vw: VivWorkspace, va: int, reg: int) -> Tuple[int, Optional[int]]: def find_definition(vw, va, reg):
""" """
scan backwards from the given address looking for assignments to the given register. scan backwards from the given address looking for assignments to the given register.
if a constant, return that value. if a constant, return that value.
@@ -87,8 +83,8 @@ def find_definition(vw: VivWorkspace, va: int, reg: int) -> Tuple[int, Optional[
raises: raises:
NotFoundError: when the definition cannot be found. NotFoundError: when the definition cannot be found.
""" """
q: Deque[int] = collections.deque() q = collections.deque()
seen: Set[int] = set() seen = set([])
q.extend(get_previous_instructions(vw, va)) q.extend(get_previous_instructions(vw, va))
while q: while q:
@@ -132,14 +128,14 @@ def find_definition(vw: VivWorkspace, va: int, reg: int) -> Tuple[int, Optional[
raise NotFoundError() raise NotFoundError()
def is_indirect_call(vw: VivWorkspace, va: int, insn: envi.Opcode) -> bool: def is_indirect_call(vw, va, insn=None):
if insn is None: if insn is None:
insn = vw.parseOpcode(va) insn = vw.parseOpcode(va)
return insn.mnem in ("call", "jmp") and isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper) return insn.mnem in ("call", "jmp") and isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper)
def resolve_indirect_call(vw: VivWorkspace, va: int, insn: envi.Opcode) -> Tuple[int, Optional[int]]: def resolve_indirect_call(vw, va, insn=None):
""" """
inspect the given indirect call instruction and attempt to resolve the target address. inspect the given indirect call instruction and attempt to resolve the target address.

View File

@@ -1,29 +1,26 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
from typing import List, Tuple, Callable, Iterator
import envi
import envi.exc
import viv_utils
import envi.memory import envi.memory
import viv_utils.flirt
import envi.archs.i386.regs
import envi.archs.amd64.regs
import envi.archs.i386.disasm import envi.archs.i386.disasm
import envi.archs.amd64.disasm
import capa.features.extractors.helpers import capa.features.extractors.helpers
import capa.features.extractors.viv.helpers import capa.features.extractors.viv.helpers
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset from capa.features import (
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic ARCH_X32,
from capa.features.address import Address, AbsoluteVirtualAddress ARCH_X64,
from capa.features.extractors.elf import SymTab MAX_BYTES_FEATURE_SIZE,
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle THUNK_CHAIN_DEPTH_DELTA,
Bytes,
String,
Characteristic,
)
from capa.features.insn import API, Number, Offset, Mnemonic
from capa.features.extractors.viv.indirect_calls import NotFoundError, resolve_indirect_call from capa.features.extractors.viv.indirect_calls import NotFoundError, resolve_indirect_call
# security cookie checks may perform non-zeroing XORs, these are expected within a certain # security cookie checks may perform non-zeroing XORs, these are expected within a certain
@@ -31,21 +28,27 @@ from capa.features.extractors.viv.indirect_calls import NotFoundError, resolve_i
SECURITY_COOKIE_BYTES_DELTA = 0x40 SECURITY_COOKIE_BYTES_DELTA = 0x40
def interface_extract_instruction_XXX( def get_arch(vw):
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle arch = vw.getMeta("Architecture")
) -> Iterator[Tuple[Feature, Address]]: if arch == "i386":
return ARCH_X32
elif arch == "amd64":
return ARCH_X64
def interface_extract_instruction_XXX(f, bb, insn):
""" """
parse features from the given instruction. parse features from the given instruction.
args: args:
fh: the function handle to process. f (viv_utils.Function): the function to process.
bbh: the basic block handle to process. bb (viv_utils.BasicBlock): the basic block to process.
ih: the instruction handle to process. insn (vivisect...Instruction): the instruction to process.
yields: yields:
(Feature, Address): the feature and the address at which its found. (Feature, int): the feature and the address at which its found.
""" """
raise NotImplementedError yield NotImplementedError("feature"), NotImplementedError("virtual address")
def get_imports(vw): def get_imports(vw):
@@ -65,15 +68,13 @@ def get_imports(vw):
return imports return imports
def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_insn_api_features(f, bb, insn):
""" """parse API features from the given instruction."""
parse API features from the given instruction.
# example:
#
# call dword [0x00473038]
example:
call dword [0x00473038]
"""
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
if insn.mnem not in ("call", "jmp"): if insn.mnem not in ("call", "jmp"):
return return
@@ -90,12 +91,12 @@ def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterato
if target in imports: if target in imports:
dll, symbol = imports[target] dll, symbol = imports[target]
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol): for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield API(name), ih.address yield API(name), insn.va
# call via thunk on x86, # call via thunk on x86,
# see 9324d1a8ae37a36ae560c37448c9705a at 0x407985 # see 9324d1a8ae37a36ae560c37448c9705a at 0x407985
# #
# this is also how calls to internal functions may be decoded on x32 and x64. # this is also how calls to internal functions may be decoded on x64.
# see Lab21-01.exe_:0x140001178 # see Lab21-01.exe_:0x140001178
# #
# follow chained thunks, e.g. in 82bf6347acf15e5d883715dc289d8a2b at 0x14005E0FF in # follow chained thunks, e.g. in 82bf6347acf15e5d883715dc289d8a2b at 0x14005E0FF in
@@ -110,46 +111,11 @@ def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterato
if not target: if not target:
return return
if f.vw.metadata["Format"] == "elf":
if "symtab" not in fh.ctx["cache"]:
# the symbol table gets stored as a function's attribute in order to avoid running
# this code everytime the call is made, thus preventing the computational overhead.
try:
fh.ctx["cache"]["symtab"] = SymTab.from_viv(f.vw.parsedbin)
except Exception:
fh.ctx["cache"]["symtab"] = None
symtab = fh.ctx["cache"]["symtab"]
if symtab:
for symbol in symtab.get_symbols():
sym_name = symtab.get_name(symbol)
sym_value = symbol.value
sym_info = symbol.info
STT_FUNC = 0x2
if sym_value == target and sym_info & STT_FUNC != 0:
yield API(sym_name), ih.address
if viv_utils.flirt.is_library_function(f.vw, target):
name = viv_utils.get_function_name(f.vw, target)
yield API(name), ih.address
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions.
# extract features for both the mangled and un-mangled representations.
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield API(name[1:]), ih.address
return
for _ in range(THUNK_CHAIN_DEPTH_DELTA): for _ in range(THUNK_CHAIN_DEPTH_DELTA):
if target in imports: if target in imports:
dll, symbol = imports[target] dll, symbol = imports[target]
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol): for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield API(name), ih.address yield API(name), insn.va
# if jump leads to an ENDBRANCH instruction, skip it
if f.vw.getByteDef(target)[1].startswith(b"\xf3\x0f\x1e"):
target += 4
target = capa.features.extractors.viv.helpers.get_coderef_from(f.vw, target) target = capa.features.extractors.viv.helpers.get_coderef_from(f.vw, target)
if not target: if not target:
@@ -165,7 +131,7 @@ def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterato
if target in imports: if target in imports:
dll, symbol = imports[target] dll, symbol = imports[target]
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol): for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield API(name), ih.address yield API(name), insn.va
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper): elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper):
try: try:
@@ -182,7 +148,38 @@ def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterato
if target in imports: if target in imports:
dll, symbol = imports[target] dll, symbol = imports[target]
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol): for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
yield API(name), ih.address yield API(name), insn.va
def extract_insn_number_features(f, bb, insn):
"""parse number features from the given instruction."""
# example:
#
# push 3136B0h ; dwControlCode
for oper in insn.opers:
# this is for both x32 and x64
if not isinstance(oper, (envi.archs.i386.disasm.i386ImmOper, envi.archs.i386.disasm.i386ImmMemOper)):
continue
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
v = oper.getOperValue(oper)
else:
v = oper.getOperAddr(oper)
if f.vw.probeMemory(v, 1, envi.memory.MM_READ):
# this is a valid address
# assume its not also a constant.
continue
if insn.mnem == "add" and insn.opers[0].isReg() and insn.opers[0].reg == envi.archs.i386.disasm.REG_ESP:
# skip things like:
#
# .text:00401140 call sub_407E2B
# .text:00401145 add esp, 0Ch
return
yield Number(v), insn.va
yield Number(v, arch=get_arch(f.vw)), insn.va
def derefs(vw, p): def derefs(vw, p):
@@ -196,13 +193,8 @@ def derefs(vw, p):
while True: while True:
if not vw.isValidPointer(p): if not vw.isValidPointer(p):
return return
yield p yield p
if vw.isProbablyString(p) or vw.isProbablyUnicode(p):
# don't deref strings that coincidentally are pointers
return
try: try:
next = vw.readMemoryPtr(p) next = vw.readMemoryPtr(p)
except Exception: except Exception:
@@ -222,7 +214,7 @@ def derefs(vw, p):
p = next p = next
def read_memory(vw, va: int, size: int) -> bytes: def read_memory(vw, va, size):
# as documented in #176, vivisect will not readMemory() when the section is not marked readable. # as documented in #176, vivisect will not readMemory() when the section is not marked readable.
# #
# but here, we don't care about permissions. # but here, we don't care about permissions.
@@ -235,10 +227,10 @@ def read_memory(vw, va: int, size: int) -> bytes:
mva, msize, mperms, mfname = mmap mva, msize, mperms, mfname = mmap
offset = va - mva offset = va - mva
return mbytes[offset : offset + size] return mbytes[offset : offset + size]
raise envi.exc.SegmentationViolation(va) raise envi.SegmentationViolation(va)
def read_bytes(vw, va: int) -> bytes: def read_bytes(vw, va):
""" """
read up to MAX_BYTES_FEATURE_SIZE from the given address. read up to MAX_BYTES_FEATURE_SIZE from the given address.
@@ -247,7 +239,7 @@ def read_bytes(vw, va: int) -> bytes:
""" """
segm = vw.getSegment(va) segm = vw.getSegment(va)
if not segm: if not segm:
raise envi.exc.SegmentationViolation(va) raise envi.SegmentationViolation(va)
segm_end = segm[0] + segm[1] segm_end = segm[0] + segm[1]
try: try:
@@ -256,19 +248,16 @@ def read_bytes(vw, va: int) -> bytes:
return read_memory(vw, va, segm_end - va) return read_memory(vw, va, segm_end - va)
else: else:
return read_memory(vw, va, MAX_BYTES_FEATURE_SIZE) return read_memory(vw, va, MAX_BYTES_FEATURE_SIZE)
except envi.exc.SegmentationViolation: except envi.SegmentationViolation:
raise raise
def extract_insn_bytes_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_insn_bytes_features(f, bb, insn):
""" """
parse byte sequence features from the given instruction. parse byte sequence features from the given instruction.
example: example:
# push offset iid_004118d4_IShellLinkA ; riid # push offset iid_004118d4_IShellLinkA ; riid
""" """
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
if insn.mnem == "call": if insn.mnem == "call":
return return
@@ -288,39 +277,30 @@ def extract_insn_bytes_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera
else: else:
continue continue
for vv in derefs(f.vw, v): for v in derefs(f.vw, v):
try: try:
buf = read_bytes(f.vw, vv) buf = read_bytes(f.vw, v)
except envi.exc.SegmentationViolation: except envi.SegmentationViolation:
continue continue
if capa.features.extractors.helpers.all_zeros(buf): if capa.features.extractors.helpers.all_zeros(buf):
continue continue
if f.vw.isProbablyString(vv) or f.vw.isProbablyUnicode(vv): yield Bytes(buf), insn.va
# don't extract byte features for obvious strings
continue
yield Bytes(buf), ih.address
def read_string(vw, offset: int) -> str: def read_string(vw, offset):
try: try:
alen = vw.detectString(offset) alen = vw.detectString(offset)
except envi.exc.SegmentationViolation: except envi.SegmentationViolation:
pass pass
else: else:
if alen > 0: if alen > 0:
buf = read_memory(vw, offset, alen) return read_memory(vw, offset, alen).decode("utf-8")
if b"\x00" in buf:
# account for bug #1271.
# remove when vivisect is fixed.
buf = buf.partition(b"\x00")[0]
return buf.decode("utf-8")
try: try:
ulen = vw.detectUnicode(offset) ulen = vw.detectUnicode(offset)
except envi.exc.SegmentationViolation: except envi.SegmentationViolation:
pass pass
except IndexError: except IndexError:
# potential vivisect bug detecting Unicode at segment end # potential vivisect bug detecting Unicode at segment end
@@ -335,24 +315,92 @@ def read_string(vw, offset: int) -> str:
# vivisect seems to mis-detect the end unicode strings # vivisect seems to mis-detect the end unicode strings
# off by two, too short # off by two, too short
ulen += 2 ulen += 2
# partition to account for bug #1271. return read_memory(vw, offset, ulen).decode("utf-16")
# remove when vivisect is fixed.
return read_memory(vw, offset, ulen).decode("utf-16").partition("\x00")[0]
raise ValueError("not a string", offset) raise ValueError("not a string", offset)
def is_security_cookie(f, bb, insn) -> bool: def extract_insn_string_features(f, bb, insn):
"""parse string features from the given instruction."""
# example:
#
# push offset aAcr ; "ACR > "
for oper in insn.opers:
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
v = oper.getOperValue(oper)
elif isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper):
# like 0x10056CB4 in `lea eax, dword [0x10056CB4]`
v = oper.imm
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
# like 0x401000 in `mov eax, 0x401000[2 * ebx]`
v = oper.imm
elif isinstance(oper, envi.archs.amd64.disasm.Amd64RipRelOper):
v = oper.getOperAddr(insn)
else:
continue
for v in derefs(f.vw, v):
try:
s = read_string(f.vw, v)
except ValueError:
continue
else:
yield String(s.rstrip("\x00")), insn.va
def extract_insn_offset_features(f, bb, insn):
"""parse structure offset features from the given instruction."""
# example:
#
# .text:0040112F cmp [esi+4], ebx
for oper in insn.opers:
# this is for both x32 and x64
# like [esi + 4]
# reg ^
# disp
if isinstance(oper, envi.archs.i386.disasm.i386RegMemOper):
if oper.reg == envi.archs.i386.disasm.REG_ESP:
continue
if oper.reg == envi.archs.i386.disasm.REG_EBP:
continue
# TODO: do x64 support for real.
if oper.reg == envi.archs.amd64.disasm.REG_RBP:
continue
# viv already decodes offsets as signed
v = oper.disp
yield Offset(v), insn.va
yield Offset(v, arch=get_arch(f.vw)), insn.va
# like: [esi + ecx + 16384]
# reg ^ ^
# index ^
# disp
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
# viv already decodes offsets as signed
v = oper.disp
yield Offset(v), insn.va
yield Offset(v, arch=get_arch(f.vw)), insn.va
def is_security_cookie(f, bb, insn):
""" """
check if an instruction is related to security cookie checks check if an instruction is related to security cookie checks
""" """
# security cookie check should use SP or BP # security cookie check should use SP or BP
oper = insn.opers[1] oper = insn.opers[1]
if oper.isReg() and oper.reg not in [ if oper.isReg() and oper.reg not in [
envi.archs.i386.regs.REG_ESP, envi.archs.i386.disasm.REG_ESP,
envi.archs.i386.regs.REG_EBP, envi.archs.i386.disasm.REG_EBP,
envi.archs.amd64.regs.REG_RBP, # TODO: do x64 support for real.
envi.archs.amd64.regs.REG_RSP, envi.archs.amd64.disasm.REG_RBP,
envi.archs.amd64.disasm.REG_RSP,
]: ]:
return False return False
@@ -369,17 +417,11 @@ def is_security_cookie(f, bb, insn) -> bool:
return False return False
def extract_insn_nzxor_characteristic_features( def extract_insn_nzxor_characteristic_features(f, bb, insn):
fh: FunctionHandle, bbhandle: BBHandle, ih: InsnHandle
) -> Iterator[Tuple[Feature, Address]]:
""" """
parse non-zeroing XOR instruction from the given instruction. parse non-zeroing XOR instruction from the given instruction.
ignore expected non-zeroing XORs, e.g. security cookies. ignore expected non-zeroing XORs, e.g. security cookies.
""" """
insn: envi.Opcode = ih.inner
bb: viv_utils.BasicBlock = bbhandle.inner
f: viv_utils.Function = fh.inner
if insn.mnem not in ("xor", "xorpd", "xorps", "pxor"): if insn.mnem not in ("xor", "xorpd", "xorps", "pxor"):
return return
@@ -389,37 +431,19 @@ def extract_insn_nzxor_characteristic_features(
if is_security_cookie(f, bb, insn): if is_security_cookie(f, bb, insn):
return return
yield Characteristic("nzxor"), ih.address yield Characteristic("nzxor"), insn.va
def extract_insn_mnemonic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_insn_mnemonic_features(f, bb, insn):
"""parse mnemonic features from the given instruction.""" """parse mnemonic features from the given instruction."""
yield Mnemonic(ih.inner.mnem), ih.address yield Mnemonic(insn.mnem), insn.va
def extract_insn_obfs_call_plus_5_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_insn_peb_access_characteristic_features(f, bb, insn):
"""
parse call $+5 instruction from the given instruction.
"""
insn: envi.Opcode = ih.inner
if insn.mnem != "call":
return
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386PcRelOper):
if insn.va + 5 == insn.opers[0].getOperValue(insn):
yield Characteristic("call $+5"), ih.address
if isinstance(insn.opers[0], (envi.archs.i386.disasm.i386ImmMemOper, envi.archs.amd64.disasm.Amd64RipRelOper)):
if insn.va + 5 == insn.opers[0].getOperAddr(insn):
yield Characteristic("call $+5"), ih.address
def extract_insn_peb_access_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
""" """
parse peb access from the given function. fs:[0x30] on x86, gs:[0x60] on x64 parse peb access from the given function. fs:[0x30] on x86, gs:[0x60] on x64
""" """
insn: envi.Opcode = ih.inner # TODO handle where fs/gs are loaded into a register or onto the stack and used later
if insn.mnem not in ["push", "mov"]: if insn.mnem not in ["push", "mov"]:
return return
@@ -438,7 +462,7 @@ def extract_insn_peb_access_characteristic_features(f, bb, ih: InsnHandle) -> It
if (isinstance(oper, envi.archs.i386.disasm.i386RegMemOper) and oper.disp == 0x30) or ( if (isinstance(oper, envi.archs.i386.disasm.i386RegMemOper) and oper.disp == 0x30) or (
isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper) and oper.imm == 0x30 isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper) and oper.imm == 0x30
): ):
yield Characteristic("peb access"), ih.address yield Characteristic("peb access"), insn.va
elif "gs" in prefix: elif "gs" in prefix:
for oper in insn.opers: for oper in insn.opers:
if ( if (
@@ -446,25 +470,23 @@ def extract_insn_peb_access_characteristic_features(f, bb, ih: InsnHandle) -> It
or (isinstance(oper, envi.archs.amd64.disasm.i386SibOper) and oper.imm == 0x60) or (isinstance(oper, envi.archs.amd64.disasm.i386SibOper) and oper.imm == 0x60)
or (isinstance(oper, envi.archs.amd64.disasm.i386ImmMemOper) and oper.imm == 0x60) or (isinstance(oper, envi.archs.amd64.disasm.i386ImmMemOper) and oper.imm == 0x60)
): ):
yield Characteristic("peb access"), ih.address yield Characteristic("peb access"), insn.va
else: else:
pass pass
def extract_insn_segment_access_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_insn_segment_access_features(f, bb, insn):
"""parse the instruction for access to fs or gs""" """ parse the instruction for access to fs or gs """
insn: envi.Opcode = ih.inner
prefix = insn.getPrefixName() prefix = insn.getPrefixName()
if prefix == "fs": if prefix == "fs":
yield Characteristic("fs access"), ih.address yield Characteristic("fs access"), insn.va
if prefix == "gs": if prefix == "gs":
yield Characteristic("gs access"), ih.address yield Characteristic("gs access"), insn.va
def get_section(vw, va: int): def get_section(vw, va):
for start, length, _, __ in vw.getMemoryMaps(): for start, length, _, __ in vw.getMemoryMaps():
if start <= va < start + length: if start <= va < start + length:
return start return start
@@ -472,13 +494,10 @@ def get_section(vw, va: int):
raise KeyError(va) raise KeyError(va)
def extract_insn_cross_section_cflow(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_insn_cross_section_cflow(f, bb, insn):
""" """
inspect the instruction for a CALL or JMP that crosses section boundaries. inspect the instruction for a CALL or JMP that crosses section boundaries.
""" """
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
for va, flags in insn.getBranches(): for va, flags in insn.getBranches():
if va is None: if va is None:
# va may be none for dynamic branches that haven't been resolved, such as `jmp eax`. # va may be none for dynamic branches that haven't been resolved, such as `jmp eax`.
@@ -505,7 +524,7 @@ def extract_insn_cross_section_cflow(fh: FunctionHandle, bb, ih: InsnHandle) ->
continue continue
if get_section(f.vw, insn.va) != get_section(f.vw, va): if get_section(f.vw, insn.va) != get_section(f.vw, va):
yield Characteristic("cross section flow"), ih.address yield Characteristic("cross section flow"), insn.va
except KeyError: except KeyError:
continue continue
@@ -513,10 +532,7 @@ def extract_insn_cross_section_cflow(fh: FunctionHandle, bb, ih: InsnHandle) ->
# this is a feature that's most relevant at the function scope, # this is a feature that's most relevant at the function scope,
# however, its most efficient to extract at the instruction scope. # however, its most efficient to extract at the instruction scope.
def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_function_calls_from(f, bb, insn):
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
if insn.mnem != "call": if insn.mnem != "call":
return return
@@ -526,8 +542,7 @@ def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper): if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper):
oper = insn.opers[0] oper = insn.opers[0]
target = oper.getOperAddr(insn) target = oper.getOperAddr(insn)
if target >= 0: yield Characteristic("calls from"), target
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
# call via thunk on x86, # call via thunk on x86,
# see 9324d1a8ae37a36ae560c37448c9705a at 0x407985 # see 9324d1a8ae37a36ae560c37448c9705a at 0x407985
@@ -536,192 +551,43 @@ def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera
# see Lab21-01.exe_:0x140001178 # see Lab21-01.exe_:0x140001178
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386PcRelOper): elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386PcRelOper):
target = insn.opers[0].getOperValue(insn) target = insn.opers[0].getOperValue(insn)
if target >= 0: yield Characteristic("calls from"), target
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
# call via IAT, x64 # call via IAT, x64
elif isinstance(insn.opers[0], envi.archs.amd64.disasm.Amd64RipRelOper): elif isinstance(insn.opers[0], envi.archs.amd64.disasm.Amd64RipRelOper):
op = insn.opers[0] op = insn.opers[0]
target = op.getOperAddr(insn) target = op.getOperAddr(insn)
if target >= 0: yield Characteristic("calls from"), target
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
if target and target == f.va: if target and target == f.va:
# if we found a jump target and it's the function address # if we found a jump target and it's the function address
# mark as recursive # mark as recursive
yield Characteristic("recursive call"), AbsoluteVirtualAddress(target) yield Characteristic("recursive call"), target
# this is a feature that's most relevant at the function or basic block scope, # this is a feature that's most relevant at the function or basic block scope,
# however, its most efficient to extract at the instruction scope. # however, its most efficient to extract at the instruction scope.
def extract_function_indirect_call_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: def extract_function_indirect_call_characteristic_features(f, bb, insn):
""" """
extract indirect function call characteristic (e.g., call eax or call dword ptr [edx+4]) extract indirect function call characteristic (e.g., call eax or call dword ptr [edx+4])
does not include calls like => call ds:dword_ABD4974 does not include calls like => call ds:dword_ABD4974
""" """
insn: envi.Opcode = ih.inner
if insn.mnem != "call": if insn.mnem != "call":
return return
# Checks below work for x86 and x64 # Checks below work for x86 and x64
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper): if isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper):
# call edx # call edx
yield Characteristic("indirect call"), ih.address yield Characteristic("indirect call"), insn.va
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegMemOper): elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegMemOper):
# call dword ptr [eax+50h] # call dword ptr [eax+50h]
yield Characteristic("indirect call"), ih.address yield Characteristic("indirect call"), insn.va
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386SibOper): elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386SibOper):
# call qword ptr [rsp+78h] # call qword ptr [rsp+78h]
yield Characteristic("indirect call"), ih.address yield Characteristic("indirect call"), insn.va
def extract_op_number_features( def extract_features(f, bb, insn):
fh: FunctionHandle, bb, ih: InsnHandle, i, oper: envi.Operand
) -> Iterator[Tuple[Feature, Address]]:
"""parse number features from the given operand.
example:
push 3136B0h ; dwControlCode
"""
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
# this is for both x32 and x64
if not isinstance(oper, (envi.archs.i386.disasm.i386ImmOper, envi.archs.i386.disasm.i386ImmMemOper)):
return
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
v = oper.getOperValue(oper)
else:
v = oper.getOperAddr(oper)
if f.vw.probeMemory(v, 1, envi.memory.MM_READ):
# this is a valid address
# assume its not also a constant.
return
if insn.mnem == "add" and insn.opers[0].isReg() and insn.opers[0].reg == envi.archs.i386.regs.REG_ESP:
# skip things like:
#
# .text:00401140 call sub_407E2B
# .text:00401145 add esp, 0Ch
return
yield Number(v), ih.address
yield OperandNumber(i, v), ih.address
if insn.mnem == "add" and 0 < v < MAX_STRUCTURE_SIZE and isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
# for pattern like:
#
# add eax, 0x10
#
# assume 0x10 is also an offset (imagine eax is a pointer).
yield Offset(v), ih.address
yield OperandOffset(i, v), ih.address
def extract_op_offset_features(
fh: FunctionHandle, bb, ih: InsnHandle, i, oper: envi.Operand
) -> Iterator[Tuple[Feature, Address]]:
"""parse structure offset features from the given operand."""
# example:
#
# .text:0040112F cmp [esi+4], ebx
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
# this is for both x32 and x64
# like [esi + 4]
# reg ^
# disp
if isinstance(oper, envi.archs.i386.disasm.i386RegMemOper):
if oper.reg == envi.archs.i386.regs.REG_ESP:
return
if oper.reg == envi.archs.i386.regs.REG_EBP:
return
if oper.reg == envi.archs.amd64.regs.REG_RBP:
return
# viv already decodes offsets as signed
v = oper.disp
yield Offset(v), ih.address
yield OperandOffset(i, v), ih.address
if insn.mnem == "lea" and i == 1 and not f.vw.probeMemory(v, 1, envi.memory.MM_READ):
# for pattern like:
#
# lea eax, [ebx + 1]
#
# assume 1 is also an offset (imagine ebx is a zero register).
yield Number(v), ih.address
yield OperandNumber(i, v), ih.address
# like: [esi + ecx + 16384]
# reg ^ ^
# index ^
# disp
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
# viv already decodes offsets as signed
v = oper.disp
yield Offset(v), ih.address
yield OperandOffset(i, v), ih.address
def extract_op_string_features(
fh: FunctionHandle, bb, ih: InsnHandle, i, oper: envi.Operand
) -> Iterator[Tuple[Feature, Address]]:
"""parse string features from the given operand."""
# example:
#
# push offset aAcr ; "ACR > "
insn: envi.Opcode = ih.inner
f: viv_utils.Function = fh.inner
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
v = oper.getOperValue(oper)
elif isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper):
# like 0x10056CB4 in `lea eax, dword [0x10056CB4]`
v = oper.imm
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
# like 0x401000 in `mov eax, 0x401000[2 * ebx]`
v = oper.imm
elif isinstance(oper, envi.archs.amd64.disasm.Amd64RipRelOper):
v = oper.getOperAddr(insn)
else:
return
for vv in derefs(f.vw, v):
try:
s = read_string(f.vw, vv).rstrip("\x00")
except ValueError:
continue
else:
if len(s) >= 4:
yield String(s), ih.address
def extract_operand_features(f: FunctionHandle, bb, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
for i, oper in enumerate(insn.inner.opers):
for op_handler in OPERAND_HANDLERS:
for feature, addr in op_handler(f, bb, insn, i, oper):
yield feature, addr
OPERAND_HANDLERS: List[
Callable[[FunctionHandle, BBHandle, InsnHandle, int, envi.Operand], Iterator[Tuple[Feature, Address]]]
] = [
extract_op_number_features,
extract_op_offset_features,
extract_op_string_features,
]
def extract_features(f, bb, insn) -> Iterator[Tuple[Feature, Address]]:
""" """
extract features from the given insn. extract features from the given insn.
@@ -731,23 +597,24 @@ def extract_features(f, bb, insn) -> Iterator[Tuple[Feature, Address]]:
insn (vivisect...Instruction): the instruction to process. insn (vivisect...Instruction): the instruction to process.
yields: yields:
Tuple[Feature, Address]: the features and their location found in this insn. Feature, set[VA]: the features and their location found in this insn.
""" """
for insn_handler in INSTRUCTION_HANDLERS: for insn_handler in INSTRUCTION_HANDLERS:
for feature, addr in insn_handler(f, bb, insn): for feature, va in insn_handler(f, bb, insn):
yield feature, addr yield feature, va
INSTRUCTION_HANDLERS: List[Callable[[FunctionHandle, BBHandle, InsnHandle], Iterator[Tuple[Feature, Address]]]] = [ INSTRUCTION_HANDLERS = (
extract_insn_api_features, extract_insn_api_features,
extract_insn_number_features,
extract_insn_string_features,
extract_insn_bytes_features, extract_insn_bytes_features,
extract_insn_offset_features,
extract_insn_nzxor_characteristic_features, extract_insn_nzxor_characteristic_features,
extract_insn_mnemonic_features, extract_insn_mnemonic_features,
extract_insn_obfs_call_plus_5_characteristic_features,
extract_insn_peb_access_characteristic_features, extract_insn_peb_access_characteristic_features,
extract_insn_cross_section_cflow, extract_insn_cross_section_cflow,
extract_insn_segment_access_features, extract_insn_segment_access_features,
extract_function_calls_from, extract_function_calls_from,
extract_function_indirect_call_characteristic_features, extract_function_indirect_call_characteristic_features,
extract_operand_features, )
]

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,33 +6,22 @@
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 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. # See the License for the specific language governing permissions and limitations under the License.
from capa.features.common import Feature from capa.features import Feature
class Export(Feature): class Export(Feature):
def __init__(self, value: str, description=None): def __init__(self, value, description=None):
# value is export name # value is export name
super().__init__(value, description=description) super(Export, self).__init__(value, description=description)
class Import(Feature): class Import(Feature):
def __init__(self, value: str, description=None): def __init__(self, value, description=None):
# value is import name # value is import name
super().__init__(value, description=description) super(Import, self).__init__(value, description=description)
class Section(Feature): class Section(Feature):
def __init__(self, value: str, description=None): def __init__(self, value, description=None):
# value is section name # value is section name
super().__init__(value, description=description) super(Section, self).__init__(value, description=description)
class FunctionName(Feature):
"""recognized name for statically linked function"""
def __init__(self, name: str, description=None):
# value is function name
super().__init__(name, description=description)
# override the name property set by `capa.features.Feature`
# that would be `functionname` (note missing dash)
self.name = "function-name"

299
capa/features/freeze.py Normal file
View File

@@ -0,0 +1,299 @@
"""
capa freeze file format: `| capa0000 | + zlib(utf-8(json(...)))`
json format:
{
'version': 1,
'base address': int(base address),
'functions': {
int(function va): {
'basic blocks': {
int(basic block va): {
'instructions': [instruction va, ...]
},
...
},
...
},
...
},
'scopes': {
'file': [
(str(name), [any(arg), ...], int(va), ()),
...
},
'function': [
(str(name), [any(arg), ...], int(va), (int(function va), )),
...
],
'basic block': [
(str(name), [any(arg), ...], int(va), (int(function va),
int(basic block va))),
...
],
'instruction': [
(str(name), [any(arg), ...], int(va), (int(function va),
int(basic block va),
int(instruction va))),
...
],
}
}
Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at: [package root]/LICENSE.txt
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
"""
import json
import zlib
import logging
import capa.features
import capa.features.file
import capa.features.insn
import capa.features.basicblock
import capa.features.extractors
from capa.helpers import hex
logger = logging.getLogger(__name__)
def serialize_feature(feature):
return feature.freeze_serialize()
KNOWN_FEATURES = {F.__name__: F for F in capa.features.Feature.__subclasses__()}
def deserialize_feature(doc):
F = KNOWN_FEATURES[doc[0]]
return F.freeze_deserialize(doc[1])
def dumps(extractor):
"""
serialize the given extractor to a string
args:
extractor: capa.features.extractor.FeatureExtractor:
returns:
str: the serialized features.
"""
ret = {
"version": 1,
"base address": extractor.get_base_address(),
"functions": {},
"scopes": {
"file": [],
"function": [],
"basic block": [],
"instruction": [],
},
}
for feature, va in extractor.extract_file_features():
ret["scopes"]["file"].append(serialize_feature(feature) + (hex(va), ()))
for f in extractor.get_functions():
ret["functions"][hex(f)] = {}
for feature, va in extractor.extract_function_features(f):
ret["scopes"]["function"].append(serialize_feature(feature) + (hex(va), (hex(f),)))
for bb in extractor.get_basic_blocks(f):
ret["functions"][hex(f)][hex(bb)] = []
for feature, va in extractor.extract_basic_block_features(f, bb):
ret["scopes"]["basic block"].append(
serialize_feature(feature)
+ (
hex(va),
(
hex(f),
hex(bb),
),
)
)
for insnva, insn in sorted(
[(insn.__int__(), insn) for insn in extractor.get_instructions(f, bb)], key=lambda p: p[0]
):
ret["functions"][hex(f)][hex(bb)].append(hex(insnva))
for feature, va in extractor.extract_insn_features(f, bb, insn):
ret["scopes"]["instruction"].append(
serialize_feature(feature)
+ (
hex(va),
(
hex(f),
hex(bb),
hex(insnva),
),
)
)
return json.dumps(ret)
def loads(s):
"""deserialize a set of features (as a NullFeatureExtractor) from a string."""
doc = json.loads(s)
if doc.get("version") != 1:
raise ValueError("unsupported freeze format version: %d" % (doc.get("version")))
features = {
"base address": doc.get("base address"),
"file features": [],
"functions": {},
}
for fva, function in doc.get("functions", {}).items():
fva = int(fva, 0x10)
features["functions"][fva] = {
"features": [],
"basic blocks": {},
}
for bbva, bb in function.items():
bbva = int(bbva, 0x10)
features["functions"][fva]["basic blocks"][bbva] = {
"features": [],
"instructions": {},
}
for insnva in bb:
insnva = int(insnva, 0x10)
features["functions"][fva]["basic blocks"][bbva]["instructions"][insnva] = {
"features": [],
}
# in the following blocks, each entry looks like:
#
# ('MatchedRule', ('foo', ), '0x401000', ('0x401000', ))
# ^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^
# feature name args addr func/bb/insn
for feature in doc.get("scopes", {}).get("file", []):
va, loc = feature[2:]
va = int(va, 0x10)
feature = deserialize_feature(feature[:2])
features["file features"].append((va, feature))
for feature in doc.get("scopes", {}).get("function", []):
# fetch the pair like:
#
# ('0x401000', ('0x401000', ))
# ^^^^^^^^^^ ^^^^^^^^^^^^^^
# addr func/bb/insn
va, loc = feature[2:]
va = int(va, 0x10)
loc = [int(lo, 0x10) for lo in loc]
# decode the feature from the pair like:
#
# ('MatchedRule', ('foo', ))
# ^^^^^^^^^^^^^ ^^^^^^^^^
# feature name args
feature = deserialize_feature(feature[:2])
features["functions"][loc[0]]["features"].append((va, feature))
for feature in doc.get("scopes", {}).get("basic block", []):
va, loc = feature[2:]
va = int(va, 0x10)
loc = [int(lo, 0x10) for lo in loc]
feature = deserialize_feature(feature[:2])
features["functions"][loc[0]]["basic blocks"][loc[1]]["features"].append((va, feature))
for feature in doc.get("scopes", {}).get("instruction", []):
va, loc = feature[2:]
va = int(va, 0x10)
loc = [int(lo, 0x10) for lo in loc]
feature = deserialize_feature(feature[:2])
features["functions"][loc[0]]["basic blocks"][loc[1]]["instructions"][loc[2]]["features"].append((va, feature))
return capa.features.extractors.NullFeatureExtractor(features)
MAGIC = "capa0000".encode("ascii")
def dump(extractor):
"""serialize the given extractor to a byte array."""
return MAGIC + zlib.compress(dumps(extractor).encode("utf-8"))
def is_freeze(buf):
return buf[: len(MAGIC)] == MAGIC
def load(buf):
"""deserialize a set of features (as a NullFeatureExtractor) from a byte array."""
if not is_freeze(buf):
raise ValueError("missing magic header")
return loads(zlib.decompress(buf[len(MAGIC) :]).decode("utf-8"))
def main(argv=None):
import sys
import argparse
import capa.main
if argv is None:
argv = sys.argv[1:]
formats = [
("auto", "(default) detect file type automatically"),
("pe", "Windows PE file"),
("sc32", "32-bit shellcode"),
("sc64", "64-bit shellcode"),
]
format_help = ", ".join(["%s: %s" % (f[0], f[1]) for f in formats])
parser = argparse.ArgumentParser(description="save capa features to a file")
parser.add_argument("sample", type=str, help="Path to sample to analyze")
parser.add_argument("output", type=str, help="Path to output file")
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
parser.add_argument("-q", "--quiet", action="store_true", help="Disable all output but errors")
parser.add_argument(
"-f", "--format", choices=[f[0] for f in formats], default="auto", help="Select sample format, %s" % format_help
)
if sys.version_info >= (3, 0):
parser.add_argument(
"-b",
"--backend",
type=str,
help="select the backend to use",
choices=(capa.main.BACKEND_VIV, capa.main.BACKEND_SMDA),
default=capa.main.BACKEND_VIV,
)
args = parser.parse_args(args=argv)
if args.quiet:
logging.basicConfig(level=logging.ERROR)
logging.getLogger().setLevel(logging.ERROR)
elif args.verbose:
logging.basicConfig(level=logging.DEBUG)
logging.getLogger().setLevel(logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
logging.getLogger().setLevel(logging.INFO)
backend = args.backend if sys.version_info > (3, 0) else capa.main.BACKEND_VIV
extractor = capa.main.get_extractor(args.sample, args.format, backend)
with open(args.output, "wb") as f:
f.write(dump(extractor))
return 0
if __name__ == "__main__":
import sys
sys.exit(main())

View File

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

View File

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

View File

@@ -1,169 +1,40 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
import abc
from typing import Union, Optional
import capa.helpers from capa.features import Feature
from capa.features.common import VALID_FEATURE_ACCESS, Feature
def hex(n: int) -> str:
"""render the given number using upper case hex, like: 0x123ABC"""
if n < 0:
return f"-0x{(-n):X}"
else:
return f"0x{(n):X}"
class API(Feature): class API(Feature):
def __init__(self, name: str, description=None): def __init__(self, name, description=None):
super().__init__(name, description=description) # Downcase library name if given
if "." in name:
modname, _, impname = name.rpartition(".")
name = modname.lower() + "." + impname
super(API, self).__init__(name, description=description)
class _AccessFeature(Feature, abc.ABC):
# superclass: don't use directly
def __init__(self, value: str, access: Optional[str] = None, description: Optional[str] = None):
super().__init__(value, description=description)
if access is not None:
if access not in VALID_FEATURE_ACCESS:
raise ValueError(f"{self.name} access type {access} not valid")
self.access = access
def __hash__(self):
return hash((self.name, self.value, self.access))
def __eq__(self, other):
return super().__eq__(other) and self.access == other.access
def get_name_str(self) -> str:
if self.access is not None:
return f"{self.name}/{self.access}"
return self.name
class Property(_AccessFeature):
def __init__(self, value: str, access: Optional[str] = None, description=None):
super().__init__(value, access=access, description=description)
class Number(Feature): class Number(Feature):
def __init__(self, value: Union[int, float], description=None): def __init__(self, value, arch=None, description=None):
""" super(Number, self).__init__(value, arch=arch, description=description)
args:
value (int or float): positive or negative integer, or floating point number.
the range of the value is:
- if positive, the range of u64
- if negative, the range of i64
- if floating, the range and precision of double
"""
super().__init__(value, description=description)
def get_value_str(self): def get_value_str(self):
if isinstance(self.value, int): return "0x%X" % self.value
return capa.helpers.hex(self.value)
elif isinstance(self.value, float):
return str(self.value)
else:
raise ValueError(f"invalid value type {type(self.value)}")
# max recognized structure size (and therefore, offset size)
MAX_STRUCTURE_SIZE = 0x10000
class Offset(Feature): class Offset(Feature):
def __init__(self, value: int, description=None): def __init__(self, value, arch=None, description=None):
""" super(Offset, self).__init__(value, arch=arch, description=description)
args:
value (int): the offset, which can be positive or negative.
the range of the value is:
- if positive, the range of u64
- if negative, the range of i64
"""
super().__init__(value, description=description)
def get_value_str(self): def get_value_str(self):
assert isinstance(self.value, int) return "0x%X" % self.value
return hex(self.value)
class Mnemonic(Feature): class Mnemonic(Feature):
def __init__(self, value: str, description=None): def __init__(self, value, description=None):
super().__init__(value, description=description) super(Mnemonic, self).__init__(value, description=description)
# max number of operands to consider for a given instruction.
# since we only support Intel and .NET, we can assume this is 3
# which covers cases up to e.g. "vinserti128 ymm0,ymm0,ymm5,1"
MAX_OPERAND_COUNT = 4
MAX_OPERAND_INDEX = MAX_OPERAND_COUNT - 1
class _Operand(Feature, abc.ABC):
# superclass: don't use directly
# subclasses should set self.name and provide the value string formatter
def __init__(self, index: int, value: Union[int, float], description=None):
super().__init__(value, description=description)
self.index = index
def __hash__(self):
return hash((self.name, self.value))
def __eq__(self, other):
return super().__eq__(other) and self.index == other.index
class OperandNumber(_Operand):
# cached names so we don't do extra string formatting every ctor
NAMES = [f"operand[{i}].number" for i in range(MAX_OPERAND_COUNT)]
# operand[i].number: 0x12
def __init__(self, index: int, value: Union[int, float], description=None):
"""
args:
value (int or float): positive or negative integer, or floating point number.
the range of the value is:
- if positive, the range of u64
- if negative, the range of i64
- if floating, the range and precision of double
"""
super().__init__(index, value, description=description)
self.name = self.NAMES[index]
def get_value_str(self) -> str:
if isinstance(self.value, int):
return capa.helpers.hex(self.value)
elif isinstance(self.value, float):
return str(self.value)
else:
raise ValueError("invalid value type")
class OperandOffset(_Operand):
# cached names so we don't do extra string formatting every ctor
NAMES = [f"operand[{i}].offset" for i in range(MAX_OPERAND_COUNT)]
# operand[i].offset: 0x12
def __init__(self, index: int, value: int, description=None):
"""
args:
value (int): the offset, which can be positive or negative.
the range of the value is:
- if positive, the range of u64
- if negative, the range of i64
"""
super().__init__(index, value, description=description)
self.name = self.NAMES[index]
def get_value_str(self) -> str:
assert isinstance(self.value, int)
return hex(self.value)

View File

@@ -1,159 +1,36 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
import inspect
import logging
import contextlib
import importlib.util
from typing import NoReturn
from pathlib import Path
import tqdm import os
from capa.exceptions import UnsupportedFormatError _hex = hex
from capa.features.common import FORMAT_PE, FORMAT_SC32, FORMAT_SC64, FORMAT_DOTNET, FORMAT_UNKNOWN, Format
EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32")
EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64")
EXTENSIONS_ELF = "elf_"
logger = logging.getLogger("capa")
def hex(n: int) -> str: def hex(i):
"""render the given number using upper case hex, like: 0x123ABC""" # under py2.7, long integers get formatted with a trailing `L`
if n < 0: # and this is not pretty. so strip it out.
return f"-0x{(-n):X}" return _hex(oint(i)).rstrip("L")
else:
return f"0x{(n):X}"
def get_file_taste(sample_path: Path) -> bytes: def oint(i):
if not sample_path.exists(): # there seems to be some trouble with using `int(viv_utils.Function)`
raise IOError(f"sample path {sample_path} does not exist or cannot be accessed") # with the black magic we do with binding the `__int__()` routine.
taste = sample_path.open("rb").read(8) # i haven't had a chance to debug this yet (and i have no hotel wifi).
return taste # so in the meantime, detect this, and call the method directly.
def is_runtime_ida():
return importlib.util.find_spec("idc") is not None
def assert_never(value) -> NoReturn:
# careful: python -O will remove this assertion.
# but this is only used for type checking, so it's ok.
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
return FORMAT_UNKNOWN
def get_auto_format(path: Path) -> str:
format_ = get_format(path)
if format_ == FORMAT_UNKNOWN:
format_ = get_format_from_extension(path)
if format_ == FORMAT_UNKNOWN:
raise UnsupportedFormatError()
return format_
def get_format(sample: Path) -> str:
# imported locally to avoid import cycle
from capa.features.extractors.common import extract_format
from capa.features.extractors.dnfile_ import DnfileFeatureExtractor
buf = sample.read_bytes()
for feature, _ in extract_format(buf):
if feature == Format(FORMAT_PE):
dnfile_extractor = DnfileFeatureExtractor(sample)
if dnfile_extractor.is_dotnet_file():
feature = Format(FORMAT_DOTNET)
assert isinstance(feature.value, str)
return feature.value
return FORMAT_UNKNOWN
@contextlib.contextmanager
def redirecting_print_to_tqdm(disable_progress):
"""
tqdm (progress bar) expects to have fairly tight control over console output.
so calls to `print()` will break the progress bar and make things look bad.
so, this context manager temporarily replaces the `print` implementation
with one that is compatible with tqdm.
via: https://stackoverflow.com/a/42424890/87207
"""
old_print = print # noqa: T202 [reserved word print used]
def new_print(*args, **kwargs):
# If tqdm.tqdm.write raises error, use builtin print
if disable_progress:
old_print(*args, **kwargs)
else:
try:
tqdm.tqdm.write(*args, **kwargs)
except Exception:
old_print(*args, **kwargs)
try: try:
# Globally replace print with new_print. return int(i)
# Verified this works manually on Python 3.11: except TypeError:
# >>> import inspect return i.__int__()
# >>> inspect.builtins
# <module 'builtins' (built-in)>
inspect.builtins.print = new_print # type: ignore
yield
finally:
inspect.builtins.print = old_print # type: ignore
def log_unsupported_format_error(): def get_file_taste(sample_path):
logger.error("-" * 80) if not os.path.exists(sample_path):
logger.error(" Input file does not appear to be a PE or ELF file.") raise IOError("sample path %s does not exist or cannot be accessed" % sample_path)
logger.error(" ") with open(sample_path, "rb") as f:
logger.error( taste = f.read(8)
" capa currently only supports analyzing PE and ELF files (or shellcode, when using --format sc32|sc64)." return taste
)
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
logger.error("-" * 80)
def log_unsupported_os_error():
logger.error("-" * 80)
logger.error(" Input file does not appear to target a supported OS.")
logger.error(" ")
logger.error(
" capa currently only supports analyzing executables for some operating systems (including Windows and Linux)."
)
logger.error("-" * 80)
def log_unsupported_arch_error():
logger.error("-" * 80)
logger.error(" Input file does not appear to target a supported architecture.")
logger.error(" ")
logger.error(" capa currently only supports analyzing x86 (32- and 64-bit).")
logger.error("-" * 80)
def log_unsupported_runtime_error():
logger.error("-" * 80)
logger.error(" Unsupported runtime or Python interpreter.")
logger.error(" ")
logger.error(" capa supports running under Python 3.8 and higher.")
logger.error(" ")
logger.error(
" If you're seeing this message on the command line, please ensure you're running a supported Python version."
)
logger.error("-" * 80)

View File

@@ -1,252 +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
import datetime
import contextlib
from typing import List, Optional
from pathlib import Path
import idc
import idaapi
import idautils
import ida_bytes
import ida_loader
from netnode import netnode
import capa
import capa.version
import capa.render.utils as rutils
import capa.features.common
import capa.features.freeze
import capa.render.result_document as rdoc
from capa.features.address import AbsoluteVirtualAddress
logger = logging.getLogger("capa")
# file type as returned by idainfo.file_type
SUPPORTED_FILE_TYPES = (
idaapi.f_PE,
idaapi.f_ELF,
idaapi.f_BIN,
idaapi.f_COFF,
# idaapi.f_MACHO,
)
# arch type as returned by idainfo.procname
SUPPORTED_ARCH_TYPES = ("metapc",)
CAPA_NETNODE = f"$ com.mandiant.capa.v{capa.version.__version__}"
NETNODE_RESULTS = "results"
NETNODE_RULES_CACHE_ID = "rules-cache-id"
def inform_user_ida_ui(message):
# this isn't a logger, this is IDA's logging facility
idaapi.info(f"{message}. Please refer to IDA Output window for more information.") # noqa: G004
def is_supported_ida_version():
version = float(idaapi.get_kernel_version())
if version < 7.4 or version >= 9:
warning_msg = "This plugin does not support your IDA Pro version"
logger.warning(warning_msg)
logger.warning("Your IDA Pro version is: %s. Supported versions are: IDA >= 7.4 and IDA < 9.0.", version)
return False
return True
def is_supported_file_type():
file_info = idaapi.get_inf_structure()
if file_info.filetype not in SUPPORTED_FILE_TYPES:
logger.error("-" * 80)
logger.error(" Input file does not appear to be a supported file type.")
logger.error(" ")
logger.error(
" capa currently only supports analyzing PE, ELF, or binary files containing x86 (32- and 64-bit) shellcode."
)
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
logger.error("-" * 80)
return False
return True
def is_supported_arch_type():
file_info = idaapi.get_inf_structure()
if file_info.procname not in SUPPORTED_ARCH_TYPES or not any((file_info.is_32bit(), file_info.is_64bit())):
logger.error("-" * 80)
logger.error(" Input file does not appear to target a supported architecture.")
logger.error(" ")
logger.error(" capa currently only supports analyzing x86 (32- and 64-bit).")
logger.error("-" * 80)
return False
return True
def get_disasm_line(va):
""" """
return idc.generate_disasm_line(va, idc.GENDSM_FORCE_CODE)
def is_func_start(ea):
"""check if function stat exists at virtual address"""
f = idaapi.get_func(ea)
return f and f.start_ea == ea
def get_func_start_ea(ea):
""" """
f = idaapi.get_func(ea)
return f if f is None else f.start_ea
def get_file_md5():
""" """
md5 = idautils.GetInputFileMD5()
if not isinstance(md5, str):
md5 = capa.features.common.bytes_to_str(md5)
return md5
def get_file_sha256():
""" """
sha256 = idaapi.retrieve_input_file_sha256()
if not isinstance(sha256, str):
sha256 = capa.features.common.bytes_to_str(sha256)
return sha256
def collect_metadata(rules: List[Path]):
""" """
md5 = get_file_md5()
sha256 = get_file_sha256()
info: idaapi.idainfo = idaapi.get_inf_structure()
if info.procname == "metapc" and info.is_64bit():
arch = "x86_64"
elif info.procname == "metapc" and info.is_32bit():
arch = "x86"
else:
arch = "unknown arch"
format_name: str = ida_loader.get_file_type_name()
if "PE" in format_name:
os = "windows"
elif "ELF" in format_name:
with contextlib.closing(capa.ida.helpers.IDAIO()) as f:
os = capa.features.extractors.elf.detect_elf_os(f)
else:
os = "unknown os"
return rdoc.Metadata(
timestamp=datetime.datetime.now(),
version=capa.version.__version__,
argv=(),
sample=rdoc.Sample(
md5=md5,
sha1="", # not easily accessible
sha256=sha256,
path=idaapi.get_input_file_path(),
),
analysis=rdoc.Analysis(
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(
functions=(),
# this is updated after capabilities have been collected.
# will look like:
#
# "functions": { 0x401000: { "matched_basic_blocks": [ 0x401000, 0x401005, ... ] }, ... }
),
# ignore these for now - not used by IDA plugin.
feature_counts=rdoc.FeatureCounts(file=0, functions=()),
library_functions=(),
),
)
class IDAIO:
"""
An object that acts as a file-like object,
using bytes from the current IDB workspace.
"""
def __init__(self):
super().__init__()
self.offset = 0
def seek(self, offset, whence=0):
assert whence == 0
self.offset = offset
def read(self, size):
ea = ida_loader.get_fileregion_ea(self.offset)
if ea == idc.BADADDR:
logger.debug("cannot read 0x%x bytes at 0x%x (ea: BADADDR)", size, self.offset)
return b""
logger.debug("reading 0x%x bytes at 0x%x (ea: 0x%x)", size, self.offset, ea)
# get_bytes returns None on error, for consistency with read always return bytes
return ida_bytes.get_bytes(ea, size) or b""
def close(self):
return
def save_cached_results(resdoc):
logger.debug("saving cached capa results to netnode '%s'", CAPA_NETNODE)
n = netnode.Netnode(CAPA_NETNODE)
n[NETNODE_RESULTS] = resdoc.json()
def idb_contains_cached_results() -> bool:
try:
n = netnode.Netnode(CAPA_NETNODE)
return bool(n.get(NETNODE_RESULTS))
except netnode.NetnodeCorruptError as e:
logger.exception(str(e))
return False
def load_and_verify_cached_results() -> Optional[rdoc.ResultDocument]:
"""verifies that cached results have valid (mapped) addresses for the current database"""
logger.debug("loading cached capa results from netnode '%s'", CAPA_NETNODE)
n = netnode.Netnode(CAPA_NETNODE)
doc = rdoc.ResultDocument.model_validate_json(n[NETNODE_RESULTS])
for rule in rutils.capability_rules(doc):
for location_, _ in rule.matches:
location = location_.to_capa()
if isinstance(location, AbsoluteVirtualAddress):
ea = int(location)
if not idaapi.is_mapped(ea):
logger.error("cached address %s is not a valid location in this database", hex(ea))
return None
return doc
def save_rules_cache_id(ruleset_id):
logger.debug("saving ruleset ID to netnode '%s'", CAPA_NETNODE)
n = netnode.Netnode(CAPA_NETNODE)
n[NETNODE_RULES_CACHE_ID] = ruleset_id
def load_rules_cache_id():
n = netnode.Netnode(CAPA_NETNODE)
return n[NETNODE_RULES_CACHE_ID]
def delete_cached_results():
logger.debug("deleting cached capa data")
n = netnode.Netnode(CAPA_NETNODE)
del n[NETNODE_RESULTS]

View File

@@ -0,0 +1,122 @@
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import logging
import datetime
import idc
import six
import idaapi
import idautils
import capa
logger = logging.getLogger("capa")
SUPPORTED_IDA_VERSIONS = [
"7.1",
"7.2",
"7.3",
"7.4",
"7.5",
"7.6",
]
# file type names as returned by idaapi.get_file_type_name()
SUPPORTED_FILE_TYPES = [
"Portable executable for 80386 (PE)",
"Portable executable for AMD64 (PE)",
"Binary file", # x86/AMD64 shellcode support
]
def inform_user_ida_ui(message):
idaapi.info("%s. Please refer to IDA Output window for more information." % message)
def is_supported_ida_version():
version = idaapi.get_kernel_version()
if version not in SUPPORTED_IDA_VERSIONS:
warning_msg = "This plugin does not support your IDA Pro version"
logger.warning(warning_msg)
logger.warning(
"Your IDA Pro version is: %s. Supported versions are: %s." % (version, ", ".join(SUPPORTED_IDA_VERSIONS))
)
return False
return True
def is_supported_file_type():
file_type = idaapi.get_file_type_name()
if file_type not in SUPPORTED_FILE_TYPES:
logger.error("-" * 80)
logger.error(" Input file does not appear to be a PE file.")
logger.error(" ")
logger.error(
" capa currently only supports analyzing PE files (or binary files containing x86/AMD64 shellcode) with IDA."
)
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
logger.error("-" * 80)
return False
return True
def get_disasm_line(va):
""" """
return idc.generate_disasm_line(va, idc.GENDSM_FORCE_CODE)
def is_func_start(ea):
""" check if function stat exists at virtual address """
f = idaapi.get_func(ea)
return f and f.start_ea == ea
def get_func_start_ea(ea):
""" """
f = idaapi.get_func(ea)
return f if f is None else f.start_ea
def get_file_md5():
""" """
md5 = idautils.GetInputFileMD5()
if not isinstance(md5, six.string_types):
md5 = capa.features.bytes_to_str(md5)
return md5
def get_file_sha256():
""" """
sha256 = idaapi.retrieve_input_file_sha256()
if not isinstance(sha256, six.string_types):
sha256 = capa.features.bytes_to_str(sha256)
return sha256
def collect_metadata():
""" """
md5 = get_file_md5()
sha256 = get_file_sha256()
return {
"timestamp": datetime.datetime.now().isoformat(),
# "argv" is not relevant here
"sample": {
"md5": md5,
"sha1": "", # not easily accessible
"sha256": sha256,
"path": idaapi.get_input_file_path(),
},
"analysis": {
"format": idaapi.get_file_type_name(),
"extractor": "ida",
"base_address": idaapi.get_imagebase(),
},
"version": capa.version.__version__,
}

View File

@@ -1,8 +1,8 @@
![capa explorer](../../../.github/capa-explorer-logo.png) ![capa explorer](../../../.github/capa-explorer-logo.png)
capa explorer is an IDAPython plugin that integrates the FLARE team's open-source framework, capa, with IDA Pro. capa is a framework that uses a well-defined collection of rules to capa explorer is an IDAPython plugin that integrates the FLARE team's open-source framework, capa, with IDA Pro. capa is a framework that uses a well-defined collection of rules to
identify capabilities in a program. You can run capa against a PE file, ELF file, or shellcode and it tells you what it thinks the program can do. For example, it might suggest that identify capabilities in a program. You can run capa against a PE file or shellcode and it tells you what it thinks the program can do. For example, it might suggest that
the program is a backdoor, can install services, or relies on HTTP to communicate. capa explorer runs capa analysis on your IDA Pro database (IDB) without needing access the program is a backdoor, can install services, or relies on HTTP to communicate. capa explorer runs capa directly against your IDA Pro database (IDB) without requiring access
to the original binary file. Once a database has been analyzed, capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted from your IDB. to the original binary file. Once a database has been analyzed, capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted from your IDB.
We love using capa explorer during malware analysis because it teaches us what parts of a program suggest a behavior. As we click on rows, capa explorer jumps directly We love using capa explorer during malware analysis because it teaches us what parts of a program suggest a behavior. As we click on rows, capa explorer jumps directly
@@ -21,66 +21,80 @@ We can use capa explorer to navigate our Disassembly view directly to the suspec
Using the `Rule Information` and `Details` columns capa explorer shows us that the suspect function matched `self delete via COMSPEC environment variable` because it contains capa rule matches for `create process`, `get COMSPEC environment variable`, Using the `Rule Information` and `Details` columns capa explorer shows us that the suspect function matched `self delete via COMSPEC environment variable` because it contains capa rule matches for `create process`, `get COMSPEC environment variable`,
and `query environment variable`, references to the strings `COMSPEC`, ` > nul`, and `/c del `, and calls to the Windows API functions `GetEnvironmentVariableA` and `ShellExecuteEx`. and `query environment variable`, references to the strings `COMSPEC`, ` > nul`, and `/c del `, and calls to the Windows API functions `GetEnvironmentVariableA` and `ShellExecuteEx`.
capa explorer also helps you build and test new capa rules. To start, select the `Rule Generator` tab, navigate to a function in your Disassembly view, capa explorer also helps you build new capa rules. To start select the `Rule Generator` tab, navigate to a function in your Disassembly view,
and click `Analyze`. capa explorer will extract features from the function and display them in the `Features` pane. You can add features listed in this pane to the `Editor` pane and click `Analyze`. capa explorer will extract features from the function and display them in the `Features` pane. You can add features listed in this pane to the `Editor` pane
by either double-clicking a feature or using multi-select + right-click to add multiple features at once. The `Preview` and `Editor` panes help edit your rule. Use the `Preview` pane by either double-clicking a feature or using multi-select + right-click to add multiple features at once. The `Preview` and `Editor` panes help edit your rule. Use the `Preview` pane
to modify rule text directly and the `Editor` pane to construct and rearrange your hierarchy of statements and features. When you finish a rule you can save it directly to a file by clicking `Save`. to modify the rule text directly and the `Editor` pane to construct and rearrange your hierarchy of statements and features. When you finish a rule you can save it directly to a file by clicking `Save`.
![](../../../doc/img/rulegen_expanded.png) ![](../../../doc/img/rulegen_expanded.png)
For more information on the FLARE team's open-source framework, capa, check out the overview in our first [blog](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities). For more information on the FLARE team's open-source framework, capa, check out the overview in our first [blog](https://www.fireeye.com/blog/threat-research/2020/07/capa-automatically-identify-malware-capabilities.html).
## Getting Started ## Getting Started
### Installation ### Requirements
You can install capa explorer using the following steps: capa explorer supports Python 2.7 and 3.6+ and the following IDA Pro versions:
* IDA 7.4
* IDA 7.5
* IDA 7.6 (caveat below)
If you encounter issues with your specific setup, please open a new [Issue](https://github.com/fireeye/capa/issues).
#### IDA 7.6 caveat: IDA 7.6sp1 or patch required
As described [here](https://www.hex-rays.com/blog/ida-7-6-empty-qtreeview-qtreewidget/):
> A rather nasty issue evaded our testing and found its way into IDA 7.6: using the PyQt5 modules that are shipped with IDA, QTreeView (or QTreeWidget) instances will always fail to display contents.
Therefore, in order to use capa under IDA 7.6 you need the [Service Pack 1 for IDA 7.6](https://www.hex-rays.com/products/ida/news/7_6sp1). Alternatively, you can download and install the fix corresponding to your IDA installation, replacing the original QtWidgets DLL with the one contained in the .zip file (links to Hex-Rays):
- Windows: [pyqt5_qtwidgets_win](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_win.zip)
- Linux: [pyqt5_qtwidgets_linux](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_linux.zip)
- MacOS (Intel): [pyqt5_qtwidgets_mac_x64](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_mac_x64.zip)
- MacOS (AppleSilicon): [pyqt5_qtwidgets_mac_arm](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_mac_arm.zip)
1. Install capa and its dependencies from PyPI using the Python interpreter configured for your IDA installation:
```
$ pip install flare-capa
```
2. Download and extract the [official capa rules](https://github.com/mandiant/capa-rules/releases) that match the version of capa you have installed
1. Use the following command to view the version of capa you have installed:
```commandline
$ pip show flare-capa
OR
$ capa --version
```
3. Copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory
- find your plugin directories via `idaapi.get_ida_subdirs("plugins")` or see this [Hex-Rays blog](https://hex-rays.com/blog/igors-tip-of-the-week-103-sharing-plugins-between-ida-installs/)
- common paths are `%APPDATA%\Hex-Rays\IDA Pro\plugins` (Windows) or `$HOME/.idapro/plugins` on Linux/Mac
### Supported File Types ### Supported File Types
capa explorer is limited to the file types supported by capa, which include: capa explorer is limited to the file types supported by capa, which include:
* Windows x86 (32- and 64-bit) PE files * Windows 32-bit and 64-bit PE files
* Windows x86 (32- and 64-bit) shellcode * Windows 32-bit and 64-bit shellcode
* ELF files on various operating systems
### Installation
You can install capa explorer using the following steps:
1. Install capa and its dependencies from PyPI for the Python interpreter used by your IDA installation:
```
$ pip install flare-capa
```
3. Download the [standard collection of capa rules](https://github.com/fireeye/capa-rules) (capa explorer needs capa rules to analyze a database)
4. Copy [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory
### Usage ### Usage
1. Open IDA and analyze a supported file type (select the `Manual Load` and `Load Resources` options in IDA for best results) 1. Open IDA and analyze a supported file type (select the `Manual Load` and `Load Resources` options in IDA for best results)
2. Open capa explorer in IDA by navigating to `Edit > Plugins > FLARE capa explorer` or using the keyboard shortcut `Alt+F5` 2. Open capa explorer in IDA by navigating to `Edit > Plugins > FLARE capa explorer` or using the keyboard shortcut `Alt+F5`
You can also use `ida_loader.load_and_run_plugin("capa_explorer", arg)`. `arg` is a bitflag for which setting the LSB enables automatic analysis. See `capa.ida.plugin.form.Options` for more details.
3. Select the `Program Analysis` tab 3. Select the `Program Analysis` tab
4. Click the `Analyze` button 4. Click the `Analyze` button
The first time you run capa explorer you will be asked to specify a local directory containing capa rules to use for analysis. We recommend downloading and extracting the [official capa rules](https://github.com/mandiant/capa-rules/releases) that match When running capa explorer for the first time you are prompted to select a file directory containing capa rules. The plugin conveniently
the version of capa you have installed (see installation instructions above for more details). capa explorer remembers your selection for future analysis which you remembers your selection for future runs; you can change this selection and other default settings by clicking `Settings`. We recommend
can update using the `Settings` button. downloading and using the [standard collection of capa rules](https://github.com/fireeye/capa-rules) when getting started with the plugin.
#### Tips for Program Analysis #### Tips for Program Analysis
* Start analysis by clicking the `Analyze` button * Start analysis by clicking the `Analyze` button
* capa explorer caches results to the database and reuses them across IDA sessions
* Reset the plugin user interface and remove highlighting from your Disassembly view by clicking the `Reset` button * Reset the plugin user interface and remove highlighting from your Disassembly view by clicking the `Reset` button
* Change your local capa rules directory, auto analysis settings, and other default settings by clicking the `Settings` button * Change your capa rules directory and other default settings by clicking `Settings`
* Hover your cursor over a rule match to view the source content of the rule * Hover your cursor over a rule match to view the source content of the rule
* Double-click the `Address` column to navigate your Disassembly view to the address of the associated feature * Double-click the `Address` column to navigate your Disassembly view to the address of the associated feature
* Double-click a result in the `Rule Information` column to expand its children * Double-click a result in the `Rule Information` column to expand its children
* Select a checkbox in the `Rule Information` column to highlight the address of the associated feature in your Disassembly view * Select a checkbox in the `Rule Information` column to highlight the address of the associated feature in your Dissasembly view
#### Tips for Rule Generator #### Tips for Rule Generator
@@ -93,34 +107,18 @@ can update using the `Settings` button.
* Directly edit rule text and metadata fields using the `Preview` pane * Directly edit rule text and metadata fields using the `Preview` pane
* Change the default rule author and default rule scope displayed in the `Preview` pane by clicking `Settings` * Change the default rule author and default rule scope displayed in the `Preview` pane by clicking `Settings`
### Requirements
capa explorer supports Python versions >= 3.8.x and IDA Pro versions >= 7.4. The following IDA Pro versions have been tested:
* IDA 7.4
* IDA 7.5
* IDA 7.6 Service Pack 1
* IDA 7.7
* IDA 8.0
* IDA 8.1
* IDA 8.2
capa explorer is however limited to the Python versions supported by your IDA installation (which may not include all Python versions >= 3.8.x).
If you encounter issues with your specific setup, please open a new [Issue](https://github.com/mandiant/capa/issues).
## Development ## Development
capa explorer is packaged with capa so you will need to install capa locally for development. You can install capa locally by following the steps outlined in `Method 3: Inspecting the capa source code` of the [capa capa explorer is packaged with capa so you will need to install capa locally for development. You can install capa locally by following the steps outlined in `Method 3: Inspecting the capa source code` of the [capa
installation guide](https://github.com/mandiant/capa/blob/master/doc/installation.md#method-3-inspecting-the-capa-source-code). Once installed, copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py) installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md#method-3-inspecting-the-capa-source-code). Once installed, copy [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py)
to your plugins directory to install capa explorer in IDA. to your plugins directory to install capa explorer in IDA.
### Components ### Components
capa explorer consists of two main components: capa explorer consists of two main components:
* An [feature extractor](https://github.com/mandiant/capa/tree/master/capa/features/extractors/ida) built on top of IDA's binary analysis engine * An [feature extractor](https://github.com/fireeye/capa/tree/master/capa/features/extractors/ida) built on top of IDA's binary analysis engine
* This component uses IDAPython to extract [capa features](https://github.com/mandiant/capa-rules/blob/master/doc/format.md#extracted-features) from your IDBs such as strings, * This component uses IDAPython to extract [capa features](https://github.com/fireeye/capa-rules/blob/master/doc/format.md#extracted-features) from your IDBs such as strings,
disassembly, and control flow; these extracted features are used by capa to find feature combinations that result in a rule match disassembly, and control flow; these extracted features are used by capa to find feature combinations that result in a rule match
* An [interactive user interface](https://github.com/mandiant/capa/tree/master/capa/ida/plugin) for displaying and exploring capa rule matches * An [interactive user interface](https://github.com/fireeye/capa/tree/master/capa/ida/plugin) for displaying and exploring capa rule matches
* This component integrates the feature extractor and capa, providing an interactive user interface to dissect rule matches found by capa using features extracted directly from your IDBs * This component integrates the feature extractor and capa, providing an interactive user interface to dissect rule matches found by capa using features extracted directly from your IDBs

View File

@@ -1,15 +1,17 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
import logging import logging
import idaapi import idaapi
import ida_kernwin import ida_kernwin
from capa.ida.helpers import is_supported_file_type, is_supported_ida_version
from capa.ida.plugin.form import CapaExplorerForm from capa.ida.plugin.form import CapaExplorerForm
from capa.ida.plugin.icon import ICON from capa.ida.plugin.icon import ICON
@@ -17,6 +19,7 @@ logger = logging.getLogger(__name__)
class CapaExplorerPlugin(idaapi.plugin_t): class CapaExplorerPlugin(idaapi.plugin_t):
# Mandatory definitions # Mandatory definitions
PLUGIN_NAME = "FLARE capa explorer" PLUGIN_NAME = "FLARE capa explorer"
PLUGIN_VERSION = "1.0.0" PLUGIN_VERSION = "1.0.0"
@@ -25,8 +28,8 @@ class CapaExplorerPlugin(idaapi.plugin_t):
wanted_name = PLUGIN_NAME wanted_name = PLUGIN_NAME
wanted_hotkey = "ALT-F5" wanted_hotkey = "ALT-F5"
comment = "IDA Pro plugin for the FLARE team's capa tool to identify capabilities in executable files." comment = "IDA Pro plugin for the FLARE team's capa tool to identify capabilities in executable files."
website = "https://github.com/mandiant/capa" website = "https://github.com/fireeye/capa"
help = "See https://github.com/mandiant/capa/blob/master/doc/usage.md" help = "See https://github.com/fireeye/capa/blob/master/doc/usage.md"
version = "" version = ""
flags = 0 flags = 0
@@ -38,20 +41,10 @@ class CapaExplorerPlugin(idaapi.plugin_t):
"""called when IDA is loading the plugin""" """called when IDA is loading the plugin"""
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# do not load plugin unless hosted in idaq (IDA Qt)
if not idaapi.is_idaq():
# note: it does not appear that IDA calls "init" by default when hosted in idat; we keep this
# check here for good measure
return idaapi.PLUGIN_SKIP
import capa.ida.helpers
# do not load plugin if IDA version/file type not supported # do not load plugin if IDA version/file type not supported
if not capa.ida.helpers.is_supported_ida_version(): if not is_supported_ida_version():
return idaapi.PLUGIN_SKIP return idaapi.PLUGIN_SKIP
if not capa.ida.helpers.is_supported_file_type(): if not is_supported_file_type():
return idaapi.PLUGIN_SKIP
if not capa.ida.helpers.is_supported_arch_type():
return idaapi.PLUGIN_SKIP return idaapi.PLUGIN_SKIP
return idaapi.PLUGIN_OK return idaapi.PLUGIN_OK
@@ -60,23 +53,8 @@ class CapaExplorerPlugin(idaapi.plugin_t):
pass pass
def run(self, arg): def run(self, arg):
""" """called when IDA is running the plugin as a script"""
called when IDA is running the plugin as a script self.form = CapaExplorerForm(self.PLUGIN_NAME)
args:
arg (int): bitflag. Setting LSB enables automatic analysis upon
loading. The other bits are currently undefined. See `form.Options`.
"""
if not self.form:
self.form = CapaExplorerForm(self.PLUGIN_NAME, arg)
else:
widget = idaapi.find_widget(self.form.form_title)
if widget:
idaapi.activate_widget(widget, True)
else:
self.form.Show()
self.form.load_capa_results(False, True)
return True return True
@@ -99,7 +77,7 @@ class CapaExplorerPlugin(idaapi.plugin_t):
# so we need to register a callback that's invoked from the main thread after the plugin is registered. # so we need to register a callback that's invoked from the main thread after the plugin is registered.
# #
# after a lot of guess-and-check, we can use `UI_Hooks.updated_actions` to # after a lot of guess-and-check, we can use `UI_Hooks.updated_actions` to
# receive notifications after IDA has created an action for each plugin. # receive notications after IDA has created an action for each plugin.
# so, create this hook, wait for capa plugin to load, set the icon, and unhook. # so, create this hook, wait for capa plugin to load, set the icon, and unhook.
@@ -107,7 +85,7 @@ class OnUpdatedActionsHook(ida_kernwin.UI_Hooks):
"""register a callback to be invoked each time the UI actions are updated""" """register a callback to be invoked each time the UI actions are updated"""
def __init__(self, cb): def __init__(self, cb):
super().__init__() super(OnUpdatedActionsHook, self).__init__()
self.cb = cb self.cb = cb
def updated_actions(self): def updated_actions(self):

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -16,7 +16,7 @@ class CapaExplorerIdaHooks(idaapi.UI_Hooks):
@param screen_ea_changed_hook: function hook for IDA screen ea changed @param screen_ea_changed_hook: function hook for IDA screen ea changed
@param action_hooks: dict of IDA action handles @param action_hooks: dict of IDA action handles
""" """
super().__init__() super(CapaExplorerIdaHooks, self).__init__()
self.screen_ea_changed_hook = screen_ea_changed_hook self.screen_ea_changed_hook = screen_ea_changed_hook
self.process_action_hooks = action_hooks self.process_action_hooks = action_hooks
@@ -30,7 +30,7 @@ class CapaExplorerIdaHooks(idaapi.UI_Hooks):
@retval must be 0 @retval must be 0
""" """
self.process_action_handle = self.process_action_hooks.get(name) self.process_action_handle = self.process_action_hooks.get(name, None)
if self.process_action_handle: if self.process_action_handle:
self.process_action_handle(self.process_action_meta) self.process_action_handle(self.process_action_meta)

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,15 +6,14 @@
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 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. # See the License for the specific language governing permissions and limitations under the License.
import sys
import codecs import codecs
from typing import List, Iterator, Optional
import idc import idc
import idaapi import idaapi
from PyQt5 import QtCore from PyQt5 import QtCore
import capa.ida.helpers import capa.ida.helpers
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
def info_to_name(display): def info_to_name(display):
@@ -28,19 +27,19 @@ def info_to_name(display):
return "" return ""
def ea_to_hex(ea): def location_to_hex(location):
"""convert effective address (ea) to hex for display""" """convert location to hex for display"""
return f"{hex(ea)}" return "%08X" % location
class CapaExplorerDataItem: class CapaExplorerDataItem(object):
"""store data for CapaExplorerDataModel""" """store data for CapaExplorerDataModel"""
def __init__(self, parent: Optional["CapaExplorerDataItem"], data: List[str], can_check=True): def __init__(self, parent, data, can_check=True):
"""initialize item""" """initialize item"""
self.pred = parent self.pred = parent
self._data = data self._data = data
self._children: List["CapaExplorerDataItem"] = [] self.children = []
self._checked = False self._checked = False
self._can_check = can_check self._can_check = can_check
@@ -78,29 +77,29 @@ class CapaExplorerDataItem:
"""get item is checked""" """get item is checked"""
return self._checked return self._checked
def appendChild(self, item: "CapaExplorerDataItem"): def appendChild(self, item):
"""add a new child to specified item """add a new child to specified item
@param item: CapaExplorerDataItem @param item: CapaExplorerDataItem
""" """
self._children.append(item) self.children.append(item)
def child(self, row: int) -> "CapaExplorerDataItem": def child(self, row):
"""get child row """get child row
@param row: row number @param row: row number
""" """
return self._children[row] return self.children[row]
def childCount(self) -> int: def childCount(self):
"""get child count""" """get child count"""
return len(self._children) return len(self.children)
def columnCount(self) -> int: def columnCount(self):
"""get column count""" """get column count"""
return len(self._data) return len(self._data)
def data(self, column: int) -> Optional[str]: def data(self, column):
"""get data at column """get data at column
@param: column number @param: column number
@@ -110,17 +109,17 @@ class CapaExplorerDataItem:
except IndexError: except IndexError:
return None return None
def parent(self) -> Optional["CapaExplorerDataItem"]: def parent(self):
"""get parent""" """get parent"""
return self.pred return self.pred
def row(self) -> int: def row(self):
"""get row location""" """get row location"""
if self.pred: if self.pred:
return self.pred._children.index(self) return self.pred.children.index(self)
return 0 return 0
def setData(self, column: int, value: str): def setData(self, column, value):
"""set data in column """set data in column
@param column: column number @param column: column number
@@ -128,13 +127,14 @@ class CapaExplorerDataItem:
""" """
self._data[column] = value self._data[column] = value
def children(self) -> Iterator["CapaExplorerDataItem"]: def children(self):
"""yield children""" """yield children"""
yield from self._children for child in self.children:
yield child
def removeChildren(self): def removeChildren(self):
"""remove children""" """remove children"""
del self._children[:] del self.children[:]
def __str__(self): def __str__(self):
"""get string representation of columns """get string representation of columns
@@ -149,7 +149,7 @@ class CapaExplorerDataItem:
return self._data[0] return self._data[0]
@property @property
def location(self) -> Optional[int]: def location(self):
"""return data stored in location column""" """return data stored in location column"""
try: try:
# address stored as str, convert to int before return # address stored as str, convert to int before return
@@ -168,9 +168,7 @@ class CapaExplorerRuleItem(CapaExplorerDataItem):
fmt = "%s (%d matches)" fmt = "%s (%d matches)"
def __init__( def __init__(self, parent, name, namespace, count, source, can_check=True):
self, parent: CapaExplorerDataItem, name: str, namespace: str, count: int, source: str, can_check=True
):
"""initialize item """initialize item
@param parent: parent node @param parent: parent node
@@ -180,7 +178,7 @@ class CapaExplorerRuleItem(CapaExplorerDataItem):
@param source: rule source (tooltip) @param source: rule source (tooltip)
""" """
display = self.fmt % (name, count) if count > 1 else name display = self.fmt % (name, count) if count > 1 else name
super().__init__(parent, [display, "", namespace], can_check) super(CapaExplorerRuleItem, self).__init__(parent, [display, "", namespace], can_check)
self._source = source self._source = source
@property @property
@@ -192,19 +190,19 @@ class CapaExplorerRuleItem(CapaExplorerDataItem):
class CapaExplorerRuleMatchItem(CapaExplorerDataItem): class CapaExplorerRuleMatchItem(CapaExplorerDataItem):
"""store data for rule match""" """store data for rule match"""
def __init__(self, parent: CapaExplorerDataItem, display: str, source=""): def __init__(self, parent, display, source=""):
"""initialize item """initialize item
@param parent: parent node @param parent: parent node
@param display: text to display in UI @param display: text to display in UI
@param source: rule match source to display (tooltip) @param source: rule match source to display (tooltip)
""" """
super().__init__(parent, [display, "", ""]) super(CapaExplorerRuleMatchItem, self).__init__(parent, [display, "", ""])
self._source = source self._source = source
@property @property
def source(self): def source(self):
"""return rule contents for display""" """ return rule contents for display """
return self._source return self._source
@@ -213,20 +211,20 @@ class CapaExplorerFunctionItem(CapaExplorerDataItem):
fmt = "function(%s)" fmt = "function(%s)"
def __init__(self, parent: CapaExplorerDataItem, location: Address, can_check=True): def __init__(self, parent, location, can_check=True):
"""initialize item """initialize item
@param parent: parent node @param parent: parent node
@param location: virtual address of function as seen by IDA @param location: virtual address of function as seen by IDA
""" """
assert isinstance(location, AbsoluteVirtualAddress) super(CapaExplorerFunctionItem, self).__init__(
ea = int(location) parent, [self.fmt % idaapi.get_name(location), location_to_hex(location), ""], can_check
super().__init__(parent, [self.fmt % idaapi.get_name(ea), ea_to_hex(ea), ""], can_check) )
@property @property
def info(self): def info(self):
"""return function name""" """return function name"""
info = super().info info = super(CapaExplorerFunctionItem, self).info
display = info_to_name(info) display = info_to_name(info)
return display if display else info return display if display else info
@@ -246,13 +244,13 @@ class CapaExplorerSubscopeItem(CapaExplorerDataItem):
fmt = "subscope(%s)" fmt = "subscope(%s)"
def __init__(self, parent: CapaExplorerDataItem, scope): def __init__(self, parent, scope):
"""initialize item """initialize item
@param parent: parent node @param parent: parent node
@param scope: subscope name @param scope: subscope name
""" """
super().__init__(parent, [self.fmt % scope, "", ""]) super(CapaExplorerSubscopeItem, self).__init__(parent, [self.fmt % scope, "", ""])
class CapaExplorerBlockItem(CapaExplorerDataItem): class CapaExplorerBlockItem(CapaExplorerDataItem):
@@ -260,29 +258,19 @@ class CapaExplorerBlockItem(CapaExplorerDataItem):
fmt = "basic block(loc_%08X)" fmt = "basic block(loc_%08X)"
def __init__(self, parent: CapaExplorerDataItem, location: Address): def __init__(self, parent, location):
"""initialize item """initialize item
@param parent: parent node @param parent: parent node
@param location: virtual address of basic block as seen by IDA @param location: virtual address of basic block as seen by IDA
""" """
assert isinstance(location, AbsoluteVirtualAddress) super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % location, location_to_hex(location), ""])
ea = int(location)
super().__init__(parent, [self.fmt % ea, ea_to_hex(ea), ""])
class CapaExplorerInstructionItem(CapaExplorerBlockItem):
"""store data for instruction match"""
fmt = "instruction(loc_%08X)"
class CapaExplorerDefaultItem(CapaExplorerDataItem): class CapaExplorerDefaultItem(CapaExplorerDataItem):
"""store data for default match e.g. statement (and, or)""" """store data for default match e.g. statement (and, or)"""
def __init__( def __init__(self, parent, display, details="", location=None):
self, parent: CapaExplorerDataItem, display: str, details: str = "", location: Optional[Address] = None
):
"""initialize item """initialize item
@param parent: parent node @param parent: parent node
@@ -290,20 +278,14 @@ class CapaExplorerDefaultItem(CapaExplorerDataItem):
@param details: text to display in details section of UI @param details: text to display in details section of UI
@param location: virtual address as seen by IDA @param location: virtual address as seen by IDA
""" """
ea = None location = location_to_hex(location) if location else ""
if location: super(CapaExplorerDefaultItem, self).__init__(parent, [display, location, details])
assert isinstance(location, AbsoluteVirtualAddress)
ea = int(location)
super().__init__(parent, [display, ea_to_hex(ea) if ea is not None else "", details])
class CapaExplorerFeatureItem(CapaExplorerDataItem): class CapaExplorerFeatureItem(CapaExplorerDataItem):
"""store data for feature match""" """store data for feature match"""
def __init__( def __init__(self, parent, display, location="", details=""):
self, parent: CapaExplorerDataItem, display: str, location: Optional[Address] = None, details: str = ""
):
"""initialize item """initialize item
@param parent: parent node @param parent: parent node
@@ -311,18 +293,14 @@ class CapaExplorerFeatureItem(CapaExplorerDataItem):
@param details: text to display in details section of UI @param details: text to display in details section of UI
@param location: virtual address as seen by IDA @param location: virtual address as seen by IDA
""" """
if location: location = location_to_hex(location) if location else ""
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress)) super(CapaExplorerFeatureItem, self).__init__(parent, [display, location, details])
ea = int(location)
super().__init__(parent, [display, ea_to_hex(ea), details])
else:
super().__init__(parent, [display, "", details])
class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem): class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
"""store data for instruction match""" """store data for instruction match"""
def __init__(self, parent: CapaExplorerDataItem, display: str, location: Address): def __init__(self, parent, display, location):
"""initialize item """initialize item
details section shows disassembly view for match details section shows disassembly view for match
@@ -331,17 +309,15 @@ class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
@param display: text to display in UI @param display: text to display in UI
@param location: virtual address as seen by IDA @param location: virtual address as seen by IDA
""" """
assert isinstance(location, AbsoluteVirtualAddress) details = capa.ida.helpers.get_disasm_line(location)
ea = int(location) super(CapaExplorerInstructionViewItem, self).__init__(parent, display, location=location, details=details)
details = capa.ida.helpers.get_disasm_line(ea) self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
super().__init__(parent, display, location=location, details=details)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
class CapaExplorerByteViewItem(CapaExplorerFeatureItem): class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
"""store data for byte match""" """store data for byte match"""
def __init__(self, parent: CapaExplorerDataItem, display: str, location: Address): def __init__(self, parent, display, location):
"""initialize item """initialize item
details section shows byte preview for match details section shows byte preview for match
@@ -350,32 +326,30 @@ class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
@param display: text to display in UI @param display: text to display in UI
@param location: virtual address as seen by IDA @param location: virtual address as seen by IDA
""" """
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress)) byte_snap = idaapi.get_bytes(location, 32)
ea = int(location)
byte_snap = idaapi.get_bytes(ea, 32)
details = ""
if byte_snap: if byte_snap:
byte_snap = codecs.encode(byte_snap, "hex").upper() byte_snap = codecs.encode(byte_snap, "hex").upper()
details = " ".join([byte_snap[i : i + 2].decode() for i in range(0, len(byte_snap), 2)]) if sys.version_info >= (3, 0):
details = " ".join([byte_snap[i : i + 2].decode() for i in range(0, len(byte_snap), 2)])
else:
details = " ".join([byte_snap[i : i + 2] for i in range(0, len(byte_snap), 2)])
else:
details = ""
super().__init__(parent, display, location=location, details=details) super(CapaExplorerByteViewItem, self).__init__(parent, display, location=location, details=details)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM) self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
class CapaExplorerStringViewItem(CapaExplorerFeatureItem): class CapaExplorerStringViewItem(CapaExplorerFeatureItem):
"""store data for string match""" """store data for string match"""
def __init__(self, parent: CapaExplorerDataItem, display: str, location: Address, value: str): def __init__(self, parent, display, location, value):
"""initialize item """initialize item
@param parent: parent node @param parent: parent node
@param display: text to display in UI @param display: text to display in UI
@param location: virtual address as seen by IDA @param location: virtual address as seen by IDA
""" """
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress)) super(CapaExplorerStringViewItem, self).__init__(parent, display, location=location, details=value)
ea = int(location) self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
super().__init__(parent, display, location=location, details=value)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # You may obtain a copy of the License at: [package root]/LICENSE.txt
@@ -6,20 +6,14 @@
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 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. # See the License for the specific language governing permissions and limitations under the License.
from typing import Set, Dict, List, Tuple, Optional from collections import deque, defaultdict
from collections import deque
import idc import idc
import idaapi import idaapi
from PyQt5 import QtGui, QtCore from PyQt5 import QtGui, QtCore
import capa.rules
import capa.ida.helpers import capa.ida.helpers
import capa.render.utils as rutils import capa.render.utils as rutils
import capa.features.common
import capa.features.freeze as frz
import capa.render.result_document as rd
import capa.features.freeze.features as frzf
from capa.ida.plugin.item import ( from capa.ida.plugin.item import (
CapaExplorerDataItem, CapaExplorerDataItem,
CapaExplorerRuleItem, CapaExplorerRuleItem,
@@ -31,10 +25,8 @@ from capa.ida.plugin.item import (
CapaExplorerSubscopeItem, CapaExplorerSubscopeItem,
CapaExplorerRuleMatchItem, CapaExplorerRuleMatchItem,
CapaExplorerStringViewItem, CapaExplorerStringViewItem,
CapaExplorerInstructionItem,
CapaExplorerInstructionViewItem, CapaExplorerInstructionViewItem,
) )
from capa.features.address import Address, AbsoluteVirtualAddress
# default highlight color used in IDA window # default highlight color used in IDA window
DEFAULT_HIGHLIGHT = 0xE6C700 DEFAULT_HIGHLIGHT = 0xE6C700
@@ -51,7 +43,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
def __init__(self, parent=None): def __init__(self, parent=None):
"""initialize model""" """initialize model"""
super().__init__(parent) super(CapaExplorerDataModel, self).__init__(parent)
# root node does not have parent, contains header columns # root node does not have parent, contains header columns
self.root_node = CapaExplorerDataItem(None, ["Rule Information", "Address", "Details"]) self.root_node = CapaExplorerDataItem(None, ["Rule Information", "Address", "Details"])
@@ -143,7 +135,6 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
CapaExplorerFunctionItem, CapaExplorerFunctionItem,
CapaExplorerFeatureItem, CapaExplorerFeatureItem,
CapaExplorerSubscopeItem, CapaExplorerSubscopeItem,
CapaExplorerInstructionItem,
), ),
) )
and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION
@@ -349,14 +340,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
return item.childCount() return item.childCount()
def render_capa_doc_statement_node( def render_capa_doc_statement_node(self, parent, statement, locations, doc):
self,
parent: CapaExplorerDataItem,
match: rd.Match,
statement: rd.Statement,
locations: List[Address],
doc: rd.ResultDocument,
):
"""render capa statement read from doc """render capa statement read from doc
@param parent: parent to which new child is assigned @param parent: parent to which new child is assigned
@@ -364,156 +348,126 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
@param locations: locations of children (applies to range only?) @param locations: locations of children (applies to range only?)
@param doc: result doc @param doc: result doc
""" """
if statement["type"] in ("and", "or", "optional"):
if isinstance(statement, rd.CompoundStatement): display = statement["type"]
if statement.type != rd.CompoundStatementType.NOT: if statement.get("description"):
display = statement.type display += " (%s)" % statement["description"]
if statement.description:
display += f" ({statement.description})"
return CapaExplorerDefaultItem(parent, display)
elif isinstance(statement, rd.CompoundStatement) and statement.type == rd.CompoundStatementType.NOT:
# TODO(mike-hunhoff): verify that we can display NOT statements
# https://github.com/mandiant/capa/issues/1602
pass
elif isinstance(statement, rd.SomeStatement):
display = f"{statement.count} or more"
if statement.description:
display += f" ({statement.description})"
return CapaExplorerDefaultItem(parent, display) return CapaExplorerDefaultItem(parent, display)
elif isinstance(statement, rd.RangeStatement): elif statement["type"] == "not":
# TODO: do we display 'not'
pass
elif statement["type"] == "some":
display = "%d or more" % statement["count"]
if statement.get("description"):
display += " (%s)" % statement["description"]
return CapaExplorerDefaultItem(parent, display)
elif statement["type"] == "range":
# `range` is a weird node, its almost a hybrid of statement + feature. # `range` is a weird node, its almost a hybrid of statement + feature.
# it is a specific feature repeated multiple times. # it is a specific feature repeated multiple times.
# there's no additional logic in the feature part, just the existence of a feature. # there's no additional logic in the feature part, just the existence of a feature.
# so, we have to inline some of the feature rendering here. # so, we have to inline some of the feature rendering here.
display = f"count({self.capa_doc_feature_to_display(statement.child)}): " display = "count(%s): " % self.capa_doc_feature_to_display(statement["child"])
if statement.max == statement.min: if statement["max"] == statement["min"]:
display += f"{statement.min}" display += "%d" % (statement["min"])
elif statement.min == 0: elif statement["min"] == 0:
display += f"{statement.max} or fewer" display += "%d or fewer" % (statement["max"])
elif statement.max == (1 << 64 - 1): elif statement["max"] == (1 << 64 - 1):
display += f"{statement.min} or more" display += "%d or more" % (statement["min"])
else: else:
display += f"between {statement.min} and {statement.max}" display += "between %d and %d" % (statement["min"], statement["max"])
if statement.description: if statement.get("description"):
display += f" ({statement.description})" display += " (%s)" % statement["description"]
parent2 = CapaExplorerFeatureItem(parent, display=display) parent2 = CapaExplorerFeatureItem(parent, display=display)
for location in locations: for location in locations:
# for each location render child node for range statement # for each location render child node for range statement
self.render_capa_doc_feature(parent2, match, statement.child, location, doc) self.render_capa_doc_feature(parent2, statement["child"], location, doc)
return parent2 return parent2
elif isinstance(statement, rd.SubscopeStatement): elif statement["type"] == "subscope":
display = str(statement.scope) display = statement[statement["type"]]
if statement.description: if statement.get("description"):
display += f" ({statement.description})" display += " (%s)" % statement["description"]
return CapaExplorerSubscopeItem(parent, display) return CapaExplorerSubscopeItem(parent, display)
else: else:
raise RuntimeError("unexpected match statement type: " + str(statement)) raise RuntimeError("unexpected match statement type: " + str(statement))
def render_capa_doc_match(self, parent: CapaExplorerDataItem, match: rd.Match, doc: rd.ResultDocument): def render_capa_doc_match(self, parent, match, doc):
"""render capa match read from doc """render capa match read from doc
@param parent: parent node to which new child is assigned @param parent: parent node to which new child is assigned
@param match: match read from doc @param match: match read from doc
@param doc: result doc @param doc: result doc
""" """
if not match.success: if not match["success"]:
# TODO(mike-hunhoff): display failed branches at some point? Help with debugging rules? # TODO: display failed branches at some point? Help with debugging rules?
# https://github.com/mandiant/capa/issues/1601
return return
# optional statement with no successful children is empty # optional statement with no successful children is empty
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL: if match["node"].get("statement", {}).get("type") == "optional" and not any(
if not any(m.success for m in match.children): map(lambda m: m["success"], match["children"])
return ):
return
if isinstance(match.node, rd.StatementNode): if match["node"]["type"] == "statement":
parent2 = self.render_capa_doc_statement_node( parent2 = self.render_capa_doc_statement_node(
parent, match, match.node.statement, [addr.to_capa() for addr in match.locations], doc parent, match["node"]["statement"], match.get("locations", []), doc
) )
elif isinstance(match.node, rd.FeatureNode): elif match["node"]["type"] == "feature":
parent2 = self.render_capa_doc_feature_node( parent2 = self.render_capa_doc_feature_node(
parent, match, match.node.feature, [addr.to_capa() for addr in match.locations], doc parent, match["node"]["feature"], match.get("locations", []), doc
) )
else: else:
raise RuntimeError("unexpected node type: " + str(match.node.type)) raise RuntimeError("unexpected node type: " + str(match["node"]["type"]))
for child in match.children: for child in match.get("children", []):
self.render_capa_doc_match(parent2, child, doc) self.render_capa_doc_match(parent2, child, doc)
def render_capa_doc_by_function(self, doc: rd.ResultDocument): def render_capa_doc_by_function(self, doc):
"""render rule matches by function meaning each rule match is nested under function where it was found""" """ """
matches_by_function: Dict[AbsoluteVirtualAddress, Tuple[CapaExplorerFunctionItem, Set[str]]] = {} matches_by_function = {}
for rule in rutils.capability_rules(doc): for rule in rutils.capability_rules(doc):
match_eas: List[int] = [] for ea in rule["matches"].keys():
ea = capa.ida.helpers.get_func_start_ea(ea)
# initial pass of rule matches if ea is None:
for addr_, _ in rule.matches: # file scope, skip for rendering in this mode
addr: Address = addr_.to_capa()
if isinstance(addr, AbsoluteVirtualAddress):
match_eas.append(int(addr))
for ea in match_eas:
func_ea: Optional[int] = capa.ida.helpers.get_func_start_ea(ea)
if func_ea is None:
# rule match address is not located in a defined function
continue continue
if None is matches_by_function.get(ea, None):
func_address: AbsoluteVirtualAddress = AbsoluteVirtualAddress(func_ea) matches_by_function[ea] = CapaExplorerFunctionItem(self.root_node, ea, can_check=False)
if not matches_by_function.get(func_address, ()):
# create a new function root to nest its rule matches; Note: we must use the address of the
# function here so everything is displayed properly
matches_by_function[func_address] = (
CapaExplorerFunctionItem(self.root_node, func_address, can_check=False),
set(),
)
func_root, func_match_cache = matches_by_function[func_address]
if rule.meta.name in func_match_cache:
# only nest each rule once, so if found, skip
continue
# add matched rule to its function cache; create a new rule node whose parent is the matched
# function node
func_match_cache.add(rule.meta.name)
CapaExplorerRuleItem( CapaExplorerRuleItem(
func_root, matches_by_function[ea],
rule.meta.name, rule["meta"]["name"],
rule.meta.namespace or "", rule["meta"].get("namespace"),
len([ea for ea in match_eas if capa.ida.helpers.get_func_start_ea(ea) == func_ea]), len(rule["matches"]),
rule.source, rule["source"],
can_check=False, can_check=False,
) )
def render_capa_doc_by_program(self, doc: rd.ResultDocument): def render_capa_doc_by_program(self, doc):
""" """ """ """
for rule in rutils.capability_rules(doc): for rule in rutils.capability_rules(doc):
rule_name = rule.meta.name rule_name = rule["meta"]["name"]
rule_namespace = rule.meta.namespace or "" rule_namespace = rule["meta"].get("namespace")
parent = CapaExplorerRuleItem(self.root_node, rule_name, rule_namespace, len(rule.matches), rule.source) parent = CapaExplorerRuleItem(
self.root_node, rule_name, rule_namespace, len(rule["matches"]), rule["source"]
)
for location_, match in rule.matches: for (location, match) in doc["rules"][rule["meta"]["name"]]["matches"].items():
location = location_.to_capa() if rule["meta"]["scope"] == capa.rules.FILE_SCOPE:
parent2: CapaExplorerDataItem
if rule.meta.scope == capa.rules.FILE_SCOPE:
parent2 = parent parent2 = parent
elif rule.meta.scope == capa.rules.FUNCTION_SCOPE: elif rule["meta"]["scope"] == capa.rules.FUNCTION_SCOPE:
parent2 = CapaExplorerFunctionItem(parent, location) parent2 = CapaExplorerFunctionItem(parent, location)
elif rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE: elif rule["meta"]["scope"] == capa.rules.BASIC_BLOCK_SCOPE:
parent2 = CapaExplorerBlockItem(parent, location) parent2 = CapaExplorerBlockItem(parent, location)
elif rule.meta.scope == capa.rules.INSTRUCTION_SCOPE:
parent2 = CapaExplorerInstructionItem(parent, location)
else: else:
raise RuntimeError("unexpected rule scope: " + str(rule.meta.scope)) raise RuntimeError("unexpected rule scope: " + str(rule["meta"]["scope"]))
self.render_capa_doc_match(parent2, match, doc) self.render_capa_doc_match(parent2, match, doc)
def render_capa_doc(self, doc: rd.ResultDocument, by_function: bool): def render_capa_doc(self, doc, by_function):
"""render capa features specified in doc """render capa features specified in doc
@param doc: capa result doc @param doc: capa result doc
@@ -529,44 +483,27 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
# inform model changes have ended # inform model changes have ended
self.endResetModel() self.endResetModel()
def capa_doc_feature_to_display(self, feature: frzf.Feature): def capa_doc_feature_to_display(self, feature):
"""convert capa doc feature type string to display string for ui """convert capa doc feature type string to display string for ui
@param feature: capa feature read from doc @param feature: capa feature read from doc
""" """
key = feature.type key = feature["type"]
value = feature.dict(by_alias=True).get(feature.type) value = feature[feature["type"]]
if value: if value:
if isinstance(feature, frzf.StringFeature): if key == "string":
value = f'"{capa.features.common.escape_string(value)}"' value = '"%s"' % capa.features.escape_string(value)
if feature.get("description", ""):
if isinstance(feature, frzf.PropertyFeature) and feature.access is not None: return "%s(%s = %s)" % (key, value, feature["description"])
key = f"property/{feature.access}"
elif isinstance(feature, frzf.OperandNumberFeature):
key = f"operand[{feature.index}].number"
elif isinstance(feature, frzf.OperandOffsetFeature):
key = f"operand[{feature.index}].offset"
if feature.description:
return f"{key}({value} = {feature.description})"
else: else:
return f"{key}({value})" return "%s(%s)" % (key, value)
else: else:
return f"{key}" return "%s" % key
def render_capa_doc_feature_node( def render_capa_doc_feature_node(self, parent, feature, locations, doc):
self,
parent: CapaExplorerDataItem,
match: rd.Match,
feature: frzf.Feature,
locations: List[Address],
doc: rd.ResultDocument,
):
"""process capa doc feature node """process capa doc feature node
@param parent: parent node to which child is assigned @param parent: parent node to which child is assigned
@param match: match information
@param feature: capa doc feature node @param feature: capa doc feature node
@param locations: locations identified for feature @param locations: locations identified for feature
@param doc: capa doc @param doc: capa doc
@@ -577,7 +514,6 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
# only one location for feature so no need to nest children # only one location for feature so no need to nest children
parent2 = self.render_capa_doc_feature( parent2 = self.render_capa_doc_feature(
parent, parent,
match,
feature, feature,
next(iter(locations)), next(iter(locations)),
doc, doc,
@@ -588,114 +524,73 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
parent2 = CapaExplorerFeatureItem(parent, display) parent2 = CapaExplorerFeatureItem(parent, display)
for location in sorted(locations): for location in sorted(locations):
self.render_capa_doc_feature(parent2, match, feature, location, doc) self.render_capa_doc_feature(parent2, feature, location, doc)
return parent2 return parent2
def render_capa_doc_feature( def render_capa_doc_feature(self, parent, feature, location, doc, display="-"):
self,
parent: CapaExplorerDataItem,
match: rd.Match,
feature: frzf.Feature,
location: Address,
doc: rd.ResultDocument,
display="-",
):
"""render capa feature read from doc """render capa feature read from doc
@param parent: parent node to which new child is assigned @param parent: parent node to which new child is assigned
@param match: match information
@param feature: feature read from doc @param feature: feature read from doc
@param doc: capa feature doc @param doc: capa feature doc
@param location: address of feature @param location: address of feature
@param display: text to display in plugin UI @param display: text to display in plugin UI
""" """
# special handling for characteristic pending type # special handling for characteristic pending type
if isinstance(feature, frzf.CharacteristicFeature): if feature["type"] == "characteristic":
characteristic = feature.characteristic if feature[feature["type"]] in ("embedded pe",):
if characteristic in ("embedded pe",):
return CapaExplorerByteViewItem(parent, display, location) return CapaExplorerByteViewItem(parent, display, location)
if characteristic in ("loop", "recursive call", "tight loop"): if feature[feature["type"]] in ("loop", "recursive call", "tight loop"):
return CapaExplorerFeatureItem(parent, display=display) return CapaExplorerFeatureItem(parent, display=display)
# default to instruction view for all other characteristics # default to instruction view for all other characteristics
return CapaExplorerInstructionViewItem(parent, display, location) return CapaExplorerInstructionViewItem(parent, display, location)
elif isinstance(feature, frzf.MatchFeature): if feature["type"] == "match":
# display content of rule for all rule matches # display content of rule for all rule matches
matched_rule_source = "" return CapaExplorerRuleMatchItem(
parent, display, source=doc["rules"].get(feature[feature["type"]], {}).get("source", "")
)
# check if match is a matched rule if feature["type"] == "regex":
matched_rule = doc.rules.get(feature.match) return CapaExplorerStringViewItem(
if matched_rule is not None: parent, display, location, '"%s"' % capa.features.escape_string(feature["match"])
matched_rule_source = matched_rule.source )
return CapaExplorerRuleMatchItem(parent, display, source=matched_rule_source) if feature["type"] == "basicblock":
elif isinstance(feature, (frzf.RegexFeature, frzf.SubstringFeature)):
for capture, addrs in sorted(match.captures.items()):
for addr in addrs:
assert isinstance(addr, frz.Address)
if location == addr.value:
return CapaExplorerStringViewItem(
parent, display, location, '"' + capa.features.common.escape_string(capture) + '"'
)
# programming error: the given location should always be found in the regex matches
raise ValueError("regex match at location not found")
elif isinstance(feature, frzf.BasicBlockFeature):
return CapaExplorerBlockItem(parent, location) return CapaExplorerBlockItem(parent, location)
elif isinstance( if feature["type"] in (
feature, "bytes",
( "api",
frzf.BytesFeature, "mnemonic",
frzf.APIFeature, "number",
frzf.MnemonicFeature, "offset",
frzf.NumberFeature, "number/x32",
frzf.OffsetFeature, "number/x64",
frzf.OperandNumberFeature, "offset/x32",
frzf.OperandOffsetFeature, "offset/x64",
),
): ):
# display instruction preview # display instruction preview
return CapaExplorerInstructionViewItem(parent, display, location) return CapaExplorerInstructionViewItem(parent, display, location)
elif isinstance(feature, frzf.SectionFeature): if feature["type"] in ("section",):
# display byte preview # display byte preview
return CapaExplorerByteViewItem(parent, display, location) return CapaExplorerByteViewItem(parent, display, location)
elif isinstance(feature, frzf.StringFeature): if feature["type"] in ("string",):
# display string preview # display string preview
return CapaExplorerStringViewItem( return CapaExplorerStringViewItem(
parent, display, location, f'"{capa.features.common.escape_string(feature.string)}"' parent, display, location, '"%s"' % capa.features.escape_string(feature[feature["type"]])
) )
elif isinstance( if feature["type"] in ("import", "export"):
feature,
(
frzf.ImportFeature,
frzf.ExportFeature,
frzf.FunctionNameFeature,
),
):
# display no preview # display no preview
return CapaExplorerFeatureItem(parent, location=location, display=display)
elif isinstance(
feature,
(
frzf.ArchFeature,
frzf.OSFeature,
frzf.FormatFeature,
),
):
return CapaExplorerFeatureItem(parent, display=display) return CapaExplorerFeatureItem(parent, display=display)
raise RuntimeError("unexpected feature type: " + str(feature.type)) raise RuntimeError("unexpected feature type: " + str(feature["type"]))
def update_function_name(self, old_name, new_name): def update_function_name(self, old_name, new_name):
"""update all instances of old function name with new function name """update all instances of old function name with new function name

View File

@@ -1,10 +1,11 @@
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. # Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # 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 # 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. # 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. # See the License for the specific language governing permissions and limitations under the License.
import six
from PyQt5 import QtCore from PyQt5 import QtCore
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@@ -22,7 +23,7 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None): def __init__(self, parent=None):
"""initialize proxy filter""" """initialize proxy filter"""
super().__init__(parent) super(CapaExplorerRangeProxyModel, self).__init__(parent)
self.min_ea = None self.min_ea = None
self.max_ea = None self.max_ea = None
@@ -92,7 +93,7 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
@param parent: QModelIndex of parent @param parent: QModelIndex of parent
""" """
# filter not set # filter not set
if self.min_ea is None or self.max_ea is None: if self.min_ea is None and self.max_ea is None:
return True return True
index = self.sourceModel().index(row, 0, parent) index = self.sourceModel().index(row, 0, parent)
@@ -145,7 +146,7 @@ class CapaExplorerSearchProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None): def __init__(self, parent=None):
""" """ """ """
super().__init__(parent) super(CapaExplorerSearchProxyModel, self).__init__(parent)
self.query = "" self.query = ""
self.setFilterKeyColumn(-1) # all columns self.setFilterKeyColumn(-1) # all columns
@@ -207,7 +208,7 @@ class CapaExplorerSearchProxyModel(QtCore.QSortFilterProxyModel):
if not data: if not data:
continue continue
if not isinstance(data, str): if not isinstance(data, six.string_types):
# sanity check: should already be a string, but double check # sanity check: should already be a string, but double check
continue continue

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