diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ae6720..ff0fcb78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - 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 diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index ede94568..01a3a8f5 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -74,6 +74,7 @@ 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" @@ -81,6 +82,7 @@ class Scope(str, Enum): 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 @@ -115,6 +117,14 @@ SUPPORTED_FEATURES: Dict[str, Set] = { 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, @@ -160,7 +170,10 @@ 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 function features +SUPPORTED_FEATURES[FUNCTION_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 @@ -457,6 +470,15 @@ def build_statements(d, scope: str): 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") @@ -1118,6 +1140,7 @@ class RuleSet: 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) @@ -1129,6 +1152,7 @@ class RuleSet: (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 ) @@ -1381,6 +1405,9 @@ class RuleSet: 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 diff --git a/tests/test_main.py b/tests/test_main.py index 3eb4e44b..8d62b706 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -145,12 +145,25 @@ def test_ruleset(): """ ) ), + 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): diff --git a/tests/test_rules.py b/tests/test_rules.py index 79228145..cfef61c7 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -361,6 +361,21 @@ def test_subscope_rules(): """ ) ), + 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 two rules: @@ -372,8 +387,13 @@ def test_subscope_rules(): 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 + # - 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():