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 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 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 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.PluginRequirement(name = 'pslist',
                                           plugin = pslist.PsList,
                                           version = (2, 0, 0))]

This is a classmethod, because it is 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 TranslationLayer and a SymbolTable, which are fulfilled by two subrequirements: a TranslationLayerRequirement and a 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 TranslationLayer 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 TranslationLayer. 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 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 SymbolTable to be loaded. This gets populated by various Automagic as the nearest sibling to a particular TranslationLayerRequirement. This means that if the TranslationLayerRequirement is satisfied and the Automagic can determine the appropriate SymbolTable, the name of the SymbolTable 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 TreeGrid object. These are typically constructed based on a generator that carries out the bulk of the plugin’s processing. The TreeGrid also specifies the column names and types that will be output as part of the TreeGrid.

def run(self):

    filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
    kernel = self.context.modules[self.config['kernel']]

    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(self.context,
                                                                           kernel.layer_name,
                                                                           kernel.symbol_table_name,
                                                                           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 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 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 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 values from the configuration for the layer and symbol table from the kernel module object, constructed from the kernel configuration requirement. This will generate a list of EPROCESS objects, as provided by the 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 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 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 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 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 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 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 cast() method which takes a type (either a native type, such as string or pointer, or a structure type defined in a SymbolTable 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.