diff --git a/.github/pyinstaller/pyinstaller.spec b/.github/pyinstaller/pyinstaller.spec index 6d0854a9..f1c4fb1b 100644 --- a/.github/pyinstaller/pyinstaller.spec +++ b/.github/pyinstaller/pyinstaller.spec @@ -95,7 +95,7 @@ exe = EXE(pyz, a.datas, exclude_binaries=False, name='capa', - icon='logo.ico', + # TODO not working anymore for unknown reason icon='logo.ico', debug=False, strip=None, upx=True, diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0681c15d..022fc0ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,8 @@ name: build on: - push: - branches: [master, dotnet-main] + pull_request: + branches: [ master ] release: types: [edited, published] @@ -11,6 +11,8 @@ jobs: name: PyInstaller for ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: + # set to false for debugging + fail-fast: true matrix: include: - os: ubuntu-18.04 @@ -37,12 +39,10 @@ jobs: run: sudo apt-get install -y libyaml-dev - name: Upgrade pip, setuptools run: pip install --upgrade pip setuptools - - name: Install PyInstaller - run: pip install 'pyinstaller==4.10' - - name: Install capa - run: pip install -e . + - name: Install capa with build requirements + run: pip install -e .[build] - name: Build standalone executable - run: pyinstaller .github/pyinstaller/pyinstaller.spec + run: pyinstaller --log-level DEBUG .github/pyinstaller/pyinstaller.spec - name: Does it run (PE)? run: dist/capa "tests/data/Practical Malware Analysis Lab 01-01.dll_" - name: Does it run (Shellcode)? diff --git a/CHANGELOG.md b/CHANGELOG.md index 14baf7e0..172c7485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,16 @@ ### New Features -- add new scope "instruction" for matching mnemonics and operands #767 @williballenthin -- add new feature "operand[{0, 1, 2}].number" for matching instruction operand immediate values #767 @williballenthin -- add new feature "operand[{0, 1, 2}].offset" for matching instruction operand offsets #767 @williballenthin -- main: detect dotnet binaries #955 @mr-tz -- render: support Addresses that aren't simple integers, like .NET token+offset #981 @williballenthin -- extract additional offset/number features in certain circumstances #320 @williballenthin -- add detection and basic feature extraction for dotnet #987 @mr-tz, @mike-hunhoff, @williballenthin + - add new scope "instruction" for matching mnemonics and operands #767 @williballenthin + - add new feature "operand[{0, 1, 2}].number" for matching instruction operand immediate values #767 @williballenthin + - add new feature "operand[{0, 1, 2}].offset" for matching instruction operand offsets #767 @williballenthin + - extract additional offset/number features in certain circumstances #320 @williballenthin + - add detection and basic feature extraction for dotnet #987 @mr-tz, @mike-hunhoff, @williballenthin + - add file string extraction for dotnet files #1012 @mike-hunhoff + - add file function-name extraction for dotnet files #1015 @mike-hunhoff + - add unmanaged call characteristic for dotnet files #1023 @mike-hunhoff + - add mixed mode characteristic feature extraction for dotnet files #1024 @mike-hunhoff + - render: support Addresses that aren't simple integers, like .NET token+offset #981 @williballenthin ### Breaking Changes @@ -20,13 +23,14 @@ - the tool now accepts multiple paths to rules, and JSON doc updated accordingly @williballenthin - extractors must use handles to identify functions/basic blocks/instructions #981 @williballenthin -### New Rules (5) +### New Rules (6) - data-manipulation/encryption/aes/manually-build-aes-constants huynh.t.nhan@gmail.com - nursery/get-process-image-filename michael.hunhoff@mandiant.com - compiler/v/compiled-with-v jakub.jozwiak@mandiant.com - compiler/zig/compiled-with-zig jakub.jozwiak@mandiant.com - anti-analysis/packer/huan/packed-with-huan jakub.jozwiak@mandiant.com +- internal/limitation/file/internal-dotnet-file-limitation william.ballenthin@mandiant.com - ### Bug Fixes diff --git a/capa/features/extractors/dnfile/file.py b/capa/features/extractors/dnfile/file.py index eac653d6..5de9dc33 100644 --- a/capa/features/extractors/dnfile/file.py +++ b/capa/features/extractors/dnfile/file.py @@ -12,20 +12,31 @@ from typing import TYPE_CHECKING, Tuple, Iterator if TYPE_CHECKING: import dnfile - from capa.features.common import Feature, Format - from capa.features.file import Import + from capa.features.common import Feature, Format, String, Characteristic + from capa.features.file import Import, FunctionName from capa.features.address import Address import capa.features.extractors - def extract_file_import_names(pe: dnfile.dnPE) -> Iterator[Tuple[Import, Address]]: - yield from capa.features.extractors.dotnetfile.extract_file_import_names(pe) + yield from capa.features.extractors.dotnetfile.extract_file_import_names(pe=pe) def extract_file_format(pe: dnfile.dnPE) -> Iterator[Tuple[Format, Address]]: yield from capa.features.extractors.dotnetfile.extract_file_format(pe=pe) + +def extract_file_function_names(pe: dnfile.dnPE) -> Iterator[Tuple[FunctionName, Address]]: + yield from capa.features.extractors.dotnetfile.extract_file_function_names(pe=pe) + + +def extract_file_strings(pe: dnfile.dnPE) -> Iterator[Tuple[String, Address]]: + yield from capa.features.extractors.dotnetfile.extract_file_strings(pe=pe) + + +def extract_mixed_mode_characteristic_features(pe: dnfile.dnPE) -> Iterator[Tuple[Characteristic, Address]]: + yield from capa.features.extractors.dotnetfile.extract_mixed_mode_characteristic_features(pe=pe) + def extract_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]: for file_handler in FILE_HANDLERS: @@ -35,7 +46,8 @@ def extract_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]: FILE_HANDLERS = ( extract_file_import_names, - # TODO extract_file_strings, - # TODO extract_file_function_names, + extract_file_function_names, + extract_file_strings, extract_file_format, + extract_mixed_mode_characteristic_features, ) diff --git a/capa/features/extractors/dnfile/helpers.py b/capa/features/extractors/dnfile/helpers.py index ac9b0bb9..55c9fc3c 100644 --- a/capa/features/extractors/dnfile/helpers.py +++ b/capa/features/extractors/dnfile/helpers.py @@ -105,18 +105,24 @@ def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[Tuple[int, str]]: TypeName (index into String heap) TypeNamespace (index into String heap) """ - if not hasattr(pe.net.mdtables, "MemberRef"): + if not is_dotnet_table_valid(pe, "MemberRef"): return for (rid, row) in enumerate(pe.net.mdtables.MemberRef): if not isinstance(row.Class.row, (dnfile.mdtable.TypeRefRow,)): continue - token: int = calculate_dotnet_token_value(dnfile.enums.MetadataTables.MemberRef.value, rid + 1) - # like System.IO.File::OpenRead - imp: str = f"{row.Class.row.TypeNamespace}.{row.Class.row.TypeName}::{row.Name}" + # like File::OpenRead + name = f"{row.Class.row.TypeName}::{row.Name}" - yield token, imp + # ECMA II.22.38: TypeNamespace can be null or non-null + if row.Class.row.TypeNamespace: + # like System.IO.File::OpenRead + name = f"{row.Class.row.TypeNamespace}.{name}" + + token: int = calculate_dotnet_token_value(pe.net.mdtables.MemberRef.number, rid + 1) + + yield token, name def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[Tuple[int, str]]: @@ -130,7 +136,7 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[Tuple[int, str]]: ImportName (index into the String heap) ImportScope (index into the ModuleRef table) """ - if not hasattr(pe.net.mdtables, "ImplMap"): + if not is_dotnet_table_valid(pe, "ImplMap"): return for row in pe.net.mdtables.ImplMap: @@ -147,14 +153,14 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[Tuple[int, str]]: dll = dll.split(".")[0] # like kernel32.CreateFileA - imp: str = f"{dll}.{symbol}" + name: str = f"{dll}.{symbol}" - yield token, imp + yield token, name def get_dotnet_managed_method_bodies(pe: dnfile.dnPE) -> Iterator[Tuple[int, CilMethodBody]]: """get managed methods from MethodDef table""" - if not hasattr(pe.net.mdtables, "MethodDef"): + if not is_dotnet_table_valid(pe, "MethodDef"): return for (rid, row) in enumerate(pe.net.mdtables.MethodDef): @@ -168,3 +174,40 @@ def get_dotnet_managed_method_bodies(pe: dnfile.dnPE) -> Iterator[Tuple[int, Cil token: int = calculate_dotnet_token_value(dnfile.enums.MetadataTables.MethodDef.value, rid + 1) yield token, body + + +def is_dotnet_table_valid(pe: dnfile.dnPE, table_name: str) -> bool: + return bool(getattr(pe.net.mdtables, table_name, None)) + + +def get_dotnet_managed_method_names(pe: dnfile.dnPE) -> Iterator[Tuple[int, str]]: + """get managed method names from TypeDef table + + see https://www.ntcore.com/files/dotnetformat.htm + + 02 - TypeDef Table + Each row represents a class in the current assembly. + TypeName (index into String heap) + TypeNamespace (index into String heap) + MethodList (index into MethodDef table; it marks the first of a continguous run of Methods owned by this Type) + """ + if not is_dotnet_table_valid(pe, "TypeDef"): + return + + for row in pe.net.mdtables.TypeDef: + for index in row.MethodList: + # like File::OpenRead + name = f"{row.TypeName}::{index.row.Name}" + + # ECMA II.22.37: TypeNamespace can be null or non-null + if row.TypeNamespace: + # like System.IO.File::OpenRead + name = f"{row.TypeNamespace}.{name}" + + token = calculate_dotnet_token_value(index.table.number, index.row_index) + + yield token, name + + +def is_dotnet_mixed_mode(pe: dnfile.dnPE) -> bool: + return not bool(pe.net.Flags.CLR_ILONLY) diff --git a/capa/features/extractors/dnfile/insn.py b/capa/features/extractors/dnfile/insn.py index dc0beaef..07730b4d 100644 --- a/capa/features/extractors/dnfile/insn.py +++ b/capa/features/extractors/dnfile/insn.py @@ -8,8 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Tuple, Iterator, Optional -from itertools import chain +from typing import TYPE_CHECKING, Any, Dict, Tuple, Iterator, Optional from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle @@ -19,26 +18,56 @@ if TYPE_CHECKING: from capa.features.common import Feature from capa.features.address import Address -from dncil.clr.token import StringToken +import dnfile +from dncil.clr.token import StringToken, InvalidToken from dncil.cil.opcode import OpCodes import capa.features.extractors.helpers from capa.features.insn import API, Number -from capa.features.common import String +from capa.features.common import String, Characteristic from capa.features.extractors.dnfile.helpers import ( + resolve_dotnet_token, read_dotnet_user_string, get_dotnet_managed_imports, get_dotnet_unmanaged_imports, + get_dotnet_managed_method_names, ) -def get_imports(ctx: Dict) -> Dict: - if "imports_cache" not in ctx: - ctx["imports_cache"] = { - token: imp - for (token, imp) in chain(get_dotnet_managed_imports(ctx["pe"]), get_dotnet_unmanaged_imports(ctx["pe"])) - } - return ctx["imports_cache"] +def get_managed_imports(ctx: Dict) -> Dict: + if "managed_imports_cache" not in ctx: + ctx["managed_imports_cache"] = {} + for (token, name) in get_dotnet_managed_imports(ctx["pe"]): + ctx["managed_imports_cache"][token] = name + return ctx["managed_imports_cache"] + + +def get_unmanaged_imports(ctx: Dict) -> Dict: + if "unmanaged_imports_cache" not in ctx: + ctx["unmanaged_imports_cache"] = {} + for (token, name) in get_dotnet_unmanaged_imports(ctx["pe"]): + ctx["unmanaged_imports_cache"][token] = name + return ctx["unmanaged_imports_cache"] + + +def get_methods(ctx: Dict) -> Dict: + if "methods_cache" not in ctx: + ctx["methods_cache"] = {} + for (token, name) in get_dotnet_managed_method_names(ctx["pe"]): + ctx["methods_cache"][token] = name + return ctx["methods_cache"] + + +def get_callee_name(ctx: Dict, token: int) -> str: + """map dotnet token to method name""" + name: str = get_managed_imports(ctx).get(token, "") + if not name: + # we must check unmanaged imports before managed methods because we map forwarded managed methods + # to their unmanaged imports; we prefer a forwarded managed method be mapped to its unmanaged import for analysis + name = get_unmanaged_imports(ctx).get(token, "") + if not name: + name = get_methods(ctx).get(token, "") + return name def extract_insn_api_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]: @@ -49,7 +78,7 @@ def extract_insn_api_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterato if insn.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli): return - name: str = get_imports(fh.ctx).get(insn.operand.value, "") + name: str = get_callee_name(f.ctx, insn.operand.value) if not name: return @@ -89,7 +118,23 @@ def extract_insn_string_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iter yield String(user_string), ih.address -def extract_features(f: FunctionHandle, bb: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]: +def extract_unmanaged_call_characteristic_features( + f: CilMethodBody, bb: CilMethodBody, insn: Instruction +) -> Iterator[Tuple[Characteristic, Address]]: + if insn.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli): + return + + token: Any = resolve_dotnet_token(f.ctx["pe"], insn.operand) + if isinstance(token, InvalidToken): + return + if not isinstance(token, dnfile.mdtable.MethodDefRow): + return + + if any((token.Flags.mdPinvokeImpl, token.ImplFlags.miUnmanaged, token.ImplFlags.miNative)): + yield Characteristic("unmanaged call"), insn.offset + + +def extract_features(f: CilMethodBody, bb: CilMethodBody, insn: Instruction) -> Iterator[Tuple[Feature, Address]]: """extract instruction features""" for inst_handler in INSTRUCTION_HANDLERS: for (feature, addr) in inst_handler(f, bb, insn): @@ -100,4 +145,5 @@ INSTRUCTION_HANDLERS = ( extract_insn_api_features, extract_insn_number_features, extract_insn_string_features, + extract_unmanaged_call_characteristic_features, ) diff --git a/capa/features/extractors/dotnetfile.py b/capa/features/extractors/dotnetfile.py index a541b965..1aadd0cc 100644 --- a/capa/features/extractors/dotnetfile.py +++ b/capa/features/extractors/dotnetfile.py @@ -1,6 +1,5 @@ import logging from typing import Tuple, Iterator -from itertools import chain import dnfile import pefile @@ -10,8 +9,28 @@ import capa.features.extractors.helpers from capa.features.file import Import from capa.features.common import OS, OS_ANY, ARCH_ANY, ARCH_I386, ARCH_AMD64, FORMAT_DOTNET, Arch, Format, Feature from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress +from capa.features.file import Import, FunctionName +from capa.features.common import ( + OS, + OS_ANY, + ARCH_ANY, + ARCH_I386, + ARCH_AMD64, + FORMAT_DOTNET, + Arch, + Format, + String, + Feature, + Characteristic, +) from capa.features.extractors.base_extractor import FeatureExtractor -from capa.features.extractors.dnfile.helpers import get_dotnet_managed_imports, get_dotnet_unmanaged_imports +from capa.features.extractors.dnfile.helpers import ( + is_dotnet_mixed_mode, + get_dotnet_managed_imports, + calculate_dotnet_token_value, + get_dotnet_unmanaged_imports, + get_dotnet_managed_method_names, +) logger = logging.getLogger(__name__) @@ -21,15 +40,20 @@ def extract_file_format(**kwargs) -> Iterator[Tuple[Format, Address]]: def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Import, Address]]: - for (token, imp) in chain(get_dotnet_managed_imports(pe), get_dotnet_unmanaged_imports(pe)): - if "::" in imp: - # like System.IO.File::OpenRead - yield Import(imp), DNTokenAddress(Token(token)) - else: - # like kernel32.CreateFileA - dll, _, symbol = imp.rpartition(".") - for symbol_variant in capa.features.extractors.helpers.generate_symbols(dll, symbol): - yield Import(symbol_variant), DNTokenAddress(Token(token)) + for (token, name) in get_dotnet_managed_imports(pe): + # like System.IO.File::OpenRead + yield Import(name), DNTokenAddress(Token(token)) + + for (token, name) in get_dotnet_unmanaged_imports(pe): + # like kernel32.CreateFileA + dll, _, symbol = name.rpartition(".") + for name_variant in capa.features.extractors.helpers.generate_symbols(dll, symbol): + yield Import(name_variant), DNTokenAddress(Token(token)) + + +def extract_file_function_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[FunctionName, Address]]: + for (token, name) in get_dotnet_managed_method_names(pe): + yield FunctionName(name), token def extract_file_os(**kwargs) -> Iterator[Tuple[OS, Address]]: @@ -47,6 +71,15 @@ def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Arch, Address yield Arch(ARCH_ANY), NO_ADDRESS +def extract_file_strings(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[String, Address]]: + yield from capa.features.extractors.common.extract_file_strings(pe.__data__) + + +def extract_mixed_mode_characteristic_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Characteristic, Address]]: + if is_dotnet_mixed_mode(pe): + yield Characteristic("mixed mode"), 0x0 + + def extract_file_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]: for file_handler in FILE_HANDLERS: for feature, addr in file_handler(pe=pe): # type: ignore @@ -55,9 +88,10 @@ def extract_file_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]: FILE_HANDLERS = ( extract_file_import_names, - # TODO extract_file_strings, - # TODO extract_file_function_names, + extract_file_function_names, + extract_file_strings, extract_file_format, + extract_mixed_mode_characteristic_features, ) @@ -98,7 +132,7 @@ class DotnetFileFeatureExtractor(FeatureExtractor): return bool(self.pe.net) def is_mixed_mode(self) -> bool: - return not bool(self.pe.net.Flags.CLR_ILONLY) + return is_dotnet_mixed_mode(self.pe) def get_runtime_version(self) -> Tuple[int, int]: return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion diff --git a/capa/rules.py b/capa/rules.py index 537014e0..939b08cf 100644 --- a/capa/rules.py +++ b/capa/rules.py @@ -101,6 +101,7 @@ SUPPORTED_FEATURES: Dict[str, Set] = { capa.features.common.Characteristic("embedded pe"), capa.features.common.String, capa.features.common.Format, + capa.features.common.Characteristic("mixed mode"), }, FUNCTION_SCOPE: { capa.features.common.MatchedRule, @@ -134,6 +135,7 @@ SUPPORTED_FEATURES: Dict[str, Set] = { capa.features.common.Characteristic("indirect call"), capa.features.common.Characteristic("call $+5"), capa.features.common.Characteristic("cross section flow"), + capa.features.common.Characteristic("unmanaged call"), }, } @@ -259,6 +261,13 @@ def parse_feature(key: str): return capa.features.insn.Number elif key == "offset": return capa.features.insn.Offset + # TODO remove x32/x64 flavor keys once fixed master/rules + elif key.startswith("number/"): + logger.warning("x32/x64 flavor currently not supported and deprecated") + return capa.features.insn.Number + elif key.startswith("offset/"): + logger.warning("x32/x64 flavor currently not supported and deprecated") + return capa.features.insn.Offset elif key == "mnemonic": return capa.features.insn.Mnemonic elif key == "basic blocks": diff --git a/rules b/rules index f8a03a30..52ff654c 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit f8a03a3014c9e7fa97cfd5b681cfe089d6083de0 +Subproject commit 52ff654ca0a73235df7d2e9bfbd52961f957cbc8 diff --git a/setup.py b/setup.py index 9dea5043..6960d9ec 100644 --- a/setup.py +++ b/setup.py @@ -18,15 +18,15 @@ requirements = [ "termcolor==1.1.0", "wcwidth==0.2.5", "ida-settings==2.1.0", - "viv-utils[flirt]==0.6.11", + "viv-utils[flirt]==0.7.4", "halo==0.0.31", "networkx==2.5.1", "ruamel.yaml==0.17.21", - "vivisect==1.0.7", + "vivisect==1.0.8", "smda==1.7.1", "pefile==2021.9.3", "pyelftools==0.28", - "dnfile==0.10.0", + "dnfile==0.11.0", "dncil==1.0.0", ] @@ -68,25 +68,28 @@ setuptools.setup( install_requires=requirements, extras_require={ "dev": [ - "pytest==7.1.1", + "pytest==7.1.2", "pytest-sugar==0.9.4", "pytest-instafail==0.4.2", "pytest-cov==3.0.0", "pycodestyle==2.8.0", "black==22.3.0", "isort==5.10.1", - "mypy==0.942", + "mypy==0.950", "psutil==5.9.0", "stix2==3.0.1", "requests==2.27.1", # type stubs for mypy "types-backports==0.1.3", - "types-colorama==0.4.10", - "types-PyYAML==6.0.5", - "types-tabulate==0.8.6", - "types-termcolor==1.1.3", - "types-psutil==5.8.20", - "types_requests==2.27.16", + "types-colorama==0.4.14", + "types-PyYAML==6.0.7", + "types-tabulate==0.8.9", + "types-termcolor==1.1.4", + "types-psutil==5.8.22", + "types_requests==2.27.25", + ], + "build": [ + "pyinstaller==5.0.1", ], }, zip_safe=False, diff --git a/tests/fixtures.py b/tests/fixtures.py index 09b8494a..acaee042 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -669,12 +669,20 @@ FEATURE_PRESENCE_TESTS_DOTNET = sorted( ("b9f5b", "file", Arch(ARCH_AMD64), False), ("mixed-mode-64", "file", Arch(ARCH_AMD64), True), ("mixed-mode-64", "file", Arch(ARCH_I386), False), + ("mixed-mode-64", "file", capa.features.common.Characteristic("mixed mode"), True), + ("hello-world", "file", capa.features.common.Characteristic("mixed mode"), False), ("b9f5b", "file", OS(OS_ANY), True), ("b9f5b", "file", Format(FORMAT_DOTNET), True), + ("hello-world", "file", capa.features.file.FunctionName("HelloWorld::Main"), True), + ("hello-world", "file", capa.features.file.FunctionName("HelloWorld::.ctor"), True), + ("hello-world", "file", capa.features.file.FunctionName("HelloWorld::.cctor"), False), + ("hello-world", "file", capa.features.common.String("Hello World!"), True), ("hello-world", "function=0x250", capa.features.common.String("Hello World!"), True), ("hello-world", "function=0x250, bb=0x250, insn=0x252", capa.features.common.String("Hello World!"), True), ("hello-world", "function=0x250", capa.features.insn.API("System.Console::WriteLine"), True), ("hello-world", "file", capa.features.file.Import("System.Console::WriteLine"), True), + ("_1c444", "file", capa.features.common.String(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), True), + ("_1c444", "file", capa.features.common.String("get_IsAlive"), True), ("_1c444", "file", capa.features.file.Import("gdi32.CreateCompatibleBitmap"), True), ("_1c444", "file", capa.features.file.Import("CreateCompatibleBitmap"), True), ("_1c444", "file", capa.features.file.Import("gdi32::CreateCompatibleBitmap"), False), @@ -683,6 +691,13 @@ FEATURE_PRESENCE_TESTS_DOTNET = sorted( ("_1c444", "function=0x1F68", capa.features.insn.Number(0xCC0020), True), ("_1c444", "function=0x1F68", capa.features.insn.Number(0x0), True), ("_1c444", "function=0x1F68", capa.features.insn.Number(0x1), False), + ( + "_1c444", + "function=0x1F59, bb=0x1F59, insn=0x1F5B", + capa.features.common.Characteristic("unmanaged call"), + True, + ), + ("_1c444", "function=0x2544", capa.features.common.Characteristic("unmanaged call"), False), ( "_1c444", "function=0x1F68, bb=0x1F68, insn=0x1FF9",