From a30a2787d78c7bee3768ddf2d33d9df1c8c61fe0 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:00:13 +0300 Subject: [PATCH 01/20] process_spoofing plugin --- .../plugins/linux/process_spoofing.py | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 volatility3/framework/plugins/linux/process_spoofing.py diff --git a/volatility3/framework/plugins/linux/process_spoofing.py b/volatility3/framework/plugins/linux/process_spoofing.py new file mode 100644 index 0000000000..2546a36238 --- /dev/null +++ b/volatility3/framework/plugins/linux/process_spoofing.py @@ -0,0 +1,253 @@ +# This file is Copyright 2025 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 +from pathlib import PurePosixPath +from typing import Optional, Tuple, Iterator + +from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.symbols import linux +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +# https://github.com/SolitudePy/linux-mal +class ProcessSpoofing(plugins.PluginInterface): + """Detects process spoofing by comparing executable path to cmdline & comm fields""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + ] + + def _get_executable_path( + self, task: interfaces.objects.ObjectInterface + ) -> Optional[str]: + """ + Extract the executable path from task_struct.mm.exe_file + + Args: + task: task_struct object of the process + + Returns: + Executable path or None if not available + """ + try: + mm = task.mm + if not mm or not mm.is_readable(): + # Kernel threads doesn't have + return None + + exe_file = mm.exe_file + if not exe_file or not exe_file.is_readable(): + return None + + # Use LinuxUtilities.path_for_file to extract the path + exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) + + return exe_path if exe_path else None + + except (exceptions.InvalidAddressException, AttributeError): + return None + + def _get_cmdline_basename( + self, task: interfaces.objects.ObjectInterface + ) -> Optional[str]: + """ + Extract the command line arguments and return the basename of the first argument + + Args: + task: task_struct object of the process + + Returns: + Basename of the first command line argument or None if not available + """ + try: + mm = task.mm + if not mm or not mm.is_readable(): + return renderers.NotAvailableValue() + + proc_layer_name = task.add_process_layer() + if proc_layer_name is None: + return None + + proc_layer = self.context.layers[proc_layer_name] + + # Read argv from userland + start = task.mm.arg_start + size_to_read = task.mm.arg_end - task.mm.arg_start + + if not (0 < size_to_read <= 4096): + return None + + # Attempt to read command line arguments + try: + argv = proc_layer.read(start, size_to_read) + except exceptions.InvalidAddressException: + return None + + # Parse the arguments - they are null byte terminated + args_str = argv.decode(encoding="utf8", errors="replace") + args_list = args_str.split("\x00") + if args_list and args_list[0]: + basename = PurePosixPath(args_list[0]).name + return basename + else: + return None + + except (exceptions.InvalidAddressException, AttributeError): + return None + + def _get_comm(self, task: interfaces.objects.ObjectInterface) -> Optional[str]: + """ + Extract the comm field from task_struct + + Args: + task: task_struct object of the process + + Returns: + Process name from comm field or None if not available + """ + try: + return utility.array_to_string(task.comm) + except (exceptions.InvalidAddressException, AttributeError): + return None + + def _extract_process_names( + self, task: interfaces.objects.ObjectInterface + ) -> Tuple[Optional[str], Optional[str], Optional[str]]: + """ + Extract all three process name sources for comparison + + Args: + task: task_struct object of the process + + Returns: + Tuple of (exe_path_basename, cmdline_basename, comm) + """ + exe_path = self._get_executable_path(task) + exe_basename = PurePosixPath(exe_path).name + cmdline_basename = self._get_cmdline_basename(task) + comm = self._get_comm(task) + + return exe_basename, cmdline_basename, comm + + def _detect_spoofing( + self, + exe_basename: Optional[str], + cmdline_basename: Optional[str], + comm: Optional[str], + ) -> Optional[list]: + """ + Analyze the three name sources to detect potential spoofing + + Args: + exe_basename: Basename from exe_file path + cmdline_basename: Basename from command line + comm: Name from comm field + + Returns: + notes: List of notes indicating potential spoofing, or None if no issues found + """ + notes = [] + + # Count how many name sources we have + available_sources = sum( + 1 for name in [exe_basename, cmdline_basename, comm] if name + ) + + if available_sources < 2: + return None + + if exe_basename != cmdline_basename: + notes.append( + f"'Potential cmdline spoofing: exe_file={exe_basename};cmdline={cmdline_basename}'" + ) + if exe_basename[:15] != comm: + notes.append( + f"'Potential comm spoofing: exe_file={exe_basename};comm={comm}'" + ) + return notes + + def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: + """ + Generate process spoofing detection results + + Args: + tasks: Iterator of task_struct objects + + Yields: + Tuple containing process information and spoofing analysis + """ + for task in tasks: + try: + pid = task.pid + ppid = task.get_parent_pid() + + exe_basename, cmdline_basename, comm = self._extract_process_names(task) + + notes = self._detect_spoofing(exe_basename, cmdline_basename, comm) + + exe_render = exe_basename if exe_basename else "N/A" + cmdline_render = cmdline_basename if cmdline_basename else "N/A" + comm_render = comm if comm else "N/A" + + yield ( + 0, + ( + pid, + ppid, + exe_render, + cmdline_render, + comm_render, + "[" + ", ".join(notes) + "]" if notes else "OK", + ), + ) + + except Exception as e: + vollog.debug(f"Error processing task at {task.vol.offset:#x}: {e}") + continue + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("PPID", int), + ("Exe_Basename", str), + ("Cmdline_Basename", str), + ("Comm", str), + ("Notes", str), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) From 8d5552e519d7fece3e2b0e8e5f361e14bbf4bf4b Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:10:44 +0300 Subject: [PATCH 02/20] cosmetics --- volatility3/framework/plugins/linux/process_spoofing.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/linux/process_spoofing.py b/volatility3/framework/plugins/linux/process_spoofing.py index 2546a36238..9d2e55acbd 100644 --- a/volatility3/framework/plugins/linux/process_spoofing.py +++ b/volatility3/framework/plugins/linux/process_spoofing.py @@ -60,14 +60,12 @@ def _get_executable_path( try: mm = task.mm if not mm or not mm.is_readable(): - # Kernel threads doesn't have + # Kernel threads doesn't have mm return None exe_file = mm.exe_file if not exe_file or not exe_file.is_readable(): return None - - # Use LinuxUtilities.path_for_file to extract the path exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) return exe_path if exe_path else None @@ -97,15 +95,12 @@ def _get_cmdline_basename( return None proc_layer = self.context.layers[proc_layer_name] - - # Read argv from userland start = task.mm.arg_start size_to_read = task.mm.arg_end - task.mm.arg_start if not (0 < size_to_read <= 4096): return None - # Attempt to read command line arguments try: argv = proc_layer.read(start, size_to_read) except exceptions.InvalidAddressException: @@ -176,7 +171,7 @@ def _detect_spoofing( """ notes = [] - # Count how many name sources we have + # Skip kernel threads available_sources = sum( 1 for name in [exe_basename, cmdline_basename, comm] if name ) From a47a8a7d14f2b93b851fd6f3115d815bb2311899 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:10:11 +0300 Subject: [PATCH 03/20] added mechanism for deleted exe --- .../framework/plugins/linux/process_spoofing.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/process_spoofing.py b/volatility3/framework/plugins/linux/process_spoofing.py index 9d2e55acbd..451889029f 100644 --- a/volatility3/framework/plugins/linux/process_spoofing.py +++ b/volatility3/framework/plugins/linux/process_spoofing.py @@ -21,7 +21,8 @@ class ProcessSpoofing(plugins.PluginInterface): """Detects process spoofing by comparing executable path to cmdline & comm fields""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 1, 0) + deleted = " (deleted)" @classmethod def get_requirements(cls): @@ -64,10 +65,17 @@ def _get_executable_path( return None exe_file = mm.exe_file + if not exe_file or not exe_file.is_readable(): return None + + exe_inode = exe_file.dereference().f_path.dentry.d_inode exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) + # If the inode link count is 0, the process image has been deleted + if exe_inode.i_nlink == 0: + exe_path += self.deleted + return exe_path if exe_path else None except (exceptions.InvalidAddressException, AttributeError): @@ -176,6 +184,11 @@ def _detect_spoofing( 1 for name in [exe_basename, cmdline_basename, comm] if name ) + is_deleted = exe_basename.endswith(self.deleted) + if is_deleted: + notes.append(f"'Potential Process image deletion: exe_file={exe_basename}'") + exe_basename = exe_basename[: len(self.deleted) * -1] + if available_sources < 2: return None From 69c2762e95368b31e86af492398df36bc95240e5 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:49:44 +0300 Subject: [PATCH 04/20] categorize as a malware plugin --- .../framework/plugins/linux/{ => malware}/process_spoofing.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename volatility3/framework/plugins/linux/{ => malware}/process_spoofing.py (100%) diff --git a/volatility3/framework/plugins/linux/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py similarity index 100% rename from volatility3/framework/plugins/linux/process_spoofing.py rename to volatility3/framework/plugins/linux/malware/process_spoofing.py From 27a951e5744e3b5b922ad218614a5a5103a28dbb Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:05:10 +0300 Subject: [PATCH 05/20] Plugins: precise exception handling in process_spoofing --- .../framework/plugins/linux/malware/process_spoofing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 451889029f..b14dd208c6 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -237,8 +237,10 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: ), ) - except Exception as e: - vollog.debug(f"Error processing task at {task.vol.offset:#x}: {e}") + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.warning( + f"Unable to process task PID {getattr(task, 'pid', 'unknown')} at {task.vol.offset:#x}: {e}" + ) continue def run(self): From 61f5bb66d19574f920788c6c6b438c97eb45c921 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:07:17 +0300 Subject: [PATCH 06/20] Plugins: remove exe_file dereference() --- .../framework/plugins/linux/malware/process_spoofing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index b14dd208c6..f32f80f8a8 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -61,7 +61,7 @@ def _get_executable_path( try: mm = task.mm if not mm or not mm.is_readable(): - # Kernel threads doesn't have mm + # Kernel threads don't have mm struct return None exe_file = mm.exe_file @@ -69,7 +69,7 @@ def _get_executable_path( if not exe_file or not exe_file.is_readable(): return None - exe_inode = exe_file.dereference().f_path.dentry.d_inode + exe_inode = exe_file.f_path.dentry.d_inode exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) # If the inode link count is 0, the process image has been deleted From 6a905ee43e34f9d0dca5c4af20d62e51441b8182 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:14:40 +0300 Subject: [PATCH 07/20] Plugins: convert useful methods to classmethods --- .../plugins/linux/malware/process_spoofing.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index f32f80f8a8..7302f2a719 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -46,13 +46,17 @@ def get_requirements(cls): ), ] - def _get_executable_path( - self, task: interfaces.objects.ObjectInterface + @classmethod + def get_executable_path( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, ) -> Optional[str]: """ Extract the executable path from task_struct.mm.exe_file Args: + context: The context to operate on task: task_struct object of the process Returns: @@ -70,24 +74,28 @@ def _get_executable_path( return None exe_inode = exe_file.f_path.dentry.d_inode - exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) + exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file) # If the inode link count is 0, the process image has been deleted if exe_inode.i_nlink == 0: - exe_path += self.deleted + exe_path += cls.deleted return exe_path if exe_path else None except (exceptions.InvalidAddressException, AttributeError): return None - def _get_cmdline_basename( - self, task: interfaces.objects.ObjectInterface + @classmethod + def get_cmdline_basename( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, ) -> Optional[str]: """ Extract the command line arguments and return the basename of the first argument Args: + context: The context to operate on task: task_struct object of the process Returns: @@ -102,7 +110,7 @@ def _get_cmdline_basename( if proc_layer_name is None: return None - proc_layer = self.context.layers[proc_layer_name] + proc_layer = context.layers[proc_layer_name] start = task.mm.arg_start size_to_read = task.mm.arg_end - task.mm.arg_start @@ -126,7 +134,8 @@ def _get_cmdline_basename( except (exceptions.InvalidAddressException, AttributeError): return None - def _get_comm(self, task: interfaces.objects.ObjectInterface) -> Optional[str]: + @classmethod + def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]: """ Extract the comm field from task_struct @@ -153,10 +162,10 @@ def _extract_process_names( Returns: Tuple of (exe_path_basename, cmdline_basename, comm) """ - exe_path = self._get_executable_path(task) - exe_basename = PurePosixPath(exe_path).name - cmdline_basename = self._get_cmdline_basename(task) - comm = self._get_comm(task) + exe_path = self.get_executable_path(self.context, task) + exe_basename = PurePosixPath(exe_path).name if exe_path else None + cmdline_basename = self.get_cmdline_basename(self.context, task) + comm = self.get_comm(task) return exe_basename, cmdline_basename, comm From 9ff54fd0c3312950ff37a381d1ce3b5e7f2a7078 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:16:52 +0300 Subject: [PATCH 08/20] Plugins: consistent return values in process_spoofing --- volatility3/framework/plugins/linux/malware/process_spoofing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 7302f2a719..01d4c1e2c0 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -104,7 +104,7 @@ def get_cmdline_basename( try: mm = task.mm if not mm or not mm.is_readable(): - return renderers.NotAvailableValue() + return None proc_layer_name = task.add_process_layer() if proc_layer_name is None: From 2a66de70d5b8d1ca92eae9cfaa12a3db207812b5 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:34:58 +0300 Subject: [PATCH 09/20] Plugins: process_spoofing log exceptions as debug --- .../plugins/linux/malware/process_spoofing.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 01d4c1e2c0..473bd7d077 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -82,7 +82,10 @@ def get_executable_path( return exe_path if exe_path else None - except (exceptions.InvalidAddressException, AttributeError): + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to read executable path for task at {task.vol.offset:#x}: {e}" + ) return None @classmethod @@ -147,7 +150,8 @@ def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]: """ try: return utility.array_to_string(task.comm) - except (exceptions.InvalidAddressException, AttributeError): + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug(f"Unable to read comm for task at {task.vol.offset:#x}: {e}") return None def _extract_process_names( @@ -193,7 +197,7 @@ def _detect_spoofing( 1 for name in [exe_basename, cmdline_basename, comm] if name ) - is_deleted = exe_basename.endswith(self.deleted) + is_deleted = exe_basename and exe_basename.endswith(self.deleted) if is_deleted: notes.append(f"'Potential Process image deletion: exe_file={exe_basename}'") exe_basename = exe_basename[: len(self.deleted) * -1] From 5bb992d5adc744f1175b078290068414b2feb0c6 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:39:05 +0300 Subject: [PATCH 10/20] Plugins: more precise exception handling in process_spoofing --- .../plugins/linux/malware/process_spoofing.py | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 473bd7d077..b3cc37cc76 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -104,37 +104,36 @@ def get_cmdline_basename( Returns: Basename of the first command line argument or None if not available """ - try: - mm = task.mm - if not mm or not mm.is_readable(): - return None - - proc_layer_name = task.add_process_layer() - if proc_layer_name is None: - return None + mm = task.mm + if not mm or not mm.is_readable(): + return None - proc_layer = context.layers[proc_layer_name] - start = task.mm.arg_start - size_to_read = task.mm.arg_end - task.mm.arg_start + proc_layer_name = task.add_process_layer() + if proc_layer_name is None: + return None - if not (0 < size_to_read <= 4096): - return None + proc_layer = context.layers[proc_layer_name] + start = task.mm.arg_start + size_to_read = task.mm.arg_end - task.mm.arg_start - try: - argv = proc_layer.read(start, size_to_read) - except exceptions.InvalidAddressException: - return None + if not (0 < size_to_read <= 4096): + return None - # Parse the arguments - they are null byte terminated - args_str = argv.decode(encoding="utf8", errors="replace") - args_list = args_str.split("\x00") - if args_list and args_list[0]: - basename = PurePosixPath(args_list[0]).name - return basename - else: - return None + try: + argv = proc_layer.read(start, size_to_read) + except exceptions.InvalidAddressException as e: + vollog.debug( + f"Unable to read cmdline for task at {task.vol.offset:#x}: {e}" + ) + return None - except (exceptions.InvalidAddressException, AttributeError): + # Parse the arguments - they are null byte terminated + args_str = argv.decode(encoding="utf8", errors="replace") + args_list = args_str.split("\x00") + if args_list and args_list[0]: + basename = PurePosixPath(args_list[0]).name + return basename + else: return None @classmethod From c5ef15ecb0fb3db94eaefee48b46b18c1b6e7d0e Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:58:17 +0300 Subject: [PATCH 11/20] Plugins: swap notes for boolean flags in process_spoofing --- .../plugins/linux/malware/process_spoofing.py | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index b3cc37cc76..7f2c9af3c5 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -177,7 +177,7 @@ def _detect_spoofing( exe_basename: Optional[str], cmdline_basename: Optional[str], comm: Optional[str], - ) -> Optional[list]: + ) -> Tuple[bool, bool, bool]: """ Analyze the three name sources to detect potential spoofing @@ -187,32 +187,34 @@ def _detect_spoofing( comm: Name from comm field Returns: - notes: List of notes indicating potential spoofing, or None if no issues found + Tuple of (is_deleted, cmdline_spoofed, comm_spoofed) boolean flags """ - notes = [] + # Check if process image has been deleted + is_deleted = exe_basename and exe_basename.endswith(self.deleted) + + # Get clean basename for comparison (without " (deleted)" suffix) + clean_exe_basename = exe_basename + if is_deleted: + clean_exe_basename = exe_basename[: len(self.deleted) * -1] - # Skip kernel threads + # Skip kernel threads - need at least 2 sources for comparison available_sources = sum( - 1 for name in [exe_basename, cmdline_basename, comm] if name + 1 for name in [clean_exe_basename, cmdline_basename, comm] if name ) + if available_sources < 2: + return False, False, False - is_deleted = exe_basename and exe_basename.endswith(self.deleted) - if is_deleted: - notes.append(f"'Potential Process image deletion: exe_file={exe_basename}'") - exe_basename = exe_basename[: len(self.deleted) * -1] + # Check for cmdline spoofing + cmdline_spoofed = False + if clean_exe_basename and cmdline_basename: + cmdline_spoofed = clean_exe_basename != cmdline_basename - if available_sources < 2: - return None + # Check for comm spoofing (comm is truncated to 15 characters) + comm_spoofed = False + if clean_exe_basename and comm: + comm_spoofed = clean_exe_basename[:15] != comm - if exe_basename != cmdline_basename: - notes.append( - f"'Potential cmdline spoofing: exe_file={exe_basename};cmdline={cmdline_basename}'" - ) - if exe_basename[:15] != comm: - notes.append( - f"'Potential comm spoofing: exe_file={exe_basename};comm={comm}'" - ) - return notes + return is_deleted, cmdline_spoofed, comm_spoofed def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: """ @@ -231,7 +233,9 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: exe_basename, cmdline_basename, comm = self._extract_process_names(task) - notes = self._detect_spoofing(exe_basename, cmdline_basename, comm) + is_deleted, cmdline_spoofed, comm_spoofed = self._detect_spoofing( + exe_basename, cmdline_basename, comm + ) exe_render = exe_basename if exe_basename else "N/A" cmdline_render = cmdline_basename if cmdline_basename else "N/A" @@ -245,7 +249,9 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: exe_render, cmdline_render, comm_render, - "[" + ", ".join(notes) + "]" if notes else "OK", + cmdline_spoofed, + comm_spoofed, + is_deleted, ), ) @@ -265,7 +271,9 @@ def run(self): ("Exe_Basename", str), ("Cmdline_Basename", str), ("Comm", str), - ("Notes", str), + ("Cmdline_Spoofed", bool), + ("Comm_Spoofed", bool), + ("Deleted", bool), ], self._generator( pslist.PsList.list_tasks( From da47c4060ec3945d9dcf80490903800e0a2c1b13 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:59:42 +0300 Subject: [PATCH 12/20] black --- volatility3/framework/plugins/linux/malware/process_spoofing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 7f2c9af3c5..a38367bd1b 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -191,7 +191,7 @@ def _detect_spoofing( """ # Check if process image has been deleted is_deleted = exe_basename and exe_basename.endswith(self.deleted) - + # Get clean basename for comparison (without " (deleted)" suffix) clean_exe_basename = exe_basename if is_deleted: From 93bdcb3832e8b26b8934ff2d9018cb61050d0eb3 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:16:59 +0300 Subject: [PATCH 13/20] Plugins: determine process exe deletion structurally --- .../plugins/linux/malware/process_spoofing.py | 97 +++++++++++-------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index a38367bd1b..1c2b1a4784 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -22,7 +22,6 @@ class ProcessSpoofing(plugins.PluginInterface): _required_framework_version = (2, 0, 0) _version = (1, 1, 0) - deleted = " (deleted)" @classmethod def get_requirements(cls): @@ -51,7 +50,7 @@ def get_executable_path( cls, context: interfaces.context.ContextInterface, task: interfaces.objects.ObjectInterface, - ) -> Optional[str]: + ) -> Tuple[Optional[str], bool]: """ Extract the executable path from task_struct.mm.exe_file @@ -60,33 +59,54 @@ def get_executable_path( task: task_struct object of the process Returns: - Executable path or None if not available + Tuple of (basename, is_deleted) or (None, False) if not available """ + is_deleted = False + try: mm = task.mm - if not mm or not mm.is_readable(): - # Kernel threads don't have mm struct - return None + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug(f"Unable to access mm for task at {task.vol.offset:#x}: {e}") + return None, is_deleted + if not mm or not mm.is_readable(): + # Kernel threads don't have mm struct + return None, is_deleted + + try: exe_file = mm.exe_file + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to access exe_file for task at {task.vol.offset:#x}: {e}" + ) + return None, is_deleted - if not exe_file or not exe_file.is_readable(): - return None + if not exe_file or not exe_file.is_readable(): + return None, is_deleted + try: exe_inode = exe_file.f_path.dentry.d_inode exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file) + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to read exe_file path for task at {task.vol.offset:#x}: {e}" + ) + return None, is_deleted - # If the inode link count is 0, the process image has been deleted - if exe_inode.i_nlink == 0: - exe_path += cls.deleted - - return exe_path if exe_path else None + if not exe_path: + return None, is_deleted + try: + # Check if the inode link count is 0 (process image has been deleted) + is_deleted = exe_inode.i_nlink == 0 except (exceptions.InvalidAddressException, AttributeError) as e: vollog.debug( - f"Unable to read executable path for task at {task.vol.offset:#x}: {e}" + f"Unable to check inode link count for task at {task.vol.offset:#x}: {e}" ) - return None + # Continue without deletion info - we still have the path + + basename = PurePosixPath(exe_path).name + return basename, is_deleted @classmethod def get_cmdline_basename( @@ -155,7 +175,7 @@ def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]: def _extract_process_names( self, task: interfaces.objects.ObjectInterface - ) -> Tuple[Optional[str], Optional[str], Optional[str]]: + ) -> Tuple[Optional[str], Optional[str], Optional[str], bool]: """ Extract all three process name sources for comparison @@ -163,21 +183,20 @@ def _extract_process_names( task: task_struct object of the process Returns: - Tuple of (exe_path_basename, cmdline_basename, comm) + Tuple of (exe_basename, cmdline_basename, comm, is_deleted) """ - exe_path = self.get_executable_path(self.context, task) - exe_basename = PurePosixPath(exe_path).name if exe_path else None + exe_basename, is_deleted = self.get_executable_path(self.context, task) cmdline_basename = self.get_cmdline_basename(self.context, task) comm = self.get_comm(task) - return exe_basename, cmdline_basename, comm + return exe_basename, cmdline_basename, comm, is_deleted def _detect_spoofing( self, exe_basename: Optional[str], cmdline_basename: Optional[str], comm: Optional[str], - ) -> Tuple[bool, bool, bool]: + ) -> Tuple[bool, bool]: """ Analyze the three name sources to detect potential spoofing @@ -187,34 +206,26 @@ def _detect_spoofing( comm: Name from comm field Returns: - Tuple of (is_deleted, cmdline_spoofed, comm_spoofed) boolean flags + Tuple of (cmdline_spoofed, comm_spoofed) boolean flags """ - # Check if process image has been deleted - is_deleted = exe_basename and exe_basename.endswith(self.deleted) - - # Get clean basename for comparison (without " (deleted)" suffix) - clean_exe_basename = exe_basename - if is_deleted: - clean_exe_basename = exe_basename[: len(self.deleted) * -1] - # Skip kernel threads - need at least 2 sources for comparison available_sources = sum( - 1 for name in [clean_exe_basename, cmdline_basename, comm] if name + 1 for name in [exe_basename, cmdline_basename, comm] if name ) if available_sources < 2: - return False, False, False + return False, False # Check for cmdline spoofing cmdline_spoofed = False - if clean_exe_basename and cmdline_basename: - cmdline_spoofed = clean_exe_basename != cmdline_basename + if exe_basename and cmdline_basename: + cmdline_spoofed = exe_basename != cmdline_basename # Check for comm spoofing (comm is truncated to 15 characters) comm_spoofed = False - if clean_exe_basename and comm: - comm_spoofed = clean_exe_basename[:15] != comm + if exe_basename and comm: + comm_spoofed = exe_basename[:15] != comm - return is_deleted, cmdline_spoofed, comm_spoofed + return cmdline_spoofed, comm_spoofed def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: """ @@ -231,13 +242,19 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: pid = task.pid ppid = task.get_parent_pid() - exe_basename, cmdline_basename, comm = self._extract_process_names(task) + exe_basename, cmdline_basename, comm, is_deleted = ( + self._extract_process_names(task) + ) - is_deleted, cmdline_spoofed, comm_spoofed = self._detect_spoofing( + cmdline_spoofed, comm_spoofed = self._detect_spoofing( exe_basename, cmdline_basename, comm ) + # Prepare display values exe_render = exe_basename if exe_basename else "N/A" + if is_deleted and exe_basename: + exe_render += " (deleted)" + cmdline_render = cmdline_basename if cmdline_basename else "N/A" comm_render = comm if comm else "N/A" @@ -273,7 +290,7 @@ def run(self): ("Comm", str), ("Cmdline_Spoofed", bool), ("Comm_Spoofed", bool), - ("Deleted", bool), + ("Exe_Deleted", bool), ], self._generator( pslist.PsList.list_tasks( From 91ff5eb2cadf454120e8ab40b8d2848502d4a1cc Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:19:23 +0300 Subject: [PATCH 14/20] Plugins: make get_executable_path more accurate in process_spoofing --- .../framework/plugins/linux/malware/process_spoofing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 1c2b1a4784..b9126c7238 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -59,7 +59,7 @@ def get_executable_path( task: task_struct object of the process Returns: - Tuple of (basename, is_deleted) or (None, False) if not available + Tuple of (full_path, is_deleted) or (None, False) if not available """ is_deleted = False @@ -105,8 +105,7 @@ def get_executable_path( ) # Continue without deletion info - we still have the path - basename = PurePosixPath(exe_path).name - return basename, is_deleted + return exe_path, is_deleted @classmethod def get_cmdline_basename( @@ -185,7 +184,8 @@ def _extract_process_names( Returns: Tuple of (exe_basename, cmdline_basename, comm, is_deleted) """ - exe_basename, is_deleted = self.get_executable_path(self.context, task) + exe_path, is_deleted = self.get_executable_path(self.context, task) + exe_basename = PurePosixPath(exe_path).name if exe_path else None cmdline_basename = self.get_cmdline_basename(self.context, task) comm = self.get_comm(task) From d13b21e7099587f9d02241768c5be7d5828a20ed Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:09:56 +0300 Subject: [PATCH 15/20] Plugins: utilize linuxutilities.path_for_file (deleted) logic solely --- .../plugins/linux/malware/process_spoofing.py | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index b9126c7238..35cbd79d48 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -71,7 +71,7 @@ def get_executable_path( if not mm or not mm.is_readable(): # Kernel threads don't have mm struct - return None, is_deleted + return None try: exe_file = mm.exe_file @@ -79,33 +79,20 @@ def get_executable_path( vollog.debug( f"Unable to access exe_file for task at {task.vol.offset:#x}: {e}" ) - return None, is_deleted + return None if not exe_file or not exe_file.is_readable(): - return None, is_deleted + return None try: - exe_inode = exe_file.f_path.dentry.d_inode exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file) except (exceptions.InvalidAddressException, AttributeError) as e: vollog.debug( f"Unable to read exe_file path for task at {task.vol.offset:#x}: {e}" ) - return None, is_deleted - - if not exe_path: - return None, is_deleted - - try: - # Check if the inode link count is 0 (process image has been deleted) - is_deleted = exe_inode.i_nlink == 0 - except (exceptions.InvalidAddressException, AttributeError) as e: - vollog.debug( - f"Unable to check inode link count for task at {task.vol.offset:#x}: {e}" - ) - # Continue without deletion info - we still have the path + return None - return exe_path, is_deleted + return exe_path @classmethod def get_cmdline_basename( @@ -184,12 +171,12 @@ def _extract_process_names( Returns: Tuple of (exe_basename, cmdline_basename, comm, is_deleted) """ - exe_path, is_deleted = self.get_executable_path(self.context, task) + exe_path = self.get_executable_path(self.context, task) exe_basename = PurePosixPath(exe_path).name if exe_path else None cmdline_basename = self.get_cmdline_basename(self.context, task) comm = self.get_comm(task) - return exe_basename, cmdline_basename, comm, is_deleted + return exe_path, exe_basename, cmdline_basename, comm def _detect_spoofing( self, @@ -242,7 +229,7 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: pid = task.pid ppid = task.get_parent_pid() - exe_basename, cmdline_basename, comm, is_deleted = ( + exe_path, exe_basename, cmdline_basename, comm = ( self._extract_process_names(task) ) @@ -250,10 +237,7 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: exe_basename, cmdline_basename, comm ) - # Prepare display values - exe_render = exe_basename if exe_basename else "N/A" - if is_deleted and exe_basename: - exe_render += " (deleted)" + is_deleted = exe_path.endswith(" (deleted)") if exe_path else False cmdline_render = cmdline_basename if cmdline_basename else "N/A" comm_render = comm if comm else "N/A" @@ -263,7 +247,7 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: ( pid, ppid, - exe_render, + exe_path, cmdline_render, comm_render, cmdline_spoofed, @@ -285,7 +269,7 @@ def run(self): [ ("PID", int), ("PPID", int), - ("Exe_Basename", str), + ("Exe_Path", str), ("Cmdline_Basename", str), ("Comm", str), ("Cmdline_Spoofed", bool), From fad88b8ef945cca6eb6195786c65ec52b79a890d Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:13:57 +0300 Subject: [PATCH 16/20] Plugins: truncate deleted to check spoofing in process_spoofing --- .../framework/plugins/linux/malware/process_spoofing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 35cbd79d48..a0cee5e4b9 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -169,10 +169,12 @@ def _extract_process_names( task: task_struct object of the process Returns: - Tuple of (exe_basename, cmdline_basename, comm, is_deleted) + Tuple of (exe_path, exe_basename, cmdline_basename, comm) """ exe_path = self.get_executable_path(self.context, task) exe_basename = PurePosixPath(exe_path).name if exe_path else None + if exe_basename.endswith(" (deleted)"): + exe_basename = exe_basename[:-len(" (deleted)")] cmdline_basename = self.get_cmdline_basename(self.context, task) comm = self.get_comm(task) @@ -210,6 +212,7 @@ def _detect_spoofing( # Check for comm spoofing (comm is truncated to 15 characters) comm_spoofed = False if exe_basename and comm: + print(exe_basename, comm) comm_spoofed = exe_basename[:15] != comm return cmdline_spoofed, comm_spoofed From c94b039f8e927c41421042d300a965c7fd108150 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:21:19 +0300 Subject: [PATCH 17/20] Plugins: process_spoofing change extract_process_names to classmethod --- .../plugins/linux/malware/process_spoofing.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index a0cee5e4b9..fafca4da1e 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -50,7 +50,7 @@ def get_executable_path( cls, context: interfaces.context.ContextInterface, task: interfaces.objects.ObjectInterface, - ) -> Tuple[Optional[str], bool]: + ) -> Optional[str]: """ Extract the executable path from task_struct.mm.exe_file @@ -59,15 +59,14 @@ def get_executable_path( task: task_struct object of the process Returns: - Tuple of (full_path, is_deleted) or (None, False) if not available + Returns executable path or None if not available """ - is_deleted = False try: mm = task.mm except (exceptions.InvalidAddressException, AttributeError) as e: vollog.debug(f"Unable to access mm for task at {task.vol.offset:#x}: {e}") - return None, is_deleted + return None if not mm or not mm.is_readable(): # Kernel threads don't have mm struct @@ -159,24 +158,24 @@ def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]: vollog.debug(f"Unable to read comm for task at {task.vol.offset:#x}: {e}") return None - def _extract_process_names( - self, task: interfaces.objects.ObjectInterface - ) -> Tuple[Optional[str], Optional[str], Optional[str], bool]: + @classmethod + def extract_process_names( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], bool]: """ - Extract all three process name sources for comparison - - Args: - task: task_struct object of the process - + Extract all process name sources for comparison + Returns: Tuple of (exe_path, exe_basename, cmdline_basename, comm) """ - exe_path = self.get_executable_path(self.context, task) + exe_path = cls.get_executable_path(context, task) exe_basename = PurePosixPath(exe_path).name if exe_path else None - if exe_basename.endswith(" (deleted)"): + if exe_basename and exe_basename.endswith(" (deleted)"): exe_basename = exe_basename[:-len(" (deleted)")] - cmdline_basename = self.get_cmdline_basename(self.context, task) - comm = self.get_comm(task) + cmdline_basename = cls.get_cmdline_basename(context, task) + comm = cls.get_comm(task) return exe_path, exe_basename, cmdline_basename, comm @@ -212,7 +211,6 @@ def _detect_spoofing( # Check for comm spoofing (comm is truncated to 15 characters) comm_spoofed = False if exe_basename and comm: - print(exe_basename, comm) comm_spoofed = exe_basename[:15] != comm return cmdline_spoofed, comm_spoofed @@ -233,7 +231,7 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: ppid = task.get_parent_pid() exe_path, exe_basename, cmdline_basename, comm = ( - self._extract_process_names(task) + self.extract_process_names(self.context, task) ) cmdline_spoofed, comm_spoofed = self._detect_spoofing( From 64ef8e3e50de08161c1c9c4002d7fe8dfd4eb46d Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:27:46 +0300 Subject: [PATCH 18/20] Plugins: process spoofing handle none values and set more classmethods --- .../framework/plugins/linux/malware/process_spoofing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index fafca4da1e..428a79bc76 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -240,6 +240,8 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: is_deleted = exe_path.endswith(" (deleted)") if exe_path else False + # Convert None values to strings for TreeGrid compatibility + exe_path_render = exe_path if exe_path else "N/A" cmdline_render = cmdline_basename if cmdline_basename else "N/A" comm_render = comm if comm else "N/A" @@ -248,7 +250,7 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: ( pid, ppid, - exe_path, + exe_path_render, cmdline_render, comm_render, cmdline_spoofed, From 33911c2d77498b1f8a690553c5df7bb4bab25223 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:28:44 +0300 Subject: [PATCH 19/20] black and ruff --- .../framework/plugins/linux/malware/process_spoofing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 428a79bc76..1048378b97 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -166,14 +166,14 @@ def extract_process_names( ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], bool]: """ Extract all process name sources for comparison - + Returns: Tuple of (exe_path, exe_basename, cmdline_basename, comm) """ exe_path = cls.get_executable_path(context, task) exe_basename = PurePosixPath(exe_path).name if exe_path else None if exe_basename and exe_basename.endswith(" (deleted)"): - exe_basename = exe_basename[:-len(" (deleted)")] + exe_basename = exe_basename[: -len(" (deleted)")] cmdline_basename = cls.get_cmdline_basename(context, task) comm = cls.get_comm(task) From 41fa262944d5eacfb22cf78e1bd7308c2763aa95 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:29:15 +0300 Subject: [PATCH 20/20] Plugins: process_spoofing bump required_framework_version --- volatility3/framework/plugins/linux/malware/process_spoofing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 1048378b97..941f1495ea 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -20,7 +20,7 @@ class ProcessSpoofing(plugins.PluginInterface): """Detects process spoofing by comparing executable path to cmdline & comm fields""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 27, 0) _version = (1, 1, 0) @classmethod