From c08dda88ded4be77a0f1f2aaf0b3bf3f25181334 Mon Sep 17 00:00:00 2001 From: Eve Date: Fri, 8 Dec 2023 22:56:17 +0000 Subject: [PATCH 1/3] Linux: Update pslist plugin so that it can be used from pstree, and update pstree to support dumping of processes --- volatility3/framework/plugins/linux/pslist.py | 113 +++++++++++------- volatility3/framework/plugins/linux/pstree.py | 12 +- 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 16e370b6e..7e38625ac 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -1,7 +1,7 @@ # This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Any, Callable, Iterable, List +from typing import Any, Callable, Iterable, List, Tuple from volatility3.framework import interfaces, renderers from volatility3.framework.configuration import requirements @@ -17,7 +17,7 @@ class PsList(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (2, 1, 0) + _version = (2, 1, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -78,6 +78,71 @@ class PsList(interfaces.plugins.PluginInterface): else: return lambda _: False + def _get_task_fields( + self, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False + ) -> Tuple[int, int, int, str]: + """Extract the fields needed for the final output + Args: + task: A task object from where to get the fields. + decorate_comm: If True, it decorates the comm string of + - User threads: in curly brackets, + - Kernel threads: in square brackets + Defaults to False. + Returns: + A tuple with the fields to show in the plugin output. + """ + pid = task.tgid + tid = task.pid + ppid = task.parent.tgid if task.parent else 0 + name = utility.array_to_string(task.comm) + if decorate_comm: + if task.is_kernel_thread: + name = f"[{name}]" + elif task.is_user_thread: + name = f"{{{name}}}" + + task_fields = (format_hints.Hex(task.vol.offset), pid, tid, ppid, name) + return task_fields + + def _get_file_output(self, task: interfaces.objects.ObjectInterface) -> str: + """Extract the elf for the process if requested + Args: + task: A task object to extract from. + Returns: + A string showing the results of the extraction, either + the filename used or an error. + """ + elf_table_name = intermed.IntermediateSymbolTable.create( + self.context, + self.config_path, + "linux", + "elf", + class_types=elf.class_types, + ) + proc_layer_name = task.add_process_layer() + if not proc_layer_name: + # if we can't build a proc layer we can't + # extract the elf + return renderers.NotApplicableValue() + else: + # Find the vma that belongs to the main ELF of the process + file_output = "Error outputting file" + for v in task.mm.get_mmap_iter(): + if v.vm_start == task.mm.start_code: + file_handle = elfs.Elfs.elf_dump( + self.context, + proc_layer_name, + elf_table_name, + v, + task, + self.open, + ) + if file_handle: + file_output = str(file_handle.preferred_filename) + file_handle.close() + break + return file_output + def _generator( self, pid_filter: Callable[[Any], bool], @@ -104,49 +169,15 @@ class PsList(interfaces.plugins.PluginInterface): for task in self.list_tasks( self.context, self.config["kernel"], pid_filter, include_threads ): - elf_table_name = intermed.IntermediateSymbolTable.create( - self.context, - self.config_path, - "linux", - "elf", - class_types=elf.class_types, - ) - file_output = "Disabled" if dump: - proc_layer_name = task.add_process_layer() - if not proc_layer_name: - continue + file_output = self._get_file_output(task) + else: + file_output = "Disabled" - # Find the vma that belongs to the main ELF of the process - file_output = "Error outputting file" - - for v in task.mm.get_mmap_iter(): - if v.vm_start == task.mm.start_code: - file_handle = elfs.Elfs.elf_dump( - self.context, - proc_layer_name, - elf_table_name, - v, - task, - self.open, - ) - if file_handle: - file_output = str(file_handle.preferred_filename) - file_handle.close() - break - - pid = task.tgid - tid = task.pid - ppid = task.parent.tgid if task.parent else 0 - name = utility.array_to_string(task.comm) - if decorate_comm: - if task.is_kernel_thread: - name = f"[{name}]" - elif task.is_user_thread: - name = f"{{{name}}}" + offset, pid, tid, ppid, name = self._get_task_fields(task, decorate_comm) yield 0, ( - format_hints.Hex(task.vol.offset), + offset, pid, tid, ppid, diff --git a/volatility3/framework/plugins/linux/pstree.py b/volatility3/framework/plugins/linux/pstree.py index e07a8aced..f42986f56 100644 --- a/volatility3/framework/plugins/linux/pstree.py +++ b/volatility3/framework/plugins/linux/pstree.py @@ -39,7 +39,11 @@ class PsTree(pslist.PsList): self._levels[pid] = level def _generator( - self, pid_filter, include_threads: bool = False, decorate_com: bool = False + self, + pid_filter, + include_threads: bool = False, + decorate_com: bool = False, + dump: bool = False, ): """Generates the tasks hierarchy tree. @@ -72,6 +76,12 @@ class PsTree(pslist.PsList): task = self._tasks[pid] row = self._get_task_fields(task, decorate_com) + if dump: + file_output = self._get_file_output(task) + else: + file_output = "Disabled" + row = self._get_task_fields(task, decorate_com) + row += (file_output,) # also add the file output column tid = task.pid yield (self._levels[tid] - 1, row) From 19499d511315b718186dff05d133fa86f3310947 Mon Sep 17 00:00:00 2001 From: Eve Date: Mon, 11 Dec 2023 06:37:17 +0000 Subject: [PATCH 2/3] Linux: update pslist with classmethod for get_task_fields --- volatility3/framework/plugins/linux/pslist.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 7e38625ac..9afd13e5a 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -17,7 +17,7 @@ class PsList(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (2, 1, 1) + _version = (2, 2, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -78,8 +78,9 @@ class PsList(interfaces.plugins.PluginInterface): else: return lambda _: False - def _get_task_fields( - self, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False + @classmethod + def get_task_fields( + cls, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False ) -> Tuple[int, int, int, str]: """Extract the fields needed for the final output Args: @@ -101,7 +102,7 @@ class PsList(interfaces.plugins.PluginInterface): elif task.is_user_thread: name = f"{{{name}}}" - task_fields = (format_hints.Hex(task.vol.offset), pid, tid, ppid, name) + task_fields = (task.vol.offset, pid, tid, ppid, name) return task_fields def _get_file_output(self, task: interfaces.objects.ObjectInterface) -> str: @@ -174,10 +175,10 @@ class PsList(interfaces.plugins.PluginInterface): else: file_output = "Disabled" - offset, pid, tid, ppid, name = self._get_task_fields(task, decorate_comm) + offset, pid, tid, ppid, name = self.get_task_fields(task, decorate_comm) yield 0, ( - offset, + format_hints.Hex(offset), pid, tid, ppid, From 3d9b0208cb3eb0f939140538054c3ac707af6123 Mon Sep 17 00:00:00 2001 From: Eve Date: Tue, 12 Dec 2023 06:56:14 +0000 Subject: [PATCH 3/3] Linux: change pstree to a basic plugin rather than inheriting from pslist --- volatility3/framework/plugins/linux/pstree.py | 102 +++++++++++++----- 1 file changed, 74 insertions(+), 28 deletions(-) diff --git a/volatility3/framework/plugins/linux/pstree.py b/volatility3/framework/plugins/linux/pstree.py index f42986f56..efe5223df 100644 --- a/volatility3/framework/plugins/linux/pstree.py +++ b/volatility3/framework/plugins/linux/pstree.py @@ -2,18 +2,49 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints from volatility3.plugins.linux import pslist -class PsTree(pslist.PsList): +class PsTree(interfaces.plugins.PluginInterface): """Plugin for listing processes in a tree based on their parent process ID.""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._tasks = {} - self._levels = {} - self._children = {} + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 2, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.BooleanRequirement( + name="threads", + description="Include user threads", + optional=True, + default=False, + ), + requirements.BooleanRequirement( + name="decorate_comm", + description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets", + optional=True, + default=False, + ), + ] def find_level(self, pid: int) -> None: """Finds how deep the PID is in the tasks hierarchy. @@ -40,18 +71,13 @@ class PsTree(pslist.PsList): def _generator( self, - pid_filter, - include_threads: bool = False, - decorate_com: bool = False, - dump: bool = False, + tasks: list, + decorate_comm: bool = False, ): """Generates the tasks hierarchy tree. Args: - pid_filter: A function which takes a process object and returns True if the process should be ignored/filtered - include_threads: If True, the output will also show the user threads - If False, only the thread group leaders will be shown - Defaults to False. + tasks: A list of task objects to be displayed decorate_comm: If True, it decorates the comm string of - User threads: in curly brackets, - Kernel threads: in square brackets @@ -59,13 +85,12 @@ class PsTree(pslist.PsList): Yields: Each rows """ - vmlinux = self.context.modules[self.config["kernel"]] - for proc in self.list_tasks( - self.context, - vmlinux.name, - filter_func=pid_filter, - include_threads=include_threads, - ): + + self._tasks = {} + self._levels = {} + self._children = {} + + for proc in tasks: self._tasks[proc.pid] = proc # Build the child/level maps @@ -75,13 +100,10 @@ class PsTree(pslist.PsList): def yield_processes(pid): task = self._tasks[pid] - row = self._get_task_fields(task, decorate_com) - if dump: - file_output = self._get_file_output(task) - else: - file_output = "Disabled" - row = self._get_task_fields(task, decorate_com) - row += (file_output,) # also add the file output column + row = pslist.PsList.get_task_fields(task, decorate_comm) + # update the first element, the offset, in the row tuple to use format_hints.Hex + # as a simple int is returned from get_task_fields. + row = (format_hints.Hex(row[0]),) + row[1:] tid = task.pid yield (self._levels[tid] - 1, row) @@ -92,3 +114,27 @@ class PsTree(pslist.PsList): for pid, level in self._levels.items(): if level == 1: yield from yield_processes(pid) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + include_threads = self.config.get("threads") + decorate_comm = self.config.get("decorate_comm") + + return renderers.TreeGrid( + [ + ("OFFSET (V)", format_hints.Hex), + ("PID", int), + ("TID", int), + ("PPID", int), + ("COMM", str), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, + self.config["kernel"], + filter_func=filter_func, + include_threads=include_threads, + ), + decorate_comm=decorate_comm, + ), + )