mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-17 20:35:40 +02:00
Merge pull request #1208 from dadokkio/develop
yara-x support for yarascan
This commit is contained in:
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7"]
|
||||
python-version: ["3.8"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
host: [ ubuntu-latest, windows-latest ]
|
||||
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
|
||||
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7"]
|
||||
python-version: ["3.8"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
||||
@@ -20,7 +20,7 @@ more details.
|
||||
|
||||
## Requirements
|
||||
|
||||
Volatility 3 requires Python 3.7.3 or later. To install the most minimal set of dependencies (some plugins will not work) use a command such as:
|
||||
Volatility 3 requires Python 3.8.0 or later. To install the most minimal set of dependencies (some plugins will not work) use a command such as:
|
||||
|
||||
```shell
|
||||
pip3 install -r requirements-minimal.txt
|
||||
|
||||
@@ -4,5 +4,6 @@ sphinx_autodoc_typehints>=1.4.0
|
||||
sphinx-rtd-theme>=0.4.3
|
||||
|
||||
yara-python
|
||||
yara-x
|
||||
pycryptodome
|
||||
pefile
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Volatility Foundation", email = "volatility@volatilityfoundation.org" },
|
||||
]
|
||||
requires-python = ">=3.7.3"
|
||||
requires-python = ">=3.8.0"
|
||||
license = { text = "VSL" }
|
||||
dynamic = ["dependencies", "optional-dependencies", "version"]
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@ pefile>=2017.8.1 #foo
|
||||
|
||||
# This is required for the yara plugins
|
||||
yara-python>=3.8.0
|
||||
yara-x>=0.5.0
|
||||
|
||||
pytest>=7.0.0
|
||||
|
||||
@@ -7,7 +7,7 @@ import glob
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
required_python_version = (3, 7, 3)
|
||||
required_python_version = (3, 8, 0)
|
||||
if (
|
||||
sys.version_info.major != required_python_version[0]
|
||||
or sys.version_info.minor < required_python_version[1]
|
||||
|
||||
@@ -31,10 +31,10 @@ class VmaYaraScan(interfaces.plugins.PluginInterface):
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="yarascan", plugin=yarascan.YaraScan, version=(1, 2, 0)
|
||||
name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
|
||||
name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0)
|
||||
),
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
|
||||
@@ -29,7 +29,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
|
||||
name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0)
|
||||
),
|
||||
]
|
||||
|
||||
@@ -38,7 +38,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
|
||||
# Yara Rule to scan for MFT Header Signatures
|
||||
rules = yarascan.YaraScan.process_yara_options(
|
||||
{"yara_rules": "/FILE0|FILE\\*|BAAD/"}
|
||||
{"yara_string": "/FILE0|FILE\\*|BAAD/"}
|
||||
)
|
||||
|
||||
# Read in the Symbol File
|
||||
@@ -197,7 +197,7 @@ class ADS(interfaces.plugins.PluginInterface):
|
||||
|
||||
# Yara Rule to scan for MFT Header Signatures
|
||||
rules = yarascan.YaraScan.process_yara_options(
|
||||
{"yara_rules": "/FILE0|FILE\\*|BAAD/"}
|
||||
{"yara_string": "/FILE0|FILE\\*|BAAD/"}
|
||||
)
|
||||
|
||||
# Read in the Symbol File
|
||||
|
||||
@@ -33,7 +33,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="yarascan", plugin=yarascan.YaraScan, version=(1, 3, 0)
|
||||
name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
@@ -73,26 +73,42 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
|
||||
)
|
||||
continue
|
||||
|
||||
for match in rules.match(data=layer.read(start, size, True)):
|
||||
if yarascan.YaraScan.yara_returns_instances():
|
||||
for match_string in match.strings:
|
||||
for instance in match_string.instances:
|
||||
data = layer.read(start, size, True)
|
||||
if not yarascan.YaraScan._yara_x:
|
||||
for match in rules.match(data=data):
|
||||
if yarascan.YaraScan.yara_returns_instances():
|
||||
for match_string in match.strings:
|
||||
for instance in match_string.instances:
|
||||
yield 0, (
|
||||
format_hints.Hex(instance.offset + start),
|
||||
task.UniqueProcessId,
|
||||
match.rule,
|
||||
match_string.identifier,
|
||||
instance.matched_data,
|
||||
)
|
||||
else:
|
||||
for offset, name, value in match.strings:
|
||||
yield 0, (
|
||||
format_hints.Hex(offset + start),
|
||||
task.UniqueProcessId,
|
||||
match.rule,
|
||||
name,
|
||||
value,
|
||||
)
|
||||
else:
|
||||
for match in rules.scan(data).matching_rules:
|
||||
for match_string in match.patterns:
|
||||
for instance in match_string.matches:
|
||||
yield 0, (
|
||||
format_hints.Hex(instance.offset + start),
|
||||
task.UniqueProcessId,
|
||||
match.rule,
|
||||
f"{match.namespace}.{match.identifier}",
|
||||
match_string.identifier,
|
||||
instance.matched_data,
|
||||
data[
|
||||
instance.offset : instance.offset
|
||||
+ instance.length
|
||||
],
|
||||
)
|
||||
else:
|
||||
for offset, name, value in match.strings:
|
||||
yield 0, (
|
||||
format_hints.Hex(offset + start),
|
||||
task.UniqueProcessId,
|
||||
match.rule,
|
||||
name,
|
||||
value,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_vad_maps(
|
||||
|
||||
@@ -13,20 +13,31 @@ from volatility3.framework.renderers import format_hints
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import yara
|
||||
USE_YARA_X = False
|
||||
|
||||
try:
|
||||
import yara_x
|
||||
|
||||
USE_YARA_X = True
|
||||
|
||||
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
|
||||
try:
|
||||
import yara
|
||||
|
||||
if tuple(int(x) for x in yara.__version__.split(".")) < (3, 8):
|
||||
raise ImportError
|
||||
|
||||
vollog.debug("Using yara-python module")
|
||||
|
||||
except ImportError:
|
||||
vollog.info(
|
||||
"Neither yara-x nor yara-python (>3.8.0) module not found, plugin (and dependent plugins) not available"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
class YaraScanner(interfaces.layers.ScannerInterface):
|
||||
_version = (2, 0, 0)
|
||||
_version = (2, 1, 0)
|
||||
|
||||
# yara.Rules isn't exposed, so we can't type this properly
|
||||
def __init__(self, rules) -> None:
|
||||
@@ -34,37 +45,69 @@ 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,
|
||||
self.st_object = (
|
||||
None
|
||||
if USE_YARA_X
|
||||
else 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:
|
||||
if USE_YARA_X:
|
||||
for match in self._rules.scan(data).matching_rules:
|
||||
for match_string in match.patterns:
|
||||
for instance in match_string.matches:
|
||||
yield (
|
||||
instance.offset + data_offset,
|
||||
match.rule,
|
||||
f"{match.namespace}.{match.identifier}",
|
||||
match_string.identifier,
|
||||
instance.matched_data,
|
||||
data[instance.offset : instance.offset + instance.length],
|
||||
)
|
||||
else:
|
||||
for offset, name, value in match.strings:
|
||||
yield (offset + data_offset, match.rule, name, value)
|
||||
else:
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def get_rule(rule):
|
||||
if USE_YARA_X:
|
||||
return yara_x.compile(f"rule r1 {{strings: $a = {rule} condition: $a}}")
|
||||
return yara.compile(
|
||||
sources={"n": f"rule r1 {{strings: $a = {rule} condition: $a}}"}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_compiled_file(filepath):
|
||||
with resources.ResourceAccessor().open(filepath, "rb") as fp:
|
||||
if USE_YARA_X:
|
||||
return yara_x.Rules.deserialize_from(file=fp)
|
||||
return yara.load(file=fp)
|
||||
|
||||
@staticmethod
|
||||
def from_file(filepath):
|
||||
with resources.ResourceAccessor().open(filepath, "rb") as fp:
|
||||
if USE_YARA_X:
|
||||
return yara_x.compile(fp.read().decode())
|
||||
return yara.compile(file=fp)
|
||||
|
||||
|
||||
class YaraScan(plugins.PluginInterface):
|
||||
"""Scans kernel memory using yara rules (string or file)."""
|
||||
|
||||
_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
|
||||
_version = (2, 0, 0)
|
||||
_yara_x = USE_YARA_X
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -99,10 +142,14 @@ 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
|
||||
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
|
||||
@@ -121,38 +168,28 @@ 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
|
||||
return not tuple(int(x) for x in yara.__version__.split(".")) < (4, 3)
|
||||
|
||||
@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(
|
||||
file=resources.ResourceAccessor().open(
|
||||
config["yara_compiled_file"], "rb"
|
||||
)
|
||||
rules = YaraScanner.get_rule(rule)
|
||||
elif config.get("yara_file") is not None:
|
||||
vollog.debug(f"Plain file: {config['yara_file']} - yara-x: {USE_YARA_X}")
|
||||
rules = YaraScanner.from_file(config["yara_file"])
|
||||
elif config.get("yara_compiled_file") is not None:
|
||||
vollog.debug(
|
||||
f"Compiled file: {config['yara_compiled_file']} - yara-x: {USE_YARA_X}"
|
||||
)
|
||||
rules = YaraScanner.from_compiled_file(config["yara_compiled_file"])
|
||||
else:
|
||||
vollog.error("No yara rules, nor yara rules file were specified")
|
||||
return rules
|
||||
|
||||
Reference in New Issue
Block a user