diff --git a/CHANGELOG.md b/CHANGELOG.md index e477e05d..748cf800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Utility script to detect feature overlap between new and existing CAPA rules [#1451](https://github.com/mandiant/capa/issues/1451) [@Aayush-Goel-04](https://github.com/aayush-goel-04) - 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 new process scope for the dynamic analysis flavor @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 diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index 64fd7e37..6a645263 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -73,12 +73,14 @@ HIDDEN_META_KEYS = ("capa/nursery", "capa/path") class Scope(str, Enum): FILE = "file" + PROCESS = "process" FUNCTION = "function" BASIC_BLOCK = "basic block" INSTRUCTION = "instruction" FILE_SCOPE = Scope.FILE.value +PROCESS_SCOPE = Scope.PROCESS FUNCTION_SCOPE = Scope.FUNCTION.value BASIC_BLOCK_SCOPE = Scope.BASIC_BLOCK.value INSTRUCTION_SCOPE = Scope.INSTRUCTION.value @@ -106,6 +108,12 @@ SUPPORTED_FEATURES: Dict[str, Set] = { capa.features.common.Namespace, capa.features.common.Characteristic("mixed mode"), }, + PROCESS_SCOPE: { + capa.features.common.String, + capa.features.common.Substring, + capa.features.common.Regex, + capa.features.common.Characteristic("embedded pe"), + }, FUNCTION_SCOPE: { capa.features.common.MatchedRule, capa.features.basicblock.BasicBlock, @@ -150,6 +158,7 @@ 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]) # all instruction scope features are also basic block features SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE].update(SUPPORTED_FEATURES[INSTRUCTION_SCOPE]) @@ -438,6 +447,15 @@ 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 == "function": if scope != FILE_SCOPE: raise InvalidRule("function subscope supported only for file scope") @@ -1098,6 +1116,7 @@ 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.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 +1125,9 @@ 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_function_rules_by_feature, self._hard_function_rules) = self._index_rules_by_feature( self.function_rules ) @@ -1355,6 +1377,9 @@ 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.FUNCTION: easy_rules_by_feature = self._easy_function_rules_by_feature hard_rule_names = self._hard_function_rules diff --git a/tests/test_main.py b/tests/test_main.py index d17e6e64..3eb4e44b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -133,11 +133,24 @@ def test_ruleset(): """ ) ), + capa.rules.Rule.from_yaml( + textwrap.dedent( + """ + rule: + meta: + name: process rule + scope: process + features: + - string: "explorer.exe" + """ + ) + ), ] ) 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 def test_match_across_scopes_file_function(z9324d_extractor): diff --git a/tests/test_rules.py b/tests/test_rules.py index 9f07f31d..79228145 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -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,37 @@ 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://" + """ + ) + ), ] ) - # 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` depends + assert len(rules.process_rules) == 1 + def test_duplicate_rules(): with pytest.raises(capa.rules.InvalidRule):