Skip to content

Commit

Permalink
Merge pull request #1263 from gcmoreira/linux_lsof_refactoring_fixes_…
Browse files Browse the repository at this point in the history
…and_improvements

Linux: Add support for threads in both lsof and sockstat plugins.
  • Loading branch information
ikelos authored Oct 7, 2024
2 parents 9addf6b + ee75964 commit eb74437
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 103 deletions.
231 changes: 140 additions & 91 deletions volatility3/framework/plugins/linux/lsof.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,11 +17,100 @@
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]:
Expand All @@ -45,110 +134,59 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
),
]

@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),
Expand All @@ -157,14 +195,25 @@ def run(self):
("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
25 changes: 15 additions & 10 deletions volatility3/framework/plugins/linux/sockstat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -449,10 +450,10 @@ def get_requirements(cls):
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)
Expand Down Expand Up @@ -507,8 +508,9 @@ def list_sockets(
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
Expand Down Expand Up @@ -548,7 +550,7 @@ def list_sockets(
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
Expand Down Expand Up @@ -595,7 +597,6 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str):
)

for (
task_comm,
task,
netns_id,
fd_num,
Expand All @@ -616,9 +617,12 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str):
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),
Expand All @@ -639,7 +643,8 @@ def run(self):
tree_grid_args = [
("NetNS", int),
("Process Name", str),
("Pid", int),
("PID", int),
("TID", int),
("FD", int),
("Sock Offset", format_hints.Hex),
("Family", str),
Expand Down
4 changes: 2 additions & 2 deletions volatility3/framework/symbols/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ def files_descriptors_for_process(
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()
Expand All @@ -326,7 +326,7 @@ def files_descriptors_for_process(
)

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
Expand Down

0 comments on commit eb74437

Please sign in to comment.