diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 59a3b6a4..969443e8 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -159,12 +159,25 @@ The process described here has several goals:
Please follow these steps to have your contribution considered by the maintainers:
+0. Sign the [Contributor License Agreement](#contributor-license-agreement)
1. 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 What if the status checks are failing?
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.
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 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
### Git Commit Messages
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 314f5261..4188cf09 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -32,14 +32,7 @@ jobs:
- name: upload package artifacts
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
- name: ${{ matrix.asset_name }}
path: dist/*
- - name: upload package to GH Release
- uses: svenstaro/upload-release-action@2728235f7dc9ff598bd86ce3c274b74f802d2208 # v2
- with:
- repo_token: ${{ secrets.GITHUB_TOKEN}}
- file: dist/*
- tag: ${{ github.ref }}
- name: publish package
uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 # release/v1
with:
diff --git a/.gitignore b/.gitignore
index 95bc3044..38d72570 100644
--- a/.gitignore
+++ b/.gitignore
@@ -124,3 +124,5 @@ Pipfile
Pipfile.lock
/cache/
.github/binja/binaryninja
+.github/binja/download_headless.py
+.github/binja/BinaryNinja-headless.zip
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f16a529c..76e944ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,19 +1,43 @@
# Change Log
## master (unreleased)
-- extract function and API names from ELF symtab entries @yelhamer https://github.com/mandiant/capa-rules/issues/736
### New Features
-- Utility script to detect feature overlap between new and existing CAPA rules [#1451](https://github.com/mandiant/capa/issues/1451) [@Aayush-Goel-04](https://github.com/aayush-goel-04)
-- use fancy box drawing characters for default output #1586 @williballenthin
-- use [pre-commit](https://pre-commit.com/) to invoke linters #1579 @williballenthin
-- publish via PyPI trusted publishing #1491 @williballenthin
-- migrate to pyproject.toml #1301 @williballenthin
### Breaking Changes
-- Update Metadata type in capa main [#1411](https://github.com/mandiant/capa/issues/1411) [@Aayush-Goel-04](https://github.com/aayush-goel-04) @manasghandat
+
+### New Rules (1)
+
+- executable/pe/export/forwarded-export ronnie.salomonsen@mandiant.com
+-
+
+### Bug Fixes
+
+### capa explorer IDA Pro plugin
+
+### Development
+
+### Raw diffs
+- [capa v6.0.0...master](https://github.com/mandiant/capa/compare/v6.0.0...master)
+- [capa-rules v6.0.0...master](https://github.com/mandiant/capa-rules/compare/v6.0.0...master)
+
+## v6.0.0
+
+capa v6.0 brings many bug fixes and quality improvements, including 64 rule updates and 26 new rules. We're now publishing to PyPI via [Trusted Publishing](https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/) and have migrated to using a `pyproject.toml` file. @Aayush-Goel-04 contributed a lot of new code across many files, so please welcome them to the project, along with @anders-v @crowface28 @dkelly2e @RonnieSalomonsen and @ejfocampo as first-time rule contributors!
+
+For those that use capa as a library, we've introduced some limited breaking changes that better represent data types (versus less-structured data like dictionaries and strings). With the recent deprecation, we've also dropped support for Python 3.7.
+
+### New Features
+- add script to detect feature overlap between new and existing capa rules [#1451](https://github.com/mandiant/capa/issues/1451) [@Aayush-Goel-04](https://github.com/aayush-goel-04)
+- extract forwarded exports from PE files #1624 @williballenthin
+- extract function and API names from ELF symtab entries @yelhamer https://github.com/mandiant/capa-rules/issues/736
+- use fancy box drawing characters for default output #1586 @williballenthin
+
+### Breaking Changes
+- use a class to represent Metadata (not dict) #1411 @Aayush-Goel-04 @manasghandat
+- use pathlib.Path to represent file paths #1534 @Aayush-Goel-04
- Python 3.8 is now the minimum supported Python version #1578 @williballenthin
-- Updated file paths to use pathlib.Path for improved path handling and compatibility [#1534](https://github.com/mandiant/capa/issues/1534) [@Aayush-Goel-04](https://github.com/aayush-goel-04)
+- Require a Contributor License Agreement (CLA) for PRs going forward #1642 @williballenthin
### New Rules (26)
@@ -42,7 +66,6 @@
- anti-analysis/anti-av/patch-event-tracing-for-windows-function jakub.jozwiak@mandiant.com
- data-manipulation/encoding/xor/covertly-decode-and-write-data-to-windows-directory-using-indirect-calls dan.kelly@mandiant.com
- linking/runtime-linking/resolve-function-by-brute-ratel-badger-hash jakub.jozwiak@mandiant.com
--
@@ -54,14 +77,15 @@
- symtab: fix struct.unpack() format for 64-bit ELF files @yelhamer
- symtab: safeguard against ZeroDivisionError for files containing a symtab with a null entry size @yelhamer
- improve ELF strtab and needed parsing @mr-tz
-- better handle exceptional cases when parsing ELF files [#1458](https://github.com/mandiant/capa/issues/1458) [@Aayush-Goel-04](https://github.com/aayush-goel-04)
-- Improved testing coverage for Binary Ninja Backend [#1446](https://github.com/mandiant/capa/issues/1446) [@Aayush-Goel-04](https://github.com/aayush-goel-04)
-- Add logging and print redirect to tqdm for capa main [#749](https://github.com/mandiant/capa/issues/749) [@Aayush-Goel-04](https://github.com/aayush-goel-04)
+- better handle exceptional cases when parsing ELF files #1458 @Aayush-Goel-04
+- improved testing coverage for Binary Ninja backend #1446 @Aayush-Goel-04
+- add logging and print redirect to tqdm for capa main #749 @Aayush-Goel-04
- extractor: fix binja installation path detection does not work with Python 3.11
- tests: refine the IDA test runner script #1513 @williballenthin
- output: don't leave behind traces of progress bar @williballenthin
- import-to-ida: fix bug introduced with JSON report changes in v5 #1584 @williballenthin
- main: don't show spinner when emitting debug messages #1636 @williballenthin
+- rules: add forwarded export characteristics to rule syntax file scope #1653 @RonnieSalomonsen
### capa explorer IDA Pro plugin
@@ -69,10 +93,14 @@
- update ATT&CK/MBC data for linting #1568 @mr-tz
- log time taken to analyze each function #1290 @williballenthin
- tests: make fixture available via conftest.py #1592 @williballenthin
+- publish via PyPI trusted publishing #1491 @williballenthin
+- migrate to pyproject.toml #1301 @williballenthin
+- use [pre-commit](https://pre-commit.com/) to invoke linters #1579 @williballenthin
+
### Raw diffs
-- [capa v5.1.0...master](https://github.com/mandiant/capa/compare/v5.1.0...master)
-- [capa-rules v5.1.0...master](https://github.com/mandiant/capa-rules/compare/v5.1.0...master)
+- [capa v5.1.0...v6.0.0](https://github.com/mandiant/capa/compare/v5.1.0...v6.0.0)
+- [capa-rules v5.1.0...v6.0.0](https://github.com/mandiant/capa-rules/compare/v5.1.0...v6.0.0)
## v5.1.0
capa version 5.1.0 adds a Protocol Buffers (protobuf) format for result documents. Additionally, the [Vector35](https://vector35.com/) team contributed a new feature extractor using Binary Ninja. Other new features are a new CLI flag to override the detected operating system, functionality to read and render existing result documents, and a output color format that's easier to read.
diff --git a/README.md b/README.md
index e7c3b5f6..1c08af30 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://pypi.org/project/flare-capa)
[](https://github.com/mandiant/capa/releases)
-[](https://github.com/mandiant/capa-rules)
+[](https://github.com/mandiant/capa-rules)
[](https://github.com/mandiant/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
[](https://github.com/mandiant/capa/releases)
[](LICENSE.txt)
diff --git a/capa/features/extractors/helpers.py b/capa/features/extractors/helpers.py
index 66399b12..e17a66f2 100644
--- a/capa/features/extractors/helpers.py
+++ b/capa/features/extractors/helpers.py
@@ -70,6 +70,23 @@ def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
yield symbol[:-1]
+def reformat_forwarded_export_name(forwarded_name: str) -> str:
+ """
+ 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))
diff --git a/capa/features/extractors/ida/file.py b/capa/features/extractors/ida/file.py
index 051ecafd..efa4b66c 100644
--- a/capa/features/extractors/ida/file.py
+++ b/capa/features/extractors/ida/file.py
@@ -12,6 +12,7 @@ from typing import Tuple, Iterator
import idc
import idaapi
import idautils
+import ida_entry
import capa.features.extractors.common
import capa.features.extractors.helpers
@@ -83,8 +84,14 @@ def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]:
def extract_file_export_names() -> Iterator[Tuple[Feature, Address]]:
"""extract function exports"""
- for _, _, ea, name in idautils.Entries():
- yield Export(name), AbsoluteVirtualAddress(ea)
+ for _, ordinal, ea, name in idautils.Entries():
+ forwarded_name = ida_entry.get_entry_forwarder(ordinal)
+ 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]]:
diff --git a/capa/features/extractors/pefile.py b/capa/features/extractors/pefile.py
index a820e7b8..c51675e8 100644
--- a/capa/features/extractors/pefile.py
+++ b/capa/features/extractors/pefile.py
@@ -40,8 +40,20 @@ def extract_file_export_names(pe, **kwargs):
name = export.name.partition(b"\x00")[0].decode("ascii")
except UnicodeDecodeError:
continue
- va = base_address + export.address
- yield Export(name), AbsoluteVirtualAddress(va)
+
+ 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):
diff --git a/capa/features/extractors/viv/file.py b/capa/features/extractors/viv/file.py
index cc078016..204d8e69 100644
--- a/capa/features/extractors/viv/file.py
+++ b/capa/features/extractors/viv/file.py
@@ -8,6 +8,7 @@
from typing import Tuple, Iterator
import PE.carve as pe_carve # vivisect PE
+import vivisect
import viv_utils
import viv_utils.flirt
@@ -25,10 +26,35 @@ def extract_file_embedded_pe(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
-def extract_file_export_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
+def get_first_vw_filename(vw: vivisect.VivWorkspace):
+ # vivisect associates metadata with each file that its loaded into the workspace.
+ # capa only loads a single file into each workspace.
+ # 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]]:
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]]:
"""
diff --git a/capa/main.py b/capa/main.py
index ff418578..07bb007d 100644
--- a/capa/main.py
+++ b/capa/main.py
@@ -85,6 +85,7 @@ SIGNATURES_PATH_DEFAULT_STRING = "(embedded signatures)"
BACKEND_VIV = "vivisect"
BACKEND_DOTNET = "dotnet"
BACKEND_BINJA = "binja"
+BACKEND_PEFILE = "pefile"
E_MISSING_RULES = 10
E_MISSING_FILE = 11
@@ -567,8 +568,12 @@ def get_extractor(
return capa.features.extractors.binja.extractor.BinjaFeatureExtractor(bv)
- # default to use vivisect backend
- else:
+ elif backend == BACKEND_PEFILE:
+ import capa.features.extractors.pefile
+
+ return capa.features.extractors.pefile.PefileFeatureExtractor(path)
+
+ elif backend == BACKEND_VIV:
import capa.features.extractors.viv.extractor
with halo.Halo(text="analyzing program", spinner="simpleDots", stream=sys.stderr, enabled=not disable_progress):
@@ -586,6 +591,9 @@ def get_extractor(
return capa.features.extractors.viv.extractor.VivisectFeatureExtractor(vw, path, os_)
+ else:
+ raise ValueError("unexpected backend: " + backend)
+
def get_file_extractors(sample: Path, format_: str) -> List[FeatureExtractor]:
file_extractors: List[FeatureExtractor] = []
@@ -911,7 +919,7 @@ def install_common_args(parser, wanted=None):
"--backend",
type=str,
help="select the backend to use",
- choices=(BACKEND_VIV, BACKEND_BINJA),
+ choices=(BACKEND_VIV, BACKEND_BINJA, BACKEND_PEFILE),
default=BACKEND_VIV,
)
diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py
index 066c3a11..45d822a5 100644
--- a/capa/rules/__init__.py
+++ b/capa/rules/__init__.py
@@ -106,6 +106,7 @@ SUPPORTED_FEATURES: Dict[str, Set] = {
capa.features.common.Class,
capa.features.common.Namespace,
capa.features.common.Characteristic("mixed mode"),
+ capa.features.common.Characteristic("forwarded export"),
},
FUNCTION_SCOPE: {
capa.features.common.MatchedRule,
diff --git a/capa/version.py b/capa/version.py
index 5ce717b2..f2f931fc 100644
--- a/capa/version.py
+++ b/capa/version.py
@@ -5,7 +5,7 @@
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
-__version__ = "5.1.0"
+__version__ = "6.0.0"
def get_major_version():
diff --git a/pyproject.toml b/pyproject.toml
index 1e73090f..a28e244c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,6 +18,7 @@ authors = [
{name = "Mike Hunhoff", email = "michael.hunhoff@mandiant.com"},
]
description = "The FLARE team's open-source tool to identify capabilities in executable files."
+readme = {file = "README.md", content-type = "text/markdown"}
license = {file = "LICENSE.txt"}
requires-python = ">=3.8"
keywords = ["malware analysis", "reverse engineering", "capability detection", "software behaviors", "capa", "FLARE"]
@@ -50,11 +51,10 @@ dependencies = [
"pydantic==1.10.9",
"protobuf==4.23.4",
]
-dynamic = ["version", "readme"]
+dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "capa.version.__version__"}
-readme = {file = "README.md"}
[tool.setuptools]
packages = ["capa"]
diff --git a/rules b/rules
index 85a980a6..a49c174f 160000
--- a/rules
+++ b/rules
@@ -1 +1 @@
-Subproject commit 85a980a6cc8557af10bc1220eac9251ce3334ab5
+Subproject commit a49c174fee5058ca3617a23e782bdcadacb12406
diff --git a/scripts/profile-time.py b/scripts/profile-time.py
index b6c48683..9acd60ff 100644
--- a/scripts/profile-time.py
+++ b/scripts/profile-time.py
@@ -58,22 +58,16 @@ import capa.features.freeze
logger = logging.getLogger("capa.profile")
+def subshell(cmd):
+ return subprocess.run(cmd, shell=True, capture_output=True, text=True).stdout.strip()
+
+
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
- label = subprocess.run(
- "git show --pretty=oneline --abbrev-commit | head -n 1", shell=True, capture_output=True, text=True
- ).stdout.strip()
- is_dirty = (
- subprocess.run(
- "git status | grep 'modified: ' | grep -v 'rules' | grep -v 'tests/data'",
- shell=True,
- capture_output=True,
- text=True,
- ).stdout
- != ""
- )
+ label = subshell("git show --pretty=oneline --abbrev-commit | head -n 1").strip()
+ is_dirty = subshell("git status | grep 'modified: ' | grep -v 'rules' | grep -v 'tests/data'") != ""
if is_dirty:
label += " (dirty)"
diff --git a/scripts/show-features.py b/scripts/show-features.py
index 25f1662b..9437caa5 100644
--- a/scripts/show-features.py
+++ b/scripts/show-features.py
@@ -68,6 +68,7 @@ import os
import sys
import logging
import argparse
+from typing import Tuple
from pathlib import Path
import capa.main
@@ -80,8 +81,10 @@ import capa.render.verbose as v
import capa.features.common
import capa.features.freeze
import capa.features.address
+import capa.features.extractors.pefile
import capa.features.extractors.base_extractor
from capa.helpers import log_unsupported_runtime_error
+from capa.features.extractors.base_extractor import FunctionHandle
logger = logging.getLogger("capa.show-features")
@@ -101,6 +104,10 @@ def main(argv=None):
args = parser.parse_args(args=argv)
capa.main.handle_common_args(args)
+ if args.function and args.backend == "pefile":
+ print("pefile backend does not support extracting function features")
+ return -1
+
try:
taste = capa.helpers.get_file_taste(Path(args.sample))
except IOError as e:
@@ -137,7 +144,12 @@ def main(argv=None):
for feature, addr in extractor.extract_file_features():
print(f"file: {format_address(addr)}: {feature}")
- function_handles = tuple(extractor.get_functions())
+ function_handles: Tuple[FunctionHandle, ...]
+ if isinstance(extractor, capa.features.extractors.pefile.PefileFeatureExtractor):
+ # pefile extractor doesn't extract function features
+ function_handles = ()
+ else:
+ function_handles = tuple(extractor.get_functions())
if args.function:
if args.format == "freeze":
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 2eaf86ae..291ba1a8 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -306,6 +306,8 @@ def get_data_path_by_name(name) -> Path:
return CD / "data" / "294b8db1f2702b60fb2e42fdc50c2cee6a5046112da9a5703a548a4fa50477bc.elf_"
elif name.startswith("2bf18d"):
return CD / "data" / "2bf18d0403677378adad9001b1243211.elf_"
+ elif name.startswith("ea2876"):
+ return CD / "data" / "ea2876e9175410b6f6719f80ee44b9553960758c7d0f7bed73c0fe9a78d8e669.dll_"
else:
raise ValueError(f"unexpected sample fixture: {name}")
@@ -366,6 +368,8 @@ def get_sample_md5_by_name(name):
return "3db3e55b16a7b1b1afb970d5e77c5d98"
elif name.startswith("2bf18d"):
return "2bf18d0403677378adad9001b1243211"
+ elif name.startswith("ea2876"):
+ return "76fa734236daa023444dec26863401dc"
else:
raise ValueError(f"unexpected sample fixture: {name}")
@@ -529,6 +533,8 @@ FEATURE_PRESENCE_TESTS = sorted(
("kernel32", "file", capa.features.file.Export("BaseThreadInitThunk"), True),
("kernel32", "file", capa.features.file.Export("lstrlenW"), True),
("kernel32", "file", capa.features.file.Export("nope"), False),
+ # forwarded export
+ ("ea2876", "file", capa.features.file.Export("vresion.GetFileVersionInfoA"), True),
# file/imports
("mimikatz", "file", capa.features.file.Import("advapi32.CryptSetHashParam"), True),
("mimikatz", "file", capa.features.file.Import("CryptSetHashParam"), True),
@@ -715,6 +721,8 @@ FEATURE_PRESENCE_TESTS = sorted(
("mimikatz", "function=0x4702FD", capa.features.common.Characteristic("calls from"), False),
# function/characteristic(calls to)
("mimikatz", "function=0x40105D", capa.features.common.Characteristic("calls to"), True),
+ # function/characteristic(forwarded export)
+ ("ea2876", "file", capa.features.common.Characteristic("forwarded export"), True),
# before this we used ambiguous (0x4556E5, False), which has a data reference / indirect recursive call, see #386
("mimikatz", "function=0x456BB9", capa.features.common.Characteristic("calls to"), False),
# file/function-name
diff --git a/tests/test_binja_features.py b/tests/test_binja_features.py
index b2256f80..4daaa790 100644
--- a/tests/test_binja_features.py
+++ b/tests/test_binja_features.py
@@ -12,6 +12,8 @@ import pytest
import fixtures
import capa.main
+import capa.features.file
+import capa.features.common
logger = logging.getLogger(__file__)
@@ -40,6 +42,13 @@ except ImportError:
def test_binja_features(sample, scope, feature, expected):
if feature == capa.features.common.Characteristic("stack string"):
pytest.xfail("skip failing Binja stack string detection temporarily, see #1473")
+
+ if isinstance(feature, capa.features.file.Export) and "." in str(feature.value):
+ pytest.xfail("skip Binja unsupported forwarded export feature, see #1646")
+
+ if feature == capa.features.common.Characteristic("forwarded export"):
+ pytest.xfail("skip Binja unsupported forwarded export feature, see #1646")
+
fixtures.do_test_feature_presence(fixtures.get_binja_extractor, sample, scope, feature, expected)