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
77from typing import List , Callable
88
9- from volatility3 .framework import renderers , interfaces , constants , exceptions
9+ from volatility3 .framework import renderers , interfaces , constants
1010from volatility3 .framework .configuration import requirements
1111from volatility3 .framework .interfaces import plugins
1212from volatility3 .framework .objects import utility
1717vollog = 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+
20103class 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
0 commit comments