Merge remote-tracking branch 'parentrepo/dynamic-feature-extraction' into analysis-flavor

This commit is contained in:
Yacine Elhamer
2023-07-11 10:43:31 +01:00
94 changed files with 873 additions and 934 deletions
+5 -3
View File
@@ -169,15 +169,17 @@ def main(argv=None):
return -1
samples = []
for base, directories, files in os.walk(args.input):
for base, _, files in os.walk(args.input):
for file in files:
samples.append(os.path.join(base, file))
def pmap(f, args, parallelism=multiprocessing.cpu_count()):
cpu_count = multiprocessing.cpu_count()
def pmap(f, args, parallelism=cpu_count):
"""apply the given function f to the given args using subprocesses"""
return multiprocessing.Pool(parallelism).imap(f, args)
def tmap(f, args, parallelism=multiprocessing.cpu_count()):
def tmap(f, args, parallelism=cpu_count):
"""apply the given function f to the given args using threads"""
return multiprocessing.pool.ThreadPool(parallelism).imap(f, args)
+1 -2
View File
@@ -1,6 +1,6 @@
"""
Create a cache of the given rules.
This is only really intended to be used by CI to pre-cache rulesets
This is only really intended to be used by CI to pre-cache rulesets
that will be distributed within PyInstaller binaries.
Usage:
@@ -17,7 +17,6 @@ See the License for the specific language governing permissions and limitations
"""
import os
import sys
import time
import logging
import argparse
+77 -74
View File
@@ -54,7 +54,7 @@ var_names = ["".join(letters) for letters in itertools.product(string.ascii_lowe
# this have to be the internal names used by capa.py which are sometimes different to the ones written out in the rules, e.g. "2 or more" is "Some", count is Range
unsupported = ["characteristic", "mnemonic", "offset", "subscope", "Range"]
# TODO shorten this list, possible stuff:
# further idea: shorten this list, possible stuff:
# - 2 or more strings: e.g.
# -- https://github.com/mandiant/capa-rules/blob/master/collection/file-managers/gather-direct-ftp-information.yml
# -- https://github.com/mandiant/capa-rules/blob/master/collection/browser/gather-firefox-profile-information.yml
@@ -94,7 +94,7 @@ private rule capa_pe_file : CAPA {
def check_feature(statement, rulename):
if statement in unsupported:
logger.info("unsupported: " + statement + " in rule: " + rulename)
logger.info("unsupported: %s in rule: %s", statement, rulename)
return True
else:
return False
@@ -112,7 +112,7 @@ def convert_capa_number_to_yara_bytes(number):
sys.exit()
number = re.sub(r"^0[xX]", "", number)
logger.info("number ok: " + repr(number))
logger.info("number ok: %r", number)
# include spaces every 2 hex
bytesv = re.sub(r"(..)", r"\1 ", number)
@@ -142,9 +142,9 @@ def convert_description(statement):
desc = statement.description
if desc:
yara_desc = " // " + desc
logger.info("using desc: " + repr(yara_desc))
logger.info("using desc: %r", yara_desc)
return yara_desc
except:
except Exception:
# no description
pass
@@ -153,7 +153,7 @@ def convert_description(statement):
def convert_rule(rule, rulename, cround, depth):
depth += 1
logger.info("recursion depth: " + str(depth))
logger.info("recursion depth: %d", depth)
global var_names
@@ -164,7 +164,7 @@ def convert_rule(rule, rulename, cround, depth):
return "BREAK", s_type
elif s_type == "string":
string = kid.value
logger.info("doing string: " + repr(string))
logger.info("doing string: %r", string)
string = string.replace("\\", "\\\\")
string = string.replace("\n", "\\n")
string = string.replace("\t", "\\t")
@@ -172,14 +172,16 @@ def convert_rule(rule, rulename, cround, depth):
yara_strings += "\t$" + var_name + ' = "' + string + '" ascii wide' + convert_description(kid) + "\n"
yara_condition += "\t$" + var_name + " "
elif s_type == "api" or s_type == "import":
# TODO: is it possible in YARA to make a difference between api & import?
# research needed to decide if its possible in YARA to make a difference between api & import?
# https://github.com/mandiant/capa-rules/blob/master/doc/format.md#api
api = kid.value
logger.info("doing api: " + repr(api))
logger.info("doing api: %r", api)
# e.g. kernel32.CreateNamedPipe => look for kernel32.dll and CreateNamedPipe
# TODO: improve .NET API call handling
#
# note: the handling of .NET API calls could be improved here.
# once we have a motivation and some examples, lets do that.
if "::" in api:
mod, api = api.split("::")
@@ -204,20 +206,20 @@ def convert_rule(rule, rulename, cround, depth):
var_name = "api_" + var_names.pop(0)
# limit regex with word boundary \b but also search for appended A and W
# TODO: better use something like /(\\x00|\\x01|\\x02|\\x03|\\x04)' + api + '(A|W)?\\x00/ ???
# alternatively: use something like /(\\x00|\\x01|\\x02|\\x03|\\x04)' + api + '(A|W)?\\x00/ ???
yara_strings += "\t$" + var_name + " = /\\b" + api + "(A|W)?\\b/ ascii wide\n"
yara_condition += "\t$" + var_name + " "
elif s_type == "export":
export = kid.value
logger.info("doing export: " + repr(export))
logger.info("doing export: %r", export)
yara_condition += '\tpe.exports("' + export + '") '
elif s_type == "section":
# https://github.com/mandiant/capa-rules/blob/master/doc/format.md#section
section = kid.value
logger.info("doing section: " + repr(section))
logger.info("doing section: %r", section)
# e.g. - section: .rsrc
var_name_sec = var_names.pop(0)
@@ -229,14 +231,14 @@ def convert_rule(rule, rulename, cround, depth):
elif s_type == "match":
# https://github.com/mandiant/capa-rules/blob/master/doc/format.md#matching-prior-rule-matches-and-namespaces
match = kid.value
logger.info("doing match: " + repr(match))
logger.info("doing match: %r", match)
# e.g. - match: create process
# - match: host-interaction/file-system/write
match_rule_name = convert_rule_name(match)
if match.startswith(rulename + "/"):
logger.info("Depending on myself = basic block: " + match)
logger.info("Depending on myself = basic block: %s", match)
return "BREAK", "Depending on myself = basic block"
if match_rule_name in converted_rules:
@@ -244,14 +246,14 @@ def convert_rule(rule, rulename, cround, depth):
else:
# don't complain in the early rounds as there should be 3+ rounds (if all rules are converted)
if cround > min_rounds - 2:
logger.info("needed sub-rule not converted (yet, maybe in next round): " + repr(match))
logger.info("needed sub-rule not converted (yet, maybe in next round): %r", match)
return "BREAK", "needed sub-rule not converted"
else:
return "BREAK", "NOLOG"
elif s_type == "bytes":
bytesv = kid.get_value_str()
logger.info("doing bytes: " + repr(bytesv))
logger.info("doing bytes: %r", bytesv)
var_name = var_names.pop(0)
yara_strings += "\t$" + var_name + " = { " + bytesv + " }" + convert_description(kid) + "\n"
@@ -259,19 +261,19 @@ def convert_rule(rule, rulename, cround, depth):
elif s_type == "number":
number = kid.get_value_str()
logger.info("doing number: " + repr(number))
logger.info("doing number: %r", number)
if len(number) < 10:
logger.info("too short for byte search (until I figure out how to do it properly)" + repr(number))
logger.info("too short for byte search (until I figure out how to do it properly): %r", number)
return "BREAK", "Number too short"
# there's just one rule which contains 0xFFFFFFF but yara gives a warning if if used
if number == "0xFFFFFFFF":
return "BREAK", "slow byte pattern for YARA search"
logger.info("number ok: " + repr(number))
logger.info("number ok: %r", number)
number = convert_capa_number_to_yara_bytes(number)
logger.info("number ok: " + repr(number))
logger.info("number ok: %r", number)
var_name = "num_" + var_names.pop(0)
yara_strings += "\t$" + var_name + " = { " + number + "}" + convert_description(kid) + "\n"
@@ -279,7 +281,7 @@ def convert_rule(rule, rulename, cround, depth):
elif s_type == "regex":
regex = kid.get_value_str()
logger.info("doing regex: " + repr(regex))
logger.info("doing regex: %r", regex)
# change capas /xxx/i to yaras /xxx/ nocase, count will be used later to decide appending 'nocase'
regex, count = re.subn(r"/i$", "/", regex)
@@ -315,7 +317,7 @@ def convert_rule(rule, rulename, cround, depth):
elif s_type == "Not" or s_type == "And" or s_type == "Or":
pass
else:
logger.info("something unhandled: " + repr(s_type))
logger.info("something unhandled: %r", s_type)
sys.exit()
return yara_strings, yara_condition
@@ -329,7 +331,7 @@ def convert_rule(rule, rulename, cround, depth):
statement = rule.name
logger.info("doing statement: " + statement)
logger.info("doing statement: %s", statement)
if check_feature(statement, rulename):
return "BREAK", statement, rule_comment, incomplete
@@ -337,18 +339,18 @@ def convert_rule(rule, rulename, cround, depth):
if statement == "And" or statement == "Or":
desc = convert_description(rule)
if desc:
logger.info("description of bool statement: " + repr(desc))
logger.info("description of bool statement: %r", desc)
yara_strings_list.append("\t" * depth + desc + "\n")
elif statement == "Not":
logger.info("one of those seldom nots: " + rule.name)
logger.info("one of those seldom nots: %s", rule.name)
# check for nested statements
try:
kids = rule.children
num_kids = len(kids)
logger.info("kids: " + kids)
except:
logger.info("no kids in rule: " + rule.name)
logger.info("kids: %s", kids)
except Exception:
logger.info("no kids in rule: %s", rule.name)
try:
# maybe it's "Not" = only one child:
@@ -356,31 +358,31 @@ def convert_rule(rule, rulename, cround, depth):
kids = [kid]
num_kids = 1
logger.info("kid: %s", kids)
except:
except Exception:
logger.info("no kid in rule: %s", rule.name)
# just a single statement without 'and' or 'or' before it in this rule
if "kids" not in locals().keys():
logger.info("no kids: " + rule.name)
logger.info("no kids: %s", rule.name)
yara_strings_sub, yara_condition_sub = do_statement(statement, rule)
if yara_strings_sub == "BREAK":
logger.info("Unknown feature at1: " + rule.name)
logger.info("Unknown feature at1: %s", rule.name)
return "BREAK", yara_condition_sub, rule_comment, incomplete
yara_strings_list.append(yara_strings_sub)
yara_condition_list.append(yara_condition_sub)
else:
x = 0
logger.info("doing kids: %r - len: %s", kids, num_kids)
logger.info("doing kids: %r - len: %d", kids, num_kids)
for kid in kids:
s_type = kid.name
logger.info("doing type: " + s_type + " kidnum: " + str(x))
logger.info("doing type: %s kidnum: %d", s_type, x)
if s_type == "Some":
cmin = kid.count
logger.info("Some type with minimum: " + str(cmin))
logger.info("Some type with minimum: %d", cmin)
if not cmin:
logger.info("this is optional: which means, we can just ignore it")
@@ -395,8 +397,8 @@ def convert_rule(rule, rulename, cround, depth):
return "BREAK", "Some aka x or more (TODO)", rule_comment, incomplete
if s_type == "And" or s_type == "Or" or s_type == "Not" and not kid.name == "Some":
logger.info("doing bool with recursion: " + repr(kid))
logger.info("kid coming: " + repr(kid.name))
logger.info("doing bool with recursion: %r", kid)
logger.info("kid coming: %r", kid.name)
# logger.info("grandchildren: " + repr(kid.children))
#
@@ -406,22 +408,24 @@ def convert_rule(rule, rulename, cround, depth):
kid, rulename, cround, depth
)
logger.info("coming out of this recursion, depth: " + repr(depth) + " s_type: " + s_type)
logger.info("coming out of this recursion, depth: %d s_type: %s", depth, s_type)
if yara_strings_sub == "BREAK":
logger.info(
"Unknown feature at2: " + rule.name + " - s_type: " + s_type + " - depth: " + str(depth)
"Unknown feature at2: %s - s_type: %s - depth: %d",
rule.name,
s_type,
depth,
)
# luckily this is only a killer, if we're inside an 'And', inside 'Or' we're just missing some coverage
# only accept incomplete rules in rounds > 3 because the reason might be a reference to another rule not converted yet because of missing dependencies
logger.info("rule.name, depth, cround: " + rule.name + ", " + str(depth) + ", " + str(cround))
logger.info("rule.name, depth, cround: %s, %d, %d", rule.name, depth, cround)
if rule.name == "Or" and depth == 1 and cround > min_rounds - 1:
logger.info(
"Unknown feature, just ignore this branch and keep the rest bec we're in Or (1): "
+ s_type
+ " - depth: "
+ str(depth)
"Unknown feature, just ignore this branch and keep the rest bec we're in Or (1): %s - depth: %s",
s_type,
depth,
)
# remove last 'or'
# yara_condition = re.sub(r'\sor $', ' ', yara_condition)
@@ -442,14 +446,13 @@ def convert_rule(rule, rulename, cround, depth):
yara_strings_sub, yara_condition_sub = do_statement(s_type, kid)
if yara_strings_sub == "BREAK":
logger.info("Unknown feature at3: " + rule.name)
logger.info("rule.name, depth, cround: " + rule.name + ", " + str(depth) + ", " + str(cround))
logger.info("Unknown feature at3: %s", rule.name)
logger.info("rule.name, depth, cround: %s, %d, %d", rule.name, depth, cround)
if rule.name == "Or" and depth == 1 and cround > min_rounds - 1:
logger.info(
"Unknown feature, just ignore this branch and keep the rest bec we're in Or (2): "
+ s_type
+ " - depth: "
+ str(depth)
"Unknown feature, just ignore this branch and keep the rest bec we're in Or (2): %s - depth: %d",
s_type,
depth,
)
rule_comment += "This rule is incomplete because a branch inside an Or-statement had an unsupported feature and was skipped"
@@ -487,7 +490,7 @@ def convert_rule(rule, rulename, cround, depth):
elif statement == "Some":
cmin = rule.count
logger.info("Some type with minimum at2: " + str(cmin))
logger.info("Some type with minimum at2: %d", cmin)
if not cmin:
logger.info("this is optional: which means, we can just ignore it")
@@ -500,7 +503,7 @@ def convert_rule(rule, rulename, cround, depth):
yara_condition = "not " + "".join(yara_condition_list) + " "
else:
if len(yara_condition_list) != 1:
logger.info("something wrong around here" + repr(yara_condition_list) + " - " + statement)
logger.info("something wrong around here %r - %s", yara_condition_list, statement)
sys.exit()
# strings might be empty with only conditions
@@ -509,8 +512,10 @@ def convert_rule(rule, rulename, cround, depth):
yara_condition = "\n\t" + yara_condition_list[0]
logger.info(f"# end of convert_rule() #strings: {len(yara_strings_list)} #conditions: {len(yara_condition_list)}")
logger.info(f"strings: {yara_strings} conditions: {yara_condition}")
logger.info(
"# end of convert_rule() #strings: %d #conditions: %d", len(yara_strings_list), len(yara_condition_list)
)
logger.info("strings: %s conditions: %s", yara_strings, yara_condition)
return yara_strings, yara_condition, rule_comment, incomplete
@@ -522,7 +527,7 @@ def output_yar(yara):
def output_unsupported_capa_rules(yaml, capa_rulename, url, reason):
if reason != "NOLOG":
if capa_rulename not in unsupported_capa_rules_list:
logger.info("unsupported: " + capa_rulename + " - reason: " + reason + " - url: " + url)
logger.info("unsupported: %s - reason: %s, - url: %s", capa_rulename, reason, url)
unsupported_capa_rules_list.append(capa_rulename)
unsupported_capa_rules.write(yaml.encode("utf-8") + b"\n")
@@ -546,32 +551,32 @@ def convert_rules(rules, namespaces, cround, make_priv):
rule_name = convert_rule_name(rule.name)
if rule.is_subscope_rule():
logger.info("skipping sub scope rule capa: " + rule.name)
logger.info("skipping sub scope rule capa: %s", rule.name)
continue
if rule_name in converted_rules:
logger.info("skipping already converted rule capa: " + rule.name + " - yara rule: " + rule_name)
logger.info("skipping already converted rule capa: %s - yara rule: %s", rule.name, rule_name)
continue
logger.info("-------------------------- DOING RULE CAPA: " + rule.name + " - yara rule: " + rule_name)
logger.info("-------------------------- DOING RULE CAPA: %s - yara rule: ", rule.name, rule_name)
if "capa/path" in rule.meta:
url = get_rule_url(rule.meta["capa/path"])
else:
url = "no url"
logger.info("URL: " + url)
logger.info("statements: " + repr(rule.statement))
logger.info("URL: %s", url)
logger.info("statements: %r", rule.statement)
# don't really know what that passed empty string is good for :)
dependencies = rule.get_dependencies(namespaces)
if len(dependencies):
logger.info("Dependencies at4: " + rule.name + " - dep: " + str(dependencies))
logger.info("Dependencies at4: %s - dep: %s", rule.name, dependencies)
for dep in dependencies:
logger.info("Dependencies at44: " + dep)
logger.info("Dependencies at44: %s", dep)
if not dep.startswith(rule.name + "/"):
logger.info("Depending on another rule: " + dep)
logger.info("Depending on another rule: %s", dep)
continue
yara_strings, yara_condition, rule_comment, incomplete = convert_rule(rule.statement, rule.name, cround, 0)
@@ -580,7 +585,7 @@ def convert_rules(rules, namespaces, cround, make_priv):
# only give up if in final extra round #9000
if cround == 9000:
output_unsupported_capa_rules(rule.to_yaml(), rule.name, url, yara_condition)
logger.info("Unknown feature at5: " + rule.name)
logger.info("Unknown feature at5: %s", rule.name)
else:
yara_meta = ""
metas = rule.meta
@@ -596,24 +601,24 @@ def convert_rules(rules, namespaces, cround, make_priv):
if meta_name == "att&ck":
meta_name = "attack"
for attack in list(metas[meta]):
logger.info("attack:" + attack)
logger.info("attack: %s", attack)
# cut out tag in square brackets, e.g. Defense Evasion::Obfuscated Files or Information [T1027] => T1027
r = re.search(r"\[(T[^\]]*)", attack)
if r:
tag = r.group(1)
logger.info("attack tag:" + tag)
logger.info("attack tag: %s", tag)
tag = re.sub(r"\W", "_", tag)
rule_tags += tag + " "
# also add a line "attack = ..." to yaras 'meta:' to keep the long description:
yara_meta += '\tattack = "' + attack + '"\n'
elif meta_name == "mbc":
for mbc in list(metas[meta]):
logger.info("mbc:" + mbc)
logger.info("mbc: %s", mbc)
# cut out tag in square brackets, e.g. Cryptography::Encrypt Data::RC6 [C0027.010] => C0027.010
r = re.search(r"\[(.[^\]]*)", mbc)
if r:
tag = r.group(1)
logger.info("mbc tag:" + tag)
logger.info("mbc tag: %s", tag)
tag = re.sub(r"\W", "_", tag)
rule_tags += tag + " "
@@ -676,8 +681,6 @@ def convert_rules(rules, namespaces, cround, make_priv):
yara += " condition:" + condition_header + yara_condition + "\n}"
# TODO: now the rule is finished and could be automatically checked with the capa-testfile(s) named in meta
# (doing it for all of them using yara-ci upload at the moment)
output_yar(yara)
converted_rules.append(rule_name)
count_incomplete += incomplete
@@ -713,10 +716,10 @@ def main(argv=None):
try:
rules = capa.main.get_rules([args.rules])
namespaces = capa.rules.index_rules_by_namespace(list(rules.rules.values()))
logger.info("successfully loaded %s rules (including subscope rules which will be ignored)", len(rules))
logger.info("successfully loaded %d rules (including subscope rules which will be ignored)", len(rules))
if args.tag:
rules = rules.filter_rules_by_meta(args.tag)
logger.debug("selected %s rules", len(rules))
logger.debug("selected %d rules", len(rules))
for i, r in enumerate(rules.rules, 1):
logger.debug(" %d. %s", i, r)
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
@@ -748,7 +751,7 @@ def main(argv=None):
count_incomplete = 0
while num_rules != len(converted_rules) or cround < min_rounds:
cround += 1
logger.info("doing convert_rules(), round: " + str(cround))
logger.info("doing convert_rules(), round: %d", cround)
num_rules = len(converted_rules)
count_incomplete += convert_rules(rules, namespaces, cround, make_priv)
@@ -758,7 +761,7 @@ def main(argv=None):
stats = "\n// converted rules : " + str(len(converted_rules))
stats += "\n// among those are incomplete : " + str(count_incomplete)
stats += "\n// unconverted rules : " + str(len(unsupported_capa_rules_list)) + "\n"
logger.info(stats)
logger.info("%s", stats)
output_yar(stats)
return 0
+7 -8
View File
@@ -2,7 +2,7 @@
import json
import collections
from typing import Any, Dict
from typing import Any, Set, Dict
import capa.main
import capa.rules
@@ -13,7 +13,6 @@ import capa.render.utils as rutils
import capa.render.default
import capa.render.result_document as rd
import capa.features.freeze.features as frzf
from capa.engine import *
from capa.features.common import OS_AUTO, FORMAT_AUTO
@@ -30,7 +29,7 @@ def find_subrule_matches(doc: rd.ResultDocument) -> Set[str]:
collect the rule names that have been matched as a subrule match.
this way we can avoid displaying entries for things that are too specific.
"""
matches = set([])
matches = set()
def rec(node: rd.Match):
if not node.success:
@@ -66,7 +65,7 @@ def render_capabilities(doc: rd.ResultDocument, result):
"""
subrule_matches = find_subrule_matches(doc)
result["CAPABILITY"] = dict()
result["CAPABILITY"] = {}
for rule in rutils.capability_rules(doc):
if rule.meta.name in subrule_matches:
# rules that are also matched by other rules should not get rendered by default.
@@ -80,7 +79,7 @@ def render_capabilities(doc: rd.ResultDocument, result):
else:
capability = f"{rule.meta.name} ({count} matches)"
result["CAPABILITY"].setdefault(rule.meta.namespace, list())
result["CAPABILITY"].setdefault(rule.meta.namespace, [])
result["CAPABILITY"][rule.meta.namespace].append(capability)
@@ -97,7 +96,7 @@ def render_attack(doc, result):
'EXECUTION': ['Shared Modules [T1129]']}
}
"""
result["ATTCK"] = dict()
result["ATTCK"] = {}
tactics = collections.defaultdict(set)
for rule in rutils.capability_rules(doc):
if not rule.meta.attack:
@@ -130,7 +129,7 @@ def render_mbc(doc, result):
'[C0021.004]']}
}
"""
result["MBC"] = dict()
result["MBC"] = {}
objectives = collections.defaultdict(set)
for rule in rutils.capability_rules(doc):
if not rule.meta.mbc:
@@ -150,7 +149,7 @@ def render_mbc(doc, result):
def render_dictionary(doc: rd.ResultDocument) -> Dict[str, Any]:
result: Dict[str, Any] = dict()
result: Dict[str, Any] = {}
render_meta(doc, result)
render_attack(doc, result)
render_mbc(doc, result)
-78
View File
@@ -1,78 +0,0 @@
#!/usr/bin/env bash
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# 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: [package root]/LICENSE.txt
# 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.
# Use a console with emojis support for a better experience
# Use venv to ensure that `python` calls the correct python version
# Stash uncommitted changes
MSG="pre-push-$(date +%s)";
git stash push -kum "$MSG" &>/dev/null ;
STASH_LIST=$(git stash list);
if [[ "$STASH_LIST" == *"$MSG"* ]]; then
echo "Uncommitted changes stashed with message '$MSG', if you abort before they are restored run \`git stash pop\`";
fi
restore_stashed() {
if [[ "$STASH_LIST" == *"$MSG"* ]]; then
git stash pop --index &>/dev/null ;
echo "Stashed changes '$MSG' restored";
fi
}
# Run isort and print state
python -m isort --profile black --length-sort --line-width 120 -c . > isort-output.log 2>&1;
if [ $? == 0 ]; then
echo 'isort succeeded!! 💖';
else
echo 'isort FAILED! 😭';
echo 'Check isort-output.log for details';
restore_stashed;
exit 1;
fi
# Run black and print state
python -m black -l 120 --check . > black-output.log 2>&1;
if [ $? == 0 ]; then
echo 'black succeeded!! 💝';
else
echo 'black FAILED! 😭';
echo 'Check black-output.log for details';
restore_stashed;
exit 2;
fi
# Run rule linter and print state
python ./scripts/lint.py ./rules/ > rule-linter-output.log 2>&1;
if [ $? == 0 ]; then
echo 'Rule linter succeeded!! 💘';
else
echo 'Rule linter FAILED! 😭';
echo 'Check rule-linter-output.log for details';
restore_stashed;
exit 3;
fi
# Run tests except if first argument is no_tests
if [ "$1" != 'no_tests' ]; then
echo 'Running tests, please wait ⌛';
python -m pytest tests/ --maxfail=1;
if [ $? == 0 ]; then
echo 'Tests succeed!! 🎉';
else
echo 'Tests FAILED! 😓';
echo 'Run `pytest -v --cov=capa test/` if you need more details';
restore_stashed;
exit 4;
fi
fi
restore_stashed;
echo 'SUCCEEDED 🎉🎉';
+3 -3
View File
@@ -42,12 +42,12 @@ def get_features(rule_path: str) -> list:
list: A list of all feature statements contained within the rule file.
"""
feature_list = []
with open(rule_path, "r") as f:
with open(rule_path, "r", encoding="utf-8") as f:
try:
new_rule = capa.rules.Rule.from_yaml(f.read())
feature_list = get_child_features(new_rule.statement)
except Exception as e:
logger.error("Error: New rule " + rule_path + " " + str(type(e)) + " " + str(e))
logger.error("Error: New rule %s %s %s", rule_path, str(type(e)), str(e))
sys.exit(-1)
return feature_list
@@ -73,7 +73,7 @@ def find_overlapping_rules(new_rule_path, rules_path):
continue
count += 1
# Checks if any features match between existing and new rule.
if any([feature in rule_features for feature in new_rule_features]):
if any(feature in rule_features for feature in new_rule_features):
overlapping_rules.append(rule_name)
result = {"overlapping_rules": overlapping_rules, "count": count}
+21 -17
View File
@@ -28,13 +28,17 @@ Unless required by applicable law or agreed to in writing, software distributed
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 json
import logging
import binascii
import ida_nalt
import ida_funcs
import ida_kernwin
import capa.rules
import capa.features.freeze
import capa.render.result_document
logger = logging.getLogger("capa")
@@ -64,37 +68,37 @@ def main():
if not path:
return 0
with open(path, "rb") as f:
doc = json.loads(f.read().decode("utf-8"))
if "meta" not in doc or "rules" not in doc:
logger.error("doesn't appear to be a capa report")
return -1
result_doc = capa.render.result_document.ResultDocument.parse_file(path)
meta, capabilities = result_doc.to_capa()
# in IDA 7.4, the MD5 hash may be truncated, for example:
# wanted: 84882c9d43e23d63b82004fae74ebb61
# found: b'84882C9D43E23D63B82004FAE74EBB6\x00'
#
# see: https://github.com/idapython/bin/issues/11
a = doc["meta"]["sample"]["md5"].lower()
b = ida_nalt.retrieve_input_file_md5().lower()
a = meta.sample.md5.lower()
b = binascii.hexlify(ida_nalt.retrieve_input_file_md5()).decode("ascii").lower()
if not a.startswith(b):
logger.error("sample mismatch")
return -2
rows = []
for rule in doc["rules"].values():
if rule["meta"].get("lib"):
for name in capabilities.keys():
rule = result_doc.rules[name]
if rule.meta.lib:
continue
if rule["meta"].get("capa/subscope"):
if rule.meta.is_subscope_rule:
continue
if rule["meta"]["scope"] != "function":
if rule.meta.scope != capa.rules.Scope.FUNCTION:
continue
name = rule["meta"]["name"]
ns = rule["meta"].get("namespace", "")
for va in rule["matches"].keys():
va = int(va)
ns = rule.meta.namespace
for address, _ in rule.matches:
if address.type != capa.features.freeze.AddressType.ABSOLUTE:
continue
va = address.value
rows.append((ns, name, va))
# order by (namespace, name) so that like things show up together
+7 -7
View File
@@ -43,7 +43,7 @@ import capa.engine
import capa.helpers
import capa.features.insn
from capa.rules import Rule, RuleSet
from capa.features.common import OS_AUTO, FORMAT_PE, FORMAT_DOTNET, String, Feature, Substring
from capa.features.common import OS_AUTO, String, Feature, Substring
from capa.render.result_document import RuleMetadata
logger = logging.getLogger("lint")
@@ -355,7 +355,7 @@ class DoesntMatchExample(Lint):
try:
capabilities = get_sample_capabilities(ctx, path)
except Exception as e:
logger.error("failed to extract capabilities: %s %s %s", rule.name, str(path), e, exc_info=True)
logger.exception("failed to extract capabilities: %s %s %s", rule.name, str(path), e)
return True
if rule.name not in capabilities:
@@ -516,7 +516,7 @@ class FeatureNegativeNumber(Lint):
recommendation = "specify the number's two's complement representation"
recommendation_template = (
"capa treats number features as unsigned values; you may specify the number's two's complement "
'representation; will not match on "{:d}"'
+ 'representation; will not match on "{:d}"'
)
def check_features(self, ctx: Context, features: List[Feature]):
@@ -534,7 +534,7 @@ class FeatureNtdllNtoskrnlApi(Lint):
level = Lint.WARN
recommendation_template = (
"check if {:s} is exported by both ntdll and ntoskrnl; if true, consider removing {:s} "
"module requirement to improve detection"
+ "module requirement to improve detection"
)
def check_features(self, ctx: Context, features: List[Feature]):
@@ -825,7 +825,7 @@ def lint_rule(ctx: Context, rule: Rule):
print("")
if is_nursery_rule(rule):
has_examples = not any(map(lambda v: v.level == Lint.FAIL and v.name == "missing examples", violations))
has_examples = not any(v.level == Lint.FAIL and v.name == "missing examples" for v in violations)
lints_failed = len(
tuple(
filter(
@@ -873,7 +873,7 @@ def lint(ctx: Context):
ret = {}
source_rules = [rule for rule in ctx.rules.rules.values() if not rule.is_subscope_rule()]
with tqdm.contrib.logging.tqdm_logging_redirect(source_rules, unit="rule") as pbar:
with tqdm.contrib.logging.tqdm_logging_redirect(source_rules, unit="rule", leave=False) as pbar:
with capa.helpers.redirecting_print_to_tqdm(False):
for rule in pbar:
name = rule.name
@@ -888,7 +888,7 @@ def collect_samples(path) -> Dict[str, Path]:
recurse through the given path, collecting all file paths, indexed by their content sha256, md5, and filename.
"""
samples = {}
for root, dirs, files in os.walk(path):
for root, _, files in os.walk(path):
for name in files:
if name.endswith(".viv"):
continue
+20 -5
View File
@@ -54,6 +54,7 @@
"T1583.005": "Acquire Infrastructure::Botnet",
"T1583.006": "Acquire Infrastructure::Web Services",
"T1583.007": "Acquire Infrastructure::Serverless",
"T1583.008": "Acquire Infrastructure::Malvertising",
"T1584": "Compromise Infrastructure",
"T1584.001": "Compromise Infrastructure::Domains",
"T1584.002": "Compromise Infrastructure::DNS Server",
@@ -88,7 +89,8 @@
"T1608.003": "Stage Capabilities::Install Digital Certificate",
"T1608.004": "Stage Capabilities::Drive-by Target",
"T1608.005": "Stage Capabilities::Link Target",
"T1608.006": "Stage Capabilities::SEO Poisoning"
"T1608.006": "Stage Capabilities::SEO Poisoning",
"T1650": "Acquire Access"
},
"Initial Access": {
"T1078": "Valid Accounts",
@@ -128,6 +130,7 @@
"T1059.006": "Command and Scripting Interpreter::Python",
"T1059.007": "Command and Scripting Interpreter::JavaScript",
"T1059.008": "Command and Scripting Interpreter::Network Device CLI",
"T1059.009": "Command and Scripting Interpreter::Cloud API",
"T1072": "Software Deployment Tools",
"T1106": "Native API",
"T1129": "Shared Modules",
@@ -145,7 +148,8 @@
"T1569.002": "System Services::Service Execution",
"T1609": "Container Administration Command",
"T1610": "Deploy Container",
"T1648": "Serverless Execution"
"T1648": "Serverless Execution",
"T1651": "Cloud Administration Command"
},
"Persistence": {
"T1037": "Boot or Logon Initialization Scripts",
@@ -247,6 +251,7 @@
"T1556.005": "Modify Authentication Process::Reversible Encryption",
"T1556.006": "Modify Authentication Process::Multi-Factor Authentication",
"T1556.007": "Modify Authentication Process::Hybrid Identity",
"T1556.008": "Modify Authentication Process::Network Provider DLL",
"T1574": "Hijack Execution Flow",
"T1574.001": "Hijack Execution Flow::DLL Search Order Hijacking",
"T1574.002": "Hijack Execution Flow::DLL Side-Loading",
@@ -372,6 +377,8 @@
"T1027.007": "Obfuscated Files or Information::Dynamic API Resolution",
"T1027.008": "Obfuscated Files or Information::Stripped Payloads",
"T1027.009": "Obfuscated Files or Information::Embedded Payloads",
"T1027.010": "Obfuscated Files or Information::Command Obfuscation",
"T1027.011": "Obfuscated Files or Information::Fileless Storage",
"T1036": "Masquerading",
"T1036.001": "Masquerading::Invalid Code Signature",
"T1036.002": "Masquerading::Right-to-Left Override",
@@ -380,6 +387,7 @@
"T1036.005": "Masquerading::Match Legitimate Name or Location",
"T1036.006": "Masquerading::Space after Filename",
"T1036.007": "Masquerading::Double File Extension",
"T1036.008": "Masquerading::Masquerade File Type",
"T1055": "Process Injection",
"T1055.001": "Process Injection::Dynamic-link Library Injection",
"T1055.002": "Process Injection::Portable Executable Injection",
@@ -487,6 +495,7 @@
"T1556.005": "Modify Authentication Process::Reversible Encryption",
"T1556.006": "Modify Authentication Process::Multi-Factor Authentication",
"T1556.007": "Modify Authentication Process::Hybrid Identity",
"T1556.008": "Modify Authentication Process::Network Provider DLL",
"T1562": "Impair Defenses",
"T1562.001": "Impair Defenses::Disable or Modify Tools",
"T1562.002": "Impair Defenses::Disable Windows Event Logging",
@@ -497,6 +506,7 @@
"T1562.008": "Impair Defenses::Disable Cloud Logs",
"T1562.009": "Impair Defenses::Safe Mode Boot",
"T1562.010": "Impair Defenses::Downgrade Attack",
"T1562.011": "Impair Defenses::Spoof Security Alerting",
"T1564": "Hide Artifacts",
"T1564.001": "Hide Artifacts::Hidden Files and Directories",
"T1564.002": "Hide Artifacts::Hidden Users",
@@ -574,6 +584,7 @@
"T1552.005": "Unsecured Credentials::Cloud Instance Metadata API",
"T1552.006": "Unsecured Credentials::Group Policy Preferences",
"T1552.007": "Unsecured Credentials::Container API",
"T1552.008": "Unsecured Credentials::Chat Messages",
"T1555": "Credentials from Password Stores",
"T1555.001": "Credentials from Password Stores::Keychain",
"T1555.002": "Credentials from Password Stores::Securityd Memory",
@@ -588,6 +599,7 @@
"T1556.005": "Modify Authentication Process::Reversible Encryption",
"T1556.006": "Modify Authentication Process::Multi-Factor Authentication",
"T1556.007": "Modify Authentication Process::Hybrid Identity",
"T1556.008": "Modify Authentication Process::Network Provider DLL",
"T1557": "Adversary-in-the-Middle",
"T1557.001": "Adversary-in-the-Middle::LLMNR/NBT-NS Poisoning and SMB Relay",
"T1557.002": "Adversary-in-the-Middle::ARP Cache Poisoning",
@@ -630,7 +642,7 @@
"T1124": "System Time Discovery",
"T1135": "Network Share Discovery",
"T1201": "Password Policy Discovery",
"T1217": "Browser Bookmark Discovery",
"T1217": "Browser Information Discovery",
"T1482": "Domain Trust Discovery",
"T1497": "Virtualization/Sandbox Evasion",
"T1497.001": "Virtualization/Sandbox Evasion::System Checks",
@@ -646,7 +658,8 @@
"T1614.001": "System Location Discovery::System Language Discovery",
"T1615": "Group Policy Discovery",
"T1619": "Cloud Storage Object Discovery",
"T1622": "Debugger Evasion"
"T1622": "Debugger Evasion",
"T1652": "Device Driver Discovery"
},
"Lateral Movement": {
"T1021": "Remote Services",
@@ -656,6 +669,7 @@
"T1021.004": "Remote Services::SSH",
"T1021.005": "Remote Services::VNC",
"T1021.006": "Remote Services::Windows Remote Management",
"T1021.007": "Remote Services::Cloud Services",
"T1072": "Software Deployment Tools",
"T1080": "Taint Shared Content",
"T1091": "Replication Through Removable Media",
@@ -768,7 +782,8 @@
"T1537": "Transfer Data to Cloud Account",
"T1567": "Exfiltration Over Web Service",
"T1567.001": "Exfiltration Over Web Service::Exfiltration to Code Repository",
"T1567.002": "Exfiltration Over Web Service::Exfiltration to Cloud Storage"
"T1567.002": "Exfiltration Over Web Service::Exfiltration to Cloud Storage",
"T1567.003": "Exfiltration Over Web Service::Exfiltration to Text Storage Sites"
},
"Impact": {
"T1485": "Data Destruction",
+6 -6
View File
@@ -27,7 +27,7 @@ example:
|--------------------------------------|----------------------|-------------|-------------|-------------|
| 18c30e4 main: remove perf debug msgs | 66,561,622 | 132.13s | 125.14s | 139.12s |
^^^ --label or git hash
^^^ --label or git hash
"""
import sys
import timeit
@@ -112,7 +112,7 @@ def main(argv=None):
)
assert isinstance(extractor, StaticFeatureExtractor)
with tqdm.tqdm(total=args.number * args.repeat) as pbar:
with tqdm.tqdm(total=args.number * args.repeat, leave=False) as pbar:
def do_iteration():
capa.perf.reset()
@@ -121,12 +121,12 @@ def main(argv=None):
samples = timeit.repeat(do_iteration, number=args.number, repeat=args.repeat)
logger.debug("perf: find capabilities: min: %0.2fs" % (min(samples) / float(args.number)))
logger.debug("perf: find capabilities: avg: %0.2fs" % (sum(samples) / float(args.repeat) / float(args.number)))
logger.debug("perf: find capabilities: max: %0.2fs" % (max(samples) / float(args.number)))
logger.debug("perf: find capabilities: min: %0.2fs", (min(samples) / float(args.number)))
logger.debug("perf: find capabilities: avg: %0.2fs", (sum(samples) / float(args.repeat) / float(args.number)))
logger.debug("perf: find capabilities: max: %0.2fs", (max(samples) / float(args.number)))
for counter, count in capa.perf.counters.most_common():
logger.debug("perf: counter: {:}: {:,}".format(counter, count))
logger.debug("perf: counter: %s: %s", counter, count)
print(
tabulate.tabulate(
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
# 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: [package root]/LICENSE.txt
# 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.
set -euo pipefail
GIT_DIR=$(git rev-parse --show-toplevel);
cd "$GIT_DIR";
# hooks may exist already (e.g. git-lfs configuration)
# If the `.git/hooks/$arg` file doesn't exist it, initialize with `#!/usr/bin/env bash`
# After that append `scripts/hooks/$arg` and ensure they can be run
create_hook() {
if [[ ! -e .git/hooks/$1 ]]; then
echo "#!/usr/bin/env bash" > ".git/hooks/$1";
fi
echo "scripts/ci.sh ${2:-}" >> ".git/hooks/$1";
chmod +x .git/hooks/"$1";
}
printf 'Adding scripts/ci.sh to .git/hooks/';
create_hook 'pre-commit' 'no_tests';
create_hook 'pre-push';
+4 -4
View File
@@ -65,7 +65,7 @@ class MitreExtractor:
if self.url == "":
raise ValueError(f"URL not specified in class {self.__class__.__name__}")
logging.info(f"Downloading STIX data at: {self.url}")
logging.info("Downloading STIX data at: %s", self.url)
stix_json = requests.get(self.url).json()
self._memory_store = MemoryStore(stix_data=stix_json["objects"])
@@ -170,12 +170,12 @@ def main(args: argparse.Namespace) -> None:
logging.info("Extracting MBC behaviors...")
data["mbc"] = MbcExtractor().run()
logging.info(f"Writing results to {args.output}")
logging.info("Writing results to %s", args.output)
try:
with open(args.output, "w") as jf:
with open(args.output, "w", encoding="utf-8") as jf:
json.dump(data, jf, indent=2)
except BaseException as e:
logging.error(f"Exception encountered when writing results: {e}")
logging.error("Exception encountered when writing results: %s", e)
if __name__ == "__main__":
+2 -4
View File
@@ -69,7 +69,6 @@ import sys
import logging
import os.path
import argparse
from typing import cast
import capa.main
import capa.rules
@@ -104,7 +103,7 @@ def main(argv=None):
capa.main.handle_common_args(args)
try:
taste = capa.helpers.get_file_taste(args.sample)
_ = capa.helpers.get_file_taste(args.sample)
except IOError as e:
logger.error("%s", str(e))
return -1
@@ -156,7 +155,6 @@ def print_static_analysis(extractor: StaticFeatureExtractor, args):
if args.function:
if args.format == "freeze":
# TODO fix
function_handles = tuple(filter(lambda fh: fh.address == args.function, function_handles))
else:
function_handles = tuple(filter(lambda fh: format_address(fh.address) == args.function, function_handles))
@@ -285,7 +283,7 @@ def ida_main():
if __name__ == "__main__":
if capa.main.is_runtime_ida():
if capa.helpers.is_runtime_ida():
ida_main()
else:
sys.exit(main())