From 999f3d0d2ce67aa8d41ee03af10045c9f42cb6b0 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 11 Feb 2025 17:48:34 +0100 Subject: [PATCH] add superblock or device numbers path prepending --- .../framework/plugins/linux/pagecache.py | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index d0c085719..2a137fc89 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -633,11 +633,8 @@ class InodePages(plugins.PluginInterface): class RecoverFs(plugins.PluginInterface): """Recovers the cached filesystem (directories, files, symlinks) into a compressed tarball. - Metadata aren't replicated to extracted objects and timestamps are set to the plugin run time. To prevent extraction errors related to long paths, please consider using https://github.com/mxmlnkn/ratarmount. - To mount: - "ratarmount recovered_fs.tar.gz ./recovered_fs_mounted/". - To unmount: - "umount ./recovered_fs_mounted/". + Details: level 0 directories are named after the UUID of the parent superblock; metadata aren't replicated to extracted objects; objects modification time is set to the plugin run time. + Troubleshooting: to fix extraction errors related to long paths, please consider using https://github.com/mxmlnkn/ratarmount. """ _version = (1, 0, 0) @@ -673,6 +670,7 @@ class RecoverFs(plugins.PluginInterface): layer_name: str, tar: tarfile.TarFile, reg_inode_in: InodeInternal, + path_prefix: str = "", mtime: float = None, ) -> int: """Extracts a REG inode content and writes it to a TarFile object. @@ -682,6 +680,7 @@ class RecoverFs(plugins.PluginInterface): layer_name: The name of the layer on which to operate tar: The TarFile object to write to reg_inode_in: The inode to extract content from + path_prefix: A custom path prefix to prepend the inode path with mtime: The modification time to set the TarInfo object to Returns: @@ -694,7 +693,7 @@ class RecoverFs(plugins.PluginInterface): inode_content_buffer.seek(0) handle_buffer_size = inode_content_buffer.getbuffer().nbytes - tar_info = tarfile.TarInfo(reg_inode_in.path) + tar_info = tarfile.TarInfo(path_prefix + reg_inode_in.path) # The tarfile module only has read support for sparse files: # https://docs.python.org/3.12/library/tarfile.html#tarfile.LNKTYPE:~:text=and%20longlink%20extensions%2C-,read%2Donly%20support,-for%20all%20variants tar_info.type = tarfile.REGTYPE @@ -707,20 +706,20 @@ class RecoverFs(plugins.PluginInterface): return handle_buffer_size @classmethod - def _tar_add_dir_inode( + def _tar_add_dir( cls, tar: tarfile.TarFile, - reg_dir_in: InodeInternal, + directory_path: str, mtime: float = None, ) -> None: """Adds a directory path to a TarFile object, based on a DIR inode. Args: tar: The TarFile object to write to - reg_dir_in: The inode to base the new directory on + directory_path: The directory path to create mtime: The modification time to set the TarInfo object to """ - tar_info = tarfile.TarInfo(reg_dir_in.path) + tar_info = tarfile.TarInfo(directory_path) tar_info.type = tarfile.DIRTYPE tar_info.mode = 0o755 if mtime is not None: @@ -774,11 +773,30 @@ class RecoverFs(plugins.PluginInterface): vmlinux_module_name=vmlinux_module_name, follow_symlinks=False, ) - visited_paths = set() + + # Prefix paths by the super_block uuid's to prevent overlaps. + # Switch to device major and device minor for older kernels (< 2.6.39-rc1). + uuid_as_prefix = vmlinux.get_type("super_block").has_member("s_uuid") + if not uuid_as_prefix: + vollog.warning( + "super_block struct does not support s_uuid attribute. Consequently, level 0 directories won't refer to the superblock uuid's, but to its device_major:device_minor numbers." + ) + + visited_paths = seen_prefixes = set() for inode_in in inodes_iter: - if inode_in.path in visited_paths: + if uuid_as_prefix: + prefix = f"/{inode_in.superblock.uuid}" + else: + prefix = f"/{inode_in.superblock.major}:{inode_in.superblock.minor}" + prefixed_path = prefix + inode_in.path + + if prefixed_path in visited_paths: continue - visited_paths.add(inode_in.path) + elif prefix not in seen_prefixes: + self._tar_add_dir(tar, prefix, mtime) + seen_prefixes.add(prefix) + + visited_paths.add(prefixed_path) extracted_file_size = renderers.NotApplicableValue() # Inodes parent directory is yielded first, which @@ -786,10 +804,15 @@ class RecoverFs(plugins.PluginInterface): # tarfile will take care of creating it anyway. if inode_in.inode.is_reg: extracted_file_size = self._tar_add_reg_inode( - self.context, vmlinux_layer.name, tar, inode_in, mtime + self.context, + vmlinux_layer.name, + tar, + inode_in, + prefix, + mtime, ) elif inode_in.inode.is_dir: - self._tar_add_dir_inode(tar, inode_in, mtime) + self._tar_add_dir(tar, prefixed_path, mtime) elif ( inode_in.inode.is_link and inode_in.inode.has_member("i_link") @@ -799,7 +822,7 @@ class RecoverFs(plugins.PluginInterface): symlink_dest = inode_in.inode.i_link.dereference().cast( "string", max_length=255, encoding="utf-8", errors="replace" ) - self._tar_add_lnk(tar, inode_in.path, symlink_dest, mtime) + self._tar_add_lnk(tar, prefixed_path, symlink_dest, mtime) # Set path to a user friendly representation before yielding inode_in.path = InodeUser.format_symlink(inode_in.path, symlink_dest) else: