Compare commits

..

91 Commits

Author SHA1 Message Date
Ana María Martínez Gómez
407ecab162 Merge pull request #515 from Ana06/v1-6-1 2021-04-07 18:03:56 +02:00
Ana Maria Martinez Gomez
cbc1f57b21 changelog: add master (unreleased) to CHANGELOG
Add placeholder for master (unreleased changes) in CHANGELOG. Document
this in the release checklist.
2021-04-07 17:50:19 +02:00
Ana Maria Martinez Gomez
374a9e4337 changelog: v1.6.1
This release includes several bug fixes, such as a vivisect fix for a bug, which caused that capa didn't work on Windows with Python 3. It also adds 17 new rules and a bunch of improvements in the rules and IDA rule generator. We appreciate everyone who opened issues, provided feedback, and contributed code and rules.

This is the very last capa release that supports Python 2.
2021-04-07 17:50:16 +02:00
Capa Bot
83e2f80d10 Sync capa-testfiles submodule 2021-04-07 13:53:32 +00:00
Ana Maria Martinez Gomez
576211c4ef version: bump to v1.6.1 2021-04-07 11:11:43 +02:00
Ana María Martínez Gómez
31fc5a31d6 Merge pull request #513 from Ana06/ping-dependencies
setup: pin dependencies
2021-04-07 10:19:04 +02:00
Ana Maria Martinez Gomez
eb08943d4f setup: pin dependencies
Pin all dependencies in setup to the currently used version to avoid
that a new release breaks capa without being noticed.

Closes https://github.com/fireeye/capa/issues/498
2021-04-07 09:40:13 +02:00
Ana María Martínez Gómez
c36ed71353 Merge pull request #470 from fireeye/ci/test-windows 2021-04-07 09:38:34 +02:00
Ana Maria Martinez Gomez
fa52dbcf84 ci: skip smda tests in win32
Due to a bug, two `test_smda_features` tests are failing:
https://github.com/danielplohmann/smda/issues/20

Disable them until the bug is fixed.
2021-04-06 21:53:22 +02:00
Ana Maria Martinez Gomez
d412e66cea ci: do not test Python 2.7 with Windows
The Python 2.7 tests fail in Windows with vivisect because the Windows
filesystem encoding is not UTF-8. This shouldn't be a problem when using
capa as the given filename most likely uses the same encoding, but we
force UTF-8 in our tests. As we are planing to remove Python 2 support
is not wortwhile to invest time in making this test working. Instead,
test Python 2.7 only in Ubuntu.
2021-04-06 21:39:01 +02:00
Moritz Raabe
efe50d3313 ci: test on Windows and macOS
Run the tests on Windows and macOS to avoid failures OS related.

closes #460
2021-04-06 21:38:07 +02:00
Ana María Martínez Gómez
1062ba995e doc: add milestones link to release checklist
This makes it a bit easier to check if all milestoned issues/PRs are addressed, or reassign to a new milestone.

I am committing directly to master as this is a minor change which doesn't need review.
2021-04-06 10:21:43 +02:00
Ana María Martínez Gómez
7f93bd5b59 Merge pull request #512 from fireeye/williballenthin-patch-2
setup: bump viv to v1.0.1
2021-04-06 10:17:44 +02:00
Willi Ballenthin
275d170680 setup: bump viv to v1.0.1 2021-04-05 21:22:17 -06:00
Moritz
6d7e10b804 Merge pull request #511 from fireeye/ci/fix-typos
fix submodule typos
2021-04-05 13:13:41 +02:00
Moritz Raabe
25944864f7 fix submodule typos 2021-04-05 12:52:08 +02:00
Capa Bot
5e84a16eba Sync capa rules submodule 2021-04-01 16:44:59 +00:00
Capa Bot
244ec163a3 Sync capa-testfiles submodule 2021-04-01 16:44:11 +00:00
Capa Bot
dabd2174d4 Sync capa rules submodule 2021-03-29 16:25:18 +00:00
Moritz
f8d2b41a86 Merge pull request #495 from fireeye/gh/add-pr-template
add PR template
2021-03-29 17:31:05 +02:00
Capa Bot
902972a1ee Sync capa-testfiles submodule 2021-03-29 12:49:24 +00:00
Capa Bot
bddb5fbd2f Sync capa rules submodule 2021-03-26 11:17:46 +00:00
Capa Bot
adfd769963 Sync capa-testfiles submodule 2021-03-26 11:00:35 +00:00
Capa Bot
c75e70ec74 Sync capa-testfiles submodule 2021-03-26 11:00:15 +00:00
Moritz
6118183105 Merge pull request #504 from fireeye/mr-tz-patch-1
Update setup.py
2021-03-26 11:58:52 +01:00
Moritz
da755d8411 Update setup.py 2021-03-26 11:44:04 +01:00
mike-hunhoff
742e03d90f Merge pull request #503 from fireeye/explorer/update-readme
updating capa explorer README
2021-03-25 14:51:21 -06:00
Capa Bot
744228a03e Sync capa rules submodule 2021-03-25 20:48:41 +00:00
Michael Hunhoff
5d1c6f54cd updating capa explorer README 2021-03-25 14:30:28 -06:00
mike-hunhoff
0a3dd4600b Merge pull request #468 from fireeye/features/support-string-values-special-chars
add support for string features with special characters e.g. '\n'
2021-03-25 12:58:00 -06:00
Michael Hunhoff
0289891d07 merging upstream 2021-03-25 12:43:59 -06:00
Michael Hunhoff
87cdf837e6 merging upstream 2021-03-25 12:42:36 -06:00
Capa Bot
ea4c7d6403 Sync capa rules submodule 2021-03-25 18:37:22 +00:00
Capa Bot
2807549564 Sync capa rules submodule 2021-03-25 07:21:21 +00:00
Capa Bot
c0fe96cec6 Sync capa-testfiles submodule 2021-03-25 07:17:41 +00:00
mike-hunhoff
8c967ac237 Merge pull request #500 from fireeye/explorer/improve-rulegen-search
explorer: add checks to validate matched data when searching
2021-03-24 15:55:34 -06:00
Michael Hunhoff
c48b46e932 explorer: adding checks to validate matched data when searching 2021-03-24 15:33:20 -06:00
mike-hunhoff
49d1af7798 improve unit tests for strings containing special characters
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2021-03-24 13:22:18 -06:00
mike-hunhoff
d44fd008ae improve unit tests for strings containing special characters
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2021-03-24 13:22:04 -06:00
Moritz Raabe
c0c9ea3403 incorprate Ana's feedback 2021-03-24 09:22:40 +01:00
Michael Hunhoff
21359da766 updating test for strings with special characaters 2021-03-23 16:02:47 -06:00
Michael Hunhoff
e51c79c241 adding lint for incorrect rule string format, refined rendering for strings 2021-03-23 15:55:48 -06:00
Capa Bot
195bae903f Sync capa rules submodule 2021-03-23 12:25:20 +00:00
Moritz Raabe
5aff21a9a1 add PR template 2021-03-23 10:52:01 +01:00
Ana María Martínez Gómez
6f289d1b8e Merge pull request #476 from Ana06/tag-workflow 2021-03-23 09:54:59 +01:00
Moritz
71b21aec59 Merge pull request #492 from fireeye/ignore-gitfiles
rule loading: ignore files starting with .git
2021-03-23 08:16:29 +01:00
Capa Bot
42a87d4eaa Sync capa-testfiles submodule 2021-03-23 07:14:58 +00:00
Capa Bot
51d125642f Sync capa rules submodule 2021-03-23 07:14:21 +00:00
mike-hunhoff
ddebf2e1cb Merge pull request #493 from fireeye/enhance/472
rule generator: support subscope rules
2021-03-22 17:28:43 -06:00
Michael Hunhoff
7f3e8f1fb1 adding support to match subscope rules and auto insert child statements when creating a new basic block subscope 2021-03-22 17:12:13 -06:00
Ana María Martínez Gómez
ab7dbcd2e4 Merge pull request #491 from fireeye/williballenthin-patch-3 2021-03-22 19:16:49 +01:00
Ana Maria Martinez Gomez
7e5cbddf5d doc: document release process
Add a release checklist.

Closes https://github.com/fireeye/capa/issues/184
2021-03-22 19:14:02 +01:00
Moritz Raabe
44f517c20d rule loading: ignore files starting with .git 2021-03-22 18:11:29 +01:00
Michael Hunhoff
7bf8c6e3a1 merging upstream 2021-03-22 10:33:36 -06:00
Michael Hunhoff
31ea683335 merge upstream 2021-03-22 09:53:07 -06:00
Willi Ballenthin
29d8f1fd27 ci: tests: pin OS version 2021-03-22 09:51:20 -06:00
Willi Ballenthin
a6c472bb2a ci: publish: pin OS version 2021-03-22 09:50:47 -06:00
Willi Ballenthin
b880d419a3 ci: build: pin OS versions 2021-03-22 09:50:04 -06:00
Capa Bot
a2ff87af8a Sync capa rules submodule 2021-03-22 15:45:10 +00:00
Willi Ballenthin
5b9c577380 Merge pull request #489 from fireeye/dependabot/pip/viv-utils-0.6.0
Bump viv-utils from 0.5.0 to 0.6.0
2021-03-22 09:39:52 -06:00
Capa Bot
4775e124db Sync capa rules submodule 2021-03-22 09:02:35 +00:00
Moritz
c243158d7c Merge pull request #486 from fireeye/fix/eol-improvements
EOL improvements
2021-03-22 09:58:29 +01:00
Capa Bot
8afc3f46f6 Sync capa rules submodule 2021-03-22 08:41:21 +00:00
dependabot[bot]
8b5dc54397 Bump viv-utils from 0.5.0 to 0.6.0
Bumps [viv-utils](https://github.com/williballenthin/viv-utils) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/williballenthin/viv-utils/releases)
- [Commits](https://github.com/williballenthin/viv-utils/compare/v0.5.0...v0.6.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-22 06:20:47 +00:00
Capa Bot
1dbb34df9f Sync capa-testfiles submodule 2021-03-21 19:28:58 +00:00
mike-hunhoff
9383f0bc77 Merge pull request #474 from fireeye/explorer/fix-471
explorer: adding support for multi-line tab and SHIFT + Tab
2021-03-19 19:11:14 -06:00
Moritz Raabe
13306b71e0 add file 2021-03-19 09:40:44 +01:00
Moritz Raabe
8719a23de4 dos2unix 2021-03-19 09:40:44 +01:00
Moritz Raabe
7e0b5236af better deal with CRLF/LF issues 2021-03-19 09:40:43 +01:00
Moritz Raabe
c7798b3254 ensure LF end of line 2021-03-19 09:40:43 +01:00
Willi Ballenthin
7d668550f5 Merge pull request #485 from fireeye/ci/ensure-lf-eol
ensure LF end of line
2021-03-18 14:41:13 -06:00
Capa Bot
c945eaf804 Sync capa rules submodule 2021-03-18 20:41:05 +00:00
Moritz Raabe
1bfe0e0874 ensure LF end of line 2021-03-18 20:15:23 +01:00
Capa Bot
153c6a7b01 Sync capa-testfiles submodule 2021-03-18 18:04:33 +00:00
Ana Maria Martinez Gomez
30a83fa382 doc: Fix broken link in README
Introduced in https://github.com/fireeye/capa/pull/478
2021-03-16 16:37:33 +01:00
Willi Ballenthin
c0bcefe0bf Merge pull request #479 from Ana06/viv-utils5
setup: bump viv-utils to 0.5.0
2021-03-16 07:02:43 -06:00
Ana Maria Martinez Gomez
5d16a77891 ci: tag capa-rules on release
Add GitHub Action to tag capa-rules when releasing capa. The used tag
name is the same as the one in capa.
2021-03-16 12:45:02 +01:00
Ana Maria Martinez Gomez
cd01a01894 setup: bump viv-utils to 0.5.0
In viv-utils `getWorkspace` raises `IncompatibleVivVersion` on Python 3
when `vw.loadWorkspace(viv_file)` raises `UnicodeDecodeError`.

Fixes https://github.com/fireeye/capa/issues/469

As we use the same version in py2 and py3, define the viv-utils
requirement once.
2021-03-16 10:51:50 +01:00
Willi Ballenthin
df36bb9f35 Merge pull request #478 from Ana06/badges
doc: Improve README badges
2021-03-15 14:42:57 -06:00
Ana María Martínez Gómez
030893e125 Merge pull request #475 from Ana06/incompatible-viv
changelog: document incompatibility of viv files
2021-03-15 17:30:17 +01:00
Ana Maria Martinez Gomez
b2ab8ab54c doc: Improve README badges
- Add a link to the `PyPI - Python Version` badge. Otherwise it opens
the image when clicking on it, which is inconsistent with the other
labels. I arrived too late to point this out in:
https://github.com/fireeye/capa/pull/477
- Add release badge with last release version. This may help users to
realize that a new version has been released.
- Add downloads badge.
- Order labels by color.

Closes https://github.com/fireeye/capa/issues/196
2021-03-15 16:47:15 +01:00
Willi Ballenthin
12eb1b96de Merge pull request #477 from fireeye/mr-tz-patch-1
Update README.md with Python version badge
2021-03-15 08:35:27 -06:00
Moritz
cff7d4bad4 Update README.md 2021-03-15 11:54:11 +01:00
Ana Maria Martinez Gomez
a31c616a21 changelog: document incompatibility of viv files
`.viv` files (generated by vivisect) are not compatible between Python 2
and Python 3. This causes capa to raise an `UnicodeDecodeError`
exception and should be documented better. I'll add this change to the
release notes after the review.

Related to https://github.com/fireeye/capa/issues/469
2021-03-15 10:26:32 +01:00
Michael Hunhoff
3d2b4dcc26 adding support for multi-line tab and SHIFT + Tab 2021-03-11 17:13:43 -07:00
Michael Hunhoff
c7d24ee290 adding support for string features with special characters e.g. '\n' 2021-03-10 13:56:54 -07:00
mike-hunhoff
06c958f081 Merge pull request #465 from fireeye/explorer/fix-463
explorer: improve settings modification
2021-03-10 11:30:23 -07:00
Michael Hunhoff
b8efe585d5 fix 463, improve settings UI 2021-03-09 14:56:44 -07:00
Willi Ballenthin
e7eb2152cc Merge pull request #464 from fireeye/explorer/fix-462
fix 462
2021-03-09 12:13:54 -07:00
Michael Hunhoff
e1a8641399 fixes 462, default to empty string when accessing rule path stored in ida_settings 2021-03-09 12:09:35 -07:00
Capa Bot
cffac62e68 Sync capa rules submodule 2021-03-09 10:00:48 +00:00
35 changed files with 1954 additions and 1491 deletions

9
.gitattributes vendored Normal file
View File

@@ -0,0 +1,9 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.py text
*.yml text
*.md text
*.txt text

View File

@@ -5,7 +5,7 @@ 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 sybmodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues. We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/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/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one. Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.

View File

@@ -5,7 +5,7 @@ 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 sybmodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues. We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/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/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one. Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.

32
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,32 @@
<!--
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.
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md
PR template based on https://embeddedartistry.com/blog/2017/08/04/a-github-pull-request-template-for-your-projects/
-->
### Description
<!-- Please describe the changes in this PR. Including your motivation and context helps us to review. -->
closes # (issue)
### 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

View File

@@ -15,10 +15,10 @@ jobs:
# 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-latest - os: windows-2019
artifact_name: capa.exe artifact_name: capa.exe
asset_name: windows asset_name: windows
- os: macos-latest - os: macos-10.15
artifact_name: capa artifact_name: capa
asset_name: macos asset_name: macos
steps: steps:
@@ -30,7 +30,7 @@ jobs:
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.9 python-version: 3.9
- if: matrix.os == 'ubuntu-latest' - if: matrix.os == 'ubuntu-16.04'
run: sudo apt-get install -y libyaml-dev run: sudo apt-get install -y libyaml-dev
- name: Install PyInstaller - name: Install PyInstaller
run: pip install 'pyinstaller==4.2' run: pip install 'pyinstaller==4.2'
@@ -47,7 +47,7 @@ jobs:
zip: zip:
name: zip ${{ matrix.asset_name }} name: zip ${{ matrix.asset_name }}
runs-on: ubuntu-latest runs-on: ubuntu-20.04
needs: build needs: build
strategy: strategy:
matrix: matrix:

View File

@@ -9,7 +9,7 @@ on:
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python

24
.github/workflows/tag.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: tag
on:
release:
types: [published]
jobs:
tag:
name: Tag capa rules
runs-on: ubuntu-20.04
steps:
- name: Checkout capa-rules
uses: actions/checkout@v2
with:
repository: fireeye/capa-rules
token: ${{ secrets.CAPA_TOKEN }}
- name: Tag capa-rules
run: git tag ${{ github.event.release.tag_name }}
- name: Push tag to capa-rules
uses: ad-m/github-push-action@master
with:
repository: fireeye/capa-rules
github_token: ${{ secrets.CAPA_TOKEN }}
tags: true

View File

@@ -8,7 +8,7 @@ on:
jobs: jobs:
code_style: code_style:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- name: Checkout capa - name: Checkout capa
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -24,7 +24,7 @@ jobs:
run: black -l 120 --check . run: black -l 120 --check .
rule_linter: rule_linter:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- name: Checkout capa with rules submodule - name: Checkout capa with rules submodule
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -41,30 +41,39 @@ jobs:
run: python scripts/lint.py rules/ run: python scripts/lint.py rules/
tests: tests:
name: Tests in ${{ matrix.python }} name: Tests in ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
needs: [code_style, rule_linter] needs: [code_style, rule_linter]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-20.04, windows-2019, macos-10.15]
# across all operating systems
python-version: [3.6, 3.9]
include: include:
- python: 2.7 # on Ubuntu run these as well
- python: 3.7 - os: ubuntu-20.04
- python: 3.8 python-version: 2.7
- python: 3.9.1 - os: ubuntu-20.04
python-version: 3.7
- os: ubuntu-20.04
python-version: 3.8
steps: steps:
- name: Checkout capa with submodules - name: Checkout capa with submodules
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
submodules: true submodules: true
- name: Set up Python ${{ matrix.python }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python-version }}
- name: Install pyyaml - name: Install pyyaml
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 tests/ run: pytest tests/

View File

@@ -1,5 +1,91 @@
# Change Log # Change Log
## master (unreleased)
### New Features
### New Rules
### Bug Fixes
### Changes
### Development
### Raw diffs
- [capa v1.6.1...master](https://github.com/fireeye/capa/compare/v1.6.1...master)
- [capa-rules v1.6.1...master](https://github.com/fireeye/capa-rules/compare/v1.6.1...master)
## v1.6.1 (2021-04-07)
This release includes several bug fixes, such as a vivisect issue that prevented capa from working on Windows with Python 3. It also adds 17 new rules and a bunch of improvements in the rules and IDA rule generator. We appreciate everyone who opened issues, provided feedback, and contributed code and rules.
### Upcoming changes
**This is the very last capa release that supports Python 2.** The next release will be v2.0 and will have breaking changes, including the removal of Python 2 support.
### New features
- explorer: add support for multi-line tab and SHIFT + Tab #474 @mike-hunhoff
![multi-line tab in rule generator](doc/img/changelog/tab.gif)
### New Rules (17)
- encrypt data using RC4 with custom key via WinAPI @MalwareMechanic
- encrypt data using Curve25519 @dandonov
- packaged as an IExpress self-extracting archive @recvfrom
- create registry key via offline registry library @johnk3r
- open registry key via offline registry library @johnk3r
- query registry key via offline registry library @johnk3r
- set registry key via offline registry library @johnk3r
- delete registry key via offline registry library @johnk3r
- enumerate PE sections @Ana06
- inject DLL reflectively @Ana06
- inspect section memory permissions @Ana06
- parse PE exports @Ana06
- rebuild import table @Ana06
- compare security identifiers @mike-hunhoff
- get user security identifier @mike-hunhoff
- listen for remote procedure calls @mike-hunhoff
- query remote server for available data @mike-hunhoff
### Bug Fixes
- vivisect: update to v1.0.1 which includes bug fix for #459 (capa failed in Windows with Python 3 and vivisect) #512 @williballenthin
- explorer: fix initialize rules directory #464 @mike-hunhoff
- explorer: support subscope rules #493 @mike-hunhoff
- explorer: add checks to validate matched data when searching #500 @mike-hunhoff
- features, explorer: add support for string features with special characters e.g. '\n' #468 @mike-hunhoff
### Changes
- vivisect: raises `IncompatibleVivVersion` instead of `UnicodeDecodeError` when using incompatible Python 2 `.viv` files with Python3 #479 @Ana06
- explorer: improve settings modification #465 @mike-hunhoff
- rules: improvements @mr-tz, @re-fox, @mike-hunhoff
- rules, lint: enforce string with double quotes formatting in rules #468 @mike-hunhoff
- lint: ensure LF end of line #485 #486 @mr-tz
- setup: pin dependencies #513 #504 @Ana06 @mr-tz
### Development
- ci: test on Windows, Ubuntu, macOS across Python versions #470 @mr-tz @Ana06
- ci: pin OS versions #491 @williballenthin
- ci: tag capa-rules on release #476 @Ana06
- doc: document release process #476 @Ana06
- doc: Improve README badges #477 #478 @ana06 @mr-tz
- doc: update capa explorer documentation #503 @mike-hunhoff
- doc: add PR template #495 @mr-tz
- changelog: document incompatibility of viv files #475 @Ana06
- rule loading: ignore files starting with .git #492 @mr-tz
### Raw diffs
- [capa v1.6.0...v1.6.1](https://github.com/fireeye/capa/compare/v1.6.0...v1.6.1)
- [capa-rules v1.6.0...v1.6.1](https://github.com/fireeye/capa-rules/compare/v1.6.0...v1.6.1)
## v1.6.0 (2021-03-09) ## v1.6.0 (2021-03-09)
This release adds the capa explorer rule generator plugin for IDA Pro, vivisect support for Python 3 and 12 new rules. We appreciate everyone who opened issues, provided feedback, and contributed code and rules. Thank you also to the vivisect development team (@rakuy0, @atlas0fd00m) for the Python 3 support (`vivisect==1.0.0`) and the fixes for Python 2 (`vivisect==0.2.1`). This release adds the capa explorer rule generator plugin for IDA Pro, vivisect support for Python 3 and 12 new rules. We appreciate everyone who opened issues, provided feedback, and contributed code and rules. Thank you also to the vivisect development team (@rakuy0, @atlas0fd00m) for the Python 3 support (`vivisect==1.0.0`) and the fixes for Python 2 (`vivisect==0.2.1`).
@@ -10,6 +96,15 @@ The capa explorer IDA plugin now helps you quickly build new capa rules using fe
![](doc/img/rulegen_expanded.png) ![](doc/img/rulegen_expanded.png)
### Python 2/3 vivisect workspace compatibility
This version of capa adds Python 3 support in vivisect. Note that `.viv` files (generated by vivisect) are not compatible between Python 2 and Python 3. When updating to Python 3 you need to delete all the `.viv` files for capa to work.
If you get the following error (or a similar one), you most likely need to delete `.viv` files:
```
UnicodeDecodeError: 'ascii' codec can't decode byte 0x90 in position 2: ordinal not in range(128)
```
### Upcoming changes ### Upcoming changes
**This is the last capa release that supports Python 2.** The next release will be v2.0 and will have breaking changes, including the removal of Python 2 support. **This is the last capa release that supports Python 2.** The next release will be v2.0 and will have breaking changes, including the removal of Python 2 support.

View File

@@ -1,7 +1,10 @@
![capa](.github/logo.png) ![capa](.github/logo.png)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flare-capa)](https://pypi.org/project/flare-capa)
[![Last release](https://img.shields.io/github/v/release/fireeye/capa)](https://github.com/fireeye/capa/releases)
[![Number of rules](https://img.shields.io/badge/rules-485-blue.svg)](https://github.com/fireeye/capa-rules)
[![CI status](https://github.com/fireeye/capa/workflows/CI/badge.svg)](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster) [![CI status](https://github.com/fireeye/capa/workflows/CI/badge.svg)](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
[![Number of rules](https://img.shields.io/badge/rules-469-blue.svg)](https://github.com/fireeye/capa-rules) [![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.
@@ -146,8 +149,8 @@ rule:
The [github.com/fireeye/capa-rules](https://github.com/fireeye/capa-rules) repository contains hundreds of standard library rules that are distributed with capa. The [github.com/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 plugin](capa/ida/plugin/). If you use IDA Pro, then you can use the [capa explorer](capa/ida/plugin/) plugin.
capa explorer lets you quickly identify and navigate to interesting areas of a program and help you build new capa rules out of the features extracted directly from your IDB. 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](doc/img/explorer_expanded.png) ![capa + IDA Pro integration](doc/img/explorer_expanded.png)

View File

@@ -38,6 +38,20 @@ def hex_string(h):
return " ".join(h[i : i + 2] for i in range(0, len(h), 2)).upper() 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): class Feature(object):
def __init__(self, value, arch=None, description=None): def __init__(self, value, arch=None, description=None):
""" """

View File

@@ -1,13 +1,12 @@
![capa explorer](../../../.github/capa-explorer-logo.png) ![capa explorer](../../../.github/capa-explorer-logo.png)
capa explorer is an IDA Pro plugin written in Python that integrates the FLARE team's open-source framework, capa, with IDA. 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 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. You can use capa explorer to run capa directly on an IDA database without requiring 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 or execution of the source binary. Once a database has been analyzed, capa explorer can be used to quickly identify and navigate to interesting areas of a program and manually build new capa rules out 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.
of the features extracted directly 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
to important addresses in the IDA Pro database and highlights key features in the Disassembly view so they stand out visually. To illustrate, we use capa explorer to to important addresses in the IDB and highlights key features in the Disassembly view so they stand out visually. To illustrate, we use capa explorer to
analyze Lab 14-02 from [Practical Malware Analysis](https://nostarch.com/malware) (PMA) available [here](https://practicalmalwareanalysis.com/labs/). Our goal is to understand analyze Lab 14-02 from [Practical Malware Analysis](https://nostarch.com/malware) (PMA) available [here](https://practicalmalwareanalysis.com/labs/). Our goal is to understand
the program's functionality. the program's functionality.
@@ -15,16 +14,15 @@ After loading Lab 14-02 into IDA and analyzing the database with capa explorer,
![](../../../doc/img/explorer_condensed.png) ![](../../../doc/img/explorer_condensed.png)
We can use capa explorer to navigate the IDA Disassembly view directly to the suspect function and get an assembly-level breakdown of why capa matched `self delete via COMSPEC environment variable` We can use capa explorer to navigate our Disassembly view directly to the suspect function and get an assembly-level breakdown of why capa matched `self delete via COMSPEC environment variable`.
for this particular function.
![](../../../doc/img/explorer_expanded.png) ![](../../../doc/img/explorer_expanded.png)
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 new capa rules. To start select the `Rule Generator` tab, navigate to a function in the IDA `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 this function and display them in the `Function 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 the rule text directly and the `Editor` pane to construct and rearrange your hierarchy of statements and features. When you finish a rule you can save it directly to a file by clicking `Save`. to modify 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`.
@@ -44,7 +42,7 @@ If you encounter issues with your specific setup, please open a new [Issue](http
### Supported File Types ### Supported File Types
capa explorer is limited to the file types supported by capa, which includes: capa explorer is limited to the file types supported by capa, which include:
* Windows 32-bit and 64-bit PE files * Windows 32-bit and 64-bit PE files
* Windows 32-bit and 64-bit shellcode * Windows 32-bit and 64-bit shellcode
@@ -62,50 +60,48 @@ You can install capa explorer using the following steps:
### Usage ### Usage
1. Run 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`
3. Select the `Program Analysis` tab 3. Select the `Program Analysis` tab
4. Click the `Analyze` button 4. Click the `Analyze` button
When running capa explorer for the first time you are prompted to select a file directory containing capa rules. The plugin conveniently When running capa explorer for the first time you are prompted to select a file directory containing capa rules. The plugin conveniently
remembers your selection for future runs; you can change this selection by navigating to `Settings > Change default rules directory...`. We recommend remembers your selection for future runs; you can change this selection and other default settings by clicking `Settings`. We recommend
downloading and using the [standard collection of capa rules](https://github.com/fireeye/capa-rules) when getting started with the plugin. downloading and using the [standard collection of capa rules](https://github.com/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
* Reset the plugin user interface and remove highlighting from IDA 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 capa rules directory by navigating to `Settings > Change default rules directory...` from the plugin menu * 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 the IDA Disassembly view to 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 the IDA Dissasembly 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
* Navigate to a function in the `Disassembly` view and click`Analyze` to get started * Navigate to a function in your Disassembly view and click`Analyze` to get started
* Double-click or multi-select + right-click in the `Function Features` pane to add features to the `Editor` pane * Double-click or use multi-select + right-click to add features from the `Features` pane to the `Editor` pane
* Right-click features in the `Editor` pane to make modifications * Right-click features in the `Editor` pane to make context-specific modifications
* Drag-and-drop (single click + multi-select support) features in the `Editor` pane to quickly build a hierarchy of statements and features * Drag-and-drop (single click + multi-select support) features in the `Editor` pane to construct your hierarchy of statements and features
* Right-click anywhere in the `Editor` pane not on a feature to quickly remove all features * Right-click anywhere in the `Editor` pane not on a feature to remove all features
* Add descriptions/comments by placing editing the appropriate column in the `Editor` pane * Add descriptions or comments to a feature by editing the corresponding column in the `Editor` pane
* Directly edit rule text, including rule metadata fields using the `Preview` pane * Directly edit rule text and metadata fields using the `Preview` pane
* Change the default rule author and default scope displayed in the `Preview` pane by navigating to `Settings` * Change the default rule author and default rule scope displayed in the `Preview` pane by clicking `Settings`
## Development ## Development
Because capa explorer is packaged with capa you will need to install capa locally for 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
You can install capa locally by following the steps outlined in `Method 3: Inspecting the capa source code` of the [capa
installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md#method-3-inspecting-the-capa-source-code). Once installed, copy [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py) installation guide](https://github.com/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 IDA plugins directory to run the plugin 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 IDA [feature extractor](https://github.com/fireeye/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/fireeye/capa-rules/blob/master/doc/format.md#extracted-features) from the IDA database 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/fireeye/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 IDA feature extractor and capa, providing an interactive user interface to dissect rule matches found by capa using features extracted by the IDA feature extractor * 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

@@ -37,6 +37,10 @@ from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
settings = ida_settings.IDASettings("capa") settings = ida_settings.IDASettings("capa")
CAPA_SETTINGS_RULE_PATH = "rule_path"
CAPA_SETTINGS_RULEGEN_AUTHOR = "rulegen_author"
CAPA_SETTINGS_RULEGEN_SCOPE = "rulegen_scope"
def write_file(path, data): def write_file(path, data):
""" """ """ """
@@ -166,6 +170,60 @@ class CapaExplorerFeatureExtractor(capa.features.extractors.ida.IdaFeatureExtrac
return super(CapaExplorerFeatureExtractor, self).extract_function_features(f) return super(CapaExplorerFeatureExtractor, self).extract_function_features(f)
class QLineEditClicked(QtWidgets.QLineEdit):
def __init__(self, content, parent=None):
""" """
super(QLineEditClicked, self).__init__(content, parent)
def mouseReleaseEvent(self, e):
""" """
old = self.text()
new = str(
QtWidgets.QFileDialog.getExistingDirectory(
self.parent(), "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "")
)
)
if new:
self.setText(new)
else:
self.setText(old)
class CapaSettingsInputDialog(QtWidgets.QDialog):
def __init__(self, title, parent=None):
""" """
super(CapaSettingsInputDialog, self).__init__(parent)
self.setWindowTitle(title)
self.setMinimumWidth(500)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.edit_rule_path = QLineEditClicked(settings.user.get(CAPA_SETTINGS_RULE_PATH, ""))
self.edit_rule_author = QtWidgets.QLineEdit(settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, ""))
self.edit_rule_scope = QtWidgets.QComboBox()
scopes = ("file", "function", "basic block")
self.edit_rule_scope.addItems(scopes)
self.edit_rule_scope.setCurrentIndex(scopes.index(settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function")))
buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, self)
layout = QtWidgets.QFormLayout(self)
layout.addRow("capa rules path", self.edit_rule_path)
layout.addRow("Default rule author", self.edit_rule_author)
layout.addRow("Default rule scope", self.edit_rule_scope)
layout.addWidget(buttons)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
def get_values(self):
""" """
return self.edit_rule_path.text(), self.edit_rule_author.text(), self.edit_rule_scope.currentText()
class CapaExplorerForm(idaapi.PluginForm): class CapaExplorerForm(idaapi.PluginForm):
"""form element for plugin interface""" """form element for plugin interface"""
@@ -197,11 +255,11 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_rulegen = None self.view_rulegen = None
self.view_tabs = None self.view_tabs = None
self.view_tab_rulegen = None self.view_tab_rulegen = None
self.view_menu_bar = None
self.view_status_label = None self.view_status_label = None
self.view_buttons = None self.view_buttons = None
self.view_analyze_button = None self.view_analyze_button = None
self.view_reset_button = None self.view_reset_button = None
self.view_settings_button = None
self.view_save_button = None self.view_save_button = None
self.view_rulegen_preview = None self.view_rulegen_preview = None
@@ -273,10 +331,6 @@ class CapaExplorerForm(idaapi.PluginForm):
self.load_view_status_label() self.load_view_status_label()
self.load_view_buttons() self.load_view_buttons()
# load menu bar and sub menus
self.load_view_menu_bar()
self.load_configure_menu()
# load parent view # load parent view
self.load_view_parent() self.load_view_parent()
@@ -285,11 +339,6 @@ class CapaExplorerForm(idaapi.PluginForm):
tabs = QtWidgets.QTabWidget() tabs = QtWidgets.QTabWidget()
self.view_tabs = tabs self.view_tabs = tabs
def load_view_menu_bar(self):
"""load menu bar"""
bar = QtWidgets.QMenuBar()
self.view_menu_bar = bar
def load_view_checkbox_limit_by(self): def load_view_checkbox_limit_by(self):
"""load limit results by function checkbox""" """load limit results by function checkbox"""
check = QtWidgets.QCheckBox("Limit results to current function") check = QtWidgets.QCheckBox("Limit results to current function")
@@ -319,19 +368,23 @@ class CapaExplorerForm(idaapi.PluginForm):
analyze_button = QtWidgets.QPushButton("Analyze") analyze_button = QtWidgets.QPushButton("Analyze")
reset_button = QtWidgets.QPushButton("Reset") reset_button = QtWidgets.QPushButton("Reset")
save_button = QtWidgets.QPushButton("Save") save_button = QtWidgets.QPushButton("Save")
settings_button = QtWidgets.QPushButton("Settings")
analyze_button.clicked.connect(self.slot_analyze) analyze_button.clicked.connect(self.slot_analyze)
reset_button.clicked.connect(self.slot_reset) reset_button.clicked.connect(self.slot_reset)
save_button.clicked.connect(self.slot_save) save_button.clicked.connect(self.slot_save)
settings_button.clicked.connect(self.slot_settings)
layout = QtWidgets.QHBoxLayout() layout = QtWidgets.QHBoxLayout()
layout.addWidget(analyze_button) layout.addWidget(analyze_button)
layout.addWidget(reset_button) layout.addWidget(reset_button)
layout.addStretch(2) layout.addWidget(settings_button)
layout.addStretch(3)
layout.addWidget(save_button, alignment=QtCore.Qt.AlignRight) layout.addWidget(save_button, alignment=QtCore.Qt.AlignRight)
self.view_analyze_button = analyze_button self.view_analyze_button = analyze_button
self.view_reset_button = reset_button self.view_reset_button = reset_button
self.view_settings_button = settings_button
self.view_save_button = save_button self.view_save_button = save_button
self.view_buttons = layout self.view_buttons = layout
@@ -350,7 +403,6 @@ class CapaExplorerForm(idaapi.PluginForm):
layout.addWidget(self.view_tabs) layout.addWidget(self.view_tabs)
layout.addLayout(self.view_buttons) layout.addLayout(self.view_buttons)
layout.addWidget(self.view_status_label) layout.addWidget(self.view_status_label)
layout.setMenuBar(self.view_menu_bar)
self.parent.setLayout(layout) self.parent.setLayout(layout)
@@ -450,27 +502,6 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_tabs.addTab(tab, "Rule Generator") self.view_tabs.addTab(tab, "Rule Generator")
def load_configure_menu(self):
""" """
actions = (
("Change default rules directory...", "Set default rules directory", self.slot_change_rules_dir),
("Change default rule author...", "Set default rule author", self.slot_change_rule_author),
("Change default rule scope...", "Set default rule scope", self.slot_change_rule_scope),
)
self.load_menu("Settings", actions)
def load_menu(self, title, actions):
"""load menu actions
@param title: menu name displayed in UI
@param actions: tuple of tuples containing action name, tooltip, and slot function
"""
menu = self.view_menu_bar.addMenu(title)
for (name, _, slot) in actions:
action = QtWidgets.QAction(name, self.parent)
action.triggered.connect(slot)
menu.addAction(action)
def load_ida_hooks(self): def load_ida_hooks(self):
"""load IDA UI hooks""" """load IDA UI hooks"""
# map named action (defined in idagui.cfg) to Python function # map named action (defined in idagui.cfg) to Python function
@@ -567,7 +598,7 @@ class CapaExplorerForm(idaapi.PluginForm):
try: try:
# resolve rules directory - check self and settings first, then ask user # resolve rules directory - check self and settings first, then ask user
if not os.path.exists(settings.user.get("rule_path", "")): if not os.path.exists(settings.user.get(CAPA_SETTINGS_RULE_PATH, "")):
idaapi.info("Please select a file directory containing capa rules.") idaapi.info("Please select a file directory containing capa rules.")
path = self.ask_user_directory() path = self.ask_user_directory()
if not path: if not path:
@@ -575,7 +606,7 @@ class CapaExplorerForm(idaapi.PluginForm):
"You must select a file directory containing capa rules before analysis can be run. The standard collection of capa rules can be downloaded from https://github.com/fireeye/capa-rules." "You must select a file directory containing capa rules before analysis can be run. The standard collection of capa rules can be downloaded from https://github.com/fireeye/capa-rules."
) )
return False return False
settings.user["rule_path"] = path settings.user[CAPA_SETTINGS_RULE_PATH] = path
except Exception as e: except Exception as e:
logger.error("Failed to load capa rules (error: %s).", e) logger.error("Failed to load capa rules (error: %s).", e)
return False return False
@@ -584,8 +615,9 @@ class CapaExplorerForm(idaapi.PluginForm):
logger.info("User cancelled analysis.") logger.info("User cancelled analysis.")
return False return False
rule_path = settings.user["rule_path"] rule_path = settings.user[CAPA_SETTINGS_RULE_PATH]
try: try:
# TODO refactor: this first part is identical to capa.main.get_rules
if not os.path.exists(rule_path): if not os.path.exists(rule_path):
raise IOError("rule path %s does not exist or cannot be accessed" % rule_path) raise IOError("rule path %s does not exist or cannot be accessed" % rule_path)
@@ -601,8 +633,8 @@ class CapaExplorerForm(idaapi.PluginForm):
continue continue
for file in files: for file in files:
if not file.endswith(".yml"): if not file.endswith(".yml"):
if not (file.endswith(".md") or file.endswith(".git") or file.endswith(".txt")): if not (file.startswith(".git") or file.endswith((".git", ".md", ".txt"))):
# expect to see readme.md, format.md, and maybe a .git directory # expect to see .git* files, readme.md, format.md, and maybe a .git directory
# other things maybe are rules, but are mis-named. # other things maybe are rules, but are mis-named.
logger.warning("skipping non-.yml file: %s", file) logger.warning("skipping non-.yml file: %s", file)
continue continue
@@ -613,7 +645,8 @@ class CapaExplorerForm(idaapi.PluginForm):
total_paths = len(rule_paths) total_paths = len(rule_paths)
for (i, rule_path) in enumerate(rule_paths): for (i, rule_path) in enumerate(rule_paths):
update_wait_box( update_wait_box(
"loading capa rules from %s (%d of %d)" % (settings.user["rule_path"], i + 1, total_paths) "loading capa rules from %s (%d of %d)"
% (settings.user[CAPA_SETTINGS_RULE_PATH], i + 1, total_paths)
) )
if ida_kernwin.user_cancelled(): if ida_kernwin.user_cancelled():
raise UserCancelledError("user cancelled") raise UserCancelledError("user cancelled")
@@ -632,12 +665,14 @@ class CapaExplorerForm(idaapi.PluginForm):
logger.info("User cancelled analysis.") logger.info("User cancelled analysis.")
return False return False
except Exception as e: except Exception as e:
capa.ida.helpers.inform_user_ida_ui("Failed to load capa rules from %s" % settings.user["rule_path"]) capa.ida.helpers.inform_user_ida_ui(
logger.error("Failed to load rules from %s (error: %s).", settings.user["rule_path"], e) "Failed to load capa rules from %s" % settings.user[CAPA_SETTINGS_RULE_PATH]
)
logger.error("Failed to load rules from %s (error: %s).", settings.user[CAPA_SETTINGS_RULE_PATH], e)
logger.error( logger.error(
"Make sure your file directory contains properly formatted capa rules. You can download the standard collection of capa rules from https://github.com/fireeye/capa-rules." "Make sure your file directory contains properly formatted capa rules. You can download the standard collection of capa rules from https://github.com/fireeye/capa-rules."
) )
settings.user["rule_path"] = "" settings.user[CAPA_SETTINGS_RULE_PATH] = ""
return False return False
self.ruleset_cache = ruleset self.ruleset_cache = ruleset
@@ -743,7 +778,7 @@ class CapaExplorerForm(idaapi.PluginForm):
try: try:
self.model_data.render_capa_doc(self.doc, self.view_show_results_by_function.isChecked()) self.model_data.render_capa_doc(self.doc, self.view_show_results_by_function.isChecked())
self.set_view_status_label( self.set_view_status_label(
"capa rules directory: %s (%d rules)" % (settings.user["rule_path"], len(self.rules_cache)) "capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache))
) )
except Exception as e: except Exception as e:
logger.error("Failed to render results (error: %s)", e) logger.error("Failed to render results (error: %s)", e)
@@ -881,14 +916,14 @@ class CapaExplorerForm(idaapi.PluginForm):
# load preview and feature tree # load preview and feature tree
self.view_rulegen_preview.load_preview_meta( self.view_rulegen_preview.load_preview_meta(
f.start_ea if f else None, f.start_ea if f else None,
settings.user.get("rulegen_author", "<insert_author>"), settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, "<insert_author>"),
settings.user.get("rulegen_scope", "function"), settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function"),
) )
self.view_rulegen_features.load_features(file_features, func_features) self.view_rulegen_features.load_features(file_features, func_features)
# self.view_rulegen_header_label.setText("Function Features (%s)" % trim_function_name(f)) # self.view_rulegen_header_label.setText("Function Features (%s)" % trim_function_name(f))
self.set_view_status_label( self.set_view_status_label(
"capa rules directory: %s (%d rules)" % (settings.user["rule_path"], len(self.rules_cache)) "capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache))
) )
except Exception as e: except Exception as e:
logger.error("Failed to render views (error: %s)" % e) logger.error("Failed to render views (error: %s)" % e)
@@ -985,6 +1020,12 @@ class CapaExplorerForm(idaapi.PluginForm):
# create deep copy of current rules, add our new rule # create deep copy of current rules, add our new rule
rules = copy.copy(self.rules_cache) rules = copy.copy(self.rules_cache)
# ensure subscope rules are included
for sub in rule.extract_subscope_rules():
rules.append(sub)
# include our new rule in the list
rules.append(rule) rules.append(rule)
try: try:
@@ -1066,6 +1107,16 @@ class CapaExplorerForm(idaapi.PluginForm):
elif self.view_tabs.currentIndex() == 1: elif self.view_tabs.currentIndex() == 1:
self.save_function_analysis() self.save_function_analysis()
def slot_settings(self):
""" """
dialog = CapaSettingsInputDialog("capa explorer settings", parent=self.parent)
if dialog.exec_():
(
settings.user[CAPA_SETTINGS_RULE_PATH],
settings.user[CAPA_SETTINGS_RULEGEN_AUTHOR],
settings.user[CAPA_SETTINGS_RULEGEN_SCOPE],
) = dialog.get_values()
def save_program_analysis(self): def save_program_analysis(self):
""" """ """ """
if not self.doc: if not self.doc:
@@ -1143,42 +1194,16 @@ class CapaExplorerForm(idaapi.PluginForm):
"""create Qt dialog to ask user for a directory""" """create Qt dialog to ask user for a directory"""
return str( return str(
QtWidgets.QFileDialog.getExistingDirectory( QtWidgets.QFileDialog.getExistingDirectory(
self.parent, "Please select a capa rules directory", settings.user["rule_path"] self.parent, "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "")
) )
) )
def ask_user_capa_rule_file(self): def ask_user_capa_rule_file(self):
""" """ """ """
return QtWidgets.QFileDialog.getSaveFileName( return QtWidgets.QFileDialog.getSaveFileName(
None, "Please select a capa rule to edit", settings.user["rule_path"], "*.yml" None, "Please select a capa rule to edit", settings.user.get(CAPA_SETTINGS_RULE_PATH, ""), "*.yml"
)[0] )[0]
def slot_change_rule_scope(self):
""" """
scope = idaapi.ask_str(str(settings.user.get("rulegen_scope", "function")), 0, "Enter default rule scope")
if scope:
settings.user["rulegen_scope"] = scope
idaapi.info("Run analysis again for your changes to take effect.")
def slot_change_rule_author(self):
""" """
author = idaapi.ask_str(str(settings.user.get("rulegen_author", "")), 0, "Enter default rule author")
if author:
settings.user["rulegen_author"] = author
idaapi.info("Run analysis again for your changes to take effect.")
def slot_change_rules_dir(self):
"""allow user to change rules directory
user selection stored in settings for future runs
"""
path = self.ask_user_directory()
if path:
settings.user["rule_path"] = path
self.rules_cache = None
self.ruleset_cache = None
idaapi.info("Run analysis again for your changes to take effect.")
def set_view_status_label(self, text): def set_view_status_label(self, text):
"""update status label control """update status label control

View File

@@ -488,13 +488,17 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
@param feature: capa feature read from doc @param feature: capa feature read from doc
""" """
if feature[feature["type"]]: key = feature["type"]
value = feature[feature["type"]]
if value:
if key == "string":
value = '"%s"' % capa.features.escape_string(value)
if feature.get("description", ""): if feature.get("description", ""):
return "%s(%s = %s)" % (feature["type"], feature[feature["type"]], feature["description"]) return "%s(%s = %s)" % (key, value, feature["description"])
else: else:
return "%s(%s)" % (feature["type"], feature[feature["type"]]) return "%s(%s)" % (key, value)
else: else:
return "%s" % feature["type"] return "%s" % key
def render_capa_doc_feature_node(self, parent, feature, locations, doc): def render_capa_doc_feature_node(self, parent, feature, locations, doc):
"""process capa doc feature node """process capa doc feature node
@@ -551,7 +555,9 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
) )
if feature["type"] == "regex": if feature["type"] == "regex":
return CapaExplorerStringViewItem(parent, display, location, feature["match"]) return CapaExplorerStringViewItem(
parent, display, location, '"%s"' % capa.features.escape_string(feature["match"])
)
if feature["type"] == "basicblock": if feature["type"] == "basicblock":
return CapaExplorerBlockItem(parent, location) return CapaExplorerBlockItem(parent, location)
@@ -576,7 +582,9 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
if feature["type"] in ("string",): if feature["type"] in ("string",):
# display string preview # display string preview
return CapaExplorerStringViewItem(parent, display, location, feature[feature["type"]]) return CapaExplorerStringViewItem(
parent, display, location, '"%s"' % capa.features.escape_string(feature[feature["type"]])
)
if feature["type"] in ("import", "export"): if feature["type"] in ("import", "export"):
# display no preview # display no preview

View File

@@ -178,6 +178,9 @@ def build_context_menu(o, actions):
class CapaExplorerRulgenPreview(QtWidgets.QTextEdit): class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
INDENT = " " * 2
def __init__(self, parent=None): def __init__(self, parent=None):
""" """ """ """
super(CapaExplorerRulgenPreview, self).__init__(parent) super(CapaExplorerRulgenPreview, self).__init__(parent)
@@ -210,12 +213,99 @@ class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
self.setText("\n".join(metadata_default)) self.setText("\n".join(metadata_default))
def keyPressEvent(self, e): def keyPressEvent(self, e):
""" """ """intercept key press events"""
if e.key() in (QtCore.Qt.Key_Tab, QtCore.Qt.Key_Backtab):
# apparently it's not easy to implement tabs as spaces, or multi-line tab or SHIFT + Tab
# so we need to implement it ourselves so we can retain properly formatted capa rules
# when a user uses the Tab key
if self.textCursor().selection().isEmpty():
# single line, only worry about Tab
if e.key() == QtCore.Qt.Key_Tab: if e.key() == QtCore.Qt.Key_Tab:
self.insertPlainText(" " * 2) self.insertPlainText(self.INDENT)
else:
# multi-line tab or SHIFT + Tab
cur = self.textCursor()
select_start_ppos = cur.selectionStart()
select_end_ppos = cur.selectionEnd()
scroll_ppos = self.verticalScrollBar().sliderPosition()
# determine lineno for first selected line, and column
cur.setPosition(select_start_ppos)
start_lineno = self.count_previous_lines_from_block(cur.block())
start_lineco = cur.columnNumber()
# determine lineno for last selected line
cur.setPosition(select_end_ppos)
end_lineno = self.count_previous_lines_from_block(cur.block())
# now we need to indent or dedent the selected lines. for now, we read the text, modify
# the lines between start_lineno and end_lineno accordingly, and then reset the view
# this might not be the best solution, but it avoids messing around with cursor positions
# to determine the beginning of lines
plain = self.toPlainText().splitlines()
if e.key() == QtCore.Qt.Key_Tab:
# user Tab, indent selected lines
lines_modified = end_lineno - start_lineno
first_modified = True
change = [self.INDENT + line for line in plain[start_lineno : end_lineno + 1]]
else:
# user SHIFT + Tab, dedent selected lines
lines_modified = 0
first_modified = False
change = []
for (lineno, line) in enumerate(plain[start_lineno : end_lineno + 1]):
if line.startswith(self.INDENT):
if lineno == 0:
# keep track if first line is modified, so we can properly display
# the text selection later
first_modified = True
lines_modified += 1
line = line[len(self.INDENT) :]
change.append(line)
# apply modifications, and reset view
plain[start_lineno : end_lineno + 1] = change
self.setPlainText("\n".join(plain) + "\n")
# now we need to properly adjust the selection positions, so users don't have to
# re-select when indenting or dedenting the same lines repeatedly
if e.key() == QtCore.Qt.Key_Tab:
# user Tab, increase increment selection positions
select_start_ppos += len(self.INDENT)
select_end_ppos += (lines_modified * len(self.INDENT)) + len(self.INDENT)
elif lines_modified:
# user SHIFT + Tab, decrease selection positions
if start_lineco not in (0, 1) and first_modified:
# only decrease start position if not in first column
select_start_ppos -= len(self.INDENT)
select_end_ppos -= lines_modified * len(self.INDENT)
# apply updated selection and restore previous scroll position
self.set_selection(select_start_ppos, select_end_ppos, len(self.toPlainText()))
self.verticalScrollBar().setSliderPosition(scroll_ppos)
else: else:
super(CapaExplorerRulgenPreview, self).keyPressEvent(e) super(CapaExplorerRulgenPreview, self).keyPressEvent(e)
def count_previous_lines_from_block(self, block):
"""calculate number of lines preceding block"""
count = 0
while True:
block = block.previous()
if not block.isValid():
break
count += block.lineCount()
return count
def set_selection(self, start, end, max):
"""set text selection"""
cursor = self.textCursor()
cursor.setPosition(start)
cursor.setPosition(end if end < max else max, QtGui.QTextCursor.KeepAnchor)
self.setTextCursor(cursor)
class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget): class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
@@ -325,6 +415,11 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
# create a new parent under root node, by default; new node added last position in tree # create a new parent under root node, by default; new node added last position in tree
new_parent = self.new_expression_node(self.root, (action.data()[0], "")) new_parent = self.new_expression_node(self.root, (action.data()[0], ""))
if "basic block" in action.data()[0]:
# add default child expression when nesting under basic block
new_parent.setExpanded(True)
new_parent = self.new_expression_node(new_parent, ("- or:", ""))
for o in self.get_features(selected=True): for o in self.get_features(selected=True):
# take child from its parent by index, add to new parent # take child from its parent by index, add to new parent
new_parent.addChild(o.parent().takeChild(o.parent().indexOfChild(o))) new_parent.addChild(o.parent().takeChild(o.parent().indexOfChild(o)))
@@ -335,6 +430,15 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
def slot_edit_expression(self, action): def slot_edit_expression(self, action):
""" """ """ """
expression, o = action.data() expression, o = action.data()
if "basic block" in expression and "basic block" not in o.text(
CapaExplorerRulgenEditor.get_column_feature_index()
):
# current expression is "basic block", and not changing to "basic block" expression
children = o.takeChildren()
new_parent = self.new_expression_node(o, ("- or:", ""))
for child in children:
new_parent.addChild(child)
new_parent.setExpanded(True)
o.setText(CapaExplorerRulgenEditor.get_column_feature_index(), expression) o.setText(CapaExplorerRulgenEditor.get_column_feature_index(), expression)
def slot_clear_all(self, action): def slot_clear_all(self, action):
@@ -520,11 +624,23 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
# single features # single features
for (k, v) in filter(lambda t: t[1] == 1, counted): for (k, v) in filter(lambda t: t[1] == 1, counted):
self.new_feature_node(self.root, ("- %s: %s" % (k.name.lower(), k.get_value_str()), "")) if isinstance(k, (capa.features.String,)):
value = '"%s"' % capa.features.escape_string(k.get_value_str())
else:
value = k.get_value_str()
self.new_feature_node(self.root, ("- %s: %s" % (k.name.lower(), value), ""))
# n > 1 features # n > 1 features
for (k, v) in filter(lambda t: t[1] > 1, counted): for (k, v) in filter(lambda t: t[1] > 1, counted):
self.new_feature_node(self.root, ("- count(%s): %d" % (str(k), v), "")) if k.value:
if isinstance(k, (capa.features.String,)):
value = '"%s"' % capa.features.escape_string(k.get_value_str())
else:
value = k.get_value_str()
display = "- count(%s(%s)): %d" % (k.name.lower(), value, v)
else:
display = "- count(%s): %d" % (k.name.lower(), v)
self.new_feature_node(self.root, (display, ""))
self.expandAll() self.expandAll()
self.update_preview() self.update_preview()
@@ -699,7 +815,9 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
if text: if text:
for o in iterate_tree(self): for o in iterate_tree(self):
data = o.data(0, 0x100) data = o.data(0, 0x100)
if data and text.lower() not in data.get_value_str().lower(): if data:
to_match = data.get_value_str()
if not to_match or text.lower() not in to_match.lower():
o.setHidden(True) o.setHidden(True)
continue continue
o.setHidden(False) o.setHidden(False)
@@ -776,16 +894,20 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
def format_address(e): def format_address(e):
return "%X" % e if e else "" return "%X" % e if e else ""
def format_feature(feature):
""" """
name = feature.name.lower()
value = feature.get_value_str()
if isinstance(feature, (capa.features.String,)):
value = '"%s"' % capa.features.escape_string(value)
return "%s(%s)" % (name, value)
for (feature, eas) in sorted(features.items(), key=lambda k: sorted(k[1])): for (feature, eas) in sorted(features.items(), key=lambda k: sorted(k[1])):
if isinstance(feature, capa.features.basicblock.BasicBlock): if isinstance(feature, capa.features.basicblock.BasicBlock):
# filter basic blocks for now, we may want to add these back in some time # filter basic blocks for now, we may want to add these back in some time
# in the future # in the future
continue continue
if isinstance(feature, capa.features.String):
# strip string for display
feature.value = feature.value.strip()
# level 0 # level 0
if type(feature) not in self.parent_items: if type(feature) not in self.parent_items:
self.parent_items[type(feature)] = self.new_parent_node(parent, (feature.name.lower(),)) self.parent_items[type(feature)] = self.new_parent_node(parent, (feature.name.lower(),))
@@ -794,20 +916,22 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
if feature not in self.parent_items: if feature not in self.parent_items:
if len(eas) > 1: if len(eas) > 1:
self.parent_items[feature] = self.new_parent_node( self.parent_items[feature] = self.new_parent_node(
self.parent_items[type(feature)], (str(feature),), feature=feature self.parent_items[type(feature)], (format_feature(feature),), feature=feature
) )
else: else:
self.parent_items[feature] = self.new_leaf_node( self.parent_items[feature] = self.new_leaf_node(
self.parent_items[type(feature)], (str(feature),), feature=feature self.parent_items[type(feature)], (format_feature(feature),), feature=feature
) )
# level n > 1 # level n > 1
if len(eas) > 1: if len(eas) > 1:
for ea in sorted(eas): for ea in sorted(eas):
self.new_leaf_node(self.parent_items[feature], (str(feature), format_address(ea)), feature=feature) self.new_leaf_node(
self.parent_items[feature], (format_feature(feature), format_address(ea)), feature=feature
)
else: else:
ea = eas.pop() ea = eas.pop()
for (i, v) in enumerate((str(feature), format_address(ea))): for (i, v) in enumerate((format_feature(feature), format_address(ea))):
self.parent_items[feature].setText(i, v) self.parent_items[feature].setText(i, v)
self.parent_items[feature].setData(0, 0x100, feature) self.parent_items[feature].setData(0, 0x100, feature)

View File

@@ -379,8 +379,8 @@ def get_rules(rule_path, disable_progress=False):
for file in files: for file in files:
if not file.endswith(".yml"): if not file.endswith(".yml"):
if not (file.endswith(".md") or file.endswith(".git") or file.endswith(".txt")): if not (file.startswith(".git") or file.endswith((".git", ".md", ".txt"))):
# expect to see readme.md, format.md, and maybe a .git directory # expect to see .git* files, readme.md, format.md, and maybe a .git directory
# other things maybe are rules, but are mis-named. # other things maybe are rules, but are mis-named.
logger.warning("skipping non-.yml file: %s", file) logger.warning("skipping non-.yml file: %s", file)
continue continue

View File

@@ -56,7 +56,11 @@ def render_statement(ostream, match, statement, indent=0):
child = statement["child"] child = statement["child"]
if child[child["type"]]: if child[child["type"]]:
value = rutils.bold2(child[child["type"]]) if child["type"] == "string":
value = '"%s"' % capa.features.escape_string(child[child["type"]])
else:
value = child[child["type"]]
value = rutils.bold2(value)
if child.get("description"): if child.get("description"):
ostream.write("count(%s(%s = %s)): " % (child["type"], value, child["description"])) ostream.write("count(%s(%s = %s)): " % (child["type"], value, child["description"]))
else: else:
@@ -90,6 +94,9 @@ def render_feature(ostream, match, feature, indent=0):
key = "string" # render string for regex to mirror the rule source key = "string" # render string for regex to mirror the rule source
value = feature["match"] # the match provides more information than the value for regex value = feature["match"] # the match provides more information than the value for regex
if key == "string":
value = '"%s"' % capa.features.escape_string(value)
ostream.write(key) ostream.write(key)
ostream.write(": ") ostream.write(": ")

View File

@@ -736,6 +736,8 @@ class Rule(object):
# the below regex makes these adjustments and while ugly, we don't have to explore the ruamel.yaml insides # the below regex makes these adjustments and while ugly, we don't have to explore the ruamel.yaml insides
doc = re.sub(r"!!int '0x-([0-9a-fA-F]+)'", r"-0x\1", doc) doc = re.sub(r"!!int '0x-([0-9a-fA-F]+)'", r"-0x\1", doc)
# normalize CRLF to LF
doc = doc.replace("\r\n", "\n")
return doc return doc

View File

@@ -1 +1 @@
__version__ = "1.6.0" __version__ = "1.6.1"

BIN
doc/img/changelog/tab.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 135 KiB

44
doc/release.md Normal file
View File

@@ -0,0 +1,44 @@
# Release checklist
- [ ] Ensure all [milestoned issues/PRs](https://github.com/fireeye/capa/milestones) are addressed, or reassign to a new milestone.
- [ ] Add the `dont merge` label to all PRs that are close to be ready to merge (or merge them if they are ready) in [capa](https://github.com/fireeye/capa/pulls) and [capa-rules](https://github.com/fireeye/capa-rules/pulls).
- [ ] Ensure the [CI workflow succeeds in master](https://github.com/fireeye/capa/actions/workflows/tests.yml?query=branch%3Amaster).
- [ ] Ensure that `python scripts/lint.py rules/ --thorough` succeeds (only `missing examples` offenses are allowed in the nursery).
- [ ] Review changes
- capa https://github.com/fireeye/capa/compare/\<last-release\>...master
- capa-rules https://github.com/fireeye/capa-rules/compare/\<last-release>\...master
- [ ] Update [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md)
- Do not forget to add a nice introduction thanking contributors
- Remember that we need a major release if we introduce breaking changes
- Sections
- New Features
- New Rules
- Bug Fixes
- Changes
- Development
- Raw diffs
- Update `Raw diffs` links
- Create placeholder for `master (unreleased)` section
```
## master (unreleased)
### New Features
### New Rules
### Bug Fixes
### Changes
### Development
### Raw diffs
- [capa <release>...master](https://github.com/fireeye/capa/compare/<release>...master)
- [capa-rules <release>...master](https://github.com/fireeye/capa-rules/compare/<release>...master)
```
- [ ] Update [capa/version.py](https://github.com/fireeye/capa/blob/master/capa/version.py)
- [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/fireeye/capa/blob/master/capa/version.py). Copy this checklist in the PR description.
- [ ] After PR review, merge the PR and [create the release in GH](https://github.com/fireeye/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md).
- [ ] Verify GH actions [upload artifacts](https://github.com/fireeye/capa/releases), [publish to PyPI](https://pypi.org/project/flare-capa) and [create a tag in capa rules](https://github.com/fireeye/capa-rules/tags) upon completion.
- [ ] [Spread the word](https://twitter.com)

2
rules

Submodule rules updated: 70138e7595...93ea28dd32

View File

@@ -65,6 +65,8 @@ def main(argv=None):
return 0 return 0
else: else:
logger.info("rule requires reformatting (%s)", rule.name) logger.info("rule requires reformatting (%s)", rule.name)
if "\r\n" in rule.definition:
logger.info("please make sure that the file uses LF (\\n) line endings only")
return 1 return 1
if args.in_place: if args.in_place:

View File

@@ -25,6 +25,8 @@ import argparse
import itertools import itertools
import posixpath import posixpath
import ruamel.yaml
import capa.main import capa.main
import capa.rules import capa.rules
import capa.engine import capa.engine
@@ -301,6 +303,16 @@ class FeatureNtdllNtoskrnlApi(Lint):
return False return False
class FormatLineFeedEOL(Lint):
name = "line(s) end with CRLF (\\r\\n)"
recommendation = "convert line endings to LF (\\n) for example using dos2unix"
def check_rule(self, ctx, rule):
if len(rule.definition.split("\r\n")) > 0:
return False
return True
class FormatSingleEmptyLineEOF(Lint): class FormatSingleEmptyLineEOF(Lint):
name = "EOF format" name = "EOF format"
recommendation = "end file with a single empty line" recommendation = "end file with a single empty line"
@@ -321,12 +333,43 @@ class FormatIncorrect(Lint):
if actual != expected: if actual != expected:
diff = difflib.ndiff(actual.splitlines(1), expected.splitlines(True)) diff = difflib.ndiff(actual.splitlines(1), expected.splitlines(True))
self.recommendation = self.recommendation_template.format("".join(diff)) recommendation_template = self.recommendation_template
if "\r\n" in actual:
recommendation_template = (
self.recommendation_template + "\nplease make sure that the file uses LF (\\n) line endings only"
)
self.recommendation = recommendation_template.format("".join(diff))
return True return True
return False return False
class FormatStringQuotesIncorrect(Lint):
name = "rule string quotes incorrect"
def check_rule(self, ctx, rule):
events = capa.rules.Rule._get_ruamel_yaml_parser().parse(rule.definition)
for key in events:
if not (isinstance(key, ruamel.yaml.ScalarEvent) and key.value == "string"):
continue
value = next(events) # assume value is next event
if not isinstance(value, ruamel.yaml.ScalarEvent):
# ignore non-scalar
continue
if value.value.startswith("/") and value.value.endswith(("/", "/i")):
# ignore regex for now
continue
if value.style is None:
# no quotes
self.recommendation = 'add double quotes to "%s"' % value.value
return True
if value.style == "'":
# single quote
self.recommendation = 'change single quotes to double quotes for "%s"' % value.value
return True
return False
def run_lints(lints, ctx, rule): def run_lints(lints, ctx, rule):
for lint in lints: for lint in lints:
if lint.check_rule(ctx, rule): if lint.check_rule(ctx, rule):
@@ -385,7 +428,9 @@ def lint_features(ctx, rule):
FORMAT_LINTS = ( FORMAT_LINTS = (
FormatLineFeedEOL(),
FormatSingleEmptyLineEOF(), FormatSingleEmptyLineEOF(),
FormatStringQuotesIncorrect(),
FormatIncorrect(), FormatIncorrect(),
) )
@@ -554,7 +599,7 @@ def main(argv=None):
samples_path = os.path.join(os.path.dirname(__file__), "..", "tests", "data") samples_path = os.path.join(os.path.dirname(__file__), "..", "tests", "data")
parser = argparse.ArgumentParser(description="A program.") parser = argparse.ArgumentParser(description="Lint capa rules.")
capa.main.install_common_args(parser, wanted={"tag"}) capa.main.install_common_args(parser, wanted={"tag"})
parser.add_argument("rules", type=str, help="Path to rules") parser.add_argument("rules", type=str, help="Path to rules")
parser.add_argument("--samples", type=str, default=samples_path, help="Path to samples") parser.add_argument("--samples", type=str, default=samples_path, help="Path to samples")
@@ -566,8 +611,12 @@ def main(argv=None):
args = parser.parse_args(args=argv) args = parser.parse_args(args=argv)
capa.main.handle_common_args(args) capa.main.handle_common_args(args)
logging.getLogger("capa").setLevel(logging.CRITICAL) if args.debug:
logging.getLogger("viv_utils").setLevel(logging.CRITICAL) logging.getLogger("capa").setLevel(logging.DEBUG)
logging.getLogger("viv_utils").setLevel(logging.DEBUG)
else:
logging.getLogger("capa").setLevel(logging.ERROR)
logging.getLogger("viv_utils").setLevel(logging.ERROR)
time0 = time.time() time0 = time.time()

View File

@@ -12,32 +12,32 @@ import sys
import setuptools import setuptools
requirements = [ requirements = [
"six", "six==1.15.0",
"tqdm", "tqdm==4.60.0",
"pyyaml", "pyyaml==5.4.1",
"tabulate", "tabulate==0.8.9",
"colorama", "colorama==0.4.4",
"termcolor", "termcolor==1.1.0",
"ruamel.yaml", "wcwidth==0.2.5",
"wcwidth",
"ida-settings==2.1.0", "ida-settings==2.1.0",
"viv-utils==0.6.0",
] ]
if sys.version_info >= (3, 0): if sys.version_info >= (3, 0):
# py3 # py3
requirements.append("halo") requirements.append("halo==0.0.31")
requirements.append("networkx") requirements.append("networkx==2.5.1")
requirements.append("vivisect==1.0.0") requirements.append("ruamel.yaml==0.17.0")
requirements.append("viv-utils==0.3.19") requirements.append("vivisect==1.0.1")
requirements.append("smda==1.5.13") requirements.append("smda==1.5.13")
else: else:
# py2 # py2
requirements.append("enum34==1.1.6") # v1.1.6 is needed by halo 0.0.30 / spinners 0.0.24 requirements.append("enum34==1.1.6") # v1.1.6 is needed by halo 0.0.30 / spinners 0.0.24
requirements.append("halo==0.0.30") # halo==0.0.30 is the last version to support py2.7 requirements.append("halo==0.0.30") # halo==0.0.30 is the last version to support py2.7
requirements.append("vivisect==0.2.1") requirements.append("vivisect==0.2.1")
requirements.append("viv-utils==0.3.19")
requirements.append("networkx==2.2") # v2.2 is last version supported by Python 2.7 requirements.append("networkx==2.2") # v2.2 is last version supported by Python 2.7
requirements.append("backports.functools-lru-cache") requirements.append("ruamel.yaml==0.16.13") # last version tested with Python 2.7
requirements.append("backports.functools-lru-cache==1.6.1")
# this sets __version__ # this sets __version__
# via: http://stackoverflow.com/a/7071358/87207 # via: http://stackoverflow.com/a/7071358/87207
@@ -77,13 +77,13 @@ setuptools.setup(
install_requires=requirements, install_requires=requirements,
extras_require={ extras_require={
"dev": [ "dev": [
"pytest", "pytest==4.6.11", # TODO: Change to 6.2.3 when removing py2
"pytest-sugar", "pytest-sugar==0.9.4",
"pytest-instafail", "pytest-instafail==0.4.2",
"pytest-cov", "pytest-cov==2.11.1",
"pycodestyle", "pycodestyle==2.7.0",
"black ; python_version>'3.0'", "black==20.8b1 ; python_version>'3.0'",
"isort", "isort==4.3.21", # TODO: Change to 5.8.0 when removing py2
] ]
}, },
zip_safe=False, zip_safe=False,

View File

@@ -681,6 +681,25 @@ def test_explicit_string_values_int():
assert (String("0x123") in children) == True assert (String("0x123") in children) == True
def test_string_values_special_characters():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
features:
- or:
- string: "hello\\r\\nworld"
- string: "bye\\nbye"
description: "test description"
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (String("hello\r\nworld") in children) == True
assert (String("bye\nbye") in children) == True
def test_regex_values_always_string(): def test_regex_values_always_string():
rules = [ rules = [
capa.rules.Rule.from_yaml( capa.rules.Rule.from_yaml(

View File

@@ -15,8 +15,9 @@ from fixtures import *
FEATURE_PRESENCE_TESTS, FEATURE_PRESENCE_TESTS,
indirect=["sample", "scope"], indirect=["sample", "scope"],
) )
@pytest.mark.xfail(sys.version_info < (3, 0), reason="SMDA only works on py3")
@pytest.mark.xfail(sys.platform == "win32", reason="SMDA bug: https://github.com/danielplohmann/smda/issues/20")
def test_smda_features(sample, scope, feature, expected): def test_smda_features(sample, scope, feature, expected):
with xfail(sys.version_info < (3, 0), reason="SMDA only works on py3"):
do_test_feature_presence(get_smda_extractor, sample, scope, feature, expected) do_test_feature_presence(get_smda_extractor, sample, scope, feature, expected)