From e8fb8c929c49fef5cf0e1cde2a1b6269f1675a1a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Fri, 28 Aug 2020 02:32:04 +0100 Subject: [PATCH] Plugins: Introduce FileHandlerInterface --- doc/source/using-as-a-library.rst | 2 +- volatility/cli/__init__.py | 37 +++++--- volatility/cli/volshell/generic.py | 23 +++-- volatility/framework/automagic/__init__.py | 2 +- volatility/framework/interfaces/plugins.py | 85 ++++++++++++------- volatility/framework/plugins/__init__.py | 8 +- volatility/framework/plugins/configwriter.py | 5 +- volatility/framework/plugins/layerwriter.py | 15 ++-- volatility/framework/plugins/timeliner.py | 12 ++- .../framework/plugins/windows/cmdline.py | 2 +- .../framework/plugins/windows/dlllist.py | 37 ++++---- .../framework/plugins/windows/handles.py | 2 +- .../framework/plugins/windows/malfind.py | 2 +- .../framework/plugins/windows/memmap.py | 36 ++++---- .../framework/plugins/windows/modscan.py | 29 ++++--- .../framework/plugins/windows/modules.py | 31 ++++--- .../framework/plugins/windows/pslist.py | 44 +++++----- .../plugins/windows/registry/hivelist.py | 34 ++++---- .../framework/plugins/windows/strings.py | 2 +- .../framework/plugins/windows/svcscan.py | 2 +- .../framework/plugins/windows/vadinfo.py | 5 +- .../framework/plugins/windows/vadyarascan.py | 2 +- .../framework/plugins/windows/verinfo.py | 8 +- .../plugins/windows/registry/certificates.py | 9 +- volatility/plugins/windows/statistics.py | 1 + 25 files changed, 255 insertions(+), 180 deletions(-) diff --git a/doc/source/using-as-a-library.rst b/doc/source/using-as-a-library.rst index ac67d6d60..39fe5f4c4 100644 --- a/doc/source/using-as-a-library.rst +++ b/doc/source/using-as-a-library.rst @@ -168,7 +168,7 @@ be called whenever a plugin produces an auxiliary file. :: constructed = plugin(context, plugin_config_path, progress_callback = progress_callback) - constructed.set_file_consumer(file_consumer) + constructed.set_file_handler(file_consumer) The file_consumer must adhere to the :py:class:`~volatility.framework.interfaces.plugins.FileConsumerInterface`, which has a `consume_file` method that takes a :py:class:`~volatility.framework.interfaces.plugins.FileInterface` diff --git a/volatility/cli/__init__.py b/volatility/cli/__init__.py index 1ba5fa83e..25abd2df2 100644 --- a/volatility/cli/__init__.py +++ b/volatility/cli/__init__.py @@ -12,6 +12,7 @@ User interfaces make use of the framework to: """ import argparse import inspect +import io import json import logging import os @@ -66,7 +67,7 @@ class MuteProgress(PrintedProgress): pass -class CommandLine(interfaces.plugins.FileConsumerInterface): +class CommandLine: """Constructs a command-line interface object for users to run plugins.""" CLI_NAME = 'volatility' @@ -85,7 +86,7 @@ class CommandLine(interfaces.plugins.FileConsumerInterface): """Executes the command line module, taking the system arguments, determining the plugin to run and then running it.""" - volatility.framework.require_interface_version(1, 0, 0) + volatility.framework.require_interface_version(2, 0, 0) renderers = dict([(x.name.lower(), x) for x in framework.class_subclasses(text_renderer.CLIRenderer)]) @@ -296,7 +297,8 @@ class CommandLine(interfaces.plugins.FileConsumerInterface): if args.quiet: progress_callback = MuteProgress() - constructed = plugins.construct_plugin(ctx, automagics, plugin, base_config_path, progress_callback, self) + constructed = plugins.construct_plugin(ctx, automagics, plugin, base_config_path, progress_callback, + self.file_handler_class_factory()) if args.write_config: vollog.debug("Writing out configuration data to config.json") @@ -449,22 +451,35 @@ class CommandLine(interfaces.plugins.FileConsumerInterface): extended_path = interfaces.configuration.path_join(config_path, requirement.name) context.config[extended_path] = value - def consume_file(self, filedata: interfaces.plugins.FileInterface): - """Consumes a file as produced by a plugin.""" - if self.output_dir is None: - raise TypeError("Output directory is not a string") - os.makedirs(self.output_dir, exist_ok = True) + def file_handler_class_factory(self): + output_dir = self.output_dir - pref_name_array = filedata.preferred_filename.split('.') - filename, extension = os.path.join(self.output_dir, '.'.join(pref_name_array[:-1])), pref_name_array[-1] + class CLIFileHandler(io.BytesIO, interfaces.plugins.FileHandlerInterface): + def __init__(self, filename: str, immediate_commit: bool = False): + io.BytesIO.__init__(self) + interfaces.plugins.FileHandlerInterface.__init__(self, filename, immediate_commit) + + def close(self): + if self.closed: + return + + if output_dir is None: + raise TypeError("Output directory is not a string") + os.makedirs(output_dir, exist_ok = True) + + pref_name_array = self.preferred_filename.split('.') + filename, extension = os.path.join(output_dir, '.'.join(pref_name_array[:-1])), pref_name_array[-1] output_filename = "{}.{}".format(filename, extension) if not os.path.exists(output_filename): with open(output_filename, "wb") as current_file: - current_file.write(filedata.data.getvalue()) + current_file.write(self.read()) vollog.log(logging.INFO, "Saved stored plugin file: {}".format(output_filename)) else: vollog.warning("Refusing to overwrite an existing file: {}".format(output_filename)) + super().close() + + return CLIFileHandler def populate_requirements_argparse(self, parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup], configurable: Type[interfaces.configuration.ConfigurableInterface]): diff --git a/volatility/cli/volshell/generic.py b/volatility/cli/volshell/generic.py index d3bfeec27..49ee2c5b5 100644 --- a/volatility/cli/volshell/generic.py +++ b/volatility/cli/volshell/generic.py @@ -3,6 +3,7 @@ # import binascii import code +import io import random import string import struct @@ -263,10 +264,6 @@ class Volshell(interfaces.plugins.PluginInterface): else: return hex(value.vol.offset) - def consume_file(self, file: interfaces.plugins.FileInterface) -> None: - """Dummy file consumer to satisfy the interface""" - pass - def generate_treegrid(self, plugin: Type[interfaces.plugins.PluginInterface], **kwargs) -> Optional[interfaces.renderers.TreeGrid]: """Generates a TreeGrid based on a specific plugin passing in kwarg configuration values""" @@ -281,7 +278,7 @@ class Volshell(interfaces.plugins.PluginInterface): self.config[path_join(plugin_config_suffix, plugin.__name__, name)] = value try: - constructed = plugins.construct_plugin(self.context, [], plugin, plugin_path, None, NullFileConsumer()) + constructed = plugins.construct_plugin(self.context, [], plugin, plugin_path, None, NullFileHandler()) return constructed.run() except exceptions.UnsatisfiedException as excp: print("Unable to validate the plugin requirements: {}\n".format([x for x in excp.unsatisfied])) @@ -320,9 +317,17 @@ class Volshell(interfaces.plugins.PluginInterface): print(" " * (longest_offset - len_offset), hex(symbol.address), " ", symbol.name) -class NullFileConsumer(interfaces.plugins.FileConsumerInterface): - """Null FileConsumer that swallows files whole""" +class NullFileHandler(io.BytesIO, interfaces.plugins.FileHandlerInterface): + """Null FileHandler that swallows files whole without consuming memory""" - def consume_file(self, file: interfaces.plugins.FileInterface) -> None: - """Dummy file consumer to satisfy the FileConsumerInterface""" + def __init__(self, preferred_name: str, immediate_commit: bool = False): + interfaces.plugins.FileHandlerInterface.__init__(self, preferred_name, immediate_commit) + super().__init__() + + def writelines(self, lines): + """Dummy method""" pass + + def write(self, data): + """Dummy method""" + return len(data) diff --git a/volatility/framework/automagic/__init__.py b/volatility/framework/automagic/__init__.py index 2541214a0..6d864de4f 100644 --- a/volatility/framework/automagic/__init__.py +++ b/volatility/framework/automagic/__init__.py @@ -44,7 +44,7 @@ def available(context: interfaces.context.ContextInterface) -> List[interfaces.a clazz(context, interfaces.configuration.path_join(config_path, clazz.__name__)) for clazz in class_subclasses(interfaces.automagic.AutomagicInterface) ], - key = lambda x: x.priority) + key = lambda x: x.priority) def choose_automagic( diff --git a/volatility/framework/interfaces/plugins.py b/volatility/framework/interfaces/plugins.py index ff7dcb5f3..c0bb7fac3 100644 --- a/volatility/framework/interfaces/plugins.py +++ b/volatility/framework/interfaces/plugins.py @@ -8,10 +8,10 @@ using objects constructed from symbols. """ # Configuration interfaces must be imported separately, since we're part of interfaces and can't import ourselves -import io import logging +import os from abc import ABCMeta, abstractmethod -from typing import List, Optional, Tuple +from typing import List, Tuple, Type, IO from volatility import framework from volatility.framework import exceptions, constants, interfaces @@ -19,38 +19,61 @@ from volatility.framework import exceptions, constants, interfaces vollog = logging.getLogger(__name__) -class FileInterface(metaclass = ABCMeta): +class FileHandlerInterface(IO[bytes]): """Class for storing Files in the plugin as a means to output a file or files when necessary.""" - def __init__(self, filename: str, data: bytes = None) -> None: - """ + def __init__(self, filename: str, immediate_commit: bool = False) -> None: + """Creates a FileTemplate Args: filename: The requested name of the filename for the data - data: The data to be stored in a file """ + self._immediate_commit = immediate_commit + self._committed = False + self._preferred_filename = None self.preferred_filename = filename - if data is None: - data = b'' - self.data = io.BytesIO(data) + super().__init__() + @property + def committed(self): + return self._committed -class FileConsumerInterface(object): - """Class for consuming files potentially produced by plugins. + @property + def preferred_filename(self): + return self._preferred_filename - We use the producer/consumer model to ensure we can avoid running - out of memory by storing every file produced. The downside is, we - can't provide much feedback to the producer about what happened to - their file (other than exceptions). - """ + @preferred_filename.setter + def preferred_filename(self, filename): + """Sets the preferred filename""" + if self._committed: + raise IOError + if not isinstance(filename, str): + raise TypeError("FileTemplateInterface preferred filenames must be strings") + if os.path.sep in filename: + raise ValueError("FileTemplateInterface filenames cannot contain path separators") + self._preferred_filename = filename - def consume_file(self, file: FileInterface) -> None: - """Consumes a file as passed back to a UI by a plugin. + def __enter__(self): + return self - Args: - file: A FileInterface object with the data to write to a file + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is None and exc_value is None and traceback is None: + self.close() + if self._immediate_commit: + self.commit() + else: + vollog.warning("File {} could not be written: {}".format(self._preferred_filename, str(exc_value))) + self.close() + + def commit(self): + """Commits the file to whatever medium is necessary, the file cannot be altered after this point + nor can its preferred_name be chaned + + This also ensures that a UI can determine when a file is fully complete rather than partially written """ + self.close() + self._committed = True # @@ -105,21 +128,21 @@ class PluginInterface(interfaces.configuration.ConfigurableInterface, if requirement.name not in self.config: self.config[requirement.name] = requirement.default - self._file_consumer = None # type: Optional[FileConsumerInterface] + self._file_handler = FileHandlerInterface # type: Type[FileHandlerInterface] framework.require_interface_version(*self._required_framework_version) - def set_file_consumer(self, consumer: FileConsumerInterface) -> None: - """Sets the file consumer to be used by this plugin.""" - self._file_consumer = consumer + def open(self, preferred_filename: str) -> FileHandlerInterface: + """Opens a file for output""" + if self._file_handler is not None: + return self._file_handler(preferred_filename) + raise IOError("FileTemplate not specified for this plugin") - def produce_file(self, filedata: FileInterface) -> None: - """Adds a file to the plugin's file store and returns the chosen - filename for the file.""" - if self._file_consumer: - self._file_consumer.consume_file(filedata) - else: - vollog.debug("No file consumer specified to consume: {}".format(filedata.preferred_filename)) + def set_file_handler(self, handler: Type[FileHandlerInterface]) -> None: + """Sets the file handler to be used by this plugin.""" + if not issubclass(handler, FileHandlerInterface): + raise ValueError("FileHandler must be a subclass of FileHandlerInterface") + self._file_handler = handler @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: diff --git a/volatility/framework/plugins/__init__.py b/volatility/framework/plugins/__init__.py index 84c8d465c..1418aefbb 100644 --- a/volatility/framework/plugins/__init__.py +++ b/volatility/framework/plugins/__init__.py @@ -19,7 +19,7 @@ def construct_plugin(context: interfaces.context.ContextInterface, automagics: List[interfaces.automagic.AutomagicInterface], plugin: Type[interfaces.plugins.PluginInterface], base_config_path: str, progress_callback: constants.ProgressCallback, - file_consumer: interfaces.plugins.FileConsumerInterface) -> interfaces.plugins.PluginInterface: + file_handler: Type[interfaces.plugins.FileHandlerInterface]) -> interfaces.plugins.PluginInterface: """Constructs a plugin object based on the parameters. Clever magic figures out how to fulfill each requirement that might not be fulfilled @@ -30,7 +30,7 @@ def construct_plugin(context: interfaces.context.ContextInterface, plugin: The plugin to run base_config_path: The path within the context's config containing the plugin's configuration progress_callback: Callback function to provide feedback for ongoing processes - file_consumer: Object to pass any generated files to + file_handler: Object to pass any generated files to Returns: The constructed plugin object @@ -49,6 +49,6 @@ def construct_plugin(context: interfaces.context.ContextInterface, raise exceptions.UnsatisfiedException(unsatisfied) constructed = plugin(context, plugin_config_path, progress_callback = progress_callback) - if file_consumer: - constructed.set_file_consumer(file_consumer) + if file_handler: + constructed.set_file_handler(file_handler) return constructed diff --git a/volatility/framework/plugins/configwriter.py b/volatility/framework/plugins/configwriter.py index 479dc3230..b918f4fc9 100644 --- a/volatility/framework/plugins/configwriter.py +++ b/volatility/framework/plugins/configwriter.py @@ -39,9 +39,8 @@ class ConfigWriter(plugins.PluginInterface): config = dict(self.context.config) filename = "config.extra" try: - filedata = plugins.FileInterface(filename) - filedata.data.write(bytes(json.dumps(config, sort_keys = True, indent = 2), 'raw_unicode_escape')) - self.produce_file(filedata) + with self._file_handler(filename, True) as filedata: + filedata.write(bytes(json.dumps(config, sort_keys = True, indent = 2), 'raw_unicode_escape')) except Exception: vollog.warning("Unable to JSON encode configuration") diff --git a/volatility/framework/plugins/layerwriter.py b/volatility/framework/plugins/layerwriter.py index 68839bb6b..92a7d7849 100644 --- a/volatility/framework/plugins/layerwriter.py +++ b/volatility/framework/plugins/layerwriter.py @@ -4,7 +4,7 @@ import logging import os -from typing import List, Optional +from typing import List, Optional, Type from volatility.framework import renderers, interfaces, constants, exceptions from volatility.framework.configuration import requirements @@ -43,8 +43,10 @@ class LayerWriter(plugins.PluginInterface): context: interfaces.context.ContextInterface, layer_name: str, preferred_name: str, + file_handler: Type[plugins.FileHandlerInterface], chunk_size: Optional[int] = None, - progress_callback: Optional[constants.ProgressCallback] = None) -> Optional[plugins.FileInterface]: + progress_callback: Optional[constants.ProgressCallback] = None) -> Optional[ + plugins.FileHandlerInterface]: """Produces a filedata from the named layer in the provided context Args: @@ -62,11 +64,11 @@ class LayerWriter(plugins.PluginInterface): if chunk_size is None: chunk_size = cls.default_block_size - filedata = plugins.FileInterface(preferred_name) + filedata = file_handler(preferred_name) for i in range(0, layer.maximum_address, chunk_size): current_chunk_size = min(chunk_size, layer.maximum_address - i) data = layer.read(i, current_chunk_size, pad = True) - filedata.data.write(data) + filedata.write(data) if progress_callback: progress_callback((i / layer.maximum_address) * 100, 'Writing layer {}'.format(layer_name)) return filedata @@ -80,9 +82,10 @@ class LayerWriter(plugins.PluginInterface): output_name = self.config.get('output', self.default_output_name) try: filedata = self.write_layer(self.context, self.config['primary'], output_name, + self._file_handler, self.config.get('block_size', self.default_block_size), - self._progress_callback) - self.produce_file(filedata) + progress_callback = self._progress_callback) + filedata.commit() except IOError as excp: yield 0, ('Layer cannot be written to {}: {}'.format(self.config['output_name'], excp),) diff --git a/volatility/framework/plugins/timeliner.py b/volatility/framework/plugins/timeliner.py index 78aa622d2..97fb28738 100644 --- a/volatility/framework/plugins/timeliner.py +++ b/volatility/framework/plugins/timeliner.py @@ -138,8 +138,8 @@ class Timeliner(interfaces.plugins.PluginInterface): # Write out a body file if necessary if self.config.get('create-bodyfile', True): - filedata = interfaces.plugins.FileInterface("volatility.body") - with io.TextIOWrapper(filedata.data, write_through = True) as fp: + filedata = self._file_handler("volatility.body", True) + with io.TextIOWrapper(filedata, write_through = True) as fp: for (plugin_name, item) in self.timeline: times = self.timeline[(plugin_name, item)] # Body format is: MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime @@ -151,7 +151,6 @@ class Timeliner(interfaces.plugins.PluginInterface): self._text_format(times.get(TimeLinerType.MODIFIED, "")), self._text_format(times.get(TimeLinerType.CHANGED, "")), self._text_format(times.get(TimeLinerType.CREATED, "")))) - self.produce_file(filedata) def _sanitize_body_format(self, value): return value.replace("|", "_") @@ -185,7 +184,7 @@ class Timeliner(interfaces.plugins.PluginInterface): automagics = automagic.choose_automagic(self.automagics, plugin_class) plugin = plugins.construct_plugin(self.context, automagics, plugin_class, self.config_path, - self._progress_callback, self._file_template) + self._progress_callback, self._file_handler) if isinstance(plugin, TimeLinerInterface): if not len(filter_list) or any( @@ -203,10 +202,9 @@ class Timeliner(interfaces.plugins.PluginInterface): for entry in old_dict: total_config[interfaces.configuration.path_join(plugin.__class__.__name__, entry)] = old_dict[entry] - filedata = interfaces.plugins.FileInterface("config.json") - with io.TextIOWrapper(filedata.data, write_through = True) as fp: + filedata = self._file_handler("config.json", True) + with io.TextIOWrapper(filedata, write_through = True) as fp: json.dump(total_config, fp, sort_keys = True, indent = 2) - self.produce_file(filedata) return renderers.TreeGrid(columns = [("Plugin", str), ("Description", str), ("Created Date", datetime.datetime), ("Modified Date", datetime.datetime), ("Accessed Date", datetime.datetime), diff --git a/volatility/framework/plugins/windows/cmdline.py b/volatility/framework/plugins/windows/cmdline.py index 30bca053b..d27c8513d 100644 --- a/volatility/framework/plugins/windows/cmdline.py +++ b/volatility/framework/plugins/windows/cmdline.py @@ -26,7 +26,7 @@ class CmdLine(interfaces.plugins.PluginInterface): description = 'Memory layer for the kernel', architectures = ["Intel32", "Intel64"]), requirements.SymbolTableRequirement(name = "nt_symbols", description = "Windows kernel symbols"), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (1, 0, 0)), + requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), requirements.ListRequirement(name = 'pid', element_type = int, description = "Process IDs to include (all other processes are excluded)", diff --git a/volatility/framework/plugins/windows/dlllist.py b/volatility/framework/plugins/windows/dlllist.py index c40ad1db9..a6e968efe 100644 --- a/volatility/framework/plugins/windows/dlllist.py +++ b/volatility/framework/plugins/windows/dlllist.py @@ -21,7 +21,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists the loaded modules in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -31,7 +31,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): description = 'Memory layer for the kernel', architectures = ["Intel32", "Intel64"]), requirements.SymbolTableRequirement(name = "nt_symbols", description = "Windows kernel symbols"), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (1, 0, 0)), + requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), requirements.VersionRequirement(name = 'info', component = info.Info, version = (1, 0, 0)), requirements.ListRequirement(name = 'pid', element_type = int, @@ -48,7 +48,8 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): context: interfaces.context.ContextInterface, pe_table_name: str, dll_entry: interfaces.objects.ObjectInterface, - layer_name: str = None) -> interfaces.plugins.FileInterface: + file_handler: Type[interfaces.plugins.FileHandlerInterface], + layer_name: str = None) -> interfaces.plugins.FileHandlerInterface: """Extracts the complete data for a process as a FileInterface Args: @@ -56,6 +57,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): pe_table_name: the name for the symbol table containing the PE format symbols dll_entry: the object representing the module layer_name: the layer that the DLL lives within + file_handler: class for constructing output files Returns: A FileInterface object containing the complete data for the DLL or None in the case of failure""" @@ -69,17 +71,21 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): if layer_name is None: layer_name = dll_entry.vol.layer_name - filedata = interfaces.plugins.FileInterface("{0}.{1:#x}.{2:#x}.dmp".format( - ntpath.basename(name), dll_entry.vol.offset, dll_entry.DllBase)) + filedata = file_handler( + "{0}.{1}.{2:#x}.{3:#x}.dmp".format(layer_name, ntpath.basename(name), dll_entry.vol.offset, + dll_entry.DllBase), True) dos_header = context.object(pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", offset = dll_entry.DllBase, layer_name = layer_name) - for offset, data in dos_header.reconstruct(): - filedata.data.seek(offset) - filedata.data.write(data) - except Exception as excp: + with file_handler("{0}.{1}.{2:#x}.{3:#x}.dmp".format(layer_name, ntpath.basename(name), + dll_entry.vol.offset, + dll_entry.DllBase)): + for offset, data in dos_header.reconstruct(): + filedata.seek(offset) + filedata.write(data) + except (IOError, exceptions.VolatilityException) as excp: vollog.debug("Unable to dump dll at offset {}: {}".format(dll_entry.DllBase, excp)) return filedata @@ -120,13 +126,14 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): else: DllLoadTime = renderers.NotApplicableValue() - dumped = False + file_output = "Disabled" if self.config['dump']: - filedata = self.dump_pe(self.context, pe_table_name, entry, proc_layer_name) - if filedata: - filedata.preferred_filename = "pid.{0}.".format(proc_id) + filedata.preferred_filename - dumped = True - self.produce_file(filedata) + filedata = self.dump_pe(self.context, pe_table_name, entry, self._file_handler, + proc_layer_name) + if filedata and filedata.committed: + file_output = filedata.preferred_filename + else: + file_output = "Error outputting file" yield (0, (proc.UniqueProcessId, proc.ImageFileName.cast("string", diff --git a/volatility/framework/plugins/windows/handles.py b/volatility/framework/plugins/windows/handles.py index 9bb008696..18361d3a0 100644 --- a/volatility/framework/plugins/windows/handles.py +++ b/volatility/framework/plugins/windows/handles.py @@ -46,7 +46,7 @@ class Handles(interfaces.plugins.PluginInterface): element_type = int, description = "Process IDs to include (all other processes are excluded)", optional = True), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (1, 0, 0)) + requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)) ] def _decode_pointer(self, value, magic): diff --git a/volatility/framework/plugins/windows/malfind.py b/volatility/framework/plugins/windows/malfind.py index d485ebc19..240768f68 100644 --- a/volatility/framework/plugins/windows/malfind.py +++ b/volatility/framework/plugins/windows/malfind.py @@ -35,7 +35,7 @@ class Malfind(interfaces.plugins.PluginInterface): description = "Extract injected VADs", default = False, optional = True), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (1, 1, 0)), + requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), requirements.VersionRequirement(name = 'vadinfo', component = vadinfo.VadInfo, version = (1, 1, 0)) ] diff --git a/volatility/framework/plugins/windows/memmap.py b/volatility/framework/plugins/windows/memmap.py index 1c91c51ef..78f30e2ed 100644 --- a/volatility/framework/plugins/windows/memmap.py +++ b/volatility/framework/plugins/windows/memmap.py @@ -25,7 +25,7 @@ class Memmap(interfaces.plugins.PluginInterface): description = 'Memory layer for the kernel', architectures = ["Intel32", "Intel64"]), requirements.SymbolTableRequirement(name = "nt_symbols", description = "Windows kernel symbols"), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (1, 0, 0)), + requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), requirements.IntRequirement(name = 'pid', description = "Process ID to include (all other processes are excluded)", optional = True), @@ -48,34 +48,38 @@ class Memmap(interfaces.plugins.PluginInterface): excp.layer_name)) continue - filedata = interfaces.plugins.FileInterface("pid.{}.dmp".format(pid)) + filedata = self._file_handler("pid.{}.dmp".format(pid)) for mapval in proc_layer.mapping(0x0, proc_layer.maximum_address, ignore_errors = True): offset, size, mapped_offset, mapped_size, maplayer = mapval - dumped = False + file_output = "Disabled" if self.config['dump']: try: data = proc_layer.read(offset, size, pad = True) - filedata.data.write(data) - dumped = True + filedata.write(data) + file_output = filedata.preferred_filename except exceptions.InvalidAddressException: + file_output = "Error outputting file" vollog.debug("Unable to write {}'s address {} to {}.dmp".format( proc_layer_name, offset, filedata.preferred_filename)) - yield (0, (format_hints.Hex(offset), format_hints.Hex(mapped_offset), format_hints.Hex(mapped_size), - format_hints.Hex(offset), dumped)) + yield (0, ( + format_hints.Hex(offset), + format_hints.Hex(mapped_offset), + format_hints.Hex(mapped_size), + format_hints.Hex(offset), + file_output)) offset += mapped_size - self.produce_file(filedata) - def run(self): filter_func = pslist.PsList.create_pid_filter([self.config.get('pid', None)]) - return renderers.TreeGrid([("Virtual", format_hints.Hex), ("Physical", format_hints.Hex), - ("Size", format_hints.Hex), ("Offset", format_hints.Hex), ("Dumped", bool)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = self.config['primary'], - symbol_table = self.config['nt_symbols'], - filter_func = filter_func))) + return renderers.TreeGrid( + [("Virtual", format_hints.Hex), ("Physical", format_hints.Hex), ("Size", format_hints.Hex), + ("Offset", format_hints.Hex), ("File output", str)], + self._generator( + pslist.PsList.list_processes(context = self.context, + layer_name = self.config['primary'], + symbol_table = self.config['nt_symbols'], + filter_func = filter_func))) diff --git a/volatility/framework/plugins/windows/modscan.py b/volatility/framework/plugins/windows/modscan.py index dbd8a5649..41df7bad3 100644 --- a/volatility/framework/plugins/windows/modscan.py +++ b/volatility/framework/plugins/windows/modscan.py @@ -8,7 +8,7 @@ from volatility.framework import renderers, interfaces, exceptions from volatility.framework.configuration import requirements from volatility.framework.renderers import format_hints from volatility.framework.symbols import intermed -from volatility.framework.symbols.windows import extensions +from volatility.framework.symbols.windows.extensions import pe from volatility.plugins.windows import poolscanner, dlllist @@ -27,7 +27,7 @@ class ModScan(interfaces.plugins.PluginInterface): requirements.VersionRequirement(name = 'poolerscanner', component = poolscanner.PoolScanner, version = (1, 0, 0)), - requirements.VersionRequirement(name = 'dlllist', component = dlllist.DllList, version = (1, 0, 0)), + requirements.VersionRequirement(name = 'dlllist', component = dlllist.DllList, version = (2, 0, 0)), requirements.BooleanRequirement(name = 'dump', description = "Extract listed modules", default = False, @@ -63,7 +63,7 @@ class ModScan(interfaces.plugins.PluginInterface): self.config_path, "windows", "pe", - class_types = extensions.pe.class_types) + class_types = pe.class_types) for mod in self.scan_modules(self.context, self.config['primary'], self.config['nt_symbols']): @@ -77,16 +77,23 @@ class ModScan(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: FullDllName = "" - dumped = False + file_output = "Disabled" if self.config['dump']: - filedata = dlllist.DllList.dump_pe(self.context, pe_table_name, mod) - if filedata: - self.produce_file(filedata) - dumped = True + filedata = dlllist.DllList.dump_pe(self.context, pe_table_name, mod, self._file_handler) + if filedata and filedata.committed: + file_output = filedata.preferred_filename + else: + file_output = "Error outputting file" - yield (0, (format_hints.Hex(mod.vol.offset), format_hints.Hex(mod.DllBase), - format_hints.Hex(mod.SizeOfImage), BaseDllName, FullDllName, dumped)) + yield (0, ( + format_hints.Hex(mod.vol.offset), + format_hints.Hex(mod.DllBase), + format_hints.Hex(mod.SizeOfImage), + BaseDllName, + FullDllName, + file_output + )) def run(self): return renderers.TreeGrid([("Offset", format_hints.Hex), ("Base", format_hints.Hex), ("Size", format_hints.Hex), - ("Name", str), ("Path", str), ("Dumped", bool)], self._generator()) + ("Name", str), ("Path", str), ("File output", str)], self._generator()) diff --git a/volatility/framework/plugins/windows/modules.py b/volatility/framework/plugins/windows/modules.py index 6afdccc5a..531b05f59 100644 --- a/volatility/framework/plugins/windows/modules.py +++ b/volatility/framework/plugins/windows/modules.py @@ -10,7 +10,7 @@ from volatility.framework import renderers from volatility.framework.configuration import requirements from volatility.framework.renderers import format_hints from volatility.framework.symbols import intermed -from volatility.framework.symbols.windows import extensions +from volatility.framework.symbols.windows.extensions import pe from volatility.plugins.windows import pslist, dlllist vollog = logging.getLogger(__name__) @@ -29,8 +29,8 @@ class Modules(interfaces.plugins.PluginInterface): description = 'Memory layer for the kernel', architectures = ["Intel32", "Intel64"]), requirements.SymbolTableRequirement(name = "nt_symbols", description = "Windows kernel symbols"), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (1, 1, 0)), - requirements.VersionRequirement(name = 'dlllist', component = dlllist.DllList, version = (1, 0, 0)), + requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), + requirements.VersionRequirement(name = 'dlllist', component = dlllist.DllList, version = (2, 0, 0)), requirements.BooleanRequirement(name = 'dump', description = "Extract listed modules", default = False, @@ -42,7 +42,7 @@ class Modules(interfaces.plugins.PluginInterface): self.config_path, "windows", "pe", - class_types = extensions.pe.class_types) + class_types = pe.class_types) for mod in self.list_modules(self.context, self.config['primary'], self.config['nt_symbols']): @@ -56,15 +56,22 @@ class Modules(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: FullDllName = "" - dumped = False + file_output = "Disabled" if self.config['dump']: - filedata = dlllist.DllList.dump_pe(self.context, pe_table_name, mod) - if filedata: - self.produce_file(filedata) - dumped = True + filedata = dlllist.DllList.dump_pe(self.context, pe_table_name, mod, self._file_handler) + if filedata and filedata.committed: + file_output = filedata.preferred_filename + else: + file_output = "Error outputting file" - yield (0, (format_hints.Hex(mod.vol.offset), format_hints.Hex(mod.DllBase), - format_hints.Hex(mod.SizeOfImage), BaseDllName, FullDllName, dumped)) + yield (0, ( + format_hints.Hex(mod.vol.offset), + format_hints.Hex(mod.DllBase), + format_hints.Hex(mod.SizeOfImage), + BaseDllName, + FullDllName, + file_output + )) @classmethod def get_session_layers(cls, @@ -175,4 +182,4 @@ class Modules(interfaces.plugins.PluginInterface): def run(self): return renderers.TreeGrid([("Offset", format_hints.Hex), ("Base", format_hints.Hex), ("Size", format_hints.Hex), - ("Name", str), ("Path", str), ("Dumped", bool)], self._generator()) + ("Name", str), ("Path", str), ("File output", str)], self._generator()) diff --git a/volatility/framework/plugins/windows/pslist.py b/volatility/framework/plugins/windows/pslist.py index 4a1cb494a..a784f387a 100644 --- a/volatility/framework/plugins/windows/pslist.py +++ b/volatility/framework/plugins/windows/pslist.py @@ -4,14 +4,14 @@ import datetime import logging -from typing import Callable, Iterable, List +from typing import Callable, Iterable, List, Type from volatility.framework import renderers, interfaces, layers, constants from volatility.framework.configuration import requirements from volatility.framework.objects import utility from volatility.framework.renderers import format_hints from volatility.framework.symbols import intermed -from volatility.framework.symbols.windows import extensions +from volatility.framework.symbols.windows.extensions import pe from volatility.plugins import timeliner vollog = logging.getLogger(__name__) @@ -21,7 +21,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists the processes present in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (1, 1, 0) + _version = (2, 0, 0) PHYSICAL_DEFAULT = False @classmethod @@ -46,18 +46,23 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ] @classmethod - def process_dump(cls, context: interfaces.context.ContextInterface, kernel_table_name: str, pe_table_name: str, - proc: interfaces.objects.ObjectInterface) -> interfaces.plugins.FileInterface: - """Extracts the complete data for a process as a FileInterface + def process_dump(cls, + context: interfaces.context.ContextInterface, + kernel_table_name: str, pe_table_name: str, + proc: interfaces.objects.ObjectInterface, + file_handler: Type[ + interfaces.plugins.FileHandlerInterface]) -> interfaces.plugins.FileHandlerInterface: + """Extracts the complete data for a process as a FileHandlerInterface Args: context: the context to operate upon kernel_table_name: the name for the symbol table containing the kernel's symbols pe_table_name: the name for the symbol table containing the PE format symbols proc: the process object whose memory should be output + file_handler: class to write construct for writing the file Returns: - A FileInterface object containing the complete data for the process or None in the case of failure + A FileHandlerInterface object containing the complete data for the process or None in the case of failure """ filedata = None @@ -70,11 +75,10 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): dos_header = context.object(pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", offset = peb.ImageBaseAddress, layer_name = proc_layer_name) - filedata = interfaces.plugins.FileInterface("pid.{0}.{1:#x}.dmp".format(proc.UniqueProcessId, - peb.ImageBaseAddress)) + filedata = file_handler("pid.{0}.{1:#x}.dmp".format(proc.UniqueProcessId, peb.ImageBaseAddress)) for offset, data in dos_header.reconstruct(): - filedata.data.seek(offset) - filedata.data.write(data) + filedata.seek(offset) + filedata.write(data) except Exception as excp: vollog.debug("Unable to dump PE with pid {}: {}".format(proc.UniqueProcessId, excp)) @@ -168,7 +172,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): self.config_path, "windows", "pe", - class_types = extensions.pe.class_types) + class_types = pe.class_types) memory = self.context.layers[self.config['primary']] if not isinstance(memory, layers.intel.Intel): @@ -184,17 +188,19 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): else: (_, _, offset, _, _) = list(memory.mapping(offset = proc.vol.offset, length = 0))[0] - dumped = False + file_output = "Disabled" if self.config['dump']: - filedata = self.process_dump(self.context, self.config['nt_symbols'], pe_table_name, proc) - if filedata: - dumped = True - self.produce_file(filedata) + filedata = self.process_dump(self.context, self.config['nt_symbols'], pe_table_name, proc, + self._file_handler) + if filedata and filedata.committed: + file_output = filedata.preferred_filename + else: + file_output = "Error outputting file" yield (0, (proc.UniqueProcessId, proc.InheritedFromUniqueProcessId, proc.ImageFileName.cast("string", max_length = proc.ImageFileName.vol.count, errors = 'replace'), format_hints.Hex(offset), proc.ActiveThreads, proc.get_handle_count(), proc.get_session_id(), - proc.get_is_wow64(), proc.get_create_time(), proc.get_exit_time(), dumped)) + proc.get_is_wow64(), proc.get_create_time(), proc.get_exit_time(), file_output)) def generate_timeline(self): for row in self._generator(): @@ -210,4 +216,4 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ("Offset{0}".format(offsettype), format_hints.Hex), ("Threads", int), ("Handles", int), ("SessionId", int), ("Wow64", bool), ("CreateTime", datetime.datetime), ("ExitTime", datetime.datetime), - ("Dumped", bool)], self._generator()) + ("File output", str)], self._generator()) diff --git a/volatility/framework/plugins/windows/registry/hivelist.py b/volatility/framework/plugins/windows/registry/hivelist.py index d1bac638e..12313b654 100644 --- a/volatility/framework/plugins/windows/registry/hivelist.py +++ b/volatility/framework/plugins/windows/registry/hivelist.py @@ -6,7 +6,6 @@ from typing import Iterator, List, Tuple, Iterable, Optional from volatility.framework import renderers, interfaces, exceptions from volatility.framework.configuration import requirements -from volatility.framework.interfaces import plugins from volatility.framework.layers import registry from volatility.framework.renderers import format_hints from volatility.plugins.windows.registry import hivescan @@ -71,7 +70,7 @@ class HiveList(interfaces.plugins.PluginInterface): symbol_table = self.config["nt_symbols"], filter_string = self.config.get('filter', None)): - dumped = False + file_output = "Disabled" if self.config['dump']: # Construct the hive hive = next( @@ -83,23 +82,22 @@ class HiveList(interfaces.plugins.PluginInterface): maxaddr = hive.hive.Storage[0].Length hive_name = self._sanitize_hive_name(hive.get_name()) - filedata = plugins.FileInterface('registry.{}.{}.hive'.format(hive_name, hex(hive.hive_offset))) - if hive._base_block: - hive_data = self.context.layers[hive.dependencies[0]].read(hive.hive.BaseBlock, 1 << 12) - else: - hive_data = '\x00' * (1 << 12) - filedata.data.write(hive_data) + with self._file_handler('registry.{}.{}.hive'.format(hive_name, hex(hive.hive_offset))) as filedata: + if hive._base_block: + hive_data = self.context.layers[hive.dependencies[0]].read(hive.hive.BaseBlock, 1 << 12) + else: + hive_data = '\x00' * (1 << 12) + filedata.write(hive_data) - for i in range(0, maxaddr, chunk_size): - current_chunk_size = min(chunk_size, maxaddr - i) - data = hive.read(i, current_chunk_size, pad = True) - filedata.data.write(data) - # if self._progress_callback: - # self._progress_callback((i / maxaddr) * 100, 'Writing layer {}'.format(hive_name)) - self.produce_file(filedata) - dumped = True + for i in range(0, maxaddr, chunk_size): + current_chunk_size = min(chunk_size, maxaddr - i) + data = hive.read(i, current_chunk_size, pad = True) + filedata.write(data) + # if self._progress_callback: + # self._progress_callback((i / maxaddr) * 100, 'Writing layer {}'.format(hive_name)) + file_output = filedata.preferred_filename - yield (0, (format_hints.Hex(hive_object.vol.offset), hive_object.get_name() or "", dumped)) + yield (0, (format_hints.Hex(hive_object.vol.offset), hive_object.get_name() or "", file_output)) @classmethod def list_hives(cls, @@ -238,5 +236,5 @@ class HiveList(interfaces.plugins.PluginInterface): hex(hive.vol.offset))) def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid([("Offset", format_hints.Hex), ("FileFullPath", str), ("Dumped", bool)], + return renderers.TreeGrid([("Offset", format_hints.Hex), ("FileFullPath", str), ("File output", str)], self._generator()) diff --git a/volatility/framework/plugins/windows/strings.py b/volatility/framework/plugins/windows/strings.py index 9c958fb1c..90fb82df8 100644 --- a/volatility/framework/plugins/windows/strings.py +++ b/volatility/framework/plugins/windows/strings.py @@ -25,7 +25,7 @@ class Strings(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (1, 0, 0)), + requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), requirements.TranslationLayerRequirement(name = 'primary', description = 'Memory layer for the kernel', architectures = ["Intel32", "Intel64"]), diff --git a/volatility/framework/plugins/windows/svcscan.py b/volatility/framework/plugins/windows/svcscan.py index c9a125a7d..8c9866631 100644 --- a/volatility/framework/plugins/windows/svcscan.py +++ b/volatility/framework/plugins/windows/svcscan.py @@ -31,7 +31,7 @@ class SvcScan(interfaces.plugins.PluginInterface): description = 'Memory layer for the kernel', architectures = ["Intel32", "Intel64"]), requirements.SymbolTableRequirement(name = "nt_symbols", description = "Windows kernel symbols"), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (1, 0, 0)), + requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), requirements.PluginRequirement(name = 'poolscanner', plugin = poolscanner.PoolScanner, version = (1, 0, 0)), requirements.PluginRequirement(name = 'vadyarascan', plugin = vadyarascan.VadYaraScan, version = (1, 0, 0)) ] diff --git a/volatility/framework/plugins/windows/vadinfo.py b/volatility/framework/plugins/windows/vadinfo.py index 63aab0560..63bcecf03 100644 --- a/volatility/framework/plugins/windows/vadinfo.py +++ b/volatility/framework/plugins/windows/vadinfo.py @@ -5,7 +5,7 @@ import logging from typing import Callable, List, Generator, Iterable, Optional -from volatility.framework import renderers, interfaces, exceptions +from volatility.framework import renderers, interfaces from volatility.framework.configuration import requirements from volatility.framework.objects import utility from volatility.framework.renderers import format_hints @@ -58,7 +58,7 @@ class VadInfo(interfaces.plugins.PluginInterface): description = 'Filter on specific process IDs', element_type = int, optional = True), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (1, 0, 0)), + requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), requirements.BooleanRequirement(name = 'dump', description = "Extract listed memory ranges", default = False, @@ -180,6 +180,7 @@ class VadInfo(interfaces.plugins.PluginInterface): for proc in procs: process_name = utility.array_to_string(proc.ImageFileName) + proc_layer_name = proc.add_process_layer() for vad in self.list_vads(proc, filter_func = filter_func): diff --git a/volatility/framework/plugins/windows/vadyarascan.py b/volatility/framework/plugins/windows/vadyarascan.py index 529d1b21d..cd73ee65e 100644 --- a/volatility/framework/plugins/windows/vadyarascan.py +++ b/volatility/framework/plugins/windows/vadyarascan.py @@ -44,7 +44,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface): default = 0x40000000, description = "Set the maximum size (default is 1GB)", optional = True), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (1, 0, 0)), + requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), requirements.VersionRequirement(name = 'yarascanner', component = yarascan.YaraScanner, version = (2, 0, 0)), requirements.ListRequirement(name = 'pid', diff --git a/volatility/framework/plugins/windows/verinfo.py b/volatility/framework/plugins/windows/verinfo.py index 98340f855..389974aa0 100644 --- a/volatility/framework/plugins/windows/verinfo.py +++ b/volatility/framework/plugins/windows/verinfo.py @@ -10,7 +10,7 @@ from volatility.framework import exceptions, renderers, constants, interfaces from volatility.framework.configuration import requirements from volatility.framework.renderers import format_hints from volatility.framework.symbols import intermed -from volatility.framework.symbols.windows import extensions +from volatility.framework.symbols.windows.extensions import pe from volatility.plugins.windows import pslist, modules, dlllist vollog = logging.getLogger(__name__) @@ -32,9 +32,9 @@ class VerInfo(interfaces.plugins.PluginInterface): ## TODO: we might add a regex option on the name later, but otherwise we're good ## TODO: and we don't want any CLI options from pslist, modules, or moddump return [ - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (1, 0, 0)), + requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), requirements.PluginRequirement(name = 'modules', plugin = modules.Modules, version = (1, 0, 0)), - requirements.VersionRequirement(name = 'dlllist', component = dlllist.DllList, version = (1, 0, 0)), + requirements.VersionRequirement(name = 'dlllist', component = dlllist.DllList, version = (2, 0, 0)), requirements.TranslationLayerRequirement(name = 'primary', description = 'Memory layer for the kernel', architectures = ["Intel32", "Intel64"]), @@ -101,7 +101,7 @@ class VerInfo(interfaces.plugins.PluginInterface): self.config_path, "windows", "pe", - class_types = extensions.pe.class_types) + class_types = pe.class_types) for mod in mods: try: diff --git a/volatility/plugins/windows/registry/certificates.py b/volatility/plugins/windows/registry/certificates.py index 987358e39..c76999ade 100644 --- a/volatility/plugins/windows/registry/certificates.py +++ b/volatility/plugins/windows/registry/certificates.py @@ -10,6 +10,8 @@ from volatility.plugins.windows.registry import hivelist, printkey class Certificates(interfaces.plugins.PluginInterface): """Lists the certificates in the registry's Certificate Store.""" + _required_framework_version = (2, 0, 0) + @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ @@ -55,10 +57,9 @@ class Certificates(interfaces.plugins.PluginInterface): key_hash = key_path[key_path.rindex("\\") + 1:] if not isinstance(certificate_data, interfaces.renderers.BaseAbsentValue): - filedata = interfaces.plugins.FileInterface("{} - {} - {}.crt".format( - hex(hive.hive_offset), reg_section, key_hash)) - filedata.data.write(certificate_data) - self.produce_file(filedata) + with self._file_handler("{} - {} - {}.crt".format( + hex(hive.hive_offset), reg_section, key_hash), True) as filedata: + filedata.write(certificate_data) yield (0, (top_key, reg_section, key_hash, name)) except KeyError: # Key wasn't found in this hive, carry on diff --git a/volatility/plugins/windows/statistics.py b/volatility/plugins/windows/statistics.py index df01c71a6..2e637bf6f 100644 --- a/volatility/plugins/windows/statistics.py +++ b/volatility/plugins/windows/statistics.py @@ -13,6 +13,7 @@ vollog = logging.getLogger(__name__) class Statistics(plugins.PluginInterface): + _required_framework_version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: