From bca4cdda744120a5ca868bdfd39119ebf0819201 Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Fri, 19 Jul 2024 11:09:52 +0200 Subject: [PATCH] replace yara-python with yara-x --- doc/requirements.txt | 2 +- test/requirements-testing.txt | 2 +- volatility3/framework/plugins/yarascan.py | 74 ++++++----------------- 3 files changed, 22 insertions(+), 56 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index b715e59f5..c04f71219 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -3,6 +3,6 @@ sphinx>=4.0.0,<7 sphinx_autodoc_typehints>=1.4.0 sphinx-rtd-theme>=0.4.3 -yara-python +yara-x pycryptodome pefile diff --git a/test/requirements-testing.txt b/test/requirements-testing.txt index 7afe19b94..0955d1939 100644 --- a/test/requirements-testing.txt +++ b/test/requirements-testing.txt @@ -5,6 +5,6 @@ pefile>=2017.8.1 #foo # If certain packages are not necessary, place a comment (#) at the start of the line. # This is required for the yara plugins -yara-python>=3.8.0 +yara-x>=0.5.0 pytest>=7.0.0 diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 496ec1844..32a400f9e 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -14,14 +14,9 @@ from volatility3.framework.renderers import format_hints vollog = logging.getLogger(__name__) try: - import yara + import yara_x - if tuple([int(x) for x in yara.__version__.split(".")]) < (3, 8): - raise ImportError except ImportError: - vollog.info( - "Python Yara (>3.8.0) module not found, plugin (and dependent plugins) not available" - ) raise @@ -34,27 +29,20 @@ class YaraScanner(interfaces.layers.ScannerInterface): if rules is None: raise ValueError("No rules provided to YaraScanner") self._rules = rules - self.st_object = not tuple([int(x) for x in yara.__version__.split(".")]) < ( - 4, - 3, - ) def __call__( self, data: bytes, data_offset: int ) -> Iterable[Tuple[int, str, str, bytes]]: - for match in self._rules.match(data=data): - if YaraScan.yara_returns_instances(): - for match_string in match.strings: - for instance in match_string.instances: - yield ( - instance.offset + data_offset, - match.rule, - match_string.identifier, - instance.matched_data, - ) - else: - for offset, name, value in match.strings: - yield (offset + data_offset, match.rule, name, value) + results = self._rules.scan(data) + for match in results.matching_rules: + for match_string in match.patterns: + for instance in match_string.matches: + yield ( + instance.offset + data_offset, + f"{match.namespace}.{match.identifier}", + match_string.identifier, + data[instance.offset : instance.offset + instance.length], + ) class YaraScan(plugins.PluginInterface): @@ -63,9 +51,6 @@ class YaraScan(plugins.PluginInterface): _required_framework_version = (2, 0, 0) _version = (1, 3, 0) - # TODO: When the major version is bumped, take the opportunity to rename the yara_rules config to yara_string - # or something that makes more sense - @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: """Returns the requirements needed to run yarascan directly, combining the TranslationLayerRequirement @@ -99,16 +84,13 @@ class YaraScan(plugins.PluginInterface): optional=True, ), requirements.StringRequirement( - name="yara_rules", description="Yara rules (as a string)", optional=True + name="yara_string", + description="Yara rules (as a string)", + optional=True, ), - requirements.URIRequirement( - name="yara_file", description="Yara rules (as a file)", optional=True - ), - # This additional requirement is to follow suit with upstream, who feel that compiled rules could potentially be used to execute malicious code - # As such, there's a separate option to run compiled files, as happened with yara-3.9 and later requirements.URIRequirement( name="yara_compiled_file", - description="Yara compiled rules (as a file)", + description="Yara-x compiled rules (as a file)", optional=True, ), requirements.IntRequirement( @@ -119,36 +101,20 @@ class YaraScan(plugins.PluginInterface): ), ] - @classmethod - def yara_returns_instances(cls) -> bool: - st_object = not tuple([int(x) for x in yara.__version__.split(".")]) < ( - 4, - 3, - ) - return st_object - @classmethod def process_yara_options(cls, config: Dict[str, Any]): rules = None - if config.get("yara_rules", None) is not None: - rule = config["yara_rules"] + if config.get("yara_string") is not None: + rule = config["yara_string"] if rule[0] not in ["{", "/"]: rule = f'"{rule}"' if config.get("case", False): rule += " nocase" if config.get("wide", False): rule += " wide ascii" - rules = yara.compile( - sources={"n": f"rule r1 {{strings: $a = {rule} condition: $a}}"} - ) - elif config.get("yara_source", None) is not None: - rules = yara.compile(source=config["yara_source"]) - elif config.get("yara_file", None) is not None: - rules = yara.compile( - file=resources.ResourceAccessor().open(config["yara_file"], "rb") - ) - elif config.get("yara_compiled_file", None) is not None: - rules = yara.load( + rules = yara_x.compile(f"rule r1 {{strings: $a = {rule} condition: $a}}") + elif config.get("yara_compiled_file") is not None: + rules = yara_x.Rules.deserialize_from( file=resources.ResourceAccessor().open( config["yara_compiled_file"], "rb" )