Merge pull request #1611 from Abyss-W4tcher/pre_linux_pagecache_recoverfs_2

Re-apply "Pre linux.pagecache.recoverfs support"
This commit is contained in:
ikelos
2025-02-09 18:14:24 +00:00
committed by GitHub
@@ -6,8 +6,9 @@ import math
import logging
import datetime
from dataclasses import dataclass, astuple
from typing import List, Set, Type, Iterable, Tuple
from typing import IO, List, Set, Type, Iterable, Tuple
from volatility3.framework.constants import architectures
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.renderers import format_hints
from volatility3.framework.interfaces import plugins
@@ -37,6 +38,11 @@ class InodeUser:
modification_time: str
change_time: str
path: str
inode_size: int
@classmethod
def format_symlink(cls, symlink_source: str, symlink_dest: str) -> str:
return f"{symlink_source} -> {symlink_dest}"
@dataclass
@@ -80,6 +86,7 @@ class InodeInternal:
access_time_dt = self.inode.get_access_time()
modification_time_dt = self.inode.get_modification_time()
change_time_dt = self.inode.get_change_time()
inode_size = int(self.inode.i_size)
inode_user = InodeUser(
superblock_addr=superblock_addr,
@@ -95,6 +102,7 @@ class InodeInternal:
modification_time=modification_time_dt,
change_time=change_time_dt,
path=self.path,
inode_size=inode_size,
)
return inode_user
@@ -104,7 +112,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
_required_framework_version = (2, 0, 0)
_version = (1, 0, 3)
_version = (1, 1, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -112,7 +120,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
architectures=architectures.LINUX_ARCHS,
),
requirements.PluginRequirement(
name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0)
@@ -154,10 +162,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
and inode.i_link
and inode.i_link.is_readable()
):
i_link_str = inode.i_link.dereference().cast(
symlink_dest = inode.i_link.dereference().cast(
"string", max_length=255, encoding="utf-8", errors="replace"
)
symlink_path = f"{symlink_path} -> {i_link_str}"
symlink_path = InodeUser.format_symlink(symlink_path, symlink_dest)
return symlink_path
@@ -218,12 +226,14 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
follow_symlinks: bool = True,
) -> Iterable[InodeInternal]:
"""Retrieves the inodes from the superblocks
Args:
context: The context that the plugin will operate within
vmlinux_module_name: The name of the kernel module on which to operate
follow_symlinks: Whether to follow symlinks or not
Yields:
An InodeInternal object
@@ -303,7 +313,8 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
continue
seen_inodes.add(file_inode_ptr)
file_path = cls._follow_symlink(file_inode_ptr, file_path)
if follow_symlinks:
file_path = cls._follow_symlink(file_inode_ptr, file_path)
inode_in = InodeInternal(
superblock=superblock,
mountpoint=mountpoint,
@@ -393,6 +404,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
("ModificationTime", datetime.datetime),
("ChangeTime", datetime.datetime),
("FilePath", str),
("InodeSize", int),
]
return renderers.TreeGrid(
@@ -405,7 +417,7 @@ class InodePages(plugins.PluginInterface):
_required_framework_version = (2, 0, 0)
_version = (2, 0, 2)
_version = (3, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -413,7 +425,7 @@ class InodePages(plugins.PluginInterface):
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
architectures=architectures.LINUX_ARCHS,
),
requirements.PluginRequirement(
name="files", plugin=Files, version=(1, 0, 0)
@@ -439,62 +451,82 @@ class InodePages(plugins.PluginInterface):
@classmethod
def write_inode_content_to_file(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
inode: interfaces.objects.ObjectInterface,
filename: str,
open_method: Type[interfaces.plugins.FileHandlerInterface],
vmlinux_layer: interfaces.layers.TranslationLayerInterface,
) -> None:
"""Extracts the inode's contents from the page cache and saves them to a file
Args:
context: The context on which to operate
layer_name: The name of the layer on which to operate
inode: The inode to dump
filename: Filename for writing the inode content
open_method: class for constructing output files
vmlinux_layer: The kernel layer to obtain the page size
"""
try:
with open_method(filename) as file_obj:
cls.write_inode_content_to_stream(context, layer_name, inode, file_obj)
except OSError as e:
vollog.error("Unable to write to file (%s): %s", filename, e)
@classmethod
def write_inode_content_to_stream(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
inode: interfaces.objects.ObjectInterface,
stream: IO,
) -> None:
"""Extracts the inode's contents from the page cache and saves them to a stream
Args:
context: The context on which to operate
layer_name: The name of the layer on which to operate
inode: The inode to dump
stream: An IO stream to write to, typically FileHandlerInterface or BytesIO
"""
if not inode.is_reg:
vollog.error("The inode is not a regular file")
return None
# By using truncate/seek, provided the filesystem supports it, a sparse file will be
layer = context.layers[layer_name]
# By using truncate/seek, provided the filesystem supports it, and the
# stream is a File interface, a sparse file will be
# created, saving both disk space and I/O time.
# Additionally, using the page index will guarantee that each page is written at the
# appropriate file position.
inode_size = inode.i_size
try:
file_initialized = False
with open_method(filename) as file_obj:
for page_idx, page_content in inode.get_contents():
current_fp = page_idx * vmlinux_layer.page_size
max_length = inode_size - current_fp
page_bytes_len = min(max_length, len(page_content))
if (
current_fp >= inode_size
or current_fp + page_bytes_len > inode_size
):
vollog.error(
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
inode.vol.offset,
inode_size,
page_idx,
)
continue
page_bytes = page_content[:page_bytes_len]
stream_initialized = False
for page_idx, page_content in inode.get_contents():
current_fp = page_idx * layer.page_size
max_length = inode_size - current_fp
page_bytes_len = min(max_length, len(page_content))
if current_fp >= inode_size or current_fp + page_bytes_len > inode_size:
vollog.error(
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
inode.vol.offset,
inode_size,
page_idx,
)
continue
page_bytes = page_content[:page_bytes_len]
if not file_initialized:
# Lazy initialization to avoid truncating the file until we are
# certain there is something to write
file_obj.truncate(inode_size)
file_initialized = True
if not stream_initialized:
# Lazy initialization to avoid truncating the stream until we are
# certain there is something to write
stream.truncate(inode_size)
stream_initialized = True
file_obj.seek(current_fp)
file_obj.write(page_bytes)
stream.seek(current_fp)
stream.write(page_bytes)
except exceptions.LinuxPageCacheException:
vollog.error(
f"Error dumping cached pages for inode at {inode.vol.offset:#x}"
)
except OSError as e:
vollog.error("Unable to write to file (%s): %s", filename, e)
def _generate_inode_fields(
self,
@@ -575,7 +607,7 @@ class InodePages(plugins.PluginInterface):
filename = open_method.sanitize_filename(f"inode_0x{inode_address:x}.dmp")
vollog.info("[*] Writing inode at 0x%x to '%s'", inode_address, filename)
self.write_inode_content_to_file(
inode, filename, open_method, vmlinux_layer
self.context, vmlinux_layer.name, inode, filename, open_method
)
else:
yield from self._generate_inode_fields(inode, vmlinux_layer)