From 16cb449a77555fa7514b968352771f882589f37c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 17 Dec 2021 14:23:14 +1100 Subject: [PATCH 1/7] Added changes to the Linux pslist and pstree plugins to be able to show user threads. --- .../framework/constants/linux/__init__.py | 2 + volatility3/framework/plugins/linux/pslist.py | 125 +++++++++++++++--- volatility3/framework/plugins/linux/pstree.py | 79 +++++++---- .../framework/symbols/linux/__init__.py | 9 ++ 4 files changed, 171 insertions(+), 44 deletions(-) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index c25ea0e2f..276c6015f 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -11,3 +11,5 @@ KERNEL_NAME = "__kernel__" # arch/x86/include/asm/page_types.h PAGE_SHIFT = 12 """The value hard coded from the Linux Kernel (hence not extracted from the layer itself)""" + +PF_KTHREAD = 0x00200000 # I'm a kernel thread diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 5672bb56e..516c74305 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -1,11 +1,12 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# 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 Callable, Iterable, List, Any +from typing import Callable, Iterable, List, Any, Tuple -from volatility3.framework import renderers, interfaces +from volatility3.framework import renderers, interfaces, constants from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility +from volatility3.framework.symbols import linux class PsList(interfaces.plugins.PluginInterface): @@ -13,7 +14,7 @@ class PsList(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _version = (2, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -23,7 +24,15 @@ class PsList(interfaces.plugins.PluginInterface): requirements.ListRequirement(name = 'pid', description = 'Filter on specific process IDs', element_type = int, - optional = True) + 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), ] @classmethod @@ -48,31 +57,88 @@ class PsList(interfaces.plugins.PluginInterface): else: return lambda _: False - def _generator(self): + @staticmethod + def task_is_kernel_thread(task: interfaces.objects.ObjectInterface) -> bool: + return (task.flags & constants.PF_KTHREAD) != 0 + + @staticmethod + def task_is_thread_group_leader(task: interfaces.objects.ObjectInterface) -> bool: + return task.tgid == task.pid + + @staticmethod + def task_is_user_thread(task: interfaces.objects.ObjectInterface) -> bool: + return task.tgid != task.pid + + 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 self.task_is_kernel_thread(task): + name = f"[{name}]" + elif self.task_is_user_thread(task): + name = f"{{{name}}}" + + task_fields = (pid, tid, ppid, name) + return task_fields + + def _generator( + self, + pid_filter: Callable[[Any], bool], + include_threads: bool = False, + decorate_comm: bool = False): + """Generates the tasks list. + + 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. + decorate_comm: If True, it decorates the comm string of + - User threads: in curly brackets, + - Kernel threads: in square brackets + Defaults to False. + Yields: + Each rows + """ for task in self.list_tasks(self.context, self.config['kernel'], - filter_func = self.create_pid_filter(self.config.get('pid', None))): - pid = task.pid - ppid = 0 - if task.parent: - ppid = task.parent.pid - name = utility.array_to_string(task.comm) - yield (0, (pid, ppid, name)) + pid_filter, + include_threads): + row = self._get_task_fields(task, decorate_comm) + yield (0, row) @classmethod def list_tasks( cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False) -> Iterable[interfaces.objects.ObjectInterface]: + filter_func: Callable[[int], bool] = lambda _: False, + include_threads: bool = False) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the tasks in the primary layer. Args: context: The context to retrieve required elements (layers, symbol tables) from vmlinux_module_name: The name of the kernel module on which to operate - + filter_func: A function which takes a process object and returns True if the process should be ignored/filtered + include_threads: If True, it will also return user threads. Yields: - Process objects + Task objects """ vmlinux = context.modules[vmlinux_module_name] @@ -80,8 +146,29 @@ class PsList(interfaces.plugins.PluginInterface): # Note that the init_task itself is not yielded, since "ps" also never shows it. for task in init_task.tasks: - if not filter_func(task): - yield task + if filter_func(task): + continue + + task_threads = [] + current_task = None + next_task = task.thread_group.next + while current_task is None or current_task.vol.offset != task.vol.offset: + current_task = linux.LinuxUtilities.container_of(next_task, "task_struct", "thread_group", vmlinux) + if cls.task_is_thread_group_leader(current_task): + # Making sure the first task yielded is the Task Group Leader + yield current_task + elif include_threads: + task_threads.append(current_task) + next_task = current_task.thread_group.next + + # yield the other task threads + yield from task_threads def run(self): - return renderers.TreeGrid([("PID", int), ("PPID", int), ("COMM", str)], self._generator()) + pids = self.config.get('pid') + include_threads = self.config.get('threads') + decorate_comm = self.config.get('decorate_comm') + filter_func = self.create_pid_filter(pids) + + columns = [("PID", int), ("TID", int), ("PPID", int), ("COMM", str)] + return renderers.TreeGrid(columns, self._generator(filter_func, include_threads, decorate_comm)) diff --git a/volatility3/framework/plugins/linux/pstree.py b/volatility3/framework/plugins/linux/pstree.py index 3b95a344c..174e94ec4 100644 --- a/volatility3/framework/plugins/linux/pstree.py +++ b/volatility3/framework/plugins/linux/pstree.py @@ -1,8 +1,7 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# 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 volatility3.framework.objects import utility from volatility3.plugins.linux import pslist @@ -12,44 +11,74 @@ class PsTree(pslist.PsList): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._processes = {} + self._tasks = {} self._levels = {} self._children = {} - def find_level(self, pid): - """Finds how deep the pid is in the processes list.""" - seen = set([]) - seen.add(pid) - level = 0 - proc = self._processes.get(pid, None) - while proc is not None and proc.parent != 0 and proc.parent.pid not in seen: - ppid = int(proc.parent.pid) + def find_level(self, pid: int) -> None: + """Finds how deep the PID is in the tasks hierarchy. - child_list = self._children.get(ppid, set([])) + Args: + pid: PID to find the level in the hierachy + """ + seen = set([pid]) + level = 0 + proc = self._tasks.get(pid) + while proc and proc.parent and proc.parent.pid not in seen: + if self.task_is_thread_group_leader(proc): + parent_pid = proc.parent.pid + else: + parent_pid = proc.tgid + + child_list = self._children.setdefault(parent_pid, set()) child_list.add(proc.pid) - self._children[ppid] = child_list - proc = self._processes.get(ppid, None) + + proc = self._tasks.get(parent_pid) level += 1 + self._levels[pid] = level - def _generator(self): - """Generates the.""" + def _generator( + self, + pid_filter, + include_threads: bool = False, + decorate_com: 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. + decorate_comm: If True, it decorates the comm string of + - User threads: in curly brackets, + - Kernel threads: in square brackets + Defaults to False. + Yields: + Each rows + """ vmlinux = self.context.modules[self.config['kernel']] - for proc in self.list_tasks(self.context, vmlinux.name): - self._processes[proc.pid] = proc + for proc in self.list_tasks(self.context, + vmlinux.name, + filter_func=pid_filter, + include_threads=include_threads): + self._tasks[proc.pid] = proc # Build the child/level maps - for pid in self._processes: + for pid in self._tasks: self.find_level(pid) def yield_processes(pid): - proc = self._processes[pid] - row = (proc.pid, proc.parent.pid, utility.array_to_string(proc.comm)) + task = self._tasks[pid] - yield (self._levels[pid] - 1, row) - for child_pid in self._children.get(pid, []): + row = self._get_task_fields(task, decorate_com) + + tid = task.pid + yield (self._levels[tid] - 1, row) + + for child_pid in sorted(self._children.get(tid, [])): yield from yield_processes(child_pid) - for pid in self._levels: - if self._levels[pid] == 1: + for pid, level in self._levels.items(): + if level == 1: yield from yield_processes(pid) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 36e23a35d..4df97954d 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -267,3 +267,12 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): list_struct = vmlinux.object(object_type = struct_name, offset = list_start.vol.offset) yield list_struct list_start = getattr(list_struct, list_member) + + @classmethod + def container_of(cls, addr, type_name, member_name, vmlinux): + if not addr: + return + type_dec = vmlinux.get_type(type_name) + member_offset = type_dec.relative_child_offset(member_name) + container_addr = addr - member_offset + return vmlinux.object(object_type=type_name, offset=container_addr, absolute=True) \ No newline at end of file From 86d3785bea15f4337555c39b7008510d66032943 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 17 Dec 2021 14:42:23 +1100 Subject: [PATCH 2/7] Fixing constant module and reference comment --- volatility3/framework/constants/linux/__init__.py | 1 + volatility3/framework/plugins/linux/pslist.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 276c6015f..c0f85593f 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -12,4 +12,5 @@ KERNEL_NAME = "__kernel__" PAGE_SHIFT = 12 """The value hard coded from the Linux Kernel (hence not extracted from the layer itself)""" +# include/linux/sched.h PF_KTHREAD = 0x00200000 # I'm a kernel thread diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 516c74305..b42453d3c 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -59,7 +59,7 @@ class PsList(interfaces.plugins.PluginInterface): @staticmethod def task_is_kernel_thread(task: interfaces.objects.ObjectInterface) -> bool: - return (task.flags & constants.PF_KTHREAD) != 0 + return (task.flags & constants.linux.PF_KTHREAD) != 0 @staticmethod def task_is_thread_group_leader(task: interfaces.objects.ObjectInterface) -> bool: From 39c97b8e9795892f0fbf6851154f66f62bb1871b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 20 Dec 2021 15:26:45 +1100 Subject: [PATCH 3/7] Moving thread type check methods to the task object --- volatility3/framework/plugins/linux/pslist.py | 20 +++----------- .../symbols/linux/extensions/__init__.py | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index b42453d3c..4c55f853c 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -3,7 +3,7 @@ # from typing import Callable, Iterable, List, Any, Tuple -from volatility3.framework import renderers, interfaces, constants +from volatility3.framework import renderers, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.symbols import linux @@ -57,18 +57,6 @@ class PsList(interfaces.plugins.PluginInterface): else: return lambda _: False - @staticmethod - def task_is_kernel_thread(task: interfaces.objects.ObjectInterface) -> bool: - return (task.flags & constants.linux.PF_KTHREAD) != 0 - - @staticmethod - def task_is_thread_group_leader(task: interfaces.objects.ObjectInterface) -> bool: - return task.tgid == task.pid - - @staticmethod - def task_is_user_thread(task: interfaces.objects.ObjectInterface) -> bool: - return task.tgid != task.pid - def _get_task_fields( self, task: interfaces.objects.ObjectInterface, @@ -89,9 +77,9 @@ class PsList(interfaces.plugins.PluginInterface): ppid = task.parent.tgid if task.parent else 0 name = utility.array_to_string(task.comm) if decorate_comm: - if self.task_is_kernel_thread(task): + if task.is_kernel_thread: name = f"[{name}]" - elif self.task_is_user_thread(task): + elif task.is_user_thread: name = f"{{{name}}}" task_fields = (pid, tid, ppid, name) @@ -154,7 +142,7 @@ class PsList(interfaces.plugins.PluginInterface): next_task = task.thread_group.next while current_task is None or current_task.vol.offset != task.vol.offset: current_task = linux.LinuxUtilities.container_of(next_task, "task_struct", "thread_group", vmlinux) - if cls.task_is_thread_group_leader(current_task): + if current_task.is_thread_group_leader: # Making sure the first task yielded is the Task Group Leader yield current_task elif include_threads: diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0edd60608..f28705617 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -201,6 +201,33 @@ class task_struct(generic.GenericIntelProcess): yield (start, end - start) + @property + def is_kernel_thread(self) -> bool: + """Checks if this task is a kernel thread. + + Returns: + bool: True, if this task is a kernel thread. Otherwise, False. + """ + return (self.flags & constants.linux.PF_KTHREAD) != 0 + + @property + def is_thread_group_leader(self) -> bool: + """Checks if this task is a thread group leader. + + Returns: + bool: True, if this task is a thread group leader. Otherwise, False. + """ + return self.tgid == self.pid + + @property + def is_user_thread(self) -> bool: + """Checks if this task is a user thread. + + Returns: + bool: True, if this task is a user thread. Otherwise, False. + """ + return not self.is_kernel_thread and self.tgid != self.pid + class fs_struct(objects.StructType): From 0a3c6297823dd0e49d19864efc9962f3bcef6075 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 28 Apr 2022 10:14:49 +1000 Subject: [PATCH 4/7] Updated to use task's is_thread_group_leader function --- volatility3/framework/plugins/linux/pstree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pstree.py b/volatility3/framework/plugins/linux/pstree.py index 174e94ec4..a44310147 100644 --- a/volatility3/framework/plugins/linux/pstree.py +++ b/volatility3/framework/plugins/linux/pstree.py @@ -25,7 +25,7 @@ class PsTree(pslist.PsList): level = 0 proc = self._tasks.get(pid) while proc and proc.parent and proc.parent.pid not in seen: - if self.task_is_thread_group_leader(proc): + if proc.is_thread_group_leader: parent_pid = proc.parent.pid else: parent_pid = proc.tgid From 192759b5d5c8c2622059847fbd915faf8af66e50 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 28 Apr 2022 10:17:39 +1000 Subject: [PATCH 5/7] Updated to use the new task::get_threads --- volatility3/framework/plugins/linux/pslist.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 4c55f853c..a196810aa 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -137,20 +137,10 @@ class PsList(interfaces.plugins.PluginInterface): if filter_func(task): continue - task_threads = [] - current_task = None - next_task = task.thread_group.next - while current_task is None or current_task.vol.offset != task.vol.offset: - current_task = linux.LinuxUtilities.container_of(next_task, "task_struct", "thread_group", vmlinux) - if current_task.is_thread_group_leader: - # Making sure the first task yielded is the Task Group Leader - yield current_task - elif include_threads: - task_threads.append(current_task) - next_task = current_task.thread_group.next + yield task - # yield the other task threads - yield from task_threads + if include_threads: + yield from task.get_threads() def run(self): pids = self.config.get('pid') From e1b8a0b5cc99bcc3257eb698bb77004148471ab4 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 29 Apr 2022 06:42:37 +1000 Subject: [PATCH 6/7] container_of() is no longer needed here --- volatility3/framework/symbols/linux/__init__.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 4df97954d..8d7f00e06 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -266,13 +266,4 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): while list_start: list_struct = vmlinux.object(object_type = struct_name, offset = list_start.vol.offset) yield list_struct - list_start = getattr(list_struct, list_member) - - @classmethod - def container_of(cls, addr, type_name, member_name, vmlinux): - if not addr: - return - type_dec = vmlinux.get_type(type_name) - member_offset = type_dec.relative_child_offset(member_name) - container_addr = addr - member_offset - return vmlinux.object(object_type=type_name, offset=container_addr, absolute=True) \ No newline at end of file + list_start = getattr(list_struct, list_member) \ No newline at end of file From 9aba96a1970e5a469df529e17de836c0c2a9e19b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 29 Apr 2022 06:46:58 +1000 Subject: [PATCH 7/7] Remove unused import module --- volatility3/framework/plugins/linux/pslist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index a196810aa..45d364224 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -6,7 +6,6 @@ from typing import Callable, Iterable, List, Any, Tuple from volatility3.framework import renderers, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility -from volatility3.framework.symbols import linux class PsList(interfaces.plugins.PluginInterface):