Merge pull request #1167 from volatilityfoundation/process_ghosting

Add processghosting plugin to detect process ghosting and related tec…
This commit is contained in:
ikelos
2024-07-21 13:31:35 +01:00
committed by GitHub
2 changed files with 127 additions and 0 deletions
@@ -0,0 +1,104 @@
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
import contextlib
from volatility3.framework import interfaces, exceptions
from volatility3.framework import renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import pslist
vollog = logging.getLogger(__name__)
class ProcessGhosting(interfaces.plugins.PluginInterface):
"""Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0"""
_required_framework_version = (2, 4, 0)
@classmethod
def get_requirements(cls):
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
]
def _generator(self, procs):
kernel = self.context.modules[self.config["kernel"]]
if not kernel.get_type("_EPROCESS").has_member("ImageFilePointer"):
vollog.warning(
"This plugin only supports Windows 10 builds when the ImageFilePointer member of _EPROCESS is present"
)
return
for proc in procs:
delete_pending = renderers.UnreadableValue()
process_name = utility.array_to_string(proc.ImageFileName)
# if it is 0 then its a side effect of process ghosting
if proc.ImageFilePointer.vol.offset != 0:
try:
file_object = proc.ImageFilePointer
delete_pending = file_object.DeletePending
except exceptions.InvalidAddressException:
file_object = 0
# ImageFilePointer equal to 0 means process ghosting or similar techniques were used
else:
file_object = 0
if isinstance(delete_pending, int) and delete_pending not in [0, 1]:
vollog.debug(
f"Invalid delete_pending value {delete_pending} found for {process_name} {proc.UniqueProcessId}"
)
# delete_pending besides 0 or 1 = smear
if file_object == 0 or delete_pending == 1:
path = renderers.UnreadableValue()
if file_object:
with contextlib.suppress(exceptions.InvalidAddressException):
path = file_object.FileName.String
yield (
0,
(
proc.UniqueProcessId,
process_name,
format_hints.Hex(file_object),
delete_pending,
path,
),
)
def run(self):
filter_func = pslist.PsList.create_active_process_filter()
kernel = self.context.modules[self.config["kernel"]]
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("FILE_OBJECT", format_hints.Hex),
("DeletePending", str),
("Path", str),
],
self._generator(
pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_func=filter_func,
)
),
)
@@ -136,6 +136,29 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
filter_func = lambda x: x.UniqueProcessId not in filter_list
return filter_func
@classmethod
def create_active_process_filter(
cls,
) -> Callable[[interfaces.objects.ObjectInterface], bool]:
"""A factory for producing a filter function that only returns
active, userland processes. This prevents plugins from operating on terminated
processes that are still in the process list due to smear or handle leaks as well
as kernel processes (System, Registry, etc.). Use of this filter for plugins searching
for system state anomalies significantly reduces false positive in smeared and terminated
processes.
Returns:
Filter function for passing to the `list_processes` method
"""
return lambda x: not (
x.is_valid()
and x.ActiveThreads > 0
and x.UniqueProcessId != 4
and x.InheritedFromUniqueProcessId != 4
and x.ExitTime.QuadPart == 0
and x.get_handle_count() != renderers.UnreadableValue()
)
@classmethod
def create_name_filter(
cls, name_list: List[str] = None, exclude: bool = False