66import logging
77import datetime
88from dataclasses import dataclass , astuple
9- from typing import List , Set , Type , Iterable
9+ from typing import List , Set , Type , Iterable , IO
1010
11+ from volatility3 .framework .constants import architectures
1112from volatility3 .framework import renderers , interfaces
1213from volatility3 .framework .renderers import format_hints
1314from volatility3 .framework .interfaces import plugins
@@ -37,6 +38,11 @@ class InodeUser:
3738 modification_time : str
3839 change_time : str
3940 path : str
41+ inode_size : int
42+
43+ @staticmethod
44+ def format_symlink (symlink_source : str , symlink_dest : str ) -> str :
45+ return f"{ symlink_source } -> { symlink_dest } "
4046
4147
4248@dataclass
@@ -80,6 +86,7 @@ def to_user(
8086 access_time_dt = self .inode .get_access_time ()
8187 modification_time_dt = self .inode .get_modification_time ()
8288 change_time_dt = self .inode .get_change_time ()
89+ inode_size = int (self .inode .i_size )
8390
8491 inode_user = InodeUser (
8592 superblock_addr = superblock_addr ,
@@ -95,6 +102,7 @@ def to_user(
95102 modification_time = modification_time_dt ,
96103 change_time = change_time_dt ,
97104 path = self .path ,
105+ inode_size = inode_size ,
98106 )
99107 return inode_user
100108
@@ -104,15 +112,15 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
104112
105113 _required_framework_version = (2 , 0 , 0 )
106114
107- _version = (1 , 0 , 1 )
115+ _version = (1 , 1 , 0 )
108116
109117 @classmethod
110118 def get_requirements (cls ) -> List [interfaces .configuration .RequirementInterface ]:
111119 return [
112120 requirements .ModuleRequirement (
113121 name = "kernel" ,
114122 description = "Linux kernel" ,
115- architectures = [ "Intel32" , "Intel64" ] ,
123+ architectures = architectures . LINUX_ARCHS ,
116124 ),
117125 requirements .PluginRequirement (
118126 name = "mountinfo" , plugin = mountinfo .MountInfo , version = (1 , 2 , 0 )
@@ -148,10 +156,10 @@ def _follow_symlink(
148156 """
149157 # i_link (fast symlinks) were introduced in 4.2
150158 if inode and inode .is_link and inode .has_member ("i_link" ) and inode .i_link :
151- i_link_str = inode .i_link .dereference ().cast (
159+ symlink_dest = inode .i_link .dereference ().cast (
152160 "string" , max_length = 255 , encoding = "utf-8" , errors = "replace"
153161 )
154- symlink_path = f" { symlink_path } -> { i_link_str } "
162+ symlink_path = InodeUser . format_symlink ( symlink_path , symlink_dest )
155163
156164 return symlink_path
157165
@@ -212,12 +220,14 @@ def get_inodes(
212220 cls ,
213221 context : interfaces .context .ContextInterface ,
214222 vmlinux_module_name : str ,
223+ follow_symlinks : bool = True ,
215224 ) -> Iterable [InodeInternal ]:
216225 """Retrieves the inodes from the superblocks
217226
218227 Args:
219228 context: The context that the plugin will operate within
220229 vmlinux_module_name: The name of the kernel module on which to operate
230+ follow_symlinks: Whether to follow symlinks or not
221231
222232 Yields:
223233 An InodeInternal object
@@ -289,7 +299,9 @@ def get_inodes(
289299 continue
290300 seen_inodes .add (file_inode_ptr )
291301
292- file_path = cls ._follow_symlink (file_inode_ptr , file_path )
302+ if follow_symlinks :
303+ file_path = cls ._follow_symlink (file_inode_ptr , file_path )
304+
293305 inode_in = InodeInternal (
294306 superblock = superblock ,
295307 mountpoint = mountpoint ,
@@ -377,6 +389,7 @@ def run(self):
377389 ("ModificationTime" , datetime .datetime ),
378390 ("ChangeTime" , datetime .datetime ),
379391 ("FilePath" , str ),
392+ ("InodeSize" , int ),
380393 ]
381394
382395 return renderers .TreeGrid (
@@ -389,15 +402,15 @@ class InodePages(plugins.PluginInterface):
389402
390403 _required_framework_version = (2 , 0 , 0 )
391404
392- _version = (2 , 0 , 0 )
405+ _version = (3 , 0 , 0 )
393406
394407 @classmethod
395408 def get_requirements (cls ) -> List [interfaces .configuration .RequirementInterface ]:
396409 return [
397410 requirements .ModuleRequirement (
398411 name = "kernel" ,
399412 description = "Linux kernel" ,
400- architectures = [ "Intel32" , "Intel64" ] ,
413+ architectures = architectures . LINUX_ARCHS ,
401414 ),
402415 requirements .PluginRequirement (
403416 name = "files" , plugin = Files , version = (1 , 0 , 0 )
@@ -422,49 +435,69 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
422435
423436 @staticmethod
424437 def write_inode_content_to_file (
438+ context : interfaces .context .ContextInterface ,
439+ layer_name : str ,
425440 inode : interfaces .objects .ObjectInterface ,
426441 filename : str ,
427442 open_method : Type [interfaces .plugins .FileHandlerInterface ],
428- vmlinux_layer : interfaces .layers .TranslationLayerInterface ,
429443 ) -> None :
430444 """Extracts the inode's contents from the page cache and saves them to a file
431445
432446 Args:
447+ context: The context on which to operate
448+ layer_name: The name of the layer on which to operate
433449 inode: The inode to dump
434450 filename: Filename for writing the inode content
435451 open_method: class for constructing output files
436- vmlinux_layer: The kernel layer to obtain the page size
437452 """
438453 if not inode .is_reg :
439454 vollog .error ("The inode is not a regular file" )
440455 return None
441456
442- # By using truncate/seek, provided the filesystem supports it, a sparse file will be
443- # created, saving both disk space and I/O time.
444- # Additionally, using the page index will guarantee that each page is written at the
445- # appropriate file position.
446457 try :
447458 with open_method (filename ) as f :
448- inode_size = inode .i_size
449- f .truncate (inode_size )
450-
451- for page_idx , page_content in inode .get_contents ():
452- current_fp = page_idx * vmlinux_layer .page_size
453- max_length = inode_size - current_fp
454- page_bytes = page_content [:max_length ]
455- if current_fp + len (page_bytes ) > inode_size :
456- vollog .error (
457- "Page out of file bounds: inode 0x%x, inode size %d, page index %d" ,
458- inode .vol .offset ,
459- inode_size ,
460- page_idx ,
461- )
462- f .seek (current_fp )
463- f .write (page_bytes )
464-
459+ InodePages .write_inode_content_to_stream (context , layer_name , inode , f )
465460 except OSError as e :
466461 vollog .error ("Unable to write to file (%s): %s" , filename , e )
467462
463+ @staticmethod
464+ def write_inode_content_to_stream (
465+ context : interfaces .context .ContextInterface ,
466+ layer_name : str ,
467+ inode : interfaces .objects .ObjectInterface ,
468+ stream : IO ,
469+ ) -> None :
470+ """Extracts the inode's contents from the page cache and saves them to a stream
471+
472+ Args:
473+ context: The context on which to operate
474+ layer_name: The name of the layer on which to operate
475+ inode: The inode to dump
476+ stream: An IO stream to write to, typically FileHandlerInterface or BytesIO
477+ """
478+ layer = context .layers [layer_name ]
479+ # By using truncate/seek, provided the filesystem supports it, and the
480+ # stream is a File interface, a sparse file will be
481+ # created, saving both disk space and I/O time.
482+ # Additionally, using the page index will guarantee that each page is written at the
483+ # appropriate file position.
484+ inode_size = inode .i_size
485+ stream .truncate (inode_size )
486+
487+ for page_idx , page_content in inode .get_contents ():
488+ current_fp = page_idx * layer .page_size
489+ max_length = inode_size - current_fp
490+ page_bytes = page_content [:max_length ]
491+ if current_fp + len (page_bytes ) > inode_size :
492+ vollog .error (
493+ "Page out of file bounds: inode 0x%x, inode size %d, page index %d" ,
494+ inode .vol .offset ,
495+ inode_size ,
496+ page_idx ,
497+ )
498+ stream .seek (current_fp )
499+ stream .write (page_bytes )
500+
468501 def _generator (self ):
469502 vmlinux_module_name = self .config ["kernel" ]
470503 vmlinux = self .context .modules [vmlinux_module_name ]
@@ -528,7 +561,7 @@ def _generator(self):
528561 filename = open_method .sanitize_filename (f"inode_0x{ inode_address :x} .dmp" )
529562 vollog .info ("[*] Writing inode at 0x%x to '%s'" , inode_address , filename )
530563 self .write_inode_content_to_file (
531- inode , filename , open_method , vmlinux_layer
564+ self . context , vmlinux_layer . name , inode , filename , open_method
532565 )
533566
534567 def run (self ):
0 commit comments