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
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+ @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+
20109class 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
0 commit comments