mirror of
https://github.com/mandiant/capa.git
synced 2025-12-16 09:30:46 -08:00
Compare commits
19 Commits
backend/py
...
idalib-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a0d35e826 | ||
|
|
28d107c0f3 | ||
|
|
0d44fc5414 | ||
|
|
654338dcc0 | ||
|
|
34488b35fc | ||
|
|
dc08843e2d | ||
|
|
40b01f0998 | ||
|
|
b96a3b6b23 | ||
|
|
43e5e60901 | ||
|
|
0f9f72dbd5 | ||
|
|
fd9f584cc4 | ||
|
|
c3b785e217 | ||
|
|
6ae17f7ef4 | ||
|
|
13297ad324 | ||
|
|
074f7c742c | ||
|
|
cf463676b2 | ||
|
|
b5e5840a63 | ||
|
|
f252b6bbd0 | ||
|
|
eda53ab3c1 |
49
.github/workflows/tests.yml
vendored
49
.github/workflows/tests.yml
vendored
@@ -217,3 +217,52 @@ jobs:
|
|||||||
exit_code=$(cat ../output.log | grep exit | awk '{print $NF}')
|
exit_code=$(cat ../output.log | grep exit | awk '{print $NF}')
|
||||||
exit $exit_code
|
exit $exit_code
|
||||||
|
|
||||||
|
idalib-tests:
|
||||||
|
name: IDA tests for ${{ matrix.python-version }}
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: [tests]
|
||||||
|
env:
|
||||||
|
IDA_LICENSE_ID: ${{ secrets.IDA_LICENSE_ID }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.13"]
|
||||||
|
ida:
|
||||||
|
- version: 9.0
|
||||||
|
slug: "release/9.0/ida-essential/ida-essential_90_x64linux.run"
|
||||||
|
- version: 9.2
|
||||||
|
slug: "release/9.2/ida-essential/ida-essential_92_x64linux.run"
|
||||||
|
steps:
|
||||||
|
- name: Checkout capa with submodules
|
||||||
|
# do only run if IDA_LICENSE_ID is available, have to do this in every step, see https://github.com/orgs/community/discussions/26726#discussioncomment-3253118
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Setup uv
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Install dependencies
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
run: sudo apt-get install -y libyaml-dev
|
||||||
|
- name: Install capa
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
run: |
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -e .[dev,scripts]
|
||||||
|
pip install idapro
|
||||||
|
- name: Install IDA ${{ matrix.ida.version }}
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
run: |
|
||||||
|
uv run hcli --disable-updates ida install --download-id ${{ matrix.ida.slug }} --license-id ${{ secrets.IDA_LICENSE_ID }} --set-default --yes
|
||||||
|
env:
|
||||||
|
HCLI_API_KEY: ${{ secrets.HCLI_API_KEY }}
|
||||||
|
IDA_LICENSE_ID: ${{ secrets.IDA_LICENSE_ID }}
|
||||||
|
- name: Run tests
|
||||||
|
if: ${{ env.IDA_LICENSE_ID != 0 }}
|
||||||
|
run: pytest -v tests/test_idalib_features.py # explicitly refer to the idalib tests for performance. other tests run above.
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ repos:
|
|||||||
- "--ignore=tests/test_ghidra_features.py"
|
- "--ignore=tests/test_ghidra_features.py"
|
||||||
- "--ignore=tests/test_ida_features.py"
|
- "--ignore=tests/test_ida_features.py"
|
||||||
- "--ignore=tests/test_viv_features.py"
|
- "--ignore=tests/test_viv_features.py"
|
||||||
|
- "--ignore=tests/test_idalib_features.py"
|
||||||
- "--ignore=tests/test_main.py"
|
- "--ignore=tests/test_main.py"
|
||||||
- "--ignore=tests/test_scripts.py"
|
- "--ignore=tests/test_scripts.py"
|
||||||
always_run: true
|
always_run: true
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ Additionally a Binary Ninja bug has been fixed. Released binaries now include AR
|
|||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
- ci: add support for arm64 binary releases
|
- ci: add support for arm64 binary releases
|
||||||
|
- tests: run tests against IDA via idalib @williballenthin #2742
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import idaapi
|
|||||||
import idautils
|
import idautils
|
||||||
|
|
||||||
import capa.features.extractors.ida.helpers
|
import capa.features.extractors.ida.helpers
|
||||||
|
from capa.features.file import FunctionName
|
||||||
from capa.features.common import Feature, Characteristic
|
from capa.features.common import Feature, Characteristic
|
||||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||||
from capa.features.extractors import loops
|
from capa.features.extractors import loops
|
||||||
@@ -50,10 +51,35 @@ def extract_recursive_call(fh: FunctionHandle):
|
|||||||
yield Characteristic("recursive call"), fh.address
|
yield Characteristic("recursive call"), fh.address
|
||||||
|
|
||||||
|
|
||||||
|
def extract_function_name(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
|
||||||
|
ea = fh.inner.start_ea
|
||||||
|
name = idaapi.get_name(ea)
|
||||||
|
yield FunctionName(name), fh.address
|
||||||
|
if name.startswith("_"):
|
||||||
|
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||||
|
# extract features for both the mangled and un-mangled representations.
|
||||||
|
# e.g. `_fwrite` -> `fwrite`
|
||||||
|
# see: https://stackoverflow.com/a/2628384/87207
|
||||||
|
yield FunctionName(name[1:]), fh.address
|
||||||
|
|
||||||
|
|
||||||
|
def extract_function_alternative_names(fh: FunctionHandle):
|
||||||
|
"""Get all alternative names for an address."""
|
||||||
|
|
||||||
|
for aname in capa.features.extractors.ida.helpers.get_function_alternative_names(fh.inner.start_ea):
|
||||||
|
yield FunctionName(aname), fh.address
|
||||||
|
|
||||||
|
|
||||||
def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
|
def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
|
||||||
for func_handler in FUNCTION_HANDLERS:
|
for func_handler in FUNCTION_HANDLERS:
|
||||||
for feature, addr in func_handler(fh):
|
for feature, addr in func_handler(fh):
|
||||||
yield feature, addr
|
yield feature, addr
|
||||||
|
|
||||||
|
|
||||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call)
|
FUNCTION_HANDLERS = (
|
||||||
|
extract_function_calls_to,
|
||||||
|
extract_function_loop,
|
||||||
|
extract_recursive_call,
|
||||||
|
extract_function_name,
|
||||||
|
extract_function_alternative_names,
|
||||||
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import idaapi
|
|||||||
import ida_nalt
|
import ida_nalt
|
||||||
import idautils
|
import idautils
|
||||||
import ida_bytes
|
import ida_bytes
|
||||||
|
import ida_funcs
|
||||||
import ida_segment
|
import ida_segment
|
||||||
|
|
||||||
from capa.features.address import AbsoluteVirtualAddress
|
from capa.features.address import AbsoluteVirtualAddress
|
||||||
@@ -436,3 +437,23 @@ def is_basic_block_return(bb: idaapi.BasicBlock) -> bool:
|
|||||||
def has_sib(oper: idaapi.op_t) -> bool:
|
def has_sib(oper: idaapi.op_t) -> bool:
|
||||||
# via: https://reverseengineering.stackexchange.com/a/14300
|
# via: https://reverseengineering.stackexchange.com/a/14300
|
||||||
return oper.specflag1 == 1
|
return oper.specflag1 == 1
|
||||||
|
|
||||||
|
|
||||||
|
def get_function_alternative_names(fva: int):
|
||||||
|
"""Get all alternative names for an address."""
|
||||||
|
|
||||||
|
# Check indented comment
|
||||||
|
cmt = ida_bytes.get_cmt(fva, False) # False = non-repeatable
|
||||||
|
if cmt:
|
||||||
|
for line in cmt.split("\n"):
|
||||||
|
if line.startswith("Alternative name is '") and line.endswith("'"):
|
||||||
|
name = line[len("Alternative name is '") : -1] # Extract name between quotes
|
||||||
|
yield name
|
||||||
|
|
||||||
|
# Check function comment
|
||||||
|
func_cmt = ida_funcs.get_func_cmt(idaapi.get_func(fva), False)
|
||||||
|
if func_cmt:
|
||||||
|
for line in func_cmt.split("\n"):
|
||||||
|
if line.startswith("Alternative name is '") and line.endswith("'"):
|
||||||
|
name = line[len("Alternative name is '") : -1]
|
||||||
|
yield name
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import idautils
|
|||||||
|
|
||||||
import capa.features.extractors.helpers
|
import capa.features.extractors.helpers
|
||||||
import capa.features.extractors.ida.helpers
|
import capa.features.extractors.ida.helpers
|
||||||
|
from capa.features.file import FunctionName
|
||||||
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
||||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
|
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
|
||||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||||
@@ -129,8 +130,8 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
|
|||||||
# not a function (start)
|
# not a function (start)
|
||||||
return
|
return
|
||||||
|
|
||||||
if target_func.flags & idaapi.FUNC_LIB:
|
name = idaapi.get_name(target_func.start_ea)
|
||||||
name = idaapi.get_name(target_func.start_ea)
|
if target_func.flags & idaapi.FUNC_LIB or not name.startswith("sub_"):
|
||||||
yield API(name), ih.address
|
yield API(name), ih.address
|
||||||
if name.startswith("_"):
|
if name.startswith("_"):
|
||||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||||
@@ -139,6 +140,10 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
|
|||||||
# see: https://stackoverflow.com/a/2628384/87207
|
# see: https://stackoverflow.com/a/2628384/87207
|
||||||
yield API(name[1:]), ih.address
|
yield API(name[1:]), ih.address
|
||||||
|
|
||||||
|
for altname in capa.features.extractors.ida.helpers.get_function_alternative_names(target_func.start_ea):
|
||||||
|
yield FunctionName(altname), ih.address
|
||||||
|
yield API(altname), ih.address
|
||||||
|
|
||||||
|
|
||||||
def extract_insn_number_features(
|
def extract_insn_number_features(
|
||||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
import datetime
|
||||||
@@ -23,24 +22,13 @@ from pathlib import Path
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from typing_extensions import assert_never
|
from typing_extensions import assert_never
|
||||||
|
|
||||||
import capa.perf
|
|
||||||
import capa.rules
|
import capa.rules
|
||||||
import capa.engine
|
|
||||||
import capa.helpers
|
|
||||||
import capa.version
|
import capa.version
|
||||||
import capa.render.json
|
|
||||||
import capa.rules.cache
|
|
||||||
import capa.render.default
|
|
||||||
import capa.render.verbose
|
|
||||||
import capa.features.common
|
import capa.features.common
|
||||||
import capa.features.freeze as frz
|
import capa.features.freeze as frz
|
||||||
import capa.render.vverbose
|
|
||||||
import capa.features.extractors
|
import capa.features.extractors
|
||||||
import capa.render.result_document
|
|
||||||
import capa.render.result_document as rdoc
|
import capa.render.result_document as rdoc
|
||||||
import capa.features.extractors.common
|
import capa.features.extractors.common
|
||||||
import capa.features.extractors.base_extractor
|
|
||||||
import capa.features.extractors.cape.extractor
|
|
||||||
from capa.rules import RuleSet
|
from capa.rules import RuleSet
|
||||||
from capa.engine import MatchResults
|
from capa.engine import MatchResults
|
||||||
from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError
|
from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError
|
||||||
@@ -338,12 +326,23 @@ def get_extractor(
|
|||||||
import capa.features.extractors.ida.extractor
|
import capa.features.extractors.ida.extractor
|
||||||
|
|
||||||
logger.debug("idalib: opening database...")
|
logger.debug("idalib: opening database...")
|
||||||
# idalib writes to stdout (ugh), so we have to capture that
|
idapro.enable_console_messages(False)
|
||||||
# so as not to screw up structured output.
|
with console.status("analyzing program...", spinner="dots"):
|
||||||
with capa.helpers.stdout_redirector(io.BytesIO()):
|
# we set the primary and secondary Lumina servers to 0.0.0.0 to disable Lumina,
|
||||||
with console.status("analyzing program...", spinner="dots"):
|
# which sometimes provides bad names, including overwriting names from debug info.
|
||||||
if idapro.open_database(str(input_path), run_auto_analysis=True):
|
#
|
||||||
raise RuntimeError("failed to analyze input file")
|
# return values from open_database:
|
||||||
|
# 0 - Success (database not packed)
|
||||||
|
# 1 - Success (database was packed)
|
||||||
|
# 2 - User cancelled or 32-64 bit conversion failed
|
||||||
|
# 4 - Database initialization failed
|
||||||
|
# -1 - Generic errors (database already open, auto-analysis failed, etc.)
|
||||||
|
# -2 - User cancelled operation
|
||||||
|
ret = idapro.open_database(
|
||||||
|
str(input_path), run_auto_analysis=True, args="-Olumina:host=0.0.0.0 -Osecondary_lumina:host=0.0.0.0"
|
||||||
|
)
|
||||||
|
if ret not in (0, 1):
|
||||||
|
raise RuntimeError("failed to analyze input file")
|
||||||
|
|
||||||
logger.debug("idalib: waiting for analysis...")
|
logger.debug("idalib: waiting for analysis...")
|
||||||
ida_auto.auto_wait()
|
ida_auto.auto_wait()
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ dev = [
|
|||||||
"pytest-sugar==1.1.1",
|
"pytest-sugar==1.1.1",
|
||||||
"pytest-instafail==0.5.0",
|
"pytest-instafail==0.5.0",
|
||||||
"flake8==7.3.0",
|
"flake8==7.3.0",
|
||||||
"flake8-bugbear==25.10.21",
|
"flake8-bugbear==25.11.29",
|
||||||
"flake8-encodings==0.5.1",
|
"flake8-encodings==0.5.1",
|
||||||
"flake8-comprehensions==3.17.0",
|
"flake8-comprehensions==3.17.0",
|
||||||
"flake8-logging-format==0.9.0",
|
"flake8-logging-format==0.9.0",
|
||||||
@@ -138,7 +138,7 @@ dev = [
|
|||||||
"flake8-use-pathlib==0.3.0",
|
"flake8-use-pathlib==0.3.0",
|
||||||
"flake8-copyright==0.2.4",
|
"flake8-copyright==0.2.4",
|
||||||
"ruff==0.14.7",
|
"ruff==0.14.7",
|
||||||
"black==25.11.0",
|
"black==25.12.0",
|
||||||
"isort==6.0.0",
|
"isort==6.0.0",
|
||||||
"mypy==1.17.1",
|
"mypy==1.17.1",
|
||||||
"mypy-protobuf==3.6.0",
|
"mypy-protobuf==3.6.0",
|
||||||
@@ -148,7 +148,7 @@ dev = [
|
|||||||
"types-backports==0.1.3",
|
"types-backports==0.1.3",
|
||||||
"types-colorama==0.4.15.11",
|
"types-colorama==0.4.15.11",
|
||||||
"types-PyYAML==6.0.8",
|
"types-PyYAML==6.0.8",
|
||||||
"types-psutil==7.0.0.20250218",
|
"types-psutil==7.1.3.20251202",
|
||||||
"types_requests==2.32.0.20240712",
|
"types_requests==2.32.0.20240712",
|
||||||
"types-protobuf==6.32.1.20250918",
|
"types-protobuf==6.32.1.20250918",
|
||||||
"deptry==0.23.0"
|
"deptry==0.23.0"
|
||||||
|
|||||||
@@ -44,5 +44,5 @@ six==1.17.0
|
|||||||
sortedcontainers==2.4.0
|
sortedcontainers==2.4.0
|
||||||
viv-utils==0.8.0
|
viv-utils==0.8.0
|
||||||
vivisect==1.2.1
|
vivisect==1.2.1
|
||||||
msgspec==0.19.0
|
msgspec==0.20.0
|
||||||
bump-my-version==1.2.4
|
bump-my-version==1.2.4
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
import contextlib
|
import contextlib
|
||||||
import collections
|
import collections
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -20,7 +20,6 @@ from functools import lru_cache
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import capa.main
|
|
||||||
import capa.features.file
|
import capa.features.file
|
||||||
import capa.features.insn
|
import capa.features.insn
|
||||||
import capa.features.common
|
import capa.features.common
|
||||||
@@ -53,6 +52,7 @@ from capa.features.extractors.base_extractor import (
|
|||||||
)
|
)
|
||||||
from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor
|
from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
CD = Path(__file__).resolve().parent
|
CD = Path(__file__).resolve().parent
|
||||||
DOTNET_DIR = CD / "data" / "dotnet"
|
DOTNET_DIR = CD / "data" / "dotnet"
|
||||||
DNFILE_TESTFILES = DOTNET_DIR / "dnfile-testfiles"
|
DNFILE_TESTFILES = DOTNET_DIR / "dnfile-testfiles"
|
||||||
@@ -200,6 +200,71 @@ def get_binja_extractor(path: Path):
|
|||||||
return extractor
|
return extractor
|
||||||
|
|
||||||
|
|
||||||
|
# we can't easily cache this because the extractor relies on global state (the opened database)
|
||||||
|
# which also has to be closed elsewhere. so, the idalib tests will just take a little bit to run.
|
||||||
|
def get_idalib_extractor(path: Path):
|
||||||
|
import capa.features.extractors.ida.idalib as idalib
|
||||||
|
|
||||||
|
if not idalib.has_idalib():
|
||||||
|
raise RuntimeError("cannot find IDA idalib module.")
|
||||||
|
|
||||||
|
if not idalib.load_idalib():
|
||||||
|
raise RuntimeError("failed to load IDA idalib module.")
|
||||||
|
|
||||||
|
import idapro
|
||||||
|
import ida_auto
|
||||||
|
|
||||||
|
import capa.features.extractors.ida.extractor
|
||||||
|
|
||||||
|
logger.debug("idalib: opening database...")
|
||||||
|
|
||||||
|
idapro.enable_console_messages(False)
|
||||||
|
# we set the primary and secondary Lumina servers to 0.0.0.0 to disable Lumina,
|
||||||
|
# which sometimes provides bad names, including overwriting names from debug info.
|
||||||
|
#
|
||||||
|
# return values from open_database:
|
||||||
|
# 0 - Success (database not packed)
|
||||||
|
# 1 - Success (database was packed)
|
||||||
|
# 2 - User cancelled or 32-64 bit conversion failed
|
||||||
|
# 4 - Database initialization failed
|
||||||
|
# -1 - Generic errors (database already open, auto-analysis failed, etc.)
|
||||||
|
# -2 - User cancelled operation
|
||||||
|
ret = idapro.open_database(
|
||||||
|
str(path), run_auto_analysis=True, args="-Olumina:host=0.0.0.0 -Osecondary_lumina:host=0.0.0.0"
|
||||||
|
)
|
||||||
|
if ret not in (0, 1):
|
||||||
|
raise RuntimeError("failed to analyze input file")
|
||||||
|
|
||||||
|
logger.debug("idalib: waiting for analysis...")
|
||||||
|
ida_auto.auto_wait()
|
||||||
|
logger.debug("idalib: opened database.")
|
||||||
|
|
||||||
|
extractor = capa.features.extractors.ida.extractor.IdaFeatureExtractor()
|
||||||
|
fixup_idalib(path, extractor)
|
||||||
|
return extractor
|
||||||
|
|
||||||
|
|
||||||
|
def fixup_idalib(path: Path, extractor):
|
||||||
|
"""
|
||||||
|
IDA fixups to overcome differences between backends
|
||||||
|
"""
|
||||||
|
import idaapi
|
||||||
|
import ida_funcs
|
||||||
|
|
||||||
|
def remove_library_id_flag(fva):
|
||||||
|
f = idaapi.get_func(fva)
|
||||||
|
f.flags &= ~ida_funcs.FUNC_LIB
|
||||||
|
ida_funcs.update_func(f)
|
||||||
|
|
||||||
|
if "kernel32-64" in path.name:
|
||||||
|
# remove (correct) library function id, so we can test x64 thunk
|
||||||
|
remove_library_id_flag(0x1800202B0)
|
||||||
|
|
||||||
|
if "al-khaser_x64" in path.name:
|
||||||
|
# remove (correct) library function id, so we can test x64 nested thunk
|
||||||
|
remove_library_id_flag(0x14004B4F0)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def get_cape_extractor(path):
|
def get_cape_extractor(path):
|
||||||
from capa.helpers import load_json_from_path
|
from capa.helpers import load_json_from_path
|
||||||
@@ -894,20 +959,8 @@ FEATURE_PRESENCE_TESTS = sorted(
|
|||||||
("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), False),
|
("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), False),
|
||||||
("mimikatz", "function=0x4556E5", capa.features.insn.API("LsaQueryInformationPolicy"), True),
|
("mimikatz", "function=0x4556E5", capa.features.insn.API("LsaQueryInformationPolicy"), True),
|
||||||
# insn/api: x64
|
# insn/api: x64
|
||||||
(
|
|
||||||
"kernel32-64",
|
|
||||||
"function=0x180001010",
|
|
||||||
capa.features.insn.API("RtlVirtualUnwind"),
|
|
||||||
True,
|
|
||||||
),
|
|
||||||
("kernel32-64", "function=0x180001010", capa.features.insn.API("RtlVirtualUnwind"), True),
|
("kernel32-64", "function=0x180001010", capa.features.insn.API("RtlVirtualUnwind"), True),
|
||||||
# insn/api: x64 thunk
|
# insn/api: x64 thunk
|
||||||
(
|
|
||||||
"kernel32-64",
|
|
||||||
"function=0x1800202B0",
|
|
||||||
capa.features.insn.API("RtlCaptureContext"),
|
|
||||||
True,
|
|
||||||
),
|
|
||||||
("kernel32-64", "function=0x1800202B0", capa.features.insn.API("RtlCaptureContext"), True),
|
("kernel32-64", "function=0x1800202B0", capa.features.insn.API("RtlCaptureContext"), True),
|
||||||
# insn/api: x64 nested thunk
|
# insn/api: x64 nested thunk
|
||||||
("al-khaser x64", "function=0x14004B4F0", capa.features.insn.API("__vcrt_GetModuleHandle"), True),
|
("al-khaser x64", "function=0x14004B4F0", capa.features.insn.API("__vcrt_GetModuleHandle"), True),
|
||||||
@@ -995,20 +1048,20 @@ FEATURE_PRESENCE_TESTS = sorted(
|
|||||||
("pma16-01", "file", OS(OS_WINDOWS), True),
|
("pma16-01", "file", OS(OS_WINDOWS), True),
|
||||||
("pma16-01", "file", OS(OS_LINUX), False),
|
("pma16-01", "file", OS(OS_LINUX), False),
|
||||||
("mimikatz", "file", OS(OS_WINDOWS), True),
|
("mimikatz", "file", OS(OS_WINDOWS), True),
|
||||||
("pma16-01", "function=0x404356", OS(OS_WINDOWS), True),
|
("pma16-01", "function=0x401100", OS(OS_WINDOWS), True),
|
||||||
("pma16-01", "function=0x404356,bb=0x4043B9", OS(OS_WINDOWS), True),
|
("pma16-01", "function=0x401100,bb=0x401130", OS(OS_WINDOWS), True),
|
||||||
("mimikatz", "function=0x40105D", OS(OS_WINDOWS), True),
|
("mimikatz", "function=0x40105D", OS(OS_WINDOWS), True),
|
||||||
("pma16-01", "file", Arch(ARCH_I386), True),
|
("pma16-01", "file", Arch(ARCH_I386), True),
|
||||||
("pma16-01", "file", Arch(ARCH_AMD64), False),
|
("pma16-01", "file", Arch(ARCH_AMD64), False),
|
||||||
("mimikatz", "file", Arch(ARCH_I386), True),
|
("mimikatz", "file", Arch(ARCH_I386), True),
|
||||||
("pma16-01", "function=0x404356", Arch(ARCH_I386), True),
|
("pma16-01", "function=0x401100", Arch(ARCH_I386), True),
|
||||||
("pma16-01", "function=0x404356,bb=0x4043B9", Arch(ARCH_I386), True),
|
("pma16-01", "function=0x401100,bb=0x401130", Arch(ARCH_I386), True),
|
||||||
("mimikatz", "function=0x40105D", Arch(ARCH_I386), True),
|
("mimikatz", "function=0x40105D", Arch(ARCH_I386), True),
|
||||||
("pma16-01", "file", Format(FORMAT_PE), True),
|
("pma16-01", "file", Format(FORMAT_PE), True),
|
||||||
("pma16-01", "file", Format(FORMAT_ELF), False),
|
("pma16-01", "file", Format(FORMAT_ELF), False),
|
||||||
("mimikatz", "file", Format(FORMAT_PE), True),
|
("mimikatz", "file", Format(FORMAT_PE), True),
|
||||||
# format is also a global feature
|
# format is also a global feature
|
||||||
("pma16-01", "function=0x404356", Format(FORMAT_PE), True),
|
("pma16-01", "function=0x401100", Format(FORMAT_PE), True),
|
||||||
("mimikatz", "function=0x456BB9", Format(FORMAT_PE), True),
|
("mimikatz", "function=0x456BB9", Format(FORMAT_PE), True),
|
||||||
# elf support
|
# elf support
|
||||||
("7351f.elf", "file", OS(OS_LINUX), True),
|
("7351f.elf", "file", OS(OS_LINUX), True),
|
||||||
|
|||||||
63
tests/test_idalib_features.py
Normal file
63
tests/test_idalib_features.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Copyright 2020 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import fixtures
|
||||||
|
|
||||||
|
import capa.features.extractors.ida.idalib
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
idalib_present = capa.features.extractors.ida.idalib.has_idalib()
|
||||||
|
if idalib_present:
|
||||||
|
try:
|
||||||
|
import idapro # noqa: F401 [imported but unused]
|
||||||
|
except ImportError:
|
||||||
|
idalib_present = False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(idalib_present is False, reason="Skip idalib tests if the idalib Python API is not installed")
|
||||||
|
@fixtures.parametrize(
|
||||||
|
"sample,scope,feature,expected",
|
||||||
|
fixtures.FEATURE_PRESENCE_TESTS + fixtures.FEATURE_SYMTAB_FUNC_TESTS,
|
||||||
|
indirect=["sample", "scope"],
|
||||||
|
)
|
||||||
|
def test_idalib_features(sample, scope, feature, expected):
|
||||||
|
try:
|
||||||
|
fixtures.do_test_feature_presence(fixtures.get_idalib_extractor, sample, scope, feature, expected)
|
||||||
|
finally:
|
||||||
|
logger.debug("closing database...")
|
||||||
|
import idapro
|
||||||
|
|
||||||
|
idapro.close_database(save=False)
|
||||||
|
logger.debug("opened database.")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(idalib_present is False, reason="Skip idalib tests if the idalib Python API is not installed")
|
||||||
|
@fixtures.parametrize(
|
||||||
|
"sample,scope,feature,expected",
|
||||||
|
fixtures.FEATURE_COUNT_TESTS,
|
||||||
|
indirect=["sample", "scope"],
|
||||||
|
)
|
||||||
|
def test_idalib_feature_counts(sample, scope, feature, expected):
|
||||||
|
try:
|
||||||
|
fixtures.do_test_feature_count(fixtures.get_idalib_extractor, sample, scope, feature, expected)
|
||||||
|
finally:
|
||||||
|
logger.debug("closing database...")
|
||||||
|
import idapro
|
||||||
|
|
||||||
|
idapro.close_database(save=False)
|
||||||
|
logger.debug("closed database.")
|
||||||
Reference in New Issue
Block a user