Plugins¶
The functionality that dissect.target
provides can be separated into two categories:
Interaction with target primitives, such as:
Disks
Volumes
Filesystems
Interaction with high level target attributes and artefacts, such as:
Basic OS information (hostname, domain, version, users, etc…)
OS specific forensic artefacts (Event logs, bash history, registry artefacts, etc…)
Generic forensic artefacts (filesystem artefacts, browser history, etc…)
The former is what dissect.target
provides as core functionality. High level interaction is provided by a
plugin system. This is also how you interact with a target using target-query.
On the technical side, a plugin is a Python class that exports a few of its methods to execute on a target. To learn how to write your own plugin, skip ahead to Writing your own.
Type of plugins¶
There are a couple of plugin types that you should know about. The main difference between them is how they can be used.
OS plugins¶
OS plugins are very important in dissect.target
and OS detection is an important step in the
initialisation of a target. OS plugins are the first layer between the target
primitives and the rest of the plugins. They are responsible for mounting filesystems to their correct location in the
Root filesystem and for performing any additional OS initialisation steps. Additionally,
they provide basic OS specific information, such as the hostname
, version
, and users
of a target.
Let’s compare the initialisation of WindowsPlugin
and
ESXiPlugin
. The WindowsPlugin
detects the system volume, mounts the
system volume to sysvol
, and the rest of the volumes to the correct drive letters. There is not much to the
initialisation of a Windows target. The ESXiPlugin
on the other hand, goes through an elaborate process of rebuilding
the ESXi root filesystem from .tar
files, parsing configuration, mounting volumes, and creating symlinks according to
complex rules. The end result is that from that point onward, any other plugin can interact with an ESXi targets’
filesystem as if it was a live system because it was accurately reconstructed.
Internal plugins¶
Internal plugins provide functionality for (surprise!) internal use. This means that they are unavailable through target-query and only callable from within Python.
These plugins generally provide functionality that aids other plugins or makes plugin development easier. For example, consider a plugin that provides environment variable expansion, or one that allows you to calculate UTC timestamps from the local system time zone information.
One very important internal plugin that warrants its own documentation is the Windows registry plugin.
Windows Registry¶
The Windows registry is a vital part of the Windows operating system, but also vital to the field of digital forensics. It contains a lot of interesting forensic artefacts, in addition to a lot of important information that is necessary to correctly interpret a Windows target. For example, configured log file locations, which codepage is in use, what time zone is configured, which drive letter belongs to which volume, and much more.
Because the registry is so important, there’s a special internal plugin in dissect.target
to make interacting with it
a breeze. Among other things, it has support for different sources of registry data, such as:
Binary
regf
hivesHives imported from
.reg
filesVirtual hives
It also supports multiple dimensions of the same hive (active, REGBACK
or replayed hives, multiple ControlSet
keys).
All the hives are mapped in a similar structure as Windows does, meaning registry paths like HKLM\SOFTWARE
or
HKEY_CURRENT_USER
work just as you expect them to.
Let’s look at this last concept in a bit more detail and take HKEY_CURRENT_USER
as an example. On a live Windows
system, things like HKEY_CURRENT_USER
and CurrentControlSet
map to a single key. This makes sense, because on
a live system you only have one current user, and only one current control set. However, when performing digital
forensics, you want as much data as possible. Maybe a setting was changed in an older control set and isn’t visible
anymore in the “current” control set? Maybe a persistency method exists in a REGBACK
version of a hive but no
longer in the active hive? To make working with this concept easier from an analysis and plugin development point
of view, we allowed a registry key or value to exist multiple times.
HKEY_CURRENT_USER
is really just a little bit of syntactic sugar around this concept. This allows for easy
analysis and plugin development, since you don’t have to iterate user hives yourself, and can just use HKCU
instead.
Underwater it will iterate over all the user hives for you, and simply return all of the keys that exist within those
hives. So, a query for HKCU\Software\Microsoft\Windows\CurrentVersion\Run
returns all the possible run key entries
of all the users that have registry hives.
Let’s assume we have a fully initialised Windows target on variable t
. Here are some examples of how you would
interact programmatically with the registry of the target:
# Access a single key
t.registry.key("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")
# Access a single subkey
t.registry.subkey("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList", "S-1-5-18")
# Access a single value
t.registry.value("HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion", "CSDVersion")
# Iterate over all possible keys (recommended)
for key in t.registry.keys("HKCU\\Software\\Microsoft\\Office"):
for subkey in key.subkeys():
print(subkey.subkeys())
There is also the target-reg utility to easily interact with the registry of a Windows target from the command line, which essentially is a small utility around the registry plugin!
See also
For more information, please refer to the documentation of
RegistryPlugin
.
Artefact plugins¶
Most other plugins in dissect.target
are regular plugins, or plugins that parse artefacts. For convenience we’ll
call these artefact plugins. These types of plugins are the ones you generally interact with when using
target-query and generally parse some piece of data from a target and return some kind of output.
Namespace plugins¶
Sometimes it makes sense to “namespace” your plugin. For example, the Windows System Resource Usage (SRU) database has multiple tables that are of interest. We want to make a distinction between the different tables and thus have separate plugin functions for parsing and returning those. However, we also want an easy option to parse all of the SRU tables at once.
This is where namespace plugins are useful. It allows us to nicely separate the different table parsing functions into
different plugins such as sru.network_data
and sru.application
, while simultaneously allowing us to execute
all of the namespace functions by simply calling sru
.
Caching¶
During investigations it might be useful to cache plugin output for performance reasons. dissect.target
has some
primitive file-based caching to speed up future executions of the same plugin. To configure caching, place a file
called .targetcfg.py
in the same directory as your target files with the following content:
CACHE_DIR = "/path/to/cache/directory"
Having this file in the same directory as your targets will cause the cache for those targets to be written to the
given directory. You can also have different configurations for different targets, as the parent directories are
traversed to find .targetcfg.py
:
/t/
├── domain_a
│ ├── EXAMPLE02.tar # Uses the .targetcfg.py from /t/domain_a
│ └── .targetcfg.py
├── domain_b
│ ├── EXAMPLE03.vma # Uses the .targetcfg.py from /t/domain_b
│ └── .targetcfg.py
├── domain_c
│ └── EXAMPLE04.qcow2 # Uses the .targetcfg.py from /t/
├── EXAMPLE01.E01 # Uses the .targetcfg.py from /t/
└── .targetcfg.py
You can influence caching behaviour with the --no-cache
, --only-read-cache
and --rewrite-cache
arguments
in target-query or by setting the IGNORE_CACHE
, ONLY_READ_CACHE
, or REWRITE_CACHE
environment
variables to either 0
or 1
.
Writing your own¶
Writing your own plugin is pretty easy. There are a few methods of using your own plugin in dissect.target
:
Specify the path to your plugin(s) using the
DISSECT_PLUGINS
environment variable.Specify the path to your plugin(s) using the
--plugin-path
argument with the various Dissect Tools.Add a new plugin in the
dissect.target
source tree atdissect/target/plugins
.
The last method requires you to have a source checkout and working development setup of dissect.target
.
This is the recommended method if you intend to contribute your plugin back to the project.
See also
Read more about using your own modules in dissect.target
at Loading your own modules.
Either way, you’ll need to write your plugin. Here’s an example which explains a lot of concepts:
from typing import Iterator
from dissect.target.helpers.descriptor_extensions import (
RegistryRecordDescriptorExtension,
UserRecordDescriptorExtension,
)
from dissect.target.helpers.record import (
TargetRecordDescriptor,
create_extended_descriptor,
)
from dissect.target.plugin import Plugin, arg, export, internal
ExampleRecordRecord = TargetRecordDescriptor(
"example/descriptor",
[
("string", "field_a"),
("string", "field_b"),
],
)
ExampleUserRegistryRecord = create_extended_descriptor(
[RegistryRecordDescriptorExtension, UserRecordDescriptorExtension]
)(
"example/registry/user",
[
("datetime", "ts"),
],
)
class ExamplePlugin(Plugin):
"""Example plugin.
This plugin serves as an example for new plugins. Use it as a guideline.
Docstrings are used in help messages of plugins. Make sure to document
your plugin and plugin functions. Use Google docstring format:
https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html
Plugins can optionally be namespaced by specifying the ``__namespace__``
class attribute. Namespacing results in your plugin needing to be prefixed
with this namespace when being called. For example, if your plugin has
specified ``test`` as namespace and a function called ``example``, you must
call your plugin with ``test.example``::
__namespace__ = "test"
The ``__init__`` takes the target as only argument. Perform additional
initialization here if necessary::
def __init__(self, target):
super().__init__(target)
"""
# IMPORTANT: Remove these attributes when using this as boilerplate for your own plugin!
__findable__ = False
def check_compatible(self) -> None:
"""Perform a compatibility check with the target.
This function should return ``None`` if the plugin is compatible with
the current target (``self.target``). For example, check if a certain
file exists.
Otherwise it should raise an ``UnsupportedPluginError``.
Raises:
UnsupportedPluginError: If the plugin could not be loaded.
"""
pass
@export(output="default")
@arg("--flag", action="store_true", help="optional example flag")
def example(self, flag: bool = False) -> str:
"""Example plugin function.
Docstrings are used in help messages of plugins. Make sure to document
your plugin and plugin functions. The first line must be a brief one
sentence description of the plugin function.
The ``@export`` decorator supports multiple arguments:
property (bool): Whether this function should act like a property.
Properties are implicitly cached.
record (RecordDescriptor): The record descriptor this function yield,
if any. If dynamic, use :class:`~dissect.target.helpers.record.DynamicDescriptor`.
output (str): The output type of this function. Can be one of:
- default: Single return value.
- record: Yields records. Implicit when record argument is given.
- yield: Yields printable values.
- none: No return value.
Command line arguments can be added using the ``@arg`` decorator. Arguments
to this decorator are directly forwarded to the ``add_argument`` function
of `argparse <https://docs.python.org/library/argparse.html>`_.
Resulting arguments are passed to the function using kwargs.
The keyword argument name must match the argparse argument name.
"""
return f"Example plugin. Flag argument: {flag!r}"
@export(record=ExampleRecordRecord)
def example_record(self) -> Iterator[ExampleRecordRecord]:
"""Example plugin that generates records.
To create a new plugin function that yields records, you must define a record descriptor
and pass it to ``@export``. This will implicitly mark the output type as ``record``.
"""
yield ExampleRecordRecord(
field_a="example",
field_b="record",
_target=self.target,
)
@export(record=ExampleUserRegistryRecord)
def example_user_registry_record(self) -> Iterator[ExampleUserRegistryRecord]:
"""Example plugin that generates records with registry key and user information.
To include registry or user information in a record, you must create a new record descriptor using
:func:`~dissect.target.helpers.record.create_extended_descriptor` with
:class:`~dissect.target.helpers.descriptor_extensions.RegistryRecordDescriptorExtension` and/or
:class:`~dissect.target.helpers.descriptor_extensions.UserRecordDescriptorExtension` as extensions.
"""
for key in self.target.registry.keys("HKCU\\SOFTWARE"):
user = self.target.registry.get_user(key)
yield ExampleUserRegistryRecord(
ts=key.ts,
_key=key,
_user=user,
_target=self.target,
)
@export(output="yield")
def example_yield(self) -> Iterator[str]:
"""Example plugin that yields text lines.
Setting ``output="yield"`` is useful for creating generators of text, such as human-readable timelines.
"""
for i in range(10):
yield f"Example line {i}"
@export(output="none")
def example_none(self) -> None:
"""Example plugin with no return value.
Setting ``output="none"`` means you don't return a value. This is useful when you want to print something
on your own, such as verbose information.
"""
print("Example output with no return value.")
@internal
def example_internal(self) -> str:
"""Example internal plugin.
Use the ``@internal`` plugin to mark your plugin as internal and hide it from the plugin overview.
"""
return "Example internal plugin."
Output types¶
Plugins can have a couple types of outputs:
default
Basic Python types such as
int
,str
,list
, etc. Mostly useful for simple or internal plugins.
record
Records are the recommended output for most artefact plugins.
yield
Yield basic Python types, such as a generator of
str
of a human-readable filesystem timeline.
none
No output or returns
None
, useful when the plugin prints something on its own (such as the plugin that lists which plugins are available).
Records¶
Records are the main data format used by plugins to output information. If you don’t yet know what records are, read a short explanation in the overview. The important thing to know in the context of writing your own plugin is that you need to write a record descriptor that describes what fields your record has.
The following field types are available:
boolean
bytes
datetime
digest
dynamic
filesize
float
net.ipaddress
net.ipnetwork
record
string
uint16
uint32
unix_file_mode
uri
varint
You can also create a list of any of these types by specifying the type as type[]
, so for example string[]
for
a list of strings.
Within dissect.target
, there are a couple of helpers available for working with records. Most notable is the
TargetRecordDescriptor
.
Historically, we’d manually add a couple of common fields to each record, such as the hostname or domain of a target.
Sometimes errors or typos snuck in, so we standardized this by introducing the TargetRecordDescriptor
. You can
already see its usage in the example plugin above, but the basic idea is that instead of manually passing the
hostname and domain, you pass a target as _target=target
keyword argument, and the TargetRecordDescriptor
will take care of the rest.
There are two additional so-called record extensions:
RegistryRecordDescriptorExtension
and
UserRecordDescriptorExtension
. The use of these is also
demonstrated in the example plugin above.