diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d76f33d..96cbdcc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,13 @@ - dotnet: emit namespace/class features for ldvirtftn/ldftn instructions #1241 @mike-hunhoff - dotnet: emit namespace/class features for type references #1242 @mike-hunhoff - dotnet: extract dotnet and pe format #1187 @mr-tz +- don't render all library rule matches in vverbose output #1174 @mr-tz ### Breaking Changes -- remove SMDA backend #1062 @williballenthin +- remove SMDA backend #1062 @williballenthin +- error return codes are now positive numbers #1269 @mr-tz -### New Rules (48) +### New Rules (61) - collection/use-dotnet-library-sharpclipboard @johnk3r - data-manipulation/encryption/aes/use-dotnet-library-encryptdecryptutils @johnk3r @@ -65,6 +67,17 @@ - compiler/nuitka/compiled-with-nuitka @williballenthin - nursery/authenticate-data-with-md5-mac william.ballenthin@mandiant.com - nursery/resolve-function-by-djb2-hash still@teamt5.org +- host-interaction/mutex/create-semaphore-on-linux @ramen0x3f +- host-interaction/mutex/lock-semaphore-on-linux @ramen0x3f +- host-interaction/mutex/unlock-semaphore-on-linux @ramen0x3f +- data-manipulation/hashing/sha384/hash-data-using-sha384 william.ballenthin@mandiant.com +- data-manipulation/hashing/sha512/hash-data-using-sha512 william.ballenthin@mandiant.com +- nursery/decode-data-using-url-encoding michael.hunhoff@mandiant.com +- nursery/manipulate-user-privileges michael.hunhoff@mandiant.com +- lib/get-os-version @mr-tz +- nursery/decrypt-data-using-tea william.ballenthin@mandiant.com +- nursery/encrypt-data-using-tea william.ballenthin@mandiant.com +- nursery/hash-data-using-whirlpool william.ballenthin@mandiant.com - ### Bug Fixes @@ -77,6 +90,11 @@ - dotnet: address unhandled exceptions with improved type checking #1230 @mike-hunhoff - fix import-to-ida script formatting #1208 @williballenthin - render: fix verbose rendering of scopes #1263 @williballenthin +- rules: better detect invalid rules #1282 @williballenthin +- show-features: better render strings with embedded whitespace #1267 @williballenthin +- handle vivisect bug around strings at instruction level, use min length 4 #1271 @williballenthin @mr-tz +- extractor: guard against invalid "calls from" features #1177 @mr-tz +- extractor: add format to global features #1258 @mr-tz ### capa explorer IDA Pro plugin - fix: display instruction items #1154 @mr-tz @@ -88,6 +106,7 @@ - generator: refactor caching and matching #1251 @mike-hunhoff - fix: improve exception handling to prevent IDA from locking up when errors occur #1262 @mike-hunhoff - verify rule metadata using Pydantic #1167 @mr-tz +- extractor: make read consistent with file object behavior #1254 @mr-tz ### Development diff --git a/README.md b/README.md index 699f885f..f980eab1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flare-capa)](https://pypi.org/project/flare-capa) [![Last release](https://img.shields.io/github/v/release/mandiant/capa)](https://github.com/mandiant/capa/releases) -[![Number of rules](https://img.shields.io/badge/rules-747-blue.svg)](https://github.com/mandiant/capa-rules) +[![Number of rules](https://img.shields.io/badge/rules-757-blue.svg)](https://github.com/mandiant/capa-rules) [![CI status](https://github.com/mandiant/capa/workflows/CI/badge.svg)](https://github.com/mandiant/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster) [![Downloads](https://img.shields.io/github/downloads/mandiant/capa/total)](https://github.com/mandiant/capa/releases) [![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.txt) diff --git a/capa/features/common.py b/capa/features/common.py index 5d30f10b..cf2c02f3 100644 --- a/capa/features/common.py +++ b/capa/features/common.py @@ -179,6 +179,10 @@ class String(Feature): def __init__(self, value: str, description=None): super().__init__(value, description=description) + def get_value_str(self) -> str: + assert isinstance(self.value, str) + return escape_string(self.value) + class Class(Feature): def __init__(self, value: str, description=None): @@ -232,9 +236,13 @@ class Substring(String): else: return Result(False, _MatchedSubstring(self, {}), []) + def get_value_str(self) -> str: + assert isinstance(self.value, str) + return escape_string(self.value) + def __str__(self): assert isinstance(self.value, str) - return "substring(%s)" % self.value + return "substring(%s)" % escape_string(self.value) class _MatchedSubstring(Substring): diff --git a/capa/features/extractors/common.py b/capa/features/extractors/common.py index 585c0040..d72fcefd 100644 --- a/capa/features/extractors/common.py +++ b/capa/features/extractors/common.py @@ -9,6 +9,7 @@ import pefile import capa.features import capa.features.extractors.elf import capa.features.extractors.pefile +import capa.features.extractors.strings from capa.features.common import OS, FORMAT_PE, FORMAT_ELF, OS_WINDOWS, FORMAT_FREEZE, Arch, Format, String, Feature from capa.features.freeze import is_freeze from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress diff --git a/capa/features/extractors/dnfile/extractor.py b/capa/features/extractors/dnfile/extractor.py index bd4b9c9e..9d7c6c4c 100644 --- a/capa/features/extractors/dnfile/extractor.py +++ b/capa/features/extractors/dnfile/extractor.py @@ -8,13 +8,13 @@ from __future__ import annotations -from enum import Enum from typing import Dict, List, Tuple, Union, Iterator, Optional import dnfile from dncil.cil.opcode import OpCodes import capa.features.extractors +import capa.features.extractors.dotnetfile import capa.features.extractors.dnfile.file import capa.features.extractors.dnfile.insn import capa.features.extractors.dnfile.function @@ -78,6 +78,7 @@ class DnfileFeatureExtractor(FeatureExtractor): # pre-compute these because we'll yield them at *every* scope. self.global_features: List[Tuple[Feature, Address]] = [] + self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_format()) self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_os(pe=self.pe)) self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe)) diff --git a/capa/features/extractors/ida/extractor.py b/capa/features/extractors/ida/extractor.py index 1a587fa6..0d44ba9e 100644 --- a/capa/features/extractors/ida/extractor.py +++ b/capa/features/extractors/ida/extractor.py @@ -25,6 +25,7 @@ class IdaFeatureExtractor(FeatureExtractor): def __init__(self): super().__init__() self.global_features: List[Tuple[Feature, Address]] = [] + self.global_features.extend(capa.features.extractors.ida.file.extract_file_format()) self.global_features.extend(capa.features.extractors.ida.global_.extract_os()) self.global_features.extend(capa.features.extractors.ida.global_.extract_arch()) diff --git a/capa/features/extractors/viv/extractor.py b/capa/features/extractors/viv/extractor.py index 53683f66..e2d0fb1e 100644 --- a/capa/features/extractors/viv/extractor.py +++ b/capa/features/extractors/viv/extractor.py @@ -34,6 +34,7 @@ class VivisectFeatureExtractor(FeatureExtractor): # pre-compute these because we'll yield them at *every* scope. self.global_features: List[Tuple[Feature, Address]] = [] + self.global_features.extend(capa.features.extractors.viv.file.extract_file_format(self.buf)) self.global_features.extend(capa.features.extractors.common.extract_os(self.buf)) self.global_features.extend(capa.features.extractors.viv.global_.extract_arch(self.vw)) diff --git a/capa/features/extractors/viv/insn.py b/capa/features/extractors/viv/insn.py index 738c69a7..1b36e348 100644 --- a/capa/features/extractors/viv/insn.py +++ b/capa/features/extractors/viv/insn.py @@ -281,7 +281,12 @@ def read_string(vw, offset: int) -> str: pass else: if alen > 0: - return read_memory(vw, offset, alen).decode("utf-8") + buf = read_memory(vw, offset, alen) + if b"\x00" in buf: + # account for bug #1271. + # remove when vivisect is fixed. + buf = buf.partition(b"\x00")[0] + return buf.decode("utf-8") try: ulen = vw.detectUnicode(offset) @@ -300,7 +305,9 @@ def read_string(vw, offset: int) -> str: # vivisect seems to mis-detect the end unicode strings # off by two, too short ulen += 2 - return read_memory(vw, offset, ulen).decode("utf-16") + # partition to account for bug #1271. + # remove when vivisect is fixed. + return read_memory(vw, offset, ulen).decode("utf-16").partition("\x00")[0] raise ValueError("not a string", offset) @@ -493,7 +500,8 @@ def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper): oper = insn.opers[0] target = oper.getOperAddr(insn) - yield Characteristic("calls from"), AbsoluteVirtualAddress(target) + if target >= 0: + yield Characteristic("calls from"), AbsoluteVirtualAddress(target) # call via thunk on x86, # see 9324d1a8ae37a36ae560c37448c9705a at 0x407985 @@ -509,7 +517,8 @@ def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera elif isinstance(insn.opers[0], envi.archs.amd64.disasm.Amd64RipRelOper): op = insn.opers[0] target = op.getOperAddr(insn) - yield Characteristic("calls from"), AbsoluteVirtualAddress(target) + if target >= 0: + yield Characteristic("calls from"), AbsoluteVirtualAddress(target) if target and target == f.va: # if we found a jump target and it's the function address @@ -663,11 +672,12 @@ def extract_op_string_features( for v in derefs(f.vw, v): try: - s = read_string(f.vw, v) + s = read_string(f.vw, v).rstrip("\x00") except ValueError: continue else: - yield String(s.rstrip("\x00")), ih.address + if len(s) > 4: + yield String(s), ih.address def extract_operand_features(f: FunctionHandle, bb, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]: diff --git a/capa/helpers.py b/capa/helpers.py index 2e44fc6c..a2edc812 100644 --- a/capa/helpers.py +++ b/capa/helpers.py @@ -10,7 +10,7 @@ import logging from typing import NoReturn from capa.exceptions import UnsupportedFormatError -from capa.features.common import FORMAT_SC32, FORMAT_SC64, FORMAT_UNKNOWN +from capa.features.common import FORMAT_PE, FORMAT_SC32, FORMAT_SC64, FORMAT_DOTNET, FORMAT_UNKNOWN, Format EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32") EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64") @@ -68,11 +68,17 @@ def get_auto_format(path: str) -> str: def get_format(sample: str) -> str: # imported locally to avoid import cycle from capa.features.extractors.common import extract_format + from capa.features.extractors.dnfile_ import DnfileFeatureExtractor with open(sample, "rb") as f: buf = f.read() for feature, _ in extract_format(buf): + if feature == Format(FORMAT_PE): + dnfile_extractor = DnfileFeatureExtractor(sample) + if dnfile_extractor.is_dotnet_file(): + feature = Format(FORMAT_DOTNET) + assert isinstance(feature.value, str) return feature.value diff --git a/capa/ida/helpers.py b/capa/ida/helpers.py index d1ef3093..2d12e931 100644 --- a/capa/ida/helpers.py +++ b/capa/ida/helpers.py @@ -181,11 +181,13 @@ class IDAIO: def read(self, size): ea = ida_loader.get_fileregion_ea(self.offset) if ea == idc.BADADDR: - # best guess, such as if file is mapped at address 0x0. - ea = self.offset + logger.debug("cannot read 0x%x bytes at 0x%x (ea: BADADDR)", size, self.offset) + return b"" logger.debug("reading 0x%x bytes at 0x%x (ea: 0x%x)", size, self.offset, ea) - return ida_bytes.get_bytes(ea, size) + + # get_bytes returns None on error, for consistency with read always return bytes + return ida_bytes.get_bytes(ea, size) or b"" def close(self): return diff --git a/capa/main.py b/capa/main.py index 4543ce9e..d88722a2 100644 --- a/capa/main.py +++ b/capa/main.py @@ -20,7 +20,7 @@ import textwrap import itertools import contextlib import collections -from typing import Any, Dict, List, Tuple, Optional +from typing import Any, Dict, List, Tuple import halo import tqdm @@ -74,16 +74,16 @@ SIGNATURES_PATH_DEFAULT_STRING = "(embedded signatures)" BACKEND_VIV = "vivisect" BACKEND_DOTNET = "dotnet" -E_MISSING_RULES = -10 -E_MISSING_FILE = -11 -E_INVALID_RULE = -12 -E_CORRUPT_FILE = -13 -E_FILE_LIMITATION = -14 -E_INVALID_SIG = -15 -E_INVALID_FILE_TYPE = -16 -E_INVALID_FILE_ARCH = -17 -E_INVALID_FILE_OS = -18 -E_UNSUPPORTED_IDA_VERSION = -19 +E_MISSING_RULES = 10 +E_MISSING_FILE = 11 +E_INVALID_RULE = 12 +E_CORRUPT_FILE = 13 +E_FILE_LIMITATION = 14 +E_INVALID_SIG = 15 +E_INVALID_FILE_TYPE = 16 +E_INVALID_FILE_ARCH = 17 +E_INVALID_FILE_OS = 18 +E_UNSUPPORTED_IDA_VERSION = 19 logger = logging.getLogger("capa") @@ -536,12 +536,12 @@ def get_extractor( def get_file_extractors(sample: str, format_: str) -> List[FeatureExtractor]: file_extractors: List[FeatureExtractor] = list() - if format_ == capa.features.extractors.common.FORMAT_PE: + if format_ == FORMAT_PE: file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(sample)) - dnfile_extractor = capa.features.extractors.dnfile_.DnfileFeatureExtractor(sample) - if dnfile_extractor.is_dotnet_file(): - file_extractors.append(dnfile_extractor) + elif format_ == FORMAT_DOTNET: + file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(sample)) + file_extractors.append(capa.features.extractors.dnfile_.DnfileFeatureExtractor(sample)) elif format_ == capa.features.extractors.common.FORMAT_ELF: file_extractors.append(capa.features.extractors.elffile.ElfFeatureExtractor(sample)) @@ -671,7 +671,6 @@ def collect_metadata( sample_path: str, rules_path: List[str], extractor: capa.features.extractors.base_extractor.FeatureExtractor, - format_: Optional[str] = None, ): md5 = hashlib.md5() sha1 = hashlib.sha1() @@ -687,8 +686,7 @@ def collect_metadata( if rules_path != [RULES_PATH_DEFAULT_STRING]: rules_path = [os.path.abspath(os.path.normpath(r)) for r in rules_path] - if format_ is None: - format_ = get_format(sample_path) + format_ = get_format(sample_path) arch = get_arch(sample_path) os_ = get_os(sample_path) @@ -1021,6 +1019,9 @@ def main(argv=None): if format_ == FORMAT_AUTO: try: format_ = get_auto_format(args.sample) + except PEFormatError as e: + logger.error("Input file '%s' is not a valid PE file: %s", args.sample, str(e)) + return E_CORRUPT_FILE except UnsupportedFormatError: log_unsupported_format_error() return E_INVALID_FILE_TYPE @@ -1083,9 +1084,6 @@ def main(argv=None): logger.error("Input file '%s' is not a valid ELF file: %s", args.sample, str(e)) return E_CORRUPT_FILE - if isinstance(file_extractor, capa.features.extractors.dnfile_.DnfileFeatureExtractor): - format_ = FORMAT_DOTNET - # file limitations that rely on non-file scope won't be detected here. # nor on FunctionName features, because pefile doesn't support this. if has_file_limitation(rules, pure_file_capabilities): @@ -1125,7 +1123,7 @@ def main(argv=None): log_unsupported_os_error() return E_INVALID_FILE_OS - meta = collect_metadata(argv, args.sample, args.rules, extractor, format_=format_) + meta = collect_metadata(argv, args.sample, args.rules, extractor) capabilities, counts = find_capabilities(rules, extractor, disable_progress=args.quiet) meta["analysis"].update(counts) diff --git a/capa/render/utils.py b/capa/render/utils.py index 2cf480c9..96abadcd 100644 --- a/capa/render/utils.py +++ b/capa/render/utils.py @@ -24,6 +24,10 @@ def bold2(s: str) -> str: return termcolor.colored(s, "green") +def warn(s: str) -> str: + return termcolor.colored(s, "yellow") + + def format_parts_id(data: Union[rd.AttackSpec, rd.MBCSpec]): """ format canonical representation of ATT&CK/MBC parts and ID diff --git a/capa/render/vverbose.py b/capa/render/vverbose.py index 5950275a..74be65da 100644 --- a/capa/render/vverbose.py +++ b/capa/render/vverbose.py @@ -285,17 +285,24 @@ def render_rules(ostream, doc: rd.ResultDocument): if rule.meta.is_subscope_rule: continue + lib_info = "" count = len(rule.matches) if count == 1: - capability = rutils.bold(rule.meta.name) + if rule.meta.lib: + lib_info = " (library rule)" + capability = "%s%s" % (rutils.bold(rule.meta.name), lib_info) else: - capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count) + if rule.meta.lib: + lib_info = ", only showing first match of library rule" + capability = "%s (%d matches%s)" % (rutils.bold(rule.meta.name), count, lib_info) ostream.writeln(capability) had_match = True rows = [] - rows.append(("namespace", rule.meta.namespace)) + if not rule.meta.lib: + # library rules should not have a namespace + rows.append(("namespace", rule.meta.namespace)) if rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov: rows.append( @@ -355,6 +362,10 @@ def render_rules(ostream, doc: rd.ResultDocument): ostream.write("\n") render_match(ostream, match, indent=1) + if rule.meta.lib: + # only show first match + break + ostream.write("\n") if not had_match: diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index c4d2ad77..ac344334 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -29,6 +29,7 @@ from typing import Any, Set, Dict, List, Tuple, Union, Iterator import yaml import pydantic import ruamel.yaml +import yaml.parser import capa.perf import capa.engine as ceng @@ -836,6 +837,8 @@ class Rule: raise InvalidRuleWithPath(path, str(e)) from e except pydantic.ValidationError as e: raise InvalidRuleWithPath(path, str(e)) from e + except yaml.parser.ParserError as e: + raise InvalidRuleWithPath(path, str(e)) from e def to_yaml(self) -> str: # reformat the yaml document with a common style. diff --git a/rules b/rules index 4c93fad4..09956929 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit 4c93fad4aaed8249767a9b65dee51eaa12d0ff37 +Subproject commit 09956929b5e38a2397cab767da0dc3a0e0513a90 diff --git a/scripts/lint.py b/scripts/lint.py index cf1fa871..c6639807 100644 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -307,11 +307,7 @@ def get_sample_capabilities(ctx: Context, path: Path) -> Set[str]: elif nice_path.endswith(capa.helpers.EXTENSIONS_SHELLCODE_64): format_ = "sc64" else: - format_ = "auto" - if not nice_path.endswith(capa.helpers.EXTENSIONS_ELF): - dnfile_extractor = capa.features.extractors.dnfile_.DnfileFeatureExtractor(nice_path) - if dnfile_extractor.is_dotnet_file(): - format_ = FORMAT_DOTNET + format_ = capa.main.get_auto_format(nice_path) logger.debug("analyzing sample: %s", nice_path) extractor = capa.main.get_extractor(nice_path, format_, "", DEFAULT_SIGNATURES, False, disable_progress=True) diff --git a/scripts/show-capabilities-by-function.py b/scripts/show-capabilities-by-function.py index 8cc59d81..c96920d0 100644 --- a/scripts/show-capabilities-by-function.py +++ b/scripts/show-capabilities-by-function.py @@ -175,7 +175,7 @@ def main(argv=None): capa.helpers.log_unsupported_runtime_error() return -1 - meta = capa.main.collect_metadata(argv, args.sample, args.rules, extractor, format_=format_) + meta = capa.main.collect_metadata(argv, args.sample, args.rules, extractor) capabilities, counts = capa.main.find_capabilities(rules, extractor) meta["analysis"].update(counts) meta["analysis"]["layout"] = capa.main.compute_layout(rules, extractor, capabilities) diff --git a/setup.py b/setup.py index a52f8f5b..5f33582f 100644 --- a/setup.py +++ b/setup.py @@ -15,12 +15,12 @@ requirements = [ "pyyaml==6.0", "tabulate==0.9.0", "colorama==0.4.5", - "termcolor==2.1.1", - "wcwidth==0.2.5", + "termcolor==2.2.0", + "wcwidth==0.2.6", "ida-settings==2.1.0", "viv-utils[flirt]==0.7.7", "halo==0.0.31", - "networkx==2.5.1", + "networkx==2.5.1", # newer versions no longer support py3.7. "ruamel.yaml==0.17.21", "vivisect==1.0.8", "pefile==2022.5.30", diff --git a/tests/data b/tests/data index b06307a6..45e1d0cc 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit b06307a693e3b91fe72d197acdeb0b273a720b0c +Subproject commit 45e1d0ccfa7c3a02853e035d3d5c249178c83c2e diff --git a/tests/fixtures.py b/tests/fixtures.py index f6686c4b..ae336f56 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -118,6 +118,9 @@ def fixup_viv(path, extractor): if "3b13b" in path: # vivisect only recognizes calling thunk function at 0x10001573 extractor.vw.makeFunction(0x10006860) + if "294b8d" in path: + # see vivisect/#561 + extractor.vw.makeFunction(0x404970) @lru_cache(maxsize=1) @@ -277,6 +280,8 @@ def get_data_path_by_name(name): return os.path.join(CD, "data", "b5f0524e69b3a3cf636c7ac366ca57bf5e3a8fdc8a9f01caf196c611a7918a87.elf_") elif name.startswith("bf7a9c"): return os.path.join(CD, "data", "bf7a9c8bdfa6d47e01ad2b056264acc3fd90cf43fe0ed8deec93ab46b47d76cb.elf_") + elif name.startswith("294b8d"): + return os.path.join(CD, "data", "294b8db1f2702b60fb2e42fdc50c2cee6a5046112da9a5703a548a4fa50477bc.elf_") else: raise ValueError("unexpected sample fixture: %s" % name) @@ -627,6 +632,8 @@ FEATURE_PRESENCE_TESTS = sorted( ("mimikatz", "function=0x40105D", capa.features.common.String("ACR > "), True), ("mimikatz", "function=0x40105D", capa.features.common.String("nope"), False), ("773290...", "function=0x140001140", capa.features.common.String(r"%s:\\OfficePackagesForWDAG"), True), + # overlapping string, see #1271 + ("294b8d...", "function=0x404970,bb=0x404970,insn=0x40499F", capa.features.common.String("\r\n\x00:ht"), False), # insn/regex ("pma16-01", "function=0x4021B0", capa.features.common.Regex("HTTP/1.0"), True), ("pma16-01", "function=0x402F40", capa.features.common.Regex("www.practicalmalwareanalysis.com"), True), @@ -682,14 +689,22 @@ FEATURE_PRESENCE_TESTS = sorted( # os & format & arch ("pma16-01", "file", OS(OS_WINDOWS), True), ("pma16-01", "file", OS(OS_LINUX), False), + ("mimikatz", "file", OS(OS_WINDOWS), True), ("pma16-01", "function=0x404356", OS(OS_WINDOWS), True), ("pma16-01", "function=0x404356,bb=0x4043B9", OS(OS_WINDOWS), True), + ("mimikatz", "function=0x40105D", OS(OS_WINDOWS), True), ("pma16-01", "file", Arch(ARCH_I386), True), ("pma16-01", "file", Arch(ARCH_AMD64), False), + ("mimikatz", "file", Arch(ARCH_I386), True), ("pma16-01", "function=0x404356", Arch(ARCH_I386), True), ("pma16-01", "function=0x404356,bb=0x4043B9", Arch(ARCH_I386), True), + ("mimikatz", "function=0x40105D", Arch(ARCH_I386), True), ("pma16-01", "file", Format(FORMAT_PE), True), ("pma16-01", "file", Format(FORMAT_ELF), False), + ("mimikatz", "file", Format(FORMAT_PE), True), + # format is also a global feature + ("pma16-01", "function=0x404356", Format(FORMAT_PE), True), + ("mimikatz", "function=0x456BB9", Format(FORMAT_PE), True), # elf support ("7351f.elf", "file", OS(OS_LINUX), True), ("7351f.elf", "file", OS(OS_WINDOWS), False),