Merge pull request #600 from gcmoreira/linux_pslist_pstree_improvements

Linux PSList and PSTree plugins improvements
This commit is contained in:
ikelos
2022-04-29 00:53:39 +01:00
committed by GitHub
5 changed files with 170 additions and 47 deletions
@@ -11,3 +11,6 @@ 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)"""
# include/linux/sched.h
PF_KTHREAD = 0x00200000 # I'm a kernel thread
+82 -18
View File
@@ -1,7 +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 typing import Callable, Iterable, List, Any
from typing import Callable, Iterable, List, Any, Tuple
from volatility3.framework import renderers, interfaces
from volatility3.framework.configuration import requirements
@@ -14,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]:
@@ -24,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
@@ -49,31 +57,76 @@ class PsList(interfaces.plugins.PluginInterface):
else:
return lambda _: False
def _generator(self):
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 _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, (format_hints.Hex(task.vol.offset), name, pid, ppid))
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]
@@ -81,8 +134,19 @@ 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
yield task
if include_threads:
yield from task.get_threads()
def run(self):
return renderers.TreeGrid([("OFFSET (V)", format_hints.Hex), ("COMM", str), ("PID", int), ("PPID", int)], 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 = [("OFFSET (V)", format_hints.Hex), ("PID", int), ("TID", int), ("PPID", int), ("COMM", str)]
return renderers.TreeGrid(columns, self._generator(filter_func, include_threads, decorate_comm))
+54 -25
View File
@@ -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 proc.is_thread_group_leader:
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)
@@ -266,4 +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)
list_start = getattr(list_struct, list_member)
@@ -201,15 +201,42 @@ 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
def get_threads(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Returns a list of the task_struct based on the list_head
"""Returns a list of the task_struct based on the list_head
thread_node structure."""
task_symbol_table_name = self.get_symbol_table_name()
# iterating through the thread_list from thread_group
# this allows iterating through pointers to grab the
# threads and using the thread_group offset to get the
# this allows iterating through pointers to grab the
# threads and using the thread_group offset to get the
# corresponding task_struct
for task in self.thread_group.to_list(
f"{task_symbol_table_name}{constants.BANG}task_struct",