Merge pull request #1576 from yelhamer/process-scope

This commit is contained in:
Willi Ballenthin
2023-06-28 16:34:15 +02:00
committed by GitHub
4 changed files with 141 additions and 6 deletions

View File

@@ -8,6 +8,8 @@
- Add a dynamic feature extractor for the CAPE sandbox @yelhamer [#1535](https://github.com/mandiant/capa/issues/1535)
- Add unit tests for the new CAPE extractor #1563 @yelhamer
- Add a CAPE file format and CAPE-based dynamic feature extraction to scripts/show-features.py #1566 @yelhamer
- Add a new process scope for the dynamic analysis flavor #1517 @yelhamer
- Add a new thread scope for the dynamic analysis flavor #1517 @yelhamer
### Breaking Changes
- Update Metadata type in capa main [#1411](https://github.com/mandiant/capa/issues/1411) [@Aayush-Goel-04](https://github.com/aayush-goel-04) @manasghandat

View File

@@ -73,12 +73,16 @@ HIDDEN_META_KEYS = ("capa/nursery", "capa/path")
class Scope(str, Enum):
FILE = "file"
PROCESS = "process"
THREAD = "thread"
FUNCTION = "function"
BASIC_BLOCK = "basic block"
INSTRUCTION = "instruction"
FILE_SCOPE = Scope.FILE.value
PROCESS_SCOPE = Scope.PROCESS.value
THREAD_SCOPE = Scope.THREAD.value
FUNCTION_SCOPE = Scope.FUNCTION.value
BASIC_BLOCK_SCOPE = Scope.BASIC_BLOCK.value
INSTRUCTION_SCOPE = Scope.INSTRUCTION.value
@@ -106,6 +110,21 @@ SUPPORTED_FEATURES: Dict[str, Set] = {
capa.features.common.Namespace,
capa.features.common.Characteristic("mixed mode"),
},
PROCESS_SCOPE: {
capa.features.common.MatchedRule,
capa.features.common.String,
capa.features.common.Substring,
capa.features.common.Regex,
capa.features.common.Characteristic("embedded pe"),
},
THREAD_SCOPE: {
capa.features.common.MatchedRule,
capa.features.common.String,
capa.features.common.Substring,
capa.features.common.Regex,
capa.features.insn.API,
capa.features.insn.Number,
},
FUNCTION_SCOPE: {
capa.features.common.MatchedRule,
capa.features.basicblock.BasicBlock,
@@ -150,7 +169,11 @@ SUPPORTED_FEATURES[INSTRUCTION_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
SUPPORTED_FEATURES[FUNCTION_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
SUPPORTED_FEATURES[FILE_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
SUPPORTED_FEATURES[PROCESS_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
SUPPORTED_FEATURES[THREAD_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
# all thread scope features are also process features
SUPPORTED_FEATURES[PROCESS_SCOPE].update(SUPPORTED_FEATURES[THREAD_SCOPE])
# all instruction scope features are also basic block features
SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE].update(SUPPORTED_FEATURES[INSTRUCTION_SCOPE])
# all basic block scope features are also function scope features
@@ -438,6 +461,24 @@ def build_statements(d, scope: str):
# like with `write file`, we might say that `WriteFile` is optionally found alongside `CreateFileA`.
return ceng.Some(0, [build_statements(dd, scope) for dd in d[key]], description=description)
elif key == "process":
if scope != FILE_SCOPE:
raise InvalidRule("process subscope supported only for file scope")
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(PROCESS_SCOPE, build_statements(d[key][0], PROCESS_SCOPE), description=description)
elif key == "thread":
if scope != PROCESS_SCOPE:
raise InvalidRule("thread subscope supported only for the process scope")
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(THREAD_SCOPE, build_statements(d[key][0], THREAD_SCOPE), description=description)
elif key == "function":
if scope != FILE_SCOPE:
raise InvalidRule("function subscope supported only for file scope")
@@ -1098,6 +1139,8 @@ class RuleSet:
rules = capa.optimizer.optimize_rules(rules)
self.file_rules = self._get_rules_for_scope(rules, FILE_SCOPE)
self.process_rules = self._get_rules_for_scope(rules, PROCESS_SCOPE)
self.thread_rules = self._get_rules_for_scope(rules, THREAD_SCOPE)
self.function_rules = self._get_rules_for_scope(rules, FUNCTION_SCOPE)
self.basic_block_rules = self._get_rules_for_scope(rules, BASIC_BLOCK_SCOPE)
self.instruction_rules = self._get_rules_for_scope(rules, INSTRUCTION_SCOPE)
@@ -1106,6 +1149,10 @@ class RuleSet:
# unstable
(self._easy_file_rules_by_feature, self._hard_file_rules) = self._index_rules_by_feature(self.file_rules)
(self._easy_process_rules_by_feature, self._hard_process_rules) = self._index_rules_by_feature(
self.process_rules
)
(self._easy_thread_rules_by_feature, self._hard_thread_rules) = self._index_rules_by_feature(self.thread_rules)
(self._easy_function_rules_by_feature, self._hard_function_rules) = self._index_rules_by_feature(
self.function_rules
)
@@ -1355,6 +1402,12 @@ class RuleSet:
if scope is Scope.FILE:
easy_rules_by_feature = self._easy_file_rules_by_feature
hard_rule_names = self._hard_file_rules
elif scope is Scope.PROCESS:
easy_rules_by_feature = self._easy_process_rules_by_feature
hard_rule_names = self._hard_process_rules
elif scope is Scope.THREAD:
easy_rules_by_feature = self._easy_thread_rules_by_feature
hard_rule_names = self._hard_thread_rules
elif scope is Scope.FUNCTION:
easy_rules_by_feature = self._easy_function_rules_by_feature
hard_rule_names = self._hard_function_rules

View File

@@ -133,11 +133,37 @@ def test_ruleset():
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: process rule
scope: process
features:
- string: "explorer.exe"
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: thread rule
scope: thread
features:
- api: RegDeleteKey
"""
)
),
]
)
assert len(rules.file_rules) == 1
assert len(rules.function_rules) == 1
assert len(rules.basic_block_rules) == 1
assert len(rules.process_rules) == 1
assert len(rules.thread_rules) == 1
def test_match_across_scopes_file_function(z9324d_extractor):

View File

@@ -277,6 +277,20 @@ def test_invalid_rule_feature():
)
)
with pytest.raises(capa.rules.InvalidRule):
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scope: process
features:
- mnemonic: xor
"""
)
)
def test_lib_rules():
rules = capa.rules.RuleSet(
@@ -319,7 +333,7 @@ def test_subscope_rules():
"""
rule:
meta:
name: test rule
name: test function subscope
scope: file
features:
- and:
@@ -330,17 +344,57 @@ def test_subscope_rules():
- characteristic: loop
"""
)
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test process subscope
scope: file
features:
- and:
- import: WININET.dll.HttpOpenRequestW
- process:
- and:
- substring: "http://"
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test thread subscope
scope: process
features:
- and:
- string: "explorer.exe"
- thread:
- api: HttpOpenRequestW
"""
)
),
]
)
# the file rule scope will have one rules:
# - `test rule`
assert len(rules.file_rules) == 1
# the file rule scope will have two rules:
# - `test function subscope` and `test process subscope`
assert len(rules.file_rules) == 2
# the function rule scope have one rule:
# - the rule on which `test rule` depends
# - the rule on which `test function subscope` depends
assert len(rules.function_rules) == 1
# the process rule scope has one rule:
# - the rule on which `test process subscope` and depends
# as well as `test thread scope`
assert len(rules.process_rules) == 2
# the thread rule scope has one rule:
# - the rule on which `test thread subscope` depends
assert len(rules.thread_rules) == 1
def test_duplicate_rules():
with pytest.raises(capa.rules.InvalidRule):