mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-17 20:35:40 +02:00
This replaces all uses of `requirements.PluginRequirements` with `requirements.VersionRequirement`.
310 lines
17 KiB
ReStructuredText
310 lines
17 KiB
ReStructuredText
How to Write a Simple Plugin
|
|
============================
|
|
|
|
This guide will step through how to construct a simple plugin using Volatility 3.
|
|
|
|
The example plugin we'll use is :py:class:`~volatility3.plugins.windows.dlllist.DllList`, which features the main traits
|
|
of a normal plugin, and reuses other plugins appropriately.
|
|
|
|
.. note::
|
|
|
|
This document will not include the complete code necessary for a
|
|
working plugin (such as imports, etc) since it's designed to focus on the necessary components for writing a plugin.
|
|
For complete and functioning plugins, the ``framework/plugins`` directory should be consulted.
|
|
|
|
Inherit from PluginInterface
|
|
----------------------------
|
|
|
|
The first step is to define a class that inherits from :py:class:`~volatility3.framework.interfaces.plugins.PluginInterface`.
|
|
Volatility automatically finds all plugins defined under the various plugin directories by importing them and then
|
|
making use of any classes that inherit from :py:class:`~volatility3.framework.interfaces.plugins.PluginInterface`.
|
|
|
|
::
|
|
|
|
from volatility3.framework import interfaces
|
|
|
|
class DllList(interfaces.plugins.PluginInterface):
|
|
|
|
The next step is to define the requirements of the plugin, these will be converted into options the user can provide
|
|
based on the User Interface.
|
|
|
|
Define the plugin requirements
|
|
------------------------------
|
|
|
|
These requirements are the names of variables that will need to be populated in the configuration tree for the plugin
|
|
to be able to run properly. Any that are defined as optional need not necessarily be provided.
|
|
|
|
::
|
|
|
|
_version = (1, 0, 0)
|
|
_required_framework_version = (2, 0, 0)
|
|
|
|
@classmethod
|
|
def get_requirements(cls):
|
|
return [
|
|
requirements.ModuleRequirement(
|
|
name = 'kernel',
|
|
description = 'Windows kernel',
|
|
architectures = ["Intel32", "Intel64"]
|
|
),
|
|
requirements.ListRequirement(
|
|
name = 'pid',
|
|
element_type = int,
|
|
description = "Process IDs to include (all other processes are excluded)",
|
|
optional = True
|
|
),
|
|
requirements.VersionRequirement(
|
|
name = 'pslist',
|
|
component = pslist.PsList,
|
|
version = (2, 0, 0)
|
|
),
|
|
]
|
|
|
|
|
|
This is a classmethod, so it can be called before the specific plugin object has been instantiated (in order to know how
|
|
to instantiate the plugin). At the moment these requirements are fairly straightforward:
|
|
|
|
::
|
|
|
|
requirements.ModuleRequirement(
|
|
name = 'kernel',
|
|
description = 'Windows kernel',
|
|
architectures = ["Intel32", "Intel64"]
|
|
),
|
|
|
|
This requirement specifies the need for a particular submodule. Each module requires a
|
|
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>` and a
|
|
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`, which are fulfilled by two
|
|
subrequirements: a
|
|
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement` and a
|
|
:py:class:`~volatility3.framework.configuration.requirements.SymbolTableRequirement`. At the moment, the automagic
|
|
only fills `ModuleRequirements` with kernels, and so has relatively few parameters. It requires the architecture for
|
|
the underlying TranslationLayer, and the offset of the module within that layer.
|
|
|
|
The name of the module will be stored in the ``kernel`` configuration option, and the module object itself
|
|
can be accessed from the ``context.modules`` collection. This requirement is a Complex Requirement and therefore will
|
|
not be requested directly from the user.
|
|
|
|
|
|
.. note::
|
|
|
|
In previous versions of volatility 3, there was no `ModuleRequirement`, and instead two requirements were defined
|
|
a :py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>` and a `SymbolTableRequirement`. These still exist, and can be used, most plugins just
|
|
define a single `ModuleRequirement` for the kernel, which the automagic will populate. The `ModuleRequirement` has
|
|
two automatic sub-requirements, a `TranslationLayerRequirement` and a `SymbolTableRequirement`, but the module also
|
|
includes the offset of the module, and will allow future expansion to specify specific modules when application
|
|
level plugins become more common. Below are how the requirements would be specified:
|
|
|
|
::
|
|
|
|
requirements.TranslationLayerRequirement(
|
|
name = 'primary',
|
|
description = 'Memory layer for the kernel',
|
|
architectures = ["Intel32", "Intel64"]
|
|
),
|
|
|
|
This requirement indicates that the plugin will operate on a single
|
|
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`. The name of the
|
|
loaded layer will appear in the plugin's configuration under the name ``primary``. Requirement values can be
|
|
accessed within the plugin through the plugin's `config` attribute (for example ``self.config['pid']``).
|
|
|
|
.. note:: The name itself is dynamic depending on the other layers already present in the Context. Always use the value
|
|
from the configuration rather than attempting to guess what the layer will be called.
|
|
|
|
Finally, this defines that the translation layer must be on the Intel Architecture. At the moment, this acts as a filter,
|
|
failing to be satisfied by memory images that do not match the architecture required.
|
|
|
|
Most plugins will only operate on a single layer, but it is entirely possible for a plugin to request two different
|
|
layers, for example a plugin that carries out some form of difference or statistics against multiple memory images.
|
|
|
|
This requirement (and the next two) are known as Complex Requirements, and user interfaces will likely not directly
|
|
request a value for this from a user. The value stored in the configuration tree for a
|
|
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement` is
|
|
the string name of a layer present in the context's memory that satisfies the requirement.
|
|
|
|
::
|
|
|
|
requirements.SymbolTableRequirement(
|
|
name = "nt_symbols",
|
|
description = "Windows kernel symbols"
|
|
),
|
|
|
|
This requirement specifies the need for a particular
|
|
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`
|
|
to be loaded. This gets populated by various
|
|
:py:class:`Automagic <volatility3.framework.interfaces.automagic.AutoMagicInterface>` as the nearest sibling to a particular
|
|
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement`.
|
|
This means that if the :py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement`
|
|
is satisfied and the :py:class:`Automagic <volatility3.framework.interfaces.automagic.AutoMagicInterface>` can determine
|
|
the appropriate :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`, the
|
|
name of the :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>` will be stored in the configuration.
|
|
|
|
This requirement is also a Complex Requirement and therefore will not be requested directly from the user.
|
|
|
|
::
|
|
|
|
requirements.ListRequirement(
|
|
name = 'pid',
|
|
description = 'Filter on specific process IDs',
|
|
element_type = int,
|
|
optional = True
|
|
),
|
|
|
|
The next requirement is a List Requirement, populated by integers. The description will be presented to the user to
|
|
describe what the value represents. The optional flag indicates that the plugin can function without the ``pid`` value
|
|
being defined within the configuration tree at all.
|
|
|
|
::
|
|
|
|
requirements.PluginRequirement(
|
|
name = 'pslist',
|
|
plugin = pslist.PsList,
|
|
version = (2, 0, 0)
|
|
)
|
|
|
|
This requirement indicates that the plugin will make use of another plugin's code, and specifies the version requirements
|
|
on that plugin. The version is specified in terms of Semantic Versioning meaning that, to be compatible, the major
|
|
versions must be identical and the minor version must be equal to or higher than the one provided. This requirement
|
|
does not make use of any data from the configuration, even if it were provided, it is merely a functional check before
|
|
running the plugin. To define the version of a plugin, populate the `_version` class variable as a tuple of version
|
|
numbers `(major, minor, patch)`. So for example:
|
|
|
|
::
|
|
|
|
_version = (1, 0, 0)
|
|
|
|
The plugin may also require a specific version of the framework, and this also uses Semantic Versioning, and can be
|
|
set by defining the `_required_framework_version`. The major version should match the version of volatility the plugin
|
|
is to be used with, which at the time of writing would be 2.2.0, and so would be specified as below. If only features, for example,
|
|
from 2.0.0 are used, then the lowest applicable version number should be used to support the greatest number of
|
|
installations:
|
|
|
|
::
|
|
|
|
_required_framework_version = (2, 0, 0)
|
|
|
|
Define the `run` method
|
|
-----------------------
|
|
|
|
The run method is the primary method called on a plugin. It takes no parameters (these have been passed through the
|
|
context's configuration tree, and the context is provided at plugin initialization time) and returns an unpopulated
|
|
:py:class:`~volatility3.framework.interfaces.renderers.TreeGrid` object. These are typically constructed based on a
|
|
generator that carries out the bulk of the plugin's processing. The
|
|
:py:class:`~volatility3.framework.interfaces.renderers.TreeGrid` also specifies the column names and types
|
|
that will be output as part of the :py:class:`~volatility3.framework.interfaces.renderers.TreeGrid`.
|
|
|
|
::
|
|
|
|
def run(self):
|
|
|
|
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
|
|
|
|
return renderers.TreeGrid(
|
|
[
|
|
("PID", int),
|
|
("Process", str),
|
|
("Base", format_hints.Hex),
|
|
("Size", format_hints.Hex),
|
|
("Name", str),
|
|
("Path", str),
|
|
],
|
|
self._generator(
|
|
pslist.PsList.list_processes(
|
|
context=self.context,
|
|
kernel_module_name=self.config['kernel'],
|
|
filter_func = filter_func
|
|
)
|
|
)
|
|
)
|
|
|
|
In this instance, the plugin constructs a filter (using the PsList plugin's *classmethod* for creating filters).
|
|
It checks the plugin's configuration for the ``pid`` value, and passes it in as a list if it finds it, or None if
|
|
it does not. The :py:func:`~volatility3.plugins.windows.pslist.PsList.create_pid_filter` method accepts a list of process
|
|
identifiers that are included in the list. If the list is empty, all processes are returned.
|
|
|
|
The next line specifies the columns by their name and type. The types are simple types (int, str, bytes, float, and bool)
|
|
but can also provide hints as to how the output should be displayed (such as a hexadecimal number, using
|
|
:py:class:`volatility3.framework.renderers.format_hints.Hex`).
|
|
This indicates to user interfaces that the value should be displayed in a particular way, but does not guarantee that the value
|
|
will be displayed that way (for example, if it doesn't make sense to do so in a particular interface).
|
|
|
|
Finally, the generator is provided. The generator accepts a list of processes, which is gathered using a different plugin,
|
|
the :py:class:`~volatility3.plugins.windows.pslist.PsList` plugin. That plugin features a *classmethod*,
|
|
so that other plugins can call it. As such, it takes all the necessary parameters rather than accessing them
|
|
from a configuration. Since it must be portable code, it takes a context, as well as the layer name,
|
|
symbol table and optionally a filter. In this instance we unconditionally
|
|
pass it the value from the configuration for the kernel module name, constructed from
|
|
the ``kernel`` configuration requirement. This will generate a list
|
|
of :py:class:`~volatility3.framework.symbols.windows.extensions.EPROCESS` objects, as provided by the :py:class:`~volatility.plugins.windows.pslist.PsList` plugin,
|
|
and is not covered here but is used as an example for how to share code across plugins
|
|
(both as the provider and the consumer of the shared code).
|
|
|
|
Define the generator
|
|
--------------------
|
|
The :py:class:`~volatility3.framework.interfaces.renderers.TreeGrid` can be populated without a generator,
|
|
but it is quite a common model to use. This is where the main processing for this plugin lives.
|
|
|
|
::
|
|
|
|
def _generator(self, procs):
|
|
|
|
for proc in procs:
|
|
|
|
for entry in proc.load_order_modules():
|
|
|
|
BaseDllName = FullDllName = renderers.UnreadableValue()
|
|
try:
|
|
BaseDllName = entry.BaseDllName.get_string()
|
|
# We assume that if the BaseDllName points to an invalid buffer, so will FullDllName
|
|
FullDllName = entry.FullDllName.get_string()
|
|
except exceptions.InvalidAddressException:
|
|
pass
|
|
|
|
yield (0, (proc.UniqueProcessId,
|
|
proc.ImageFileName.cast("string", max_length = proc.ImageFileName.vol.count,
|
|
errors = 'replace'),
|
|
format_hints.Hex(entry.DllBase), format_hints.Hex(entry.SizeOfImage),
|
|
BaseDllName, FullDllName))
|
|
|
|
This iterates through the list of processes and for each one calls the :py:meth:`~volatility3.framework.symbols.windows.extensions.EPROCESS.load_order_modules` method on it. This provides
|
|
a list of the loaded modules within the process.
|
|
|
|
The plugin then defaults the ``BaseDllName`` and ``FullDllName`` variables to an :py:class:`~volatility3.framework.renderers.UnreadableValue`,
|
|
which is a way of indicating to the user interface that the value couldn't be read for some reason (but that it isn't fatal).
|
|
There are currently four different reasons a value may be unreadable:
|
|
|
|
* **Unreadable**: values which are empty because the data cannot be read
|
|
* **Unparsable**: values which are empty because the data cannot be interpreted correctly
|
|
* **NotApplicable**: values which are empty because they don't make sense for this particular entry
|
|
* **NotAvailable**: values which cannot be provided now (but might in a future run, via new symbols or an updated plugin)
|
|
|
|
This is a safety provision to ensure that the data returned by the Volatility library is accurate and describes why
|
|
information may not be provided.
|
|
|
|
The plugin then takes the process's ``BaseDllName`` value, and calls :py:meth:`~volatility3.framework.symbols.windows.extensions.UNICODE_STRING.get_string` on it. All structure attributes,
|
|
as defined by the symbols, are directly accessible and use the case-style of the symbol library it came from (in Windows,
|
|
attributes are CamelCase), such as ``entry.BaseDllName`` in this instance. Any attributes not defined by the symbol but added
|
|
by Volatility extensions cannot be properties (in case they overlap with the attributes defined in the symbol libraries)
|
|
and are therefore always methods and prepended with ``get_``, in this example ``BaseDllName.get_string()``.
|
|
|
|
Finally, ``FullDllName`` is populated. These operations read from memory, and as such, the memory image may be unable to
|
|
read the data at a particular offset. This will cause an exception to be thrown. In Volatility 3, exceptions are thrown
|
|
as a means of communicating when something exceptional happens. It is the responsibility of the plugin developer to
|
|
appropriately catch and handle any non-fatal exceptions and otherwise allow the exception to be thrown by the user interface.
|
|
|
|
In this instance, the :py:class:`~volatility3.framework.exceptions.InvalidAddressException` class is caught, which is thrown
|
|
by any layer which cannot access an offset requested of it. Since we have already populated both values with ``UnreadableValue``
|
|
we do not need to write code for the exception handler.
|
|
|
|
Finally, we yield the record in the format required by the :py:class:`~volatility3.framework.interfaces.renderers.TreeGrid`,
|
|
a tuple, listing the indentation level (for trees) and then the list of values for each column.
|
|
This plugin demonstrates casting a value ``ImageFileName`` to ensure it's returned
|
|
as a string with a specific maximum length, rather than its original type (potentially an array of characters, etc).
|
|
This is carried out using the :py:meth:`~volatility3.framework.interfaces.objects.ObjectInterface.cast` method which takes a type (either a native type, such as string or pointer, or a
|
|
structure type defined in a :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`
|
|
such as ``<table>!_UNICODE``) and the parameters to that type.
|
|
|
|
Since the cast value must populate a string typed column, it had to be a Python string (such as being cast to the native
|
|
type string) and could not have been a special Structure such as ``_UNICODE``. For the format hint columns, the format
|
|
hint type must be used to ensure the error checking does not fail.
|