Merge branch 'master' of github.com:fireeye/capa into fix-703

This commit is contained in:
William Ballenthin
2021-08-10 13:12:25 -06:00
19 changed files with 230 additions and 46 deletions

View File

@@ -2,8 +2,66 @@
## master (unreleased)
The first Python 3 ONLY capa version.
It includes many new rules, including all new techniques introduced in MITRE ATT&CK v9.
### New Features
- explorer: allow user to add specified number of bytes when adding a Bytes feature in the Rule Generator #689 @mike-hunhoff
- explorer: enforce max column width Features and Editor panes #691 @mike-hunhoff
- explorer: add option to limit features to currently selected disassembly address #692 @mike-hunhoff
### Breaking Changes
### New Rules (20)
- collection/webcam/capture-webcam-image johnk3r
- nursery/list-drag-and-drop-files michael.hunhoff@fireeye.com
- nursery/monitor-clipboard-content michael.hunhoff@fireeye.com
- nursery/monitor-local-ipv4-address-changes michael.hunhoff@fireeye.com
- nursery/load-windows-common-language-runtime michael.hunhoff@fireeye.com
- nursery/resize-volume-shadow-copy-storage michael.hunhoff@fireeye.com
- nursery/add-user-account-group michael.hunhoff@fireeye.com
- nursery/add-user-account-to-group michael.hunhoff@fireeye.com
- nursery/add-user-account michael.hunhoff@fireeye.com
- nursery/change-user-account-password michael.hunhoff@fireeye.com
- nursery/delete-user-account-from-group michael.hunhoff@fireeye.com
- nursery/delete-user-account-group michael.hunhoff@fireeye.com
- nursery/delete-user-account michael.hunhoff@fireeye.com
- nursery/list-domain-servers michael.hunhoff@fireeye.com
- nursery/list-groups-for-user-account michael.hunhoff@fireeye.com
- nursery/list-user-account-groups michael.hunhoff@fireeye.com
- nursery/list-user-accounts-for-group michael.hunhoff@fireeye.com
- nursery/list-user-accounts michael.hunhoff@fireeye.com
- nursery/parse-url michael.hunhoff@fireeye.com
- nursery/register-raw-input-devices michael.hunhoff@fireeye.com
-
### Bug Fixes
### capa explorer IDA Pro plugin
- explorer: add additional filter logic when displaying matches by function #686 @mike-hunhoff
- explorer: remove duplicate check when saving file #687 @mike-hunhoff
- explorer: update IDA extractor to use non-canon mnemonics #688 @mike-hunhoff
### Development
### Raw diffs
- [capa v2.0.0...master](https://github.com/fireeye/capa/compare/v2.0.0...master)
- [capa-rules v2.0.0...master](https://github.com/fireeye/capa-rules/compare/v2.0.0...master)
## v2.0.0 (2021-07-19)
We are excited to announce version 2.0! :tada:
capa 2.0:
- enables anyone to contribute rules more easily
- is the first Python 3 ONLY version
- provides more concise and relevant result via identification of library functions using FLIRT
![capa v2.0 results ignoring library code functions](doc/img/changelog/flirt-ignore.png)
- includes many features and enhancements for the capa explorer IDA plugin
- adds 93 new rules, including all new techniques introduced in MITRE ATT&CK v9
A huge thanks to everyone who submitted issues, provided feedback, and contributed code and rules. Many colleagues across dozens of organizations have volunteered their experience to improve this tool! :heart:
### New Features
@@ -123,7 +181,6 @@ It includes many new rules, including all new techniques introduced in MITRE ATT
- nursery/get-token-privileges michael.hunhoff@fireeye.com
- nursery/prompt-user-for-credentials michael.hunhoff@fireeye.com
- nursery/spoof-parent-pid michael.hunhoff@fireeye.com
-
### Bug Fixes
@@ -151,9 +208,9 @@ It includes many new rules, including all new techniques introduced in MITRE ATT
### Raw diffs
<!-- The diff uses v1.6.1 because master doesn't include v1.6.2 -->
- [capa v1.6.1...master](https://github.com/fireeye/capa/compare/v1.6.1...master)
- [capa-rules v1.6.1...master](https://github.com/fireeye/capa-rules/compare/v1.6.1...master)
<!-- The diff uses v1.6.1 because master doesn't include v1.6.2 and v1.6.3 -->
- [capa v1.6.1...v2.0.0](https://github.com/fireeye/capa/compare/v1.6.1...v2.0.0)
- [capa-rules v1.6.1...v2.0.0](https://github.com/fireeye/capa-rules/compare/v1.6.1...v2.0.0)
## v1.6.3 (2021-04-29)

View File

@@ -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/fireeye/capa)](https://github.com/fireeye/capa/releases)
[![Number of rules](https://img.shields.io/badge/rules-579-blue.svg)](https://github.com/fireeye/capa-rules)
[![Number of rules](https://img.shields.io/badge/rules-599-blue.svg)](https://github.com/fireeye/capa-rules)
[![CI status](https://github.com/fireeye/capa/workflows/CI/badge.svg)](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
[![Downloads](https://img.shields.io/github/downloads/fireeye/capa/total)](https://github.com/fireeye/capa/releases)
[![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.txt)
@@ -11,7 +11,9 @@ capa detects capabilities in executable files.
You run it against a PE file or shellcode and it tells you what it thinks the program can do.
For example, it might suggest that the file is a backdoor, is capable of installing services, or relies on HTTP to communicate.
Check out the overview in our first [capa blog post](https://www.fireeye.com/blog/threat-research/2020/07/capa-automatically-identify-malware-capabilities.html).
Check out:
- the overview in our first [capa blog post](https://www.fireeye.com/blog/threat-research/2020/07/capa-automatically-identify-malware-capabilities.html)
- the major version 2.0 updates described in our [second blog post](https://www.fireeye.com/blog/threat-research/2021/07/capa-2-better-stronger-faster.html)
```
$ capa.exe suspicious.exe

View File

@@ -337,7 +337,7 @@ def extract_insn_mnemonic_features(f, bb, insn):
bb (IDA BasicBlock)
insn (IDA insn_t)
"""
yield Mnemonic(insn.get_canon_mnem()), insn.ea
yield Mnemonic(idc.print_insn_mnem(insn.ea)), insn.ea
def extract_insn_peb_access_characteristic_features(f, bb, insn):

View File

@@ -32,6 +32,8 @@ def extract_file_export_names(pe, file_path):
if hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
for export in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if not export.name:
continue
try:
name = export.name.partition(b"\x00")[0].decode("ascii")
except UnicodeDecodeError:

View File

@@ -54,8 +54,6 @@ class Options(IntFlag):
def write_file(path, data):
""" """
if os.path.exists(path) and 1 != idaapi.ask_yn(1, "The file already exists. Overwrite?"):
return
with open(path, "wb") as save_file:
save_file.write(data)
@@ -277,6 +275,7 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_rulegen_editor = None
self.view_rulegen_header_label = None
self.view_rulegen_search = None
self.view_rulegen_limit_features_by_ea = None
self.rulegen_current_function = None
self.rulegen_bb_features_cache = {}
self.rulegen_func_features_cache = {}
@@ -467,6 +466,10 @@ class CapaExplorerForm(idaapi.PluginForm):
label2.setText("Editor")
label2.setFont(font)
self.view_rulegen_limit_features_by_ea = QtWidgets.QCheckBox("Limit features to current dissasembly address")
self.view_rulegen_limit_features_by_ea.setChecked(False)
self.view_rulegen_limit_features_by_ea.stateChanged.connect(self.slot_checkbox_limit_features_by_ea)
self.view_rulegen_status_label = QtWidgets.QLabel()
self.view_rulegen_status_label.setAlignment(QtCore.Qt.AlignLeft)
self.view_rulegen_status_label.setText("")
@@ -497,6 +500,7 @@ class CapaExplorerForm(idaapi.PluginForm):
layout3.addWidget(self.view_rulegen_editor, 65)
layout2.addWidget(self.view_rulegen_header_label)
layout2.addWidget(self.view_rulegen_limit_features_by_ea)
layout2.addWidget(self.view_rulegen_search)
layout2.addWidget(self.view_rulegen_features)
@@ -561,6 +565,10 @@ class CapaExplorerForm(idaapi.PluginForm):
self.limit_results_to_function(idaapi.get_func(ea))
self.view_tree.reset_ui()
def update_rulegen_tree_limit_features_to_selection(self, ea):
""" """
self.view_rulegen_features.filter_items_by_ea(ea)
def ida_hook_screen_ea_changed(self, widget, new_ea, old_ea):
"""function hook for IDA "screen ea changed" action
@@ -581,6 +589,9 @@ class CapaExplorerForm(idaapi.PluginForm):
if not idaapi.get_func(new_ea):
return
if self.view_tabs.currentIndex() == 1 and self.view_rulegen_limit_features_by_ea.isChecked():
return self.update_rulegen_tree_limit_features_to_selection(new_ea)
if idaapi.get_func(new_ea) == idaapi.get_func(old_ea):
# user navigated same function - ignore
return
@@ -982,6 +993,7 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_rulegen_editor.reset_view()
self.view_rulegen_preview.reset_view()
self.view_rulegen_search.clear()
self.view_rulegen_limit_features_by_ea.setChecked(False)
self.set_rulegen_preview_border_neutral()
self.rulegen_current_function = None
self.rulegen_func_features_cache = {}
@@ -1140,7 +1152,7 @@ class CapaExplorerForm(idaapi.PluginForm):
s = json.dumps(self.doc, sort_keys=True, cls=capa.render.json.CapaJsonObjectEncoder).encode("utf-8")
path = idaapi.ask_file(True, "*.json", "Choose file to save capa program analysis JSON")
path = self.ask_user_capa_json_file()
if not path:
return
@@ -1173,6 +1185,13 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_tree.reset_ui()
def slot_checkbox_limit_features_by_ea(self, state):
""" """
if state == QtCore.Qt.Checked:
self.view_rulegen_features.filter_items_by_ea(idaapi.get_screen_ea())
else:
self.view_rulegen_features.show_all_items()
def slot_checkbox_show_results_by_function_changed(self, state):
"""slot activated if checkbox clicked
@@ -1216,7 +1235,16 @@ class CapaExplorerForm(idaapi.PluginForm):
def ask_user_capa_rule_file(self):
""" """
return QtWidgets.QFileDialog.getSaveFileName(
None, "Please select a capa rule to edit", settings.user.get(CAPA_SETTINGS_RULE_PATH, ""), "*.yml"
None,
"Please select a location to save capa rule file",
settings.user.get(CAPA_SETTINGS_RULE_PATH, ""),
"*.yml",
)[0]
def ask_user_capa_json_file(self):
""" """
return QtWidgets.QFileDialog.getSaveFileName(
None, "Please select a location to save capa JSON file", "", "*.json"
)[0]
def set_view_status_label(self, text):

View File

@@ -435,12 +435,18 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
for ea in rule["matches"].keys():
ea = capa.ida.helpers.get_func_start_ea(ea)
if ea is None:
# file scope, skip for rendering in this mode
# file scope, skip rendering in this mode
continue
if None is matches_by_function.get(ea, None):
matches_by_function[ea] = CapaExplorerFunctionItem(self.root_node, ea, can_check=False)
if not matches_by_function.get(ea, ()):
# new function root
matches_by_function[ea] = (CapaExplorerFunctionItem(self.root_node, ea, can_check=False), [])
function_root, match_cache = matches_by_function[ea]
if rule["meta"]["name"] in match_cache:
# rule match already rendered for this function root, skip it
continue
match_cache.append(rule["meta"]["name"])
CapaExplorerRuleItem(
matches_by_function[ea],
function_root,
rule["meta"]["name"],
rule["meta"].get("namespace"),
len(rule["matches"]),

View File

@@ -9,6 +9,7 @@ import re
from collections import Counter
import idc
import idaapi
from PyQt5 import QtGui, QtCore, QtWidgets
import capa.rules
@@ -178,6 +179,13 @@ def build_context_menu(o, actions):
return menu
def resize_columns_to_content(header):
""" """
header.resizeSections(QtWidgets.QHeaderView.ResizeToContents)
if header.sectionSize(0) > MAX_SECTION_SIZE:
header.resizeSection(0, MAX_SECTION_SIZE)
class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
INDENT = " " * 2
@@ -319,7 +327,6 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
self.preview = preview
self.setHeaderLabels(["Feature", "Description", "Comment"])
self.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
self.header().setStretchLastSection(False)
self.setExpandsOnDoubleClick(False)
self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
@@ -327,6 +334,10 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}")
# configure view columns to auto-resize
for idx in range(3):
self.header().setSectionResizeMode(idx, QtWidgets.QHeaderView.Interactive)
# enable drag and drop
self.setDragEnabled(True)
self.setAcceptDrops(True)
@@ -336,6 +347,8 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
self.itemChanged.connect(self.slot_item_changed)
self.customContextMenuRequested.connect(self.slot_custom_context_menu_requested)
self.itemDoubleClicked.connect(self.slot_item_double_clicked)
self.expanded.connect(self.slot_resize_columns_to_content)
self.collapsed.connect(self.slot_resize_columns_to_content)
self.root = None
self.reset_view()
@@ -396,6 +409,10 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
self.root = None
self.clear()
def slot_resize_columns_to_content(self):
""" """
resize_columns_to_content(self.header())
def slot_item_changed(self, item, column):
""" """
if self.is_editing:
@@ -645,6 +662,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
self.expandAll()
self.update_preview()
resize_columns_to_content(self.header())
def load_features_from_yaml(self, rule_text, update_preview=False):
""" """
@@ -736,9 +754,12 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
self.editor = editor
self.setHeaderLabels(["Feature", "Virtual Address"])
self.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}")
# configure view columns to auto-resize
for idx in range(2):
self.header().setSectionResizeMode(idx, QtWidgets.QHeaderView.Interactive)
self.setExpandsOnDoubleClick(False)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
@@ -746,6 +767,8 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
# connect slots
self.itemDoubleClicked.connect(self.slot_item_double_clicked)
self.customContextMenuRequested.connect(self.slot_custom_context_menu_requested)
self.expanded.connect(self.slot_resize_columns_to_content)
self.collapsed.connect(self.slot_resize_columns_to_content)
self.reset_view()
@@ -773,12 +796,24 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
""" """
self.clear()
def slot_resize_columns_to_content(self):
""" """
resize_columns_to_content(self.header())
def slot_add_selected_features(self, action):
""" """
selected = [item.data(0, 0x100) for item in self.selectedItems()]
if selected:
self.editor.update_features(selected)
def slot_add_n_bytes_feature(self, action):
""" """
count = idaapi.ask_long(16, f"Enter number of bytes (1-{capa.features.common.MAX_BYTES_FEATURE_SIZE}):")
if count and 1 <= count <= capa.features.common.MAX_BYTES_FEATURE_SIZE:
item = self.selectedItems()[0].data(0, 0x100)
item.value = item.value[:count]
self.editor.update_features([item])
def slot_custom_context_menu_requested(self, pos):
""" """
actions = []
@@ -790,6 +825,8 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
if selected_items_count == 1:
action_add_features_fmt = "Add feature"
if isinstance(self.selectedItems()[0].data(0, 0x100), capa.features.common.Bytes):
actions.append(("Add n bytes...", (), self.slot_add_n_bytes_feature))
else:
action_add_features_fmt = "Add %d features" % selected_items_count
@@ -826,6 +863,44 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
else:
self.show_all_items()
def filter_items_by_ea(self, min_ea, max_ea=None):
""" """
visited = []
def show_item_and_parents(_o):
"""iteratively show and expand an item and its' parents"""
while _o:
visited.append(_o)
_o.setHidden(False)
_o.setExpanded(True)
_o = _o.parent()
for o in iterate_tree(self):
if o in visited:
# save some cycles, only visit item once
continue
# read ea from "Address" column
o_ea = o.text(CapaExplorerRulegenFeatures.get_column_address_index())
if o_ea == "":
# ea may be empty, hide by default
o.setHidden(True)
continue
o_ea = int(o_ea, 16)
if max_ea is not None and min_ea <= o_ea <= max_ea:
show_item_and_parents(o)
elif o_ea == min_ea:
show_item_and_parents(o)
else:
# made it here, hide by default
o.setHidden(True)
# resize the view for UX
resize_columns_to_content(self.header())
def style_parent_node(self, o):
""" """
font = QtGui.QFont()
@@ -887,6 +962,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
self.parse_features_for_tree(self.new_parent_node(self, ("File Scope",)), file_features)
if func_features:
self.parse_features_for_tree(self.new_parent_node(self, ("Function/Basic Block Scope",)), func_features)
resize_columns_to_content(self.header())
def parse_features_for_tree(self, parent, features):
""" """
@@ -1000,11 +1076,7 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
def slot_resize_columns_to_content(self):
"""reset view columns to contents"""
if self.should_resize_columns:
self.header().resizeSections(QtWidgets.QHeaderView.ResizeToContents)
# limit size of first section
if self.header().sectionSize(0) > MAX_SECTION_SIZE:
self.header().resizeSection(0, MAX_SECTION_SIZE)
resize_columns_to_content(self.header())
def map_index_to_source_item(self, model_index):
"""map proxy model index to source model item

View File

@@ -330,7 +330,11 @@ def register_flirt_signature_analyzers(vw, sigpaths):
import viv_utils.flirt
for sigpath in sigpaths:
sigs = load_flirt_signature(sigpath)
try:
sigs = load_flirt_signature(sigpath)
except ValueError as e:
logger.warning("could not load %s: %s", sigpath, str(e))
continue
logger.debug("flirt: sig count: %d", len(sigs))
@@ -550,14 +554,23 @@ def get_signatures(sigs_path):
if os.path.isfile(sigs_path):
paths.append(sigs_path)
elif os.path.isdir(sigs_path):
logger.debug("reading signatures from directory %s", sigs_path)
logger.debug("reading signatures from directory %s", os.path.abspath(os.path.normpath(sigs_path)))
for root, dirs, files in os.walk(sigs_path):
for file in files:
if file.endswith((".pat", ".pat.gz", ".sig")):
sig_path = os.path.join(root, file)
logger.debug("found signature: %s", sig_path)
paths.append(sig_path)
# nicely normalize and format path so that debugging messages are clearer
paths = [os.path.abspath(os.path.normpath(path)) for path in paths]
# load signatures in deterministic order: the alphabetic sorting of filename.
# this means that `0_sigs.pat` loads before `1_sigs.pat`.
paths = sorted(paths, key=os.path.basename)
for path in paths:
logger.debug("found signature file: %s", path)
return paths
@@ -691,11 +704,11 @@ def install_common_args(parser, wanted=None):
if "signatures" in wanted:
parser.add_argument(
"--signature",
dest="signatures",
"-s",
"--signatures",
type=str,
default=SIGNATURES_PATH_DEFAULT_STRING,
help="use the given signatures to identify library functions, file system paths to .sig/.pat files.",
help="path to .sig/.pat file or directory used to identify library functions, use embedded signatures by default",
)
if "tag" in wanted:

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -27,6 +27,10 @@ To install capa as a Python library use `pip` to fetch the `flare-capa` module.
#### *Note*:
This method is appropriate for integrating capa in an existing project.
This technique doesn't pull the default rule set, so you should check it out separately from [capa-rules](https://github.com/fireeye/capa-rules/) and pass the directory to the entrypoint using `-r` or set the rules path in the IDA Pro plugin.
This technique also doesn't set up the default library identification [signatures](https://github.com/fireeye/capa/tree/master/sigs). You can pass the signature directory using the `-s` argument.
For example, to run capa with both a rule path and a signature path:
capa -r /path/to/capa-rules -s /path/to/capa-sigs suspicious.exe
Alternatively, see Method 3 below.
### 1. Install capa module

2
rules

Submodule rules updated: a207fbba0a...77d77601b1

View File

@@ -11,7 +11,7 @@ import os
import setuptools
requirements = [
"tqdm==4.61.1",
"tqdm==4.62.0",
"pyyaml==5.4.1",
"tabulate==0.8.9",
"colorama==0.4.4",
@@ -23,7 +23,7 @@ requirements = [
"networkx==2.5.1",
"ruamel.yaml==0.17.10",
"vivisect==1.0.3",
"smda==1.5.18",
"smda==1.5.19",
"pefile==2021.5.24",
"typing==3.7.4.3",
]
@@ -71,15 +71,15 @@ setuptools.setup(
"pytest-instafail==0.4.2",
"pytest-cov==2.12.1",
"pycodestyle==2.7.0",
"black==21.6b0",
"isort==5.9.1",
"black==21.7b0",
"isort==5.9.3",
"mypy==0.910",
# type stubs for mypy
"types-backports==0.1.3",
"types-colorama==0.4.2",
"types-PyYAML==5.4.3",
"types-tabulate==0.1.1",
"types-termcolor==0.1.1",
"types-colorama==0.4.3",
"types-PyYAML==5.4.6",
"types-tabulate==0.8.2",
"types-termcolor==1.1.1",
],
},
zip_safe=False,

View File

@@ -4,6 +4,6 @@ This directory contains FLIRT signatures that capa uses to identify library func
Typically, capa will ignore library functions, which reduces false positives and improves runtime.
These FLIRT signatures were generated by FireEye using the Hex-Rays FLAIR tools such as `pcf` and `sigmake`.
FireEye generated the signatures from source data that they collected; these signatures are not derived from the FLIRT signatures distributed with IDA PRo.
FireEye generated the signatures from source data that they collected; these signatures are not derived from the FLIRT signatures distributed with IDA Pro.
The signatures in this directory have the same license as capa: Apache 2.0.
The signatures in this directory have the same license as capa: Apache 2.0.

View File

@@ -73,9 +73,9 @@ def get_viv_extractor(path):
sigpaths = [
os.path.join(CD, "data", "sigs", "test_aulldiv.pat"),
os.path.join(CD, "data", "sigs", "test_aullrem.pat.gz"),
os.path.join(CD, "..", "sigs", "flare_common_libs.sig"),
os.path.join(CD, "..", "sigs", "flare_msvc_atlmfc_32_64.sig"),
os.path.join(CD, "..", "sigs", "flare_msvc_rtf_32_64.sig"),
os.path.join(CD, "..", "sigs", "1_flare_msvc_rtf_32_64.sig"),
os.path.join(CD, "..", "sigs", "2_flare_msvc_atlmfc_32_64.sig"),
os.path.join(CD, "..", "sigs", "3_flare_common_libs.sig"),
]
if "raw32" in path: