Skip to content

Commit 867c8e3

Browse files
committed
Linux: Add support for threads in both lsof and sockstat plugins.
- lsof plugin: source code refactored - lsof plugin: Added the 'device' column to complete the inode information. An inode number is specific to the filesystem/device it belongs to. - lsof/sockstat plugins: Add threads support. Threads may or may not share the file descriptor table with the thread group leader, depending on whether the CLONE_FILES flag is included in the clone() syscall. Also, once started, a thread can unshare the fd table with its parent. Refer to the unshare() libc syscall wrapper man page, unshare(2). Additionally, note that the Linux lsof command in user space includes thread listings by default as well. Now, there are two columns to identify the thread group ID (PID) and the task/thread ID (TID). - Added inode getters from both, the dentry and file structs. From kernels +3.9 the file struct cached the inode pointer. So, when possible, we get this value. - Improve smear protection in these plugins and various APIs (volatilityfoundation#1243)
1 parent e76d512 commit 867c8e3

File tree

4 files changed

+185
-108
lines changed

4 files changed

+185
-108
lines changed
Lines changed: 136 additions & 93 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
4+
import logging
5+
from datetime import datetime
6+
from dataclasses import dataclass, astuple, field
77
from typing import List, Callable
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,94 @@
1717
vollog = logging.getLogger(__name__)
1818

1919

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

23106
_required_framework_version = (2, 0, 0)
24-
_version = (1, 2, 0)
107+
_version = (2, 0, 0)
25108

26109
@classmethod
27110
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -45,126 +128,86 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
45128
),
46129
]
47130

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-
71131
@classmethod
72132
def list_fds(
73133
cls,
74134
context: interfaces.context.ContextInterface,
75-
symbol_table: str,
135+
vmlinux_module_name: str,
76136
filter_func: Callable[[int], bool] = lambda _: False,
77-
):
137+
) -> FDInternal:
138+
"""Enumerates open file descriptors in tasks
139+
140+
Args:
141+
context: The context to retrieve required elements (layers, symbol tables) from
142+
vmlinux_module_name: The name of the kernel module on which to operate
143+
filter_func: A function which takes a process object and returns True if the process
144+
should be ignored/filtered
145+
146+
Yields:
147+
A FDInternal object
148+
"""
78149
linuxutils_symbol_table = None
79-
for task in pslist.PsList.list_tasks(context, symbol_table, filter_func):
150+
for task in pslist.PsList.list_tasks(
151+
context, vmlinux_module_name, filter_func, include_threads=True
152+
):
80153
if linuxutils_symbol_table is None:
81154
if constants.BANG not in task.vol.type_name:
82155
raise ValueError("Task is not part of a symbol table")
83156
linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0]
84157

85-
task_comm = utility.array_to_string(task.comm)
86-
pid = int(task.pid)
87-
88158
fd_generator = linux.LinuxUtilities.files_descriptors_for_process(
89159
context, linuxutils_symbol_table, task
90160
)
91161

92162
for fd_fields in fd_generator:
93-
yield pid, task_comm, task, fd_fields
163+
yield FDInternal(task=task, fd_fields=fd_fields)
94164

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):
165+
def _generator(self, pids, vmlinux_module_name):
113166
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)
167+
for fd_internal in self.list_fds(
168+
self.context, vmlinux_module_name, filter_func=filter_func
169+
):
170+
fd_user = fd_internal.to_user()
171+
yield (0, astuple(fd_user))
142172

143173
def run(self):
144174
pids = self.config.get("pid", None)
145-
symbol_table = self.config["kernel"]
175+
vmlinux_module_name = self.config["kernel"]
146176

147177
tree_grid_args = [
148178
("PID", int),
179+
("TID", int),
149180
("Process", str),
150181
("FD", int),
151182
("Path", str),
183+
("Device", str),
152184
("Inode", int),
153185
("Type", str),
154186
("Mode", str),
155-
("Changed", datetime.datetime),
156-
("Modified", datetime.datetime),
157-
("Accessed", datetime.datetime),
187+
("Changed", datetime),
188+
("Modified", datetime),
189+
("Accessed", datetime),
158190
("Size", int),
159191
]
160-
return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table))
192+
return renderers.TreeGrid(
193+
tree_grid_args, self._generator(pids, vmlinux_module_name)
194+
)
161195

162196
def generate_timeline(self):
163197
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]
198+
vmlinux_module_name = self.config["kernel"]
199+
200+
filter_func = pslist.PsList.create_pid_filter(pids)
201+
for fd_internal in self.list_fds(
202+
self.context, vmlinux_module_name, filter_func=filter_func
203+
):
204+
fd_user = fd_internal.to_user()
205+
206+
description = (
207+
f"Process {fd_user.task_comm} ({fd_user.task_tgid}/{fd_user.task_tid}) "
208+
f"Open '{fd_user.full_path}'"
209+
)
210+
211+
yield description, timeliner.TimeLinerType.CHANGED, fd_user.change_time
212+
yield description, timeliner.TimeLinerType.MODIFIED, fd_user.modification_time
213+
yield description, timeliner.TimeLinerType.ACCESSED, fd_user.access_time

volatility3/framework/plugins/linux/sockstat.py

Lines changed: 11 additions & 7 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 = (1, 0, 0)
25+
_version = (1, 0, 1)
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 = (1, 0, 0)
442+
_version = (2, 0, 0)
442443

443444
@classmethod
444445
def get_requirements(cls):
@@ -452,7 +453,7 @@ def get_requirements(cls):
452453
name="SockHandlers", component=SockHandlers, version=(1, 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
@@ -617,6 +619,7 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str):
617619

618620
fields = (
619621
netns_id,
622+
task.tgid,
620623
task.pid,
621624
fd_num,
622625
format_hints.Hex(sock.vol.offset),
@@ -636,7 +639,8 @@ def run(self):
636639

637640
tree_grid_args = [
638641
("NetNS", int),
639-
("Pid", int),
642+
("PID", int),
643+
("TID", int),
640644
("FD", int),
641645
("Sock Offset", format_hints.Hex),
642646
("Family", str),

volatility3/framework/symbols/linux/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def files_descriptors_for_process(
256256
task: interfaces.objects.ObjectInterface,
257257
):
258258
# task.files can be null
259-
if not task.files:
259+
if not (task.files and task.files.is_readable()):
260260
return None
261261

262262
fd_table = task.files.get_fds()
@@ -276,7 +276,7 @@ def files_descriptors_for_process(
276276
)
277277

278278
for fd_num, filp in enumerate(fds):
279-
if filp != 0:
279+
if filp and filp.is_readable():
280280
full_path = LinuxUtilities.path_for_file(context, task, filp)
281281

282282
yield fd_num, filp, full_path

0 commit comments

Comments
 (0)