Skip to content

Commit eb74437

Browse files
authored
Merge pull request #1263 from gcmoreira/linux_lsof_refactoring_fixes_and_improvements
Linux: Add support for threads in both lsof and sockstat plugins.
2 parents 9addf6b + ee75964 commit eb74437

File tree

3 files changed

+157
-103
lines changed

3 files changed

+157
-103
lines changed
Lines changed: 140 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
22
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
33
#
4-
"""A module containing a collection of plugins that produce data typically
5-
found in Linux's /proc file system."""
6-
import logging, datetime
7-
from typing import List, Callable
4+
import logging
5+
import datetime
6+
import dataclasses
7+
from typing import List, Callable, Tuple, Iterable
88

9-
from volatility3.framework import renderers, interfaces, constants, exceptions
9+
from volatility3.framework import renderers, interfaces, constants
1010
from volatility3.framework.configuration import requirements
1111
from volatility3.framework.interfaces import plugins
1212
from volatility3.framework.objects import utility
@@ -17,11 +17,100 @@
1717
vollog = logging.getLogger(__name__)
1818

1919

20+
@dataclasses.dataclass
21+
class FDUser:
22+
"""FD user representation, featuring augmented information and formatted fields.
23+
This is the data the plugin will eventually display.
24+
"""
25+
26+
task_tgid: int
27+
task_tid: int
28+
task_comm: str
29+
fd_num: int
30+
full_path: str
31+
device: str = dataclasses.field(default=renderers.NotAvailableValue())
32+
inode_num: int = dataclasses.field(default=renderers.NotAvailableValue())
33+
inode_type: str = dataclasses.field(default=renderers.NotAvailableValue())
34+
file_mode: str = dataclasses.field(default=renderers.NotAvailableValue())
35+
change_time: datetime.datetime = dataclasses.field(
36+
default=renderers.NotAvailableValue()
37+
)
38+
modification_time: datetime.datetime = dataclasses.field(
39+
default=renderers.NotAvailableValue()
40+
)
41+
access_time: datetime.datetime = dataclasses.field(
42+
default=renderers.NotAvailableValue()
43+
)
44+
inode_size: int = dataclasses.field(default=renderers.NotAvailableValue())
45+
46+
47+
@dataclasses.dataclass
48+
class FDInternal:
49+
"""FD internal representation containing only the core objects
50+
51+
Fields:
52+
task: 'task_struct' object
53+
fd_fields: FD fields as obtained from LinuxUtilities.files_descriptors_for_process()
54+
"""
55+
56+
task: interfaces.objects.ObjectInterface
57+
fd_fields: Tuple[int, int, str]
58+
59+
def to_user(self) -> FDUser:
60+
"""Augment the FD information to be presented to the user
61+
62+
Returns:
63+
An InodeUser dataclass
64+
"""
65+
# Ensure all types are atomic immutable. Otherwise, astuple() will take a long
66+
# time doing a deepcopy of the Volatility objects.
67+
task_tgid = int(self.task.tgid)
68+
task_tid = int(self.task.pid)
69+
task_comm = utility.array_to_string(self.task.comm)
70+
fd_num, filp, full_path = self.fd_fields
71+
fd_num = int(fd_num)
72+
full_path = str(full_path)
73+
inode = filp.get_inode()
74+
if inode:
75+
superblock_ptr = inode.i_sb
76+
if superblock_ptr and superblock_ptr.is_readable():
77+
device = f"{superblock_ptr.major}:{superblock_ptr.minor}"
78+
else:
79+
device = renderers.NotAvailableValue()
80+
81+
fd_user = FDUser(
82+
task_tgid=task_tgid,
83+
task_tid=task_tid,
84+
task_comm=task_comm,
85+
fd_num=fd_num,
86+
full_path=full_path,
87+
device=device,
88+
inode_num=int(inode.i_ino),
89+
inode_type=inode.get_inode_type() or renderers.UnparsableValue(),
90+
file_mode=inode.get_file_mode(),
91+
change_time=inode.get_change_time(),
92+
modification_time=inode.get_modification_time(),
93+
access_time=inode.get_access_time(),
94+
inode_size=int(inode.i_size),
95+
)
96+
else:
97+
# We use the dataclasses' default values
98+
fd_user = FDUser(
99+
task_tgid=task_tgid,
100+
task_tid=task_tid,
101+
task_comm=task_comm,
102+
fd_num=fd_num,
103+
full_path=full_path,
104+
)
105+
106+
return fd_user
107+
108+
20109
class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
21110
"""Lists open files for each processes."""
22111

23112
_required_framework_version = (2, 0, 0)
24-
_version = (1, 2, 0)
113+
_version = (2, 0, 0)
25114

26115
@classmethod
27116
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -45,110 +134,59 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
45134
),
46135
]
47136

48-
@classmethod
49-
def get_inode_metadata(cls, filp: interfaces.objects.ObjectInterface):
50-
try:
51-
dentry = filp.get_dentry()
52-
if dentry:
53-
inode_object = dentry.d_inode
54-
if inode_object and inode_object.is_valid():
55-
itype = (
56-
inode_object.get_inode_type() or renderers.NotAvailableValue()
57-
)
58-
return (
59-
inode_object.i_ino,
60-
itype,
61-
inode_object.i_size,
62-
inode_object.get_file_mode(),
63-
inode_object.get_change_time(),
64-
inode_object.get_modification_time(),
65-
inode_object.get_access_time(),
66-
)
67-
except (exceptions.InvalidAddressException, AttributeError) as e:
68-
vollog.warning(f"Can't get inode metadata: {e}")
69-
return None
70-
71137
@classmethod
72138
def list_fds(
73139
cls,
74140
context: interfaces.context.ContextInterface,
75-
symbol_table: str,
141+
vmlinux_module_name: str,
76142
filter_func: Callable[[int], bool] = lambda _: False,
77-
):
143+
) -> Iterable[FDInternal]:
144+
"""Enumerates open file descriptors in tasks
145+
146+
Args:
147+
context: The context to retrieve required elements (layers, symbol tables) from
148+
vmlinux_module_name: The name of the kernel module on which to operate
149+
filter_func: A function which takes a process object and returns True if the process
150+
should be ignored/filtered
151+
152+
Yields:
153+
A FDInternal object
154+
"""
78155
linuxutils_symbol_table = None
79-
for task in pslist.PsList.list_tasks(context, symbol_table, filter_func):
156+
for task in pslist.PsList.list_tasks(
157+
context, vmlinux_module_name, filter_func, include_threads=True
158+
):
80159
if linuxutils_symbol_table is None:
81160
if constants.BANG not in task.vol.type_name:
82161
raise ValueError("Task is not part of a symbol table")
83162
linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0]
84163

85-
task_comm = utility.array_to_string(task.comm)
86-
pid = int(task.pid)
87-
88164
fd_generator = linux.LinuxUtilities.files_descriptors_for_process(
89165
context, linuxutils_symbol_table, task
90166
)
91167

92168
for fd_fields in fd_generator:
93-
yield pid, task_comm, task, fd_fields
169+
yield FDInternal(task=task, fd_fields=fd_fields)
94170

95-
@classmethod
96-
def list_fds_and_inodes(
97-
cls,
98-
context: interfaces.context.ContextInterface,
99-
symbol_table: str,
100-
filter_func: Callable[[int], bool] = lambda _: False,
101-
):
102-
for pid, task_comm, task, (fd_num, filp, full_path) in cls.list_fds(
103-
context, symbol_table, filter_func
104-
):
105-
inode_metadata = cls.get_inode_metadata(filp)
106-
if inode_metadata is None:
107-
inode_metadata = tuple(
108-
interfaces.renderers.BaseAbsentValue() for _ in range(7)
109-
)
110-
yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata
111-
112-
def _generator(self, pids, symbol_table):
171+
def _generator(self, pids, vmlinux_module_name):
113172
filter_func = pslist.PsList.create_pid_filter(pids)
114-
fds_generator = self.list_fds_and_inodes(
115-
self.context, symbol_table, filter_func=filter_func
116-
)
117-
118-
for (
119-
pid,
120-
task_comm,
121-
task,
122-
fd_num,
123-
filp,
124-
full_path,
125-
inode_metadata,
126-
) in fds_generator:
127-
inode_num, itype, file_size, imode, ctime, mtime, atime = inode_metadata
128-
fields = (
129-
pid,
130-
task_comm,
131-
fd_num,
132-
full_path,
133-
inode_num,
134-
itype,
135-
imode,
136-
ctime,
137-
mtime,
138-
atime,
139-
file_size,
140-
)
141-
yield (0, fields)
173+
for fd_internal in self.list_fds(
174+
self.context, vmlinux_module_name, filter_func=filter_func
175+
):
176+
fd_user = fd_internal.to_user()
177+
yield (0, dataclasses.astuple(fd_user))
142178

143179
def run(self):
144180
pids = self.config.get("pid", None)
145-
symbol_table = self.config["kernel"]
181+
vmlinux_module_name = self.config["kernel"]
146182

147183
tree_grid_args = [
148184
("PID", int),
185+
("TID", int),
149186
("Process", str),
150187
("FD", int),
151188
("Path", str),
189+
("Device", str),
152190
("Inode", int),
153191
("Type", str),
154192
("Mode", str),
@@ -157,14 +195,25 @@ def run(self):
157195
("Accessed", datetime.datetime),
158196
("Size", int),
159197
]
160-
return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table))
198+
return renderers.TreeGrid(
199+
tree_grid_args, self._generator(pids, vmlinux_module_name)
200+
)
161201

162202
def generate_timeline(self):
163203
pids = self.config.get("pid", None)
164-
symbol_table = self.config["kernel"]
165-
for row in self._generator(pids, symbol_table):
166-
_depth, row_data = row
167-
description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"'
168-
yield description, timeliner.TimeLinerType.CHANGED, row_data[7]
169-
yield description, timeliner.TimeLinerType.MODIFIED, row_data[8]
170-
yield description, timeliner.TimeLinerType.ACCESSED, row_data[9]
204+
vmlinux_module_name = self.config["kernel"]
205+
206+
filter_func = pslist.PsList.create_pid_filter(pids)
207+
for fd_internal in self.list_fds(
208+
self.context, vmlinux_module_name, filter_func=filter_func
209+
):
210+
fd_user = fd_internal.to_user()
211+
212+
description = (
213+
f"Process {fd_user.task_comm} ({fd_user.task_tgid}/{fd_user.task_tid}) "
214+
f"Open '{fd_user.full_path}'"
215+
)
216+
217+
yield description, timeliner.TimeLinerType.CHANGED, fd_user.change_time
218+
yield description, timeliner.TimeLinerType.MODIFIED, fd_user.modification_time
219+
yield description, timeliner.TimeLinerType.ACCESSED, fd_user.access_time

volatility3/framework/plugins/linux/sockstat.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
2222

2323
_required_framework_version = (2, 0, 0)
2424

25-
_version = (2, 0, 0)
25+
_version = (3, 0, 0)
2626

27-
def __init__(self, vmlinux, task):
27+
def __init__(self, vmlinux, task, *args, **kwargs):
28+
super().__init__(*args, **kwargs)
2829
self._vmlinux = vmlinux
2930
self._task = task
3031

@@ -438,7 +439,7 @@ class Sockstat(plugins.PluginInterface):
438439

439440
_required_framework_version = (2, 0, 0)
440441

441-
_version = (2, 0, 0)
442+
_version = (3, 0, 0)
442443

443444
@classmethod
444445
def get_requirements(cls):
@@ -449,10 +450,10 @@ def get_requirements(cls):
449450
architectures=["Intel32", "Intel64"],
450451
),
451452
requirements.VersionRequirement(
452-
name="SockHandlers", component=SockHandlers, version=(2, 0, 0)
453+
name="SockHandlers", component=SockHandlers, version=(3, 0, 0)
453454
),
454455
requirements.PluginRequirement(
455-
name="lsof", plugin=lsof.Lsof, version=(1, 1, 0)
456+
name="lsof", plugin=lsof.Lsof, version=(2, 0, 0)
456457
),
457458
requirements.VersionRequirement(
458459
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
@@ -507,8 +508,9 @@ def list_sockets(
507508
dfop_addr = vmlinux.object_from_symbol("sockfs_dentry_operations").vol.offset
508509

509510
fd_generator = lsof.Lsof.list_fds(context, vmlinux.name, filter_func)
510-
for _pid, task_comm, task, fd_fields in fd_generator:
511-
fd_num, filp, _full_path = fd_fields
511+
for fd_internal in fd_generator:
512+
fd_num, filp, _full_path = fd_internal.fd_fields
513+
task = fd_internal.task
512514

513515
if filp.f_op not in (sfop_addr, dfop_addr):
514516
continue
@@ -548,7 +550,7 @@ def list_sockets(
548550
except AttributeError:
549551
netns_id = NotAvailableValue()
550552

551-
yield task_comm, task, netns_id, fd_num, family, sock_type, protocol, sock_fields
553+
yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields
552554

553555
def _format_fields(self, sock_stat, protocol):
554556
"""Prepare the socket fields to be rendered
@@ -595,7 +597,6 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str):
595597
)
596598

597599
for (
598-
task_comm,
599600
task,
600601
netns_id,
601602
fd_num,
@@ -616,9 +617,12 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str):
616617
else NotAvailableValue()
617618
)
618619

620+
task_comm = utility.array_to_string(task.comm)
621+
619622
fields = (
620623
netns_id,
621624
task_comm,
625+
task.tgid,
622626
task.pid,
623627
fd_num,
624628
format_hints.Hex(sock.vol.offset),
@@ -639,7 +643,8 @@ def run(self):
639643
tree_grid_args = [
640644
("NetNS", int),
641645
("Process Name", str),
642-
("Pid", int),
646+
("PID", int),
647+
("TID", int),
643648
("FD", int),
644649
("Sock Offset", format_hints.Hex),
645650
("Family", str),

volatility3/framework/symbols/linux/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def files_descriptors_for_process(
306306
task: interfaces.objects.ObjectInterface,
307307
):
308308
# task.files can be null
309-
if not task.files:
309+
if not (task.files and task.files.is_readable()):
310310
return None
311311

312312
fd_table = task.files.get_fds()
@@ -326,7 +326,7 @@ def files_descriptors_for_process(
326326
)
327327

328328
for fd_num, filp in enumerate(fds):
329-
if filp != 0:
329+
if filp and filp.is_readable():
330330
full_path = LinuxUtilities.path_for_file(context, task, filp)
331331

332332
yield fd_num, filp, full_path

0 commit comments

Comments
 (0)