mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-13 13:17:38 +02:00
Merge pull request #1263 from gcmoreira/linux_lsof_refactoring_fixes_and_improvements
Linux: Add support for threads in both lsof and sockstat plugins.
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
"""A module containing a collection of plugins that produce data typically
|
||||
found in Linux's /proc file system."""
|
||||
import logging, datetime
|
||||
from typing import List, Callable
|
||||
import logging
|
||||
import datetime
|
||||
import dataclasses
|
||||
from typing import List, Callable, Tuple, Iterable
|
||||
|
||||
from volatility3.framework import renderers, interfaces, constants, exceptions
|
||||
from volatility3.framework import renderers, interfaces, constants
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.objects import utility
|
||||
@@ -17,11 +17,100 @@ from volatility3.plugins import timeliner
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FDUser:
|
||||
"""FD user representation, featuring augmented information and formatted fields.
|
||||
This is the data the plugin will eventually display.
|
||||
"""
|
||||
|
||||
task_tgid: int
|
||||
task_tid: int
|
||||
task_comm: str
|
||||
fd_num: int
|
||||
full_path: str
|
||||
device: str = dataclasses.field(default=renderers.NotAvailableValue())
|
||||
inode_num: int = dataclasses.field(default=renderers.NotAvailableValue())
|
||||
inode_type: str = dataclasses.field(default=renderers.NotAvailableValue())
|
||||
file_mode: str = dataclasses.field(default=renderers.NotAvailableValue())
|
||||
change_time: datetime.datetime = dataclasses.field(
|
||||
default=renderers.NotAvailableValue()
|
||||
)
|
||||
modification_time: datetime.datetime = dataclasses.field(
|
||||
default=renderers.NotAvailableValue()
|
||||
)
|
||||
access_time: datetime.datetime = dataclasses.field(
|
||||
default=renderers.NotAvailableValue()
|
||||
)
|
||||
inode_size: int = dataclasses.field(default=renderers.NotAvailableValue())
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FDInternal:
|
||||
"""FD internal representation containing only the core objects
|
||||
|
||||
Fields:
|
||||
task: 'task_struct' object
|
||||
fd_fields: FD fields as obtained from LinuxUtilities.files_descriptors_for_process()
|
||||
"""
|
||||
|
||||
task: interfaces.objects.ObjectInterface
|
||||
fd_fields: Tuple[int, int, str]
|
||||
|
||||
def to_user(self) -> FDUser:
|
||||
"""Augment the FD information to be presented to the user
|
||||
|
||||
Returns:
|
||||
An InodeUser dataclass
|
||||
"""
|
||||
# Ensure all types are atomic immutable. Otherwise, astuple() will take a long
|
||||
# time doing a deepcopy of the Volatility objects.
|
||||
task_tgid = int(self.task.tgid)
|
||||
task_tid = int(self.task.pid)
|
||||
task_comm = utility.array_to_string(self.task.comm)
|
||||
fd_num, filp, full_path = self.fd_fields
|
||||
fd_num = int(fd_num)
|
||||
full_path = str(full_path)
|
||||
inode = filp.get_inode()
|
||||
if inode:
|
||||
superblock_ptr = inode.i_sb
|
||||
if superblock_ptr and superblock_ptr.is_readable():
|
||||
device = f"{superblock_ptr.major}:{superblock_ptr.minor}"
|
||||
else:
|
||||
device = renderers.NotAvailableValue()
|
||||
|
||||
fd_user = FDUser(
|
||||
task_tgid=task_tgid,
|
||||
task_tid=task_tid,
|
||||
task_comm=task_comm,
|
||||
fd_num=fd_num,
|
||||
full_path=full_path,
|
||||
device=device,
|
||||
inode_num=int(inode.i_ino),
|
||||
inode_type=inode.get_inode_type() or renderers.UnparsableValue(),
|
||||
file_mode=inode.get_file_mode(),
|
||||
change_time=inode.get_change_time(),
|
||||
modification_time=inode.get_modification_time(),
|
||||
access_time=inode.get_access_time(),
|
||||
inode_size=int(inode.i_size),
|
||||
)
|
||||
else:
|
||||
# We use the dataclasses' default values
|
||||
fd_user = FDUser(
|
||||
task_tgid=task_tgid,
|
||||
task_tid=task_tid,
|
||||
task_comm=task_comm,
|
||||
fd_num=fd_num,
|
||||
full_path=full_path,
|
||||
)
|
||||
|
||||
return fd_user
|
||||
|
||||
|
||||
class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
"""Lists open files for each processes."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (1, 2, 0)
|
||||
_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -45,110 +134,59 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_inode_metadata(cls, filp: interfaces.objects.ObjectInterface):
|
||||
try:
|
||||
dentry = filp.get_dentry()
|
||||
if dentry:
|
||||
inode_object = dentry.d_inode
|
||||
if inode_object and inode_object.is_valid():
|
||||
itype = (
|
||||
inode_object.get_inode_type() or renderers.NotAvailableValue()
|
||||
)
|
||||
return (
|
||||
inode_object.i_ino,
|
||||
itype,
|
||||
inode_object.i_size,
|
||||
inode_object.get_file_mode(),
|
||||
inode_object.get_change_time(),
|
||||
inode_object.get_modification_time(),
|
||||
inode_object.get_access_time(),
|
||||
)
|
||||
except (exceptions.InvalidAddressException, AttributeError) as e:
|
||||
vollog.warning(f"Can't get inode metadata: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def list_fds(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
symbol_table: str,
|
||||
vmlinux_module_name: str,
|
||||
filter_func: Callable[[int], bool] = lambda _: False,
|
||||
):
|
||||
) -> Iterable[FDInternal]:
|
||||
"""Enumerates open file descriptors in tasks
|
||||
|
||||
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
|
||||
|
||||
Yields:
|
||||
A FDInternal object
|
||||
"""
|
||||
linuxutils_symbol_table = None
|
||||
for task in pslist.PsList.list_tasks(context, symbol_table, filter_func):
|
||||
for task in pslist.PsList.list_tasks(
|
||||
context, vmlinux_module_name, filter_func, include_threads=True
|
||||
):
|
||||
if linuxutils_symbol_table is None:
|
||||
if constants.BANG not in task.vol.type_name:
|
||||
raise ValueError("Task is not part of a symbol table")
|
||||
linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0]
|
||||
|
||||
task_comm = utility.array_to_string(task.comm)
|
||||
pid = int(task.pid)
|
||||
|
||||
fd_generator = linux.LinuxUtilities.files_descriptors_for_process(
|
||||
context, linuxutils_symbol_table, task
|
||||
)
|
||||
|
||||
for fd_fields in fd_generator:
|
||||
yield pid, task_comm, task, fd_fields
|
||||
yield FDInternal(task=task, fd_fields=fd_fields)
|
||||
|
||||
@classmethod
|
||||
def list_fds_and_inodes(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
symbol_table: str,
|
||||
filter_func: Callable[[int], bool] = lambda _: False,
|
||||
):
|
||||
for pid, task_comm, task, (fd_num, filp, full_path) in cls.list_fds(
|
||||
context, symbol_table, filter_func
|
||||
):
|
||||
inode_metadata = cls.get_inode_metadata(filp)
|
||||
if inode_metadata is None:
|
||||
inode_metadata = tuple(
|
||||
interfaces.renderers.BaseAbsentValue() for _ in range(7)
|
||||
)
|
||||
yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata
|
||||
|
||||
def _generator(self, pids, symbol_table):
|
||||
def _generator(self, pids, vmlinux_module_name):
|
||||
filter_func = pslist.PsList.create_pid_filter(pids)
|
||||
fds_generator = self.list_fds_and_inodes(
|
||||
self.context, symbol_table, filter_func=filter_func
|
||||
)
|
||||
|
||||
for (
|
||||
pid,
|
||||
task_comm,
|
||||
task,
|
||||
fd_num,
|
||||
filp,
|
||||
full_path,
|
||||
inode_metadata,
|
||||
) in fds_generator:
|
||||
inode_num, itype, file_size, imode, ctime, mtime, atime = inode_metadata
|
||||
fields = (
|
||||
pid,
|
||||
task_comm,
|
||||
fd_num,
|
||||
full_path,
|
||||
inode_num,
|
||||
itype,
|
||||
imode,
|
||||
ctime,
|
||||
mtime,
|
||||
atime,
|
||||
file_size,
|
||||
)
|
||||
yield (0, fields)
|
||||
for fd_internal in self.list_fds(
|
||||
self.context, vmlinux_module_name, filter_func=filter_func
|
||||
):
|
||||
fd_user = fd_internal.to_user()
|
||||
yield (0, dataclasses.astuple(fd_user))
|
||||
|
||||
def run(self):
|
||||
pids = self.config.get("pid", None)
|
||||
symbol_table = self.config["kernel"]
|
||||
vmlinux_module_name = self.config["kernel"]
|
||||
|
||||
tree_grid_args = [
|
||||
("PID", int),
|
||||
("TID", int),
|
||||
("Process", str),
|
||||
("FD", int),
|
||||
("Path", str),
|
||||
("Device", str),
|
||||
("Inode", int),
|
||||
("Type", str),
|
||||
("Mode", str),
|
||||
@@ -157,14 +195,25 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
("Accessed", datetime.datetime),
|
||||
("Size", int),
|
||||
]
|
||||
return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table))
|
||||
return renderers.TreeGrid(
|
||||
tree_grid_args, self._generator(pids, vmlinux_module_name)
|
||||
)
|
||||
|
||||
def generate_timeline(self):
|
||||
pids = self.config.get("pid", None)
|
||||
symbol_table = self.config["kernel"]
|
||||
for row in self._generator(pids, symbol_table):
|
||||
_depth, row_data = row
|
||||
description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"'
|
||||
yield description, timeliner.TimeLinerType.CHANGED, row_data[7]
|
||||
yield description, timeliner.TimeLinerType.MODIFIED, row_data[8]
|
||||
yield description, timeliner.TimeLinerType.ACCESSED, row_data[9]
|
||||
vmlinux_module_name = self.config["kernel"]
|
||||
|
||||
filter_func = pslist.PsList.create_pid_filter(pids)
|
||||
for fd_internal in self.list_fds(
|
||||
self.context, vmlinux_module_name, filter_func=filter_func
|
||||
):
|
||||
fd_user = fd_internal.to_user()
|
||||
|
||||
description = (
|
||||
f"Process {fd_user.task_comm} ({fd_user.task_tgid}/{fd_user.task_tid}) "
|
||||
f"Open '{fd_user.full_path}'"
|
||||
)
|
||||
|
||||
yield description, timeliner.TimeLinerType.CHANGED, fd_user.change_time
|
||||
yield description, timeliner.TimeLinerType.MODIFIED, fd_user.modification_time
|
||||
yield description, timeliner.TimeLinerType.ACCESSED, fd_user.access_time
|
||||
|
||||
@@ -22,9 +22,10 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (2, 0, 0)
|
||||
_version = (3, 0, 0)
|
||||
|
||||
def __init__(self, vmlinux, task):
|
||||
def __init__(self, vmlinux, task, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._vmlinux = vmlinux
|
||||
self._task = task
|
||||
|
||||
@@ -438,7 +439,7 @@ class Sockstat(plugins.PluginInterface):
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (2, 0, 0)
|
||||
_version = (3, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls):
|
||||
@@ -449,10 +450,10 @@ class Sockstat(plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="SockHandlers", component=SockHandlers, version=(2, 0, 0)
|
||||
name="SockHandlers", component=SockHandlers, version=(3, 0, 0)
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="lsof", plugin=lsof.Lsof, version=(1, 1, 0)
|
||||
name="lsof", plugin=lsof.Lsof, version=(2, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
|
||||
@@ -507,8 +508,9 @@ class Sockstat(plugins.PluginInterface):
|
||||
dfop_addr = vmlinux.object_from_symbol("sockfs_dentry_operations").vol.offset
|
||||
|
||||
fd_generator = lsof.Lsof.list_fds(context, vmlinux.name, filter_func)
|
||||
for _pid, task_comm, task, fd_fields in fd_generator:
|
||||
fd_num, filp, _full_path = fd_fields
|
||||
for fd_internal in fd_generator:
|
||||
fd_num, filp, _full_path = fd_internal.fd_fields
|
||||
task = fd_internal.task
|
||||
|
||||
if filp.f_op not in (sfop_addr, dfop_addr):
|
||||
continue
|
||||
@@ -548,7 +550,7 @@ class Sockstat(plugins.PluginInterface):
|
||||
except AttributeError:
|
||||
netns_id = NotAvailableValue()
|
||||
|
||||
yield task_comm, task, netns_id, fd_num, family, sock_type, protocol, sock_fields
|
||||
yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields
|
||||
|
||||
def _format_fields(self, sock_stat, protocol):
|
||||
"""Prepare the socket fields to be rendered
|
||||
@@ -595,7 +597,6 @@ class Sockstat(plugins.PluginInterface):
|
||||
)
|
||||
|
||||
for (
|
||||
task_comm,
|
||||
task,
|
||||
netns_id,
|
||||
fd_num,
|
||||
@@ -616,9 +617,12 @@ class Sockstat(plugins.PluginInterface):
|
||||
else NotAvailableValue()
|
||||
)
|
||||
|
||||
task_comm = utility.array_to_string(task.comm)
|
||||
|
||||
fields = (
|
||||
netns_id,
|
||||
task_comm,
|
||||
task.tgid,
|
||||
task.pid,
|
||||
fd_num,
|
||||
format_hints.Hex(sock.vol.offset),
|
||||
@@ -639,7 +643,8 @@ class Sockstat(plugins.PluginInterface):
|
||||
tree_grid_args = [
|
||||
("NetNS", int),
|
||||
("Process Name", str),
|
||||
("Pid", int),
|
||||
("PID", int),
|
||||
("TID", int),
|
||||
("FD", int),
|
||||
("Sock Offset", format_hints.Hex),
|
||||
("Family", str),
|
||||
|
||||
@@ -306,7 +306,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
task: interfaces.objects.ObjectInterface,
|
||||
):
|
||||
# task.files can be null
|
||||
if not task.files:
|
||||
if not (task.files and task.files.is_readable()):
|
||||
return None
|
||||
|
||||
fd_table = task.files.get_fds()
|
||||
@@ -326,7 +326,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
)
|
||||
|
||||
for fd_num, filp in enumerate(fds):
|
||||
if filp != 0:
|
||||
if filp and filp.is_readable():
|
||||
full_path = LinuxUtilities.path_for_file(context, task, filp)
|
||||
|
||||
yield fd_num, filp, full_path
|
||||
|
||||
Reference in New Issue
Block a user