From 5aea7f92e94af4b04e811b73ceadb51cc9a355d3 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/24] 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 000000000..2546a3623 --- /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 cf6fa422b479bdd9bc8e749e5a1d72a9b949d89b 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/24] 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 2546a3623..9d2e55acb 100644 --- a/volatility3/framework/plugins/linux/process_spoofing.py +++ b/volatility3/framework/plugins/linux/process_spoofing.py @@ -60,14 +60,12 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): """ 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 5c3fca35447247c0d59c814682ae95952fc1067a 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/24] 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 9d2e55acb..451889029 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 fd270a208d3ddb9c72cc047dd8b4901bb7f2e1b7 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/24] 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 15dc9ec4635170f70edcb8fb96c25251a3040d0c 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/24] 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 451889029..b14dd208c 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -237,8 +237,10 @@ class ProcessSpoofing(plugins.PluginInterface): ), ) - 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 d55cf9a9d0f518331e01d78fff88e2f06d0f9ef9 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/24] 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 b14dd208c..f32f80f8a 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -61,7 +61,7 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 cc6aa2117b8b4700d52a6bdafc23c1f5cd14307b 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/24] 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 f32f80f8a..7302f2a71 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -46,13 +46,17 @@ class ProcessSpoofing(plugins.PluginInterface): ), ] - 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 939034cd3344b5511ebbde43c1a3c50cdf15eb11 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/24] 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 7302f2a71..01d4c1e2c 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -104,7 +104,7 @@ class ProcessSpoofing(plugins.PluginInterface): 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 83494796432fe9bd1348e5bfbd6fa3549b9e79ef 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/24] 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 01d4c1e2c..473bd7d07 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -82,7 +82,10 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): """ 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 1a92e749ffae2e6189aec920d4fd9178004ca046 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/24] Plugins: more precise exception handling in process_spoofing --- .../plugins/linux/malware/process_spoofing.py | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 473bd7d07..b3cc37cc7 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -104,37 +104,36 @@ class ProcessSpoofing(plugins.PluginInterface): Returns: Basename of the first command line argument or None if not available """ + 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 + + proc_layer = context.layers[proc_layer_name] + 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 + try: - mm = task.mm - if not mm or not mm.is_readable(): - return None + 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 - proc_layer_name = task.add_process_layer() - if proc_layer_name is None: - 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 - - if not (0 < size_to_read <= 4096): - return None - - 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): + # 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 d11f50906ac92745d268329eb49fb1ba889c7528 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/24] Plugins: swap notes for boolean flags in process_spoofing --- .../plugins/linux/malware/process_spoofing.py | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index b3cc37cc7..7f2c9af3c 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -177,7 +177,7 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 = [] - - # Skip kernel threads - available_sources = sum( - 1 for name in [exe_basename, cmdline_basename, comm] if name - ) - + # 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: - notes.append(f"'Potential Process image deletion: exe_file={exe_basename}'") - exe_basename = exe_basename[: len(self.deleted) * -1] + 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 + ) if available_sources < 2: - return None + return False, False, False - 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 + # Check for cmdline spoofing + cmdline_spoofed = False + if clean_exe_basename and cmdline_basename: + cmdline_spoofed = clean_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 + + return is_deleted, cmdline_spoofed, comm_spoofed def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: """ @@ -231,7 +233,9 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): exe_render, cmdline_render, comm_render, - "[" + ", ".join(notes) + "]" if notes else "OK", + cmdline_spoofed, + comm_spoofed, + is_deleted, ), ) @@ -265,7 +271,9 @@ class ProcessSpoofing(plugins.PluginInterface): ("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 5c7cf1115205673692193dc4292306781dad1bfa 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/24] 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 7f2c9af3c..a38367bd1 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -191,7 +191,7 @@ class ProcessSpoofing(plugins.PluginInterface): """ # 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 41f964cfe12d3958c284e00a08860de94cbb6f52 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/24] Plugins: determine process exe deletion structurally --- .../plugins/linux/malware/process_spoofing.py | 109 ++++++++++-------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index a38367bd1..1c2b1a478 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 - - if not exe_file or not exe_file.is_readable(): - return None - - exe_inode = exe_file.f_path.dentry.d_inode - 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 += cls.deleted - - return exe_path if exe_path else None - 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 access exe_file for task at {task.vol.offset:#x}: {e}" ) - return None + return None, is_deleted + + 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 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 + + basename = PurePosixPath(exe_path).name + return basename, is_deleted @classmethod def get_cmdline_basename( @@ -155,7 +175,7 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): ("Comm", str), ("Cmdline_Spoofed", bool), ("Comm_Spoofed", bool), - ("Deleted", bool), + ("Exe_Deleted", bool), ], self._generator( pslist.PsList.list_tasks( From d384f62721aa578dfb6833768db2c043a6701447 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/24] 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 1c2b1a478..b9126c723 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -59,7 +59,7 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): ) # 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 9686b5ae8bc8b76ac9f0f81e90e5b951dfe994a3 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/24] 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 b9126c723..35cbd79d4 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -71,7 +71,7 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 + return 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 check inode link count for task at {task.vol.offset:#x}: {e}" - ) - # Continue without deletion info - we still have the path - - return exe_path, is_deleted + return exe_path @classmethod def get_cmdline_basename( @@ -184,12 +171,12 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): ( pid, ppid, - exe_render, + exe_path, cmdline_render, comm_render, cmdline_spoofed, @@ -285,7 +269,7 @@ class ProcessSpoofing(plugins.PluginInterface): [ ("PID", int), ("PPID", int), - ("Exe_Basename", str), + ("Exe_Path", str), ("Cmdline_Basename", str), ("Comm", str), ("Cmdline_Spoofed", bool), From 1359a11f24cbb6bf5d09a31924d52cb2c920caa6 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/24] 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 35cbd79d4..a0cee5e4b 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -169,10 +169,12 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): # 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 cbe5b46b8e542c4407af8389355f8c01dc15430f 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/24] 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 a0cee5e4b..fafca4da1 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -50,7 +50,7 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): # 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 @@ class ProcessSpoofing(plugins.PluginInterface): 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 c3ddc094cbcccda4fd70fdbd4375e2249d470b73 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/24] 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 fafca4da1..428a79bc7 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -240,6 +240,8 @@ class ProcessSpoofing(plugins.PluginInterface): 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 @@ class ProcessSpoofing(plugins.PluginInterface): ( pid, ppid, - exe_path, + exe_path_render, cmdline_render, comm_render, cmdline_spoofed, From 4cb326fb6ed8cefb79fb80b20014936757c52c32 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/24] 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 428a79bc7..1048378b9 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -166,14 +166,14 @@ class ProcessSpoofing(plugins.PluginInterface): ) -> 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 9c5950e5957f7e6980cc41708830aa74bdb47980 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/24] 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 1048378b9..941f1495e 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -20,7 +20,7 @@ vollog = logging.getLogger(__name__) 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 From d13de25b46c05abf19c2770d2cd6bcc13985ecdc Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 31 Dec 2025 00:26:09 +0200 Subject: [PATCH 21/24] Constants: add MAX_ARG_STRLEN linux constant --- volatility3/framework/constants/linux/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 72440e23e..793ef11e8 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -9,6 +9,10 @@ Linux-specific values that aren't found in debug symbols import enum from dataclasses import dataclass +# Exec argument limits +# Ref: include/uapi/linux/binfmts.h (linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940) +MAX_ARG_STRLEN = 32 * 4096 + KERNEL_NAME = "__kernel__" """The value hard coded from the Linux Kernel (hence not extracted from the layer itself)""" From 2478c1398eb5b0afebc78d237a710d6d2aa760f1 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:06:50 +0200 Subject: [PATCH 22/24] Plugins: get first argument using utility.address_to_string in process_spoofing --- .../plugins/linux/malware/process_spoofing.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 941f1495e..64f3274e9 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -7,6 +7,7 @@ from pathlib import PurePosixPath from typing import Optional, Tuple, Iterator from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.constants import linux as linux_constants from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility @@ -117,30 +118,35 @@ class ProcessSpoofing(plugins.PluginInterface): if proc_layer_name is None: 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 - if not (0 < size_to_read <= 4096): + if size_to_read <= 0: return None + read_length = min(size_to_read, linux_constants.MAX_ARG_STRLEN) + try: - argv = proc_layer.read(start, size_to_read) + cmdline = utility.address_to_string( + context=context, + layer_name=proc_layer_name, + address=start, + count=read_length, + errors="replace", + encoding="utf-8" + ) except exceptions.InvalidAddressException as e: vollog.debug( f"Unable to read cmdline for task at {task.vol.offset:#x}: {e}" ) 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: + if not cmdline: return None + basename = PurePosixPath(cmdline).name + return basename if basename else None + @classmethod def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]: """ From 088a64379cddf40f6d7f8c4465724233338ba111 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:13:01 +0200 Subject: [PATCH 23/24] 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 64f3274e9..0d64861b2 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -133,7 +133,7 @@ class ProcessSpoofing(plugins.PluginInterface): address=start, count=read_length, errors="replace", - encoding="utf-8" + encoding="utf-8", ) except exceptions.InvalidAddressException as e: vollog.debug( From e8087dd68d77391ca6a201abb9a64a558e104a13 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:23:05 +0200 Subject: [PATCH 24/24] Plugins: process_spoofing put reference in docstring --- .../plugins/linux/malware/process_spoofing.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 0d64861b2..493a1de9a 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -17,9 +17,11 @@ 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""" + """Detects process spoofing by comparing executable path to cmdline & comm fields. + + Examples of such behavior can be found here: https://github.com/SolitudePy/linux-mal + """ _required_framework_version = (2, 27, 0) _version = (1, 1, 0) @@ -101,7 +103,12 @@ class ProcessSpoofing(plugins.PluginInterface): task: interfaces.objects.ObjectInterface, ) -> Optional[str]: """ - Extract the command line arguments and return the basename of the first argument + Extract the command line arguments and return the basename of the first argument. + + Notes: + The read length is capped at ``MAX_ARG_STRLEN`` (32 * 4096) per the + kernel limit defined in ``include/uapi/linux/binfmts.h`` (see + linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940). Args: context: The context to operate on