mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-06 09:47:38 +02:00
Merge branch 'develop' into linux_fix_mnt_namespace_issue_1187
This commit is contained in:
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
- name: Clean up post-test
|
||||
run: |
|
||||
rm -rf *.lime
|
||||
rm -rf *.bin
|
||||
rm -rf *.img
|
||||
cd volatility3/symbols
|
||||
rm -rf linux
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# We use the SemVer 2.0.0 versioning scheme
|
||||
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
|
||||
VERSION_MINOR = 8 # Number of changes that only add to the interface
|
||||
VERSION_PATCH = 0 # Number of changes that do not change the interface
|
||||
VERSION_PATCH = 1 # Number of changes that do not change the interface
|
||||
VERSION_SUFFIX = ""
|
||||
|
||||
PACKAGE_VERSION = (
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from volatility3.framework import renderers, interfaces, exceptions
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.configuration import requirements
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EBPF(plugins.PluginInterface):
|
||||
"""Enumerate eBPF programs"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (1, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
]
|
||||
|
||||
def get_ebpf_programs(
|
||||
self,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_module_name: str,
|
||||
) -> interfaces.objects.ObjectInterface:
|
||||
"""Enumerate eBPF programs walking its IDR.
|
||||
|
||||
Args:
|
||||
context: The context to retrieve required elements (layers, symbol tables) from
|
||||
vmlinux_module_name: The name of the kernel module on which to operate
|
||||
Yields:
|
||||
eBPF program objects
|
||||
"""
|
||||
vmlinux = context.modules[vmlinux_module_name]
|
||||
|
||||
if not vmlinux.has_symbol("prog_idr"):
|
||||
raise exceptions.VolatilityException(
|
||||
"Cannot find the eBPF prog idr. Unsupported kernel"
|
||||
)
|
||||
|
||||
prog_idr = vmlinux.object_from_symbol("prog_idr")
|
||||
for page_addr in prog_idr.get_entries():
|
||||
bpf_prog = vmlinux.object("bpf_prog", offset=page_addr, absolute=True)
|
||||
yield bpf_prog
|
||||
|
||||
def _generator(self):
|
||||
for prog in self.get_ebpf_programs(self.context, self.config["kernel"]):
|
||||
prog_addr = prog.vol.offset
|
||||
prog_type = prog.get_type() or renderers.NotAvailableValue()
|
||||
prog_tag = prog.get_tag() or renderers.NotAvailableValue()
|
||||
prog_name = prog.get_name() or renderers.NotAvailableValue()
|
||||
fields = (format_hints.Hex(prog_addr), prog_name, prog_tag, prog_type)
|
||||
yield (0, fields)
|
||||
|
||||
def run(self):
|
||||
headers = [
|
||||
("Address", format_hints.Hex),
|
||||
("Name", str),
|
||||
("Tag", str),
|
||||
("Type", str),
|
||||
]
|
||||
return renderers.TreeGrid(headers, self._generator())
|
||||
@@ -1,27 +1,27 @@
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
"""A module containing a collection of plugins that produce data typically
|
||||
found in Linux's /proc file system."""
|
||||
import logging
|
||||
import logging, datetime
|
||||
from typing import List, Callable
|
||||
|
||||
from volatility3.framework import renderers, interfaces, constants
|
||||
from volatility3.framework import renderers, interfaces, constants, exceptions
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.objects import utility
|
||||
from volatility3.framework.symbols import linux
|
||||
from volatility3.plugins.linux import pslist
|
||||
from volatility3.plugins import timeliner
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Lsof(plugins.PluginInterface):
|
||||
"""Lists all memory maps for all processes."""
|
||||
class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
"""Lists open files for each processes."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (1, 1, 0)
|
||||
_version = (1, 2, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -45,6 +45,29 @@ class Lsof(plugins.PluginInterface):
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_inode_metadata(cls, filp: interfaces.objects.ObjectInterface):
|
||||
try:
|
||||
dentry = filp.get_dentry()
|
||||
if dentry:
|
||||
inode_object = dentry.d_inode
|
||||
if inode_object and inode_object.is_valid():
|
||||
itype = (
|
||||
inode_object.get_inode_type() or renderers.NotAvailableValue()
|
||||
)
|
||||
return (
|
||||
inode_object.i_ino,
|
||||
itype,
|
||||
inode_object.i_size,
|
||||
inode_object.get_file_mode(),
|
||||
inode_object.get_change_time(),
|
||||
inode_object.get_modification_time(),
|
||||
inode_object.get_access_time(),
|
||||
)
|
||||
except (exceptions.InvalidAddressException, AttributeError) as e:
|
||||
vollog.warning(f"Can't get inode metadata: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def list_fds(
|
||||
cls,
|
||||
@@ -52,7 +75,7 @@ class Lsof(plugins.PluginInterface):
|
||||
symbol_table: str,
|
||||
filter_func: Callable[[int], bool] = lambda _: False,
|
||||
):
|
||||
linuxutils_symbol_table = None # type: ignore
|
||||
linuxutils_symbol_table = None
|
||||
for task in pslist.PsList.list_tasks(context, symbol_table, filter_func):
|
||||
if linuxutils_symbol_table is None:
|
||||
if constants.BANG not in task.vol.type_name:
|
||||
@@ -69,21 +92,79 @@ class Lsof(plugins.PluginInterface):
|
||||
for fd_fields in fd_generator:
|
||||
yield pid, task_comm, task, fd_fields
|
||||
|
||||
@classmethod
|
||||
def list_fds_and_inodes(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
symbol_table: str,
|
||||
filter_func: Callable[[int], bool] = lambda _: False,
|
||||
):
|
||||
for pid, task_comm, task, (fd_num, filp, full_path) in cls.list_fds(
|
||||
context, symbol_table, filter_func
|
||||
):
|
||||
inode_metadata = cls.get_inode_metadata(filp)
|
||||
if inode_metadata is None:
|
||||
inode_metadata = tuple(
|
||||
interfaces.renderers.BaseAbsentValue() for _ in range(7)
|
||||
)
|
||||
yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata
|
||||
|
||||
def _generator(self, pids, symbol_table):
|
||||
filter_func = pslist.PsList.create_pid_filter(pids)
|
||||
fds_generator = self.list_fds(
|
||||
fds_generator = self.list_fds_and_inodes(
|
||||
self.context, symbol_table, filter_func=filter_func
|
||||
)
|
||||
|
||||
for pid, task_comm, _task, fd_fields in fds_generator:
|
||||
fd_num, _filp, full_path = fd_fields
|
||||
|
||||
fields = (pid, task_comm, fd_num, full_path)
|
||||
for (
|
||||
pid,
|
||||
task_comm,
|
||||
task,
|
||||
fd_num,
|
||||
filp,
|
||||
full_path,
|
||||
inode_metadata,
|
||||
) in fds_generator:
|
||||
inode_num, itype, file_size, imode, ctime, mtime, atime = inode_metadata
|
||||
fields = (
|
||||
pid,
|
||||
task_comm,
|
||||
fd_num,
|
||||
full_path,
|
||||
inode_num,
|
||||
itype,
|
||||
imode,
|
||||
ctime,
|
||||
mtime,
|
||||
atime,
|
||||
file_size,
|
||||
)
|
||||
yield (0, fields)
|
||||
|
||||
def run(self):
|
||||
pids = self.config.get("pid", None)
|
||||
symbol_table = self.config["kernel"]
|
||||
|
||||
tree_grid_args = [("PID", int), ("Process", str), ("FD", int), ("Path", str)]
|
||||
tree_grid_args = [
|
||||
("PID", int),
|
||||
("Process", str),
|
||||
("FD", int),
|
||||
("Path", str),
|
||||
("Inode", int),
|
||||
("Type", str),
|
||||
("Mode", str),
|
||||
("Changed", datetime.datetime),
|
||||
("Modified", datetime.datetime),
|
||||
("Accessed", datetime.datetime),
|
||||
("Size", int),
|
||||
]
|
||||
return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table))
|
||||
|
||||
def generate_timeline(self):
|
||||
pids = self.config.get("pid", None)
|
||||
symbol_table = self.config["kernel"]
|
||||
for row in self._generator(pids, symbol_table):
|
||||
_depth, row_data = row
|
||||
description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"'
|
||||
yield description, timeliner.TimeLinerType.CHANGED, row_data[7]
|
||||
yield description, timeliner.TimeLinerType.MODIFIED, row_data[8]
|
||||
yield description, timeliner.TimeLinerType.ACCESSED, row_data[9]
|
||||
|
||||
@@ -37,7 +37,7 @@ class MountInfo(plugins.PluginInterface):
|
||||
|
||||
_required_framework_version = (2, 2, 0)
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_version = (1, 2, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -143,10 +143,10 @@ class MountInfo(plugins.PluginInterface):
|
||||
sb_opts,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_tasks_mountpoints(
|
||||
self,
|
||||
tasks: Iterable[interfaces.objects.ObjectInterface],
|
||||
filtered_by_pids: bool,
|
||||
filtered_by_pids: bool = False,
|
||||
):
|
||||
seen_mountpoints = set()
|
||||
for task in tasks:
|
||||
@@ -184,8 +184,8 @@ class MountInfo(plugins.PluginInterface):
|
||||
self,
|
||||
tasks: Iterable[interfaces.objects.ObjectInterface],
|
||||
mnt_ns_ids: List[int],
|
||||
mount_format: bool,
|
||||
filtered_by_pids: bool,
|
||||
mount_format: bool = False,
|
||||
filtered_by_pids: bool = False,
|
||||
) -> Iterable[Tuple[int, Tuple]]:
|
||||
show_filter_warning = False
|
||||
for task, mnt, mnt_ns_id in self._get_tasks_mountpoints(
|
||||
@@ -247,6 +247,37 @@ class MountInfo(plugins.PluginInterface):
|
||||
"Could not filter by mount namespace id. This field is not available in this kernel."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_superblocks(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_module_name: str,
|
||||
) -> Iterable[interfaces.objects.ObjectInterface]:
|
||||
"""Yield file system superblocks based on the task's mounted filesystems.
|
||||
|
||||
Args:
|
||||
context: The context to retrieve required elements (layers, symbol tables) from
|
||||
vmlinux_module_name: The name of the kernel module on which to operate
|
||||
|
||||
Yields:
|
||||
super_block: Kernel's struct super_block object
|
||||
"""
|
||||
# No filter so that we get all the mount namespaces from all tasks
|
||||
tasks = pslist.PsList.list_tasks(context, vmlinux_module_name)
|
||||
|
||||
seen_sb_ptr = set()
|
||||
for task, mnt, _mnt_ns_id in cls._get_tasks_mountpoints(tasks):
|
||||
path_root = linux.LinuxUtilities.get_path_mnt(task, mnt)
|
||||
if not path_root:
|
||||
continue
|
||||
|
||||
sb_ptr = mnt.get_mnt_sb()
|
||||
if not sb_ptr or sb_ptr in seen_sb_ptr:
|
||||
continue
|
||||
seen_sb_ptr.add(sb_ptr)
|
||||
|
||||
yield sb_ptr.dereference(), path_root
|
||||
|
||||
def run(self):
|
||||
pids = self.config.get("pids")
|
||||
mount_ns_ids = self.config.get("mntns")
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
import math
|
||||
import logging
|
||||
import datetime
|
||||
from dataclasses import dataclass, astuple
|
||||
from typing import List, Set, Type, Iterable
|
||||
|
||||
from volatility3.framework import renderers, interfaces
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.plugins import timeliner
|
||||
from volatility3.plugins.linux import mountinfo
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class InodeUser:
|
||||
"""Inode user representation, featuring augmented information and formatted fields.
|
||||
This is the data the plugin will eventually display.
|
||||
"""
|
||||
|
||||
superblock_addr: int
|
||||
mountpoint: str
|
||||
device: str
|
||||
inode_num: int
|
||||
inode_addr: int
|
||||
type: str
|
||||
inode_pages: int
|
||||
cached_pages: int
|
||||
file_mode: str
|
||||
access_time: str
|
||||
modification_time: str
|
||||
change_time: str
|
||||
path: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class InodeInternal:
|
||||
"""Inode internal representation containing only the core objects
|
||||
|
||||
Fields:
|
||||
superblock: 'super_block' struct
|
||||
mountpoint: Superblock mountpoint path
|
||||
inode: 'inode' struct
|
||||
path: Dentry full path
|
||||
"""
|
||||
|
||||
superblock: interfaces.objects.ObjectInterface
|
||||
mountpoint: str
|
||||
inode: interfaces.objects.ObjectInterface
|
||||
path: str
|
||||
|
||||
def to_user(
|
||||
self, kernel_layer: interfaces.layers.TranslationLayerInterface
|
||||
) -> InodeUser:
|
||||
"""Augment the inode information to be presented to the user
|
||||
|
||||
Args:
|
||||
kernel_layer: The kernel layer to obtain the page size
|
||||
|
||||
Returns:
|
||||
An InodeUser dataclass
|
||||
"""
|
||||
# Ensure all types are atomic immutable. Otherwise, astuple() will take a long
|
||||
# time doing a deepcopy of the Volatility objects.
|
||||
superblock_addr = self.superblock.vol.offset
|
||||
device = f"{self.superblock.major}:{self.superblock.minor}"
|
||||
inode_num = int(self.inode.i_ino)
|
||||
inode_addr = self.inode.vol.offset
|
||||
inode_type = self.inode.get_inode_type() or renderers.UnparsableValue()
|
||||
# Round up the number of pages to fit the inode's size
|
||||
inode_pages = int(math.ceil(self.inode.i_size / float(kernel_layer.page_size)))
|
||||
cached_pages = int(self.inode.i_mapping.nrpages)
|
||||
file_mode = self.inode.get_file_mode()
|
||||
access_time_dt = self.inode.get_access_time()
|
||||
modification_time_str = self.inode.get_modification_time()
|
||||
change_time_str = self.inode.get_change_time()
|
||||
|
||||
inode_user = InodeUser(
|
||||
superblock_addr=superblock_addr,
|
||||
mountpoint=self.mountpoint,
|
||||
device=device,
|
||||
inode_num=inode_num,
|
||||
inode_addr=inode_addr,
|
||||
type=inode_type,
|
||||
inode_pages=inode_pages,
|
||||
cached_pages=cached_pages,
|
||||
file_mode=file_mode,
|
||||
access_time=access_time_dt,
|
||||
modification_time=modification_time_str,
|
||||
change_time=change_time_str,
|
||||
path=self.path,
|
||||
)
|
||||
return inode_user
|
||||
|
||||
|
||||
class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
"""Lists files from memory"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (1, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="type",
|
||||
description="List of space-separated file type filters i.e. --type REG DIR",
|
||||
element_type=str,
|
||||
optional=True,
|
||||
),
|
||||
requirements.StringRequirement(
|
||||
name="find",
|
||||
description="Filename (full path) to find",
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _follow_symlink(
|
||||
inode: interfaces.objects.ObjectInterface,
|
||||
symlink_path: str,
|
||||
) -> str:
|
||||
"""Follows (fast) symlinks (kernels >= 4.2.x).
|
||||
Fast symlinks are filesystem agnostic.
|
||||
|
||||
Args:
|
||||
inode: The inode (or pointer) to dump
|
||||
symlink_path: The symlink name
|
||||
|
||||
Returns:
|
||||
If it can resolve the symlink, it returns a string "symlink_path -> target_path"
|
||||
Otherwise, it returns the same symlink_path
|
||||
"""
|
||||
# i_link (fast symlinks) were introduced in 4.2
|
||||
if inode and inode.is_link and inode.has_member("i_link") and inode.i_link:
|
||||
i_link_str = inode.i_link.dereference().cast(
|
||||
"string", max_length=255, encoding="utf-8", errors="replace"
|
||||
)
|
||||
symlink_path = f"{symlink_path} -> {i_link_str}"
|
||||
|
||||
return symlink_path
|
||||
|
||||
@classmethod
|
||||
def _walk_dentry(
|
||||
cls,
|
||||
seen_dentries: Set[int],
|
||||
root_dentry: interfaces.objects.ObjectInterface,
|
||||
parent_dir: str,
|
||||
):
|
||||
"""Walks dentries recursively
|
||||
|
||||
Args:
|
||||
seen_dentries: A set to ensure each dentry is processed only once
|
||||
root_dentry: Root dentry object
|
||||
parent_dir: Parent directory path
|
||||
|
||||
Yields:
|
||||
file_path: Filename including path
|
||||
dentry: Dentry object
|
||||
"""
|
||||
|
||||
for dentry in root_dentry.get_subdirs():
|
||||
dentry_addr = dentry.vol.offset
|
||||
|
||||
# corruption
|
||||
if dentry_addr == root_dentry.vol.offset:
|
||||
continue
|
||||
|
||||
if dentry_addr in seen_dentries:
|
||||
continue
|
||||
|
||||
seen_dentries.add(dentry_addr)
|
||||
|
||||
inode = dentry.d_inode
|
||||
if not (inode and inode.is_valid()):
|
||||
continue
|
||||
|
||||
# This allows us to have consistent paths
|
||||
if dentry.d_name.name:
|
||||
basename = dentry.d_name.name_as_str()
|
||||
# Do NOT use os.path.join() below
|
||||
file_path = parent_dir + "/" + basename
|
||||
else:
|
||||
continue
|
||||
|
||||
yield file_path, dentry
|
||||
|
||||
if inode.is_dir:
|
||||
yield from cls._walk_dentry(seen_dentries, dentry, parent_dir=file_path)
|
||||
|
||||
@classmethod
|
||||
def get_inodes(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_module_name: str,
|
||||
) -> 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
|
||||
|
||||
Yields:
|
||||
An InodeInternal object
|
||||
"""
|
||||
|
||||
superblocks_iter = mountinfo.MountInfo.get_superblocks(
|
||||
context=context,
|
||||
vmlinux_module_name=vmlinux_module_name,
|
||||
)
|
||||
|
||||
seen_inodes = set()
|
||||
seen_dentries = set()
|
||||
for superblock, mountpoint in superblocks_iter:
|
||||
parent_dir = "" if mountpoint == "/" else mountpoint
|
||||
|
||||
# Superblock root dentry
|
||||
root_dentry_ptr = superblock.s_root
|
||||
if not root_dentry_ptr:
|
||||
continue
|
||||
|
||||
root_dentry = root_dentry_ptr.dereference()
|
||||
|
||||
# Dentry sanity check
|
||||
if not root_dentry.is_root():
|
||||
continue
|
||||
|
||||
# More dentry/inode sanity checks
|
||||
root_inode_ptr = root_dentry.d_inode
|
||||
if not root_inode_ptr:
|
||||
continue
|
||||
root_inode = root_inode_ptr.dereference()
|
||||
if not root_inode.is_valid():
|
||||
continue
|
||||
|
||||
# Inode already processed?
|
||||
if root_inode_ptr in seen_inodes:
|
||||
continue
|
||||
seen_inodes.add(root_inode_ptr)
|
||||
|
||||
root_path = mountpoint
|
||||
|
||||
inode_in = InodeInternal(
|
||||
superblock=superblock,
|
||||
mountpoint=mountpoint,
|
||||
inode=root_inode,
|
||||
path=root_path,
|
||||
)
|
||||
yield inode_in
|
||||
|
||||
# Children
|
||||
for file_path, file_dentry in cls._walk_dentry(
|
||||
seen_dentries, root_dentry, parent_dir
|
||||
):
|
||||
if not file_dentry:
|
||||
continue
|
||||
# Dentry/inode sanity checks
|
||||
file_inode_ptr = file_dentry.d_inode
|
||||
if not file_inode_ptr:
|
||||
continue
|
||||
file_inode = file_inode_ptr.dereference()
|
||||
if not file_inode.is_valid():
|
||||
continue
|
||||
|
||||
# Inode already processed?
|
||||
if file_inode_ptr in seen_inodes:
|
||||
continue
|
||||
seen_inodes.add(file_inode_ptr)
|
||||
|
||||
file_path = cls._follow_symlink(file_inode_ptr, file_path)
|
||||
inode_in = InodeInternal(
|
||||
superblock=superblock,
|
||||
mountpoint=mountpoint,
|
||||
inode=file_inode,
|
||||
path=file_path,
|
||||
)
|
||||
yield inode_in
|
||||
|
||||
def _generator(self):
|
||||
vmlinux_module_name = self.config["kernel"]
|
||||
vmlinux = self.context.modules[vmlinux_module_name]
|
||||
vmlinux_layer = self.context.layers[vmlinux.layer_name]
|
||||
|
||||
inodes_iter = self.get_inodes(
|
||||
context=self.context,
|
||||
vmlinux_module_name=vmlinux_module_name,
|
||||
)
|
||||
|
||||
types_filter = self.config["type"]
|
||||
for inode_in in inodes_iter:
|
||||
if types_filter and inode_in.inode.get_inode_type() not in types_filter:
|
||||
continue
|
||||
|
||||
if self.config["find"]:
|
||||
if inode_in.path == self.config["find"]:
|
||||
inode_out = inode_in.to_user(vmlinux_layer)
|
||||
yield (0, astuple(inode_out))
|
||||
break # Only the first match
|
||||
else:
|
||||
inode_out = inode_in.to_user(vmlinux_layer)
|
||||
yield (0, astuple(inode_out))
|
||||
|
||||
def generate_timeline(self):
|
||||
"""Generates tuples of (description, timestamp_type, timestamp)
|
||||
|
||||
These need not be generated in any particular order, sorting
|
||||
will be done later
|
||||
"""
|
||||
vmlinux_module_name = self.config["kernel"]
|
||||
vmlinux = self.context.modules[vmlinux_module_name]
|
||||
vmlinux_layer = self.context.layers[vmlinux.layer_name]
|
||||
|
||||
inodes_iter = self.get_inodes(
|
||||
context=self.context,
|
||||
vmlinux_module_name=vmlinux_module_name,
|
||||
)
|
||||
|
||||
for inode_in in inodes_iter:
|
||||
inode_out = inode_in.to_user(vmlinux_layer)
|
||||
description = f"Cached Inode for {inode_out.path}"
|
||||
yield description, timeliner.TimeLinerType.ACCESSED, inode_out.access_time
|
||||
yield description, timeliner.TimeLinerType.MODIFIED, inode_out.modification_time
|
||||
yield description, timeliner.TimeLinerType.CHANGE, inode_out.change_time
|
||||
|
||||
@staticmethod
|
||||
def format_fields_with_headers(headers, generator):
|
||||
"""Uses the headers type to cast the fields obtained from the generator"""
|
||||
for level, fields in generator:
|
||||
formatted_fields = []
|
||||
for header, field in zip(headers, fields):
|
||||
header_type = header[1]
|
||||
|
||||
if isinstance(
|
||||
field, (header_type, interfaces.renderers.BaseAbsentValue)
|
||||
):
|
||||
formatted_field = field
|
||||
else:
|
||||
formatted_field = header_type(field)
|
||||
|
||||
formatted_fields.append(formatted_field)
|
||||
yield level, formatted_fields
|
||||
|
||||
def run(self):
|
||||
headers = [
|
||||
("SuperblockAddr", format_hints.Hex),
|
||||
("MountPoint", str),
|
||||
("Device", str),
|
||||
("InodeNum", int),
|
||||
("InodeAddr", format_hints.Hex),
|
||||
("FileType", str),
|
||||
("InodePages", int),
|
||||
("CachedPages", int),
|
||||
("FileMode", str),
|
||||
("AccessTime", datetime.datetime),
|
||||
("ModificationTime", datetime.datetime),
|
||||
("ChangeTime", datetime.datetime),
|
||||
("FilePath", str),
|
||||
]
|
||||
|
||||
return renderers.TreeGrid(
|
||||
headers, self.format_fields_with_headers(headers, self._generator())
|
||||
)
|
||||
|
||||
|
||||
class InodePages(plugins.PluginInterface):
|
||||
"""Lists and recovers cached inode pages"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (1, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="files", plugin=Files, version=(1, 0, 0)
|
||||
),
|
||||
requirements.StringRequirement(
|
||||
name="find",
|
||||
description="Filename (full path) to find ",
|
||||
optional=True,
|
||||
),
|
||||
requirements.IntRequirement(
|
||||
name="inode",
|
||||
description="Inode address",
|
||||
optional=True,
|
||||
),
|
||||
requirements.StringRequirement(
|
||||
name="dump",
|
||||
description="Output file path",
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def write_inode_content_to_file(
|
||||
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:
|
||||
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
|
||||
"""
|
||||
if not inode.is_reg:
|
||||
vollog.error("The inode is not a regular file")
|
||||
return
|
||||
|
||||
# By using truncate/seek, provided the filesystem supports it, 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.
|
||||
try:
|
||||
with open_method(filename) as f:
|
||||
inode_size = inode.i_size
|
||||
f.truncate(inode_size)
|
||||
|
||||
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 = page_content[:max_length]
|
||||
if current_fp + len(page_bytes) > inode_size:
|
||||
vollog.error(
|
||||
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
|
||||
inode.vol.object,
|
||||
inode_size,
|
||||
page_idx,
|
||||
)
|
||||
f.seek(current_fp)
|
||||
f.write(page_bytes)
|
||||
|
||||
except IOError as e:
|
||||
vollog.error("Unable to write to file (%s): %s", filename, e)
|
||||
|
||||
def _generator(self):
|
||||
vmlinux_module_name = self.config["kernel"]
|
||||
vmlinux = self.context.modules[vmlinux_module_name]
|
||||
vmlinux_layer = self.context.layers[vmlinux.layer_name]
|
||||
|
||||
if self.config["inode"] and self.config["find"]:
|
||||
vollog.error("Cannot use --inode and --find simultaneously")
|
||||
return
|
||||
|
||||
if self.config["find"]:
|
||||
inodes_iter = Files.get_inodes(
|
||||
context=self.context,
|
||||
vmlinux_module_name=vmlinux_module_name,
|
||||
)
|
||||
for inode_in in inodes_iter:
|
||||
if inode_in.path == self.config["find"]:
|
||||
inode = inode_in.inode
|
||||
break # Only the first match
|
||||
|
||||
elif self.config["inode"]:
|
||||
inode = vmlinux.object("inode", self.config["inode"], absolute=True)
|
||||
else:
|
||||
vollog.error("You must use either --inode or --find")
|
||||
return
|
||||
|
||||
if not inode.is_reg:
|
||||
vollog.error("The inode is not a regular file")
|
||||
return
|
||||
|
||||
inode_size = inode.i_size
|
||||
if not inode.is_valid():
|
||||
vollog.error("Invalid inode at 0x%x", self.config["inode"])
|
||||
return
|
||||
|
||||
for page_obj in inode.get_pages():
|
||||
page_vaddr = page_obj.vol.offset
|
||||
page_paddr = page_obj.to_paddr()
|
||||
page_mapping_addr = page_obj.mapping
|
||||
page_index = int(page_obj.index)
|
||||
page_file_offset = page_index * vmlinux_layer.page_size
|
||||
dump_safe = page_file_offset < inode_size
|
||||
page_flags_list = page_obj.get_flags_list()
|
||||
page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list])
|
||||
fields = (
|
||||
page_vaddr,
|
||||
page_paddr,
|
||||
page_mapping_addr,
|
||||
page_index,
|
||||
dump_safe,
|
||||
page_flags,
|
||||
)
|
||||
|
||||
yield 0, fields
|
||||
|
||||
if self.config["dump"]:
|
||||
filename = self.config["dump"]
|
||||
vollog.info("[*] Writing inode at 0x%x to '%s'", inode.vol.offset, filename)
|
||||
self.write_inode_content_to_file(inode, filename, self.open, vmlinux_layer)
|
||||
|
||||
def run(self):
|
||||
headers = [
|
||||
("PageVAddr", format_hints.Hex),
|
||||
("PagePAddr", format_hints.Hex),
|
||||
("MappingAddr", format_hints.Hex),
|
||||
("Index", int),
|
||||
("DumpSafe", bool),
|
||||
("Flags", str),
|
||||
]
|
||||
|
||||
return renderers.TreeGrid(
|
||||
headers, Files.format_fields_with_headers(headers, self._generator())
|
||||
)
|
||||
@@ -0,0 +1,258 @@
|
||||
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from volatility3.framework import renderers, interfaces, constants
|
||||
from volatility3.framework.symbols import linux
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.plugins.linux import pslist
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PIDHashTable(plugins.PluginInterface):
|
||||
"""Enumerates processes through the PID hash table"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (1, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
|
||||
),
|
||||
requirements.BooleanRequirement(
|
||||
name="decorate_comm",
|
||||
description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets",
|
||||
optional=True,
|
||||
default=False,
|
||||
),
|
||||
]
|
||||
|
||||
def _is_valid_task(self, task) -> bool:
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
vmlinux_layer = self.context.layers[vmlinux.layer_name]
|
||||
return bool(task and task.pid > 0 and vmlinux_layer.is_valid(task.parent))
|
||||
|
||||
def _get_pidtype_pid(self):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
|
||||
# The pid_type enumeration is present since 2.5.37, just in case
|
||||
pid_type_enum = vmlinux.get_enumeration("pid_type")
|
||||
if not pid_type_enum:
|
||||
vollog.error("Cannot find pid_type enum. Unsupported kernel")
|
||||
return None
|
||||
|
||||
pidtype_pid = pid_type_enum.choices.get("PIDTYPE_PID")
|
||||
if pidtype_pid is None:
|
||||
vollog.error("Cannot find PIDTYPE_PID. Unsupported kernel")
|
||||
return None
|
||||
|
||||
# Typically PIDTYPE_PID = 0
|
||||
return pidtype_pid
|
||||
|
||||
def _get_pidhash_array(self):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
|
||||
pidhash_shift = vmlinux.object_from_symbol("pidhash_shift")
|
||||
pidhash_size = 1 << pidhash_shift
|
||||
|
||||
array_type_name = vmlinux.symbol_table_name + constants.BANG + "array"
|
||||
|
||||
pidhash_ptr = vmlinux.object_from_symbol("pid_hash")
|
||||
# pidhash is an array of hlist_heads
|
||||
pidhash = self._context.object(
|
||||
array_type_name,
|
||||
offset=pidhash_ptr,
|
||||
subtype=vmlinux.get_type("hlist_head"),
|
||||
count=pidhash_size,
|
||||
layer_name=vmlinux.layer_name,
|
||||
)
|
||||
|
||||
return pidhash
|
||||
|
||||
def _walk_upid(self, seen_upids, upid):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
vmlinux_layer = self.context.layers[vmlinux.layer_name]
|
||||
|
||||
while upid and vmlinux_layer.is_valid(upid.vol.offset):
|
||||
if upid.vol.offset in seen_upids:
|
||||
break
|
||||
seen_upids.add(upid.vol.offset)
|
||||
|
||||
pid_chain = upid.pid_chain
|
||||
if not (pid_chain and vmlinux_layer.is_valid(pid_chain.vol.offset)):
|
||||
break
|
||||
|
||||
upid = linux.LinuxUtilities.container_of(
|
||||
pid_chain.next, "upid", "pid_chain", vmlinux
|
||||
)
|
||||
|
||||
def _get_upids(self):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
vmlinux_layer = self.context.layers[vmlinux.layer_name]
|
||||
|
||||
# 2.6.24 <= kernels < 4.15
|
||||
pidhash = self._get_pidhash_array()
|
||||
|
||||
seen_upids = set()
|
||||
for hlist in pidhash:
|
||||
# each entry in the hlist is a upid which is wrapped in a pid
|
||||
ent = hlist.first
|
||||
|
||||
while ent and vmlinux_layer.is_valid(ent.vol.offset):
|
||||
# upid->pid_chain exists 2.6.24 <= kernel < 4.15
|
||||
upid = linux.LinuxUtilities.container_of(
|
||||
ent.vol.offset, "upid", "pid_chain", vmlinux
|
||||
)
|
||||
|
||||
if upid.vol.offset in seen_upids:
|
||||
break
|
||||
|
||||
self._walk_upid(seen_upids, upid)
|
||||
|
||||
ent = ent.next
|
||||
|
||||
return seen_upids
|
||||
|
||||
def _pid_hash_implementation(self):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
|
||||
# 2.6.24 <= kernels < 4.15
|
||||
task_pids_off = vmlinux.get_type("task_struct").relative_child_offset("pids")
|
||||
pidtype_pid = self._get_pidtype_pid()
|
||||
|
||||
for upid in self._get_upids():
|
||||
pid = linux.LinuxUtilities.container_of(upid, "pid", "numbers", vmlinux)
|
||||
if not pid:
|
||||
continue
|
||||
|
||||
pid_tasks_0 = pid.tasks[pidtype_pid].first
|
||||
if not pid_tasks_0:
|
||||
continue
|
||||
|
||||
task = vmlinux.object(
|
||||
"task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True
|
||||
)
|
||||
if self._is_valid_task(task):
|
||||
yield task
|
||||
|
||||
def _task_for_radix_pid_node(self, nodep):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
|
||||
# kernels >= 4.15
|
||||
pid = vmlinux.object("pid", offset=nodep, absolute=True)
|
||||
pidtype_pid = self._get_pidtype_pid()
|
||||
|
||||
pid_tasks_0 = pid.tasks[pidtype_pid].first
|
||||
if not pid_tasks_0:
|
||||
return None
|
||||
|
||||
task_struct_type = vmlinux.get_type("task_struct")
|
||||
if task_struct_type.has_member("pids"):
|
||||
member = "pids"
|
||||
elif task_struct_type.has_member("pid_links"):
|
||||
member = "pid_links"
|
||||
else:
|
||||
return None
|
||||
|
||||
task_pids_off = task_struct_type.relative_child_offset(member)
|
||||
task = vmlinux.object(
|
||||
"task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True
|
||||
)
|
||||
return task
|
||||
|
||||
def _pid_namespace_idr(self):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
|
||||
# kernels >= 4.15
|
||||
ns_addr = vmlinux.get_symbol("init_pid_ns").address
|
||||
ns = vmlinux.object("pid_namespace", offset=ns_addr)
|
||||
|
||||
for page_addr in ns.idr.get_entries():
|
||||
task = self._task_for_radix_pid_node(page_addr)
|
||||
if self._is_valid_task(task):
|
||||
yield task
|
||||
|
||||
def _determine_pid_func(self):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
|
||||
pid_hash = vmlinux.has_symbol("pid_hash") and vmlinux.has_symbol(
|
||||
"pidhash_shift"
|
||||
) # 2.5.55 <= kernels < 4.15
|
||||
|
||||
has_pid_numbers = vmlinux.has_type("pid") and vmlinux.get_type(
|
||||
"pid"
|
||||
).has_member(
|
||||
"numbers"
|
||||
) # kernels >= 2.6.24
|
||||
|
||||
has_pid_chain = vmlinux.has_type("upid") and vmlinux.get_type(
|
||||
"upid"
|
||||
).has_member(
|
||||
"pid_chain"
|
||||
) # 2.6.24 <= kernels < 4.15
|
||||
|
||||
# kernels >= 4.15
|
||||
pid_idr = vmlinux.has_type("pid_namespace") and vmlinux.get_type(
|
||||
"pid_namespace"
|
||||
).has_member("idr")
|
||||
|
||||
if pid_idr:
|
||||
# kernels >= 4.15
|
||||
return self._pid_namespace_idr
|
||||
elif pid_hash and has_pid_numbers and has_pid_numbers and has_pid_chain:
|
||||
# 2.6.24 <= kernels < 4.15
|
||||
return self._pid_hash_implementation
|
||||
|
||||
return None
|
||||
|
||||
def get_tasks(self) -> interfaces.objects.ObjectInterface:
|
||||
"""Enumerates processes through the PID hash table
|
||||
|
||||
Yields:
|
||||
task_struct objects
|
||||
"""
|
||||
pid_func = self._determine_pid_func()
|
||||
if not pid_func:
|
||||
vollog.error("Cannot determine which PID hash table this kernel is using")
|
||||
return
|
||||
|
||||
yield from sorted(pid_func(), key=lambda t: (t.tgid, t.pid))
|
||||
|
||||
def _generator(
|
||||
self, decorate_comm: bool = False
|
||||
) -> interfaces.objects.ObjectInterface:
|
||||
for task in self.get_tasks():
|
||||
offset, pid, tid, ppid, name = pslist.PsList.get_task_fields(
|
||||
task, decorate_comm
|
||||
)
|
||||
fields = format_hints.Hex(offset), pid, tid, ppid, name
|
||||
yield 0, fields
|
||||
|
||||
def run(self):
|
||||
decorate_comm = self.config.get("decorate_comm")
|
||||
|
||||
headers = [
|
||||
("OFFSET", format_hints.Hex),
|
||||
("PID", int),
|
||||
("TID", int),
|
||||
("PPID", int),
|
||||
("COMM", str),
|
||||
]
|
||||
return renderers.TreeGrid(headers, self._generator(decorate_comm=decorate_comm))
|
||||
@@ -151,17 +151,15 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
|
||||
|
||||
bpfprog = sock_filter.prog
|
||||
|
||||
BPF_PROG_TYPE_UNSPEC = 0 # cBPF filter
|
||||
try:
|
||||
bpfprog_type = bpfprog.get_type()
|
||||
if bpfprog_type == BPF_PROG_TYPE_UNSPEC:
|
||||
return # cBPF filter
|
||||
except AttributeError:
|
||||
bpfprog_type = bpfprog.get_type()
|
||||
if not bpfprog_type:
|
||||
# kernel < 3.18.140, it's a cBPF filter
|
||||
return None
|
||||
|
||||
BPF_PROG_TYPE_SOCKET_FILTER = 1 # eBPF filter
|
||||
if bpfprog_type != BPF_PROG_TYPE_SOCKET_FILTER:
|
||||
if bpfprog_type == "BPF_PROG_TYPE_UNSPEC":
|
||||
return None # cBPF filter
|
||||
|
||||
if bpfprog_type != "BPF_PROG_TYPE_SOCKET_FILTER":
|
||||
socket_filter["bpf_filter_type"] = f"UNK({bpfprog_type})"
|
||||
vollog.warning(f"Unexpected BPF type {bpfprog_type} for a socket")
|
||||
return None
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Union
|
||||
from volatility3.framework import interfaces, renderers
|
||||
|
||||
|
||||
# FIXME: Move wintime_to_datetime() and unixtime_to_datetime() out of renderers, possibly framework.objects.utility
|
||||
def wintime_to_datetime(
|
||||
wintime: int,
|
||||
) -> Union[interfaces.renderers.BaseAbsentValue, datetime.datetime]:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import math
|
||||
import contextlib
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Iterator, List, Tuple, Optional, Union
|
||||
|
||||
from volatility3 import framework
|
||||
@@ -30,9 +33,13 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
|
||||
self.set_type_class("kobject", extensions.kobject)
|
||||
self.set_type_class("cred", extensions.cred)
|
||||
self.set_type_class("inode", extensions.inode)
|
||||
self.set_type_class("idr", extensions.IDR)
|
||||
self.set_type_class("address_space", extensions.address_space)
|
||||
self.set_type_class("page", extensions.page)
|
||||
# Might not exist in the current symbols
|
||||
self.optional_set_type_class("module", extensions.module)
|
||||
self.optional_set_type_class("bpf_prog", extensions.bpf_prog)
|
||||
self.optional_set_type_class("bpf_prog_aux", extensions.bpf_prog_aux)
|
||||
self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct)
|
||||
self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t)
|
||||
|
||||
@@ -68,7 +75,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
|
||||
class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
"""Class with multiple useful linux functions."""
|
||||
|
||||
_version = (2, 1, 0)
|
||||
_version = (2, 1, 1)
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
framework.require_interface_version(*_required_framework_version)
|
||||
@@ -413,9 +420,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
Returns:
|
||||
A kernel object (vmlinux)
|
||||
"""
|
||||
symbol_table_arr = volobj.vol.type_name.split("!", 1)
|
||||
symbol_table = symbol_table_arr[0] if len(symbol_table_arr) == 2 else None
|
||||
|
||||
symbol_table = volobj.get_symbol_table_name()
|
||||
module_names = context.modules.get_modules_by_symbol_tables(symbol_table)
|
||||
module_names = list(module_names)
|
||||
|
||||
@@ -426,3 +431,353 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
kernel = context.modules[kernel_module_name]
|
||||
|
||||
return kernel
|
||||
|
||||
|
||||
class IDStorage(ABC):
|
||||
"""Abstraction to support both XArray and RadixTree"""
|
||||
|
||||
# Dynamic values, these will be initialized later
|
||||
CHUNK_SHIFT = None
|
||||
CHUNK_SIZE = None
|
||||
CHUNK_MASK = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: interfaces.context.ContextInterface,
|
||||
kernel_module_name: str,
|
||||
):
|
||||
self.vmlinux = context.modules[kernel_module_name]
|
||||
self.vmlinux_layer = self.vmlinux.context.layers[self.vmlinux.layer_name]
|
||||
|
||||
self.pointer_size = self.vmlinux.get_type("pointer").size
|
||||
# Dynamically work out the (XA_CHUNK|RADIX_TREE_MAP)_SHIFT values based on
|
||||
# the node.slots[] array size
|
||||
node_type = self.vmlinux.get_type(self.node_type_name)
|
||||
slots_array_size = node_type.child_template("slots").count
|
||||
|
||||
# Calculate the LSB index - 1
|
||||
self.CHUNK_SHIFT = slots_array_size.bit_length() - 1
|
||||
self.CHUNK_SIZE = 1 << self.CHUNK_SHIFT
|
||||
self.CHUNK_MASK = self.CHUNK_SIZE - 1
|
||||
|
||||
@classmethod
|
||||
def choose_id_storage(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
kernel_module_name: str,
|
||||
) -> "IDStorage":
|
||||
"""Returns the appropriate ID storage data structure instance for the current kernel implementation.
|
||||
This is used by the IDR and the PageCache to choose between the XArray and RadixTree.
|
||||
|
||||
Args:
|
||||
context: The context to retrieve required elements (layers, symbol tables) from
|
||||
kernel_module_name: The name of the kernel module on which to operate
|
||||
|
||||
Returns:
|
||||
The appropriate ID storage instance for the current kernel
|
||||
"""
|
||||
vmlinux = context.modules[kernel_module_name]
|
||||
address_space_type = vmlinux.get_type("address_space")
|
||||
address_space_has_i_pages = address_space_type.has_member("i_pages")
|
||||
i_pages_type_name = (
|
||||
address_space_type.child_template("i_pages").vol.type_name
|
||||
if address_space_has_i_pages
|
||||
else ""
|
||||
)
|
||||
i_pages_is_xarray = i_pages_type_name.endswith(constants.BANG + "xarray")
|
||||
i_pages_is_radix_tree_root = i_pages_type_name.endswith(
|
||||
constants.BANG + "radix_tree_root"
|
||||
) and vmlinux.get_type("radix_tree_root").has_member("xa_head")
|
||||
|
||||
if i_pages_is_xarray or i_pages_is_radix_tree_root:
|
||||
return XArray(context, kernel_module_name)
|
||||
else:
|
||||
return RadixTree(context, kernel_module_name)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def node_type_name(self) -> str:
|
||||
"""Returns the Tree implementation node type name
|
||||
|
||||
Returns:
|
||||
A string with the node type name
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def tag_internal_value(self) -> int:
|
||||
"""Returns the internal node flag for the tree"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def node_is_internal(self, nodep) -> bool:
|
||||
"""Checks if the node is internal"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def is_node_tagged(self, nodep) -> bool:
|
||||
"""Checks if the node pointer is tagged"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def untag_node(self, nodep) -> int:
|
||||
"""Untags a node pointer"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_tree_height(self, treep) -> int:
|
||||
"""Returns the tree height"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_node_height(self, nodep) -> int:
|
||||
"""Returns the node height"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_head_node(self, tree) -> int:
|
||||
"""Returns a pointer to the tree's head"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def is_valid_node(self, nodep) -> bool:
|
||||
"""Validates a node pointer"""
|
||||
raise NotImplementedError
|
||||
|
||||
def nodep_to_node(self, nodep) -> interfaces.objects.ObjectInterface:
|
||||
"""Instanciates a tree node from its pointer
|
||||
|
||||
Args:
|
||||
nodep: Pointer to the XArray/RadixTree node
|
||||
|
||||
Returns:
|
||||
A XArray/RadixTree node instance
|
||||
"""
|
||||
node = self.vmlinux.object(self.node_type_name, offset=nodep, absolute=True)
|
||||
return node
|
||||
|
||||
def _slot_to_nodep(self, slot) -> int:
|
||||
if self.node_is_internal(slot):
|
||||
nodep = slot & ~self.tag_internal_value
|
||||
else:
|
||||
nodep = slot
|
||||
|
||||
return nodep
|
||||
|
||||
def _iter_node(self, nodep, height) -> int:
|
||||
node = self.nodep_to_node(nodep)
|
||||
node_slots = node.slots
|
||||
for off in range(self.CHUNK_SIZE):
|
||||
slot = node_slots[off]
|
||||
if slot == 0:
|
||||
continue
|
||||
|
||||
nodep = self._slot_to_nodep(slot)
|
||||
|
||||
if height == 1:
|
||||
if self.is_valid_node(nodep):
|
||||
yield nodep
|
||||
else:
|
||||
for child_node in self._iter_node(nodep, height - 1):
|
||||
yield child_node
|
||||
|
||||
def get_entries(self, root: interfaces.objects.ObjectInterface) -> int:
|
||||
"""Walks the tree data structure
|
||||
|
||||
Args:
|
||||
root: The tree root object
|
||||
|
||||
Yields:
|
||||
A tree node pointer
|
||||
"""
|
||||
height = self.get_tree_height(root.vol.offset)
|
||||
|
||||
nodep = self.get_head_node(root)
|
||||
if not nodep:
|
||||
return
|
||||
|
||||
# Keep the internal flag before untagging it
|
||||
is_internal = self.node_is_internal(nodep)
|
||||
if self.is_node_tagged(nodep):
|
||||
nodep = self.untag_node(nodep)
|
||||
|
||||
if is_internal:
|
||||
height = self.get_node_height(nodep)
|
||||
|
||||
if height == 0:
|
||||
if self.is_valid_node(nodep):
|
||||
yield nodep
|
||||
else:
|
||||
for child_node in self._iter_node(nodep, height):
|
||||
yield child_node
|
||||
|
||||
|
||||
class XArray(IDStorage):
|
||||
XARRAY_TAG_MASK = 3
|
||||
XARRAY_TAG_INTERNAL = 2
|
||||
|
||||
def get_tree_height(self, treep) -> int:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def node_type_name(self) -> str:
|
||||
return "xa_node"
|
||||
|
||||
@property
|
||||
def tag_internal_value(self) -> int:
|
||||
return self.XARRAY_TAG_INTERNAL
|
||||
|
||||
def get_node_height(self, nodep) -> int:
|
||||
node = self.nodep_to_node(nodep)
|
||||
return (node.shift / self.CHUNK_SHIFT) + 1
|
||||
|
||||
def get_head_node(self, tree) -> int:
|
||||
return tree.xa_head
|
||||
|
||||
def node_is_internal(self, nodep) -> bool:
|
||||
return (nodep & self.XARRAY_TAG_MASK) == self.XARRAY_TAG_INTERNAL
|
||||
|
||||
def is_node_tagged(self, nodep) -> bool:
|
||||
return (nodep & self.XARRAY_TAG_MASK) != 0
|
||||
|
||||
def untag_node(self, nodep) -> int:
|
||||
return nodep & (~self.XARRAY_TAG_MASK)
|
||||
|
||||
def is_valid_node(self, nodep) -> bool:
|
||||
# It should have the tag mask clear
|
||||
return not self.is_node_tagged(nodep)
|
||||
|
||||
|
||||
class RadixTree(IDStorage):
|
||||
RADIX_TREE_INTERNAL_NODE = 1
|
||||
RADIX_TREE_EXCEPTIONAL_ENTRY = 2
|
||||
RADIX_TREE_ENTRY_MASK = 3
|
||||
|
||||
# Dynamic values. These will be initialized later
|
||||
RADIX_TREE_INDEX_BITS = None
|
||||
RADIX_TREE_MAX_PATH = None
|
||||
RADIX_TREE_HEIGHT_SHIFT = None
|
||||
RADIX_TREE_HEIGHT_MASK = None
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
char_bits = 8
|
||||
self.RADIX_TREE_INDEX_BITS = char_bits * self.pointer_size
|
||||
self.RADIX_TREE_MAX_PATH = int(
|
||||
math.ceil(self.RADIX_TREE_INDEX_BITS / float(self.CHUNK_SHIFT))
|
||||
)
|
||||
self.RADIX_TREE_HEIGHT_SHIFT = self.RADIX_TREE_MAX_PATH + 1
|
||||
self.RADIX_TREE_HEIGHT_MASK = (1 << self.RADIX_TREE_HEIGHT_SHIFT) - 1
|
||||
|
||||
if not self.vmlinux.has_type("radix_tree_root"):
|
||||
# In kernels 4.20, RADIX_TREE_INTERNAL_NODE flag took RADIX_TREE_EXCEPTIONAL_ENTRY's
|
||||
# value. RADIX_TREE_EXCEPTIONAL_ENTRY was removed but that's managed in is_valid_node()
|
||||
# Note that the Radix Tree is still in use for IDR, even after kernels 4.20 when XArray
|
||||
# mostly replace it
|
||||
self.RADIX_TREE_INTERNAL_NODE = 2
|
||||
|
||||
@property
|
||||
def node_type_name(self) -> str:
|
||||
return "radix_tree_node"
|
||||
|
||||
@property
|
||||
def tag_internal_value(self) -> int:
|
||||
return self.RADIX_TREE_INTERNAL_NODE
|
||||
|
||||
def get_tree_height(self, treep) -> int:
|
||||
with contextlib.suppress(exceptions.SymbolError):
|
||||
if self.vmlinux.get_type("radix_tree_root").has_member("height"):
|
||||
# kernels < 4.7.10
|
||||
radix_tree_root = self.vmlinux.object(
|
||||
"radix_tree_root", offset=treep, absolute=True
|
||||
)
|
||||
return radix_tree_root.height
|
||||
|
||||
# kernels >= 4.7.10
|
||||
return 0
|
||||
|
||||
def _radix_tree_maxindex(self, node, height) -> int:
|
||||
"""Return the maximum key which can be store into a radix tree with this height."""
|
||||
|
||||
if not self.vmlinux.has_symbol("height_to_maxindex"):
|
||||
# Kernels >= 4.7
|
||||
return (self.CHUNK_SIZE << node.shift) - 1
|
||||
else:
|
||||
# Kernels < 4.7
|
||||
height_to_maxindex_array = self.vmlinux.object_from_symbol(
|
||||
"height_to_maxindex"
|
||||
)
|
||||
maxindex = height_to_maxindex_array[height]
|
||||
return maxindex
|
||||
|
||||
def get_node_height(self, nodep) -> int:
|
||||
node = self.nodep_to_node(nodep)
|
||||
if hasattr(node, "shift"):
|
||||
# 4.7 <= Kernels < 4.20
|
||||
return (node.shift / self.CHUNK_SHIFT) + 1
|
||||
elif hasattr(node, "path"):
|
||||
# 3.15 <= Kernels < 4.7
|
||||
return node.path & self.RADIX_TREE_HEIGHT_MASK
|
||||
elif hasattr(node, "height"):
|
||||
# Kernels < 3.15
|
||||
return node.height
|
||||
else:
|
||||
raise exceptions.VolatilityException("Cannot find radix-tree node height")
|
||||
|
||||
def get_head_node(self, tree) -> int:
|
||||
return tree.rnode
|
||||
|
||||
def node_is_internal(self, nodep) -> bool:
|
||||
return (nodep & self.RADIX_TREE_INTERNAL_NODE) != 0
|
||||
|
||||
def is_node_tagged(self, nodep) -> bool:
|
||||
return self.node_is_internal(nodep)
|
||||
|
||||
def untag_node(self, nodep) -> int:
|
||||
return nodep & (~self.RADIX_TREE_ENTRY_MASK)
|
||||
|
||||
def is_valid_node(self, nodep) -> bool:
|
||||
# In kernels 4.20, exceptional nodes were removed and internal entries took their bitmask
|
||||
if self.vmlinux.has_type("radix_tree_root"):
|
||||
return (
|
||||
nodep & self.RADIX_TREE_ENTRY_MASK
|
||||
) != self.RADIX_TREE_EXCEPTIONAL_ENTRY
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class PageCache(object):
|
||||
"""Linux Page Cache abstraction"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: interfaces.context.ContextInterface,
|
||||
kernel_module_name: str,
|
||||
page_cache: interfaces.objects.ObjectInterface,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
context: interfaces.context.ContextInterface,
|
||||
kernel_module_name: The name of the kernel module on which to operate
|
||||
page_cache: Page cache address space
|
||||
"""
|
||||
self.vmlinux = context.modules[kernel_module_name]
|
||||
|
||||
self._page_cache = page_cache
|
||||
self._idstorage = IDStorage.choose_id_storage(context, kernel_module_name)
|
||||
|
||||
def get_cached_pages(self) -> interfaces.objects.ObjectInterface:
|
||||
"""Returns all page cache contents
|
||||
|
||||
Yields:
|
||||
Page objects
|
||||
"""
|
||||
|
||||
for page_addr in self._idstorage.get_entries(self._page_cache.i_pages):
|
||||
if not page_addr:
|
||||
continue
|
||||
|
||||
page = self.vmlinux.object("page", offset=page_addr, absolute=True)
|
||||
if page:
|
||||
yield page
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
|
||||
import collections.abc
|
||||
import logging
|
||||
import functools
|
||||
import binascii
|
||||
import stat
|
||||
from datetime import datetime
|
||||
import socket as socket_module
|
||||
from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union
|
||||
from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union, Dict
|
||||
|
||||
from volatility3.framework import constants, exceptions, objects, interfaces, symbols
|
||||
from volatility3.framework.renderers import conversion
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY
|
||||
from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS
|
||||
from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS
|
||||
@@ -47,7 +50,7 @@ class module(generic.GenericIntelProcess):
|
||||
).choices
|
||||
except exceptions.SymbolError:
|
||||
vollog.debug(
|
||||
f"Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4"
|
||||
"Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4"
|
||||
)
|
||||
# set to empty dict to show that the enum was not found, and so shouldn't be searched for again
|
||||
self._mod_mem_type = {}
|
||||
@@ -820,6 +823,26 @@ class dentry(objects.StructType):
|
||||
current_dentry = current_dentry.d_parent
|
||||
return None
|
||||
|
||||
def get_subdirs(self) -> interfaces.objects.ObjectInterface:
|
||||
"""Walks dentry subdirs
|
||||
|
||||
Yields:
|
||||
A dentry object
|
||||
"""
|
||||
if self.has_member("d_sib") and self.has_member("d_children"):
|
||||
# kernels >= 6.8
|
||||
walk_member = "d_sib"
|
||||
list_head_member = self.d_children.first
|
||||
elif self.has_member("d_child") and self.has_member("d_subdirs"):
|
||||
# 2.5.0 <= kernels < 6.8
|
||||
walk_member = "d_child"
|
||||
list_head_member = self.d_subdirs
|
||||
else:
|
||||
raise exceptions.VolatilityException("Unsupported dentry type")
|
||||
|
||||
dentry_type_name = self.get_symbol_table_name() + constants.BANG + "dentry"
|
||||
yield from list_head_member.to_list(dentry_type_name, walk_member)
|
||||
|
||||
|
||||
class struct_file(objects.StructType):
|
||||
def get_dentry(self) -> interfaces.objects.ObjectInterface:
|
||||
@@ -936,7 +959,8 @@ class mount(objects.StructType):
|
||||
MNT_RELATIME: "relatime",
|
||||
}
|
||||
|
||||
def get_mnt_sb(self):
|
||||
def get_mnt_sb(self) -> int:
|
||||
"""Returns a pointer to the super_block"""
|
||||
if self.has_member("mnt"):
|
||||
return self.mnt.mnt_sb
|
||||
elif self.has_member("mnt_sb"):
|
||||
@@ -1251,6 +1275,7 @@ class vfsmount(objects.StructType):
|
||||
return self._get_real_mnt().has_parent()
|
||||
|
||||
def get_mnt_sb(self):
|
||||
"""Returns a pointer to the super_block"""
|
||||
return self.mnt_sb
|
||||
|
||||
def get_flags_access(self) -> str:
|
||||
@@ -1612,20 +1637,72 @@ class xdp_sock(objects.StructType):
|
||||
|
||||
|
||||
class bpf_prog(objects.StructType):
|
||||
def get_type(self):
|
||||
def _get_vmlinux(self):
|
||||
linuxutils_required_version = (2, 1, 1)
|
||||
linuxutils_current_version = linux.LinuxUtilities._version
|
||||
if not requirements.VersionRequirement.matches_required(
|
||||
linuxutils_required_version, linuxutils_current_version
|
||||
):
|
||||
raise exceptions.PluginRequirementException(
|
||||
f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}"
|
||||
)
|
||||
|
||||
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
|
||||
return vmlinux
|
||||
|
||||
def get_type(self) -> Union[str, None]:
|
||||
"""Returns a string with the eBPF program type"""
|
||||
|
||||
# The program type was in `bpf_prog_aux::prog_type` from 3.18.140 to
|
||||
# 4.1.52 before it was moved to `bpf_prog::type`
|
||||
if self.has_member("type"):
|
||||
# kernel >= 4.1.52
|
||||
return self.type
|
||||
return self.type.description
|
||||
|
||||
if self.has_member("aux") and self.aux:
|
||||
if self.aux.has_member("prog_type"):
|
||||
# 3.18.140 <= kernel < 4.1.52
|
||||
return self.aux.prog_type
|
||||
return self.aux.prog_type.description
|
||||
|
||||
# kernel < 3.18.140
|
||||
raise AttributeError("Unable to find the BPF type")
|
||||
return None
|
||||
|
||||
def get_tag(self) -> Union[str, None]:
|
||||
"""Returns a string with the eBPF program tag"""
|
||||
# 'tag' was added in kernels 4.10
|
||||
if not self.has_member("tag"):
|
||||
return None
|
||||
|
||||
vmlinux = self._get_vmlinux()
|
||||
vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name]
|
||||
|
||||
prog_tag_addr = self.tag.vol.offset
|
||||
prog_tag_size = self.tag.count
|
||||
prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size)
|
||||
|
||||
prog_tag = binascii.hexlify(prog_tag_bytes).decode()
|
||||
return prog_tag
|
||||
|
||||
def get_name(self) -> Union[str, None]:
|
||||
"""Returns a string with the eBPF program name"""
|
||||
if not self.has_member("aux"):
|
||||
# 'prog_aux' was added in kernels 3.18
|
||||
return None
|
||||
|
||||
return self.aux.get_name()
|
||||
|
||||
|
||||
class bpf_prog_aux(objects.StructType):
|
||||
def get_name(self) -> Union[str, None]:
|
||||
"""Returns a string with the eBPF program name"""
|
||||
if not self.has_member("name"):
|
||||
# 'name' was added in kernels 4.15
|
||||
return None
|
||||
|
||||
if not self.name:
|
||||
return None
|
||||
|
||||
return utility.array_to_string(self.name)
|
||||
|
||||
|
||||
class cred(objects.StructType):
|
||||
@@ -1926,6 +2003,279 @@ class inode(objects.StructType):
|
||||
"""
|
||||
return stat.filemode(self.i_mode)
|
||||
|
||||
def get_pages(self) -> interfaces.objects.ObjectInterface:
|
||||
"""Gets the inode's cached pages
|
||||
|
||||
Yields:
|
||||
The inode's cached pages
|
||||
"""
|
||||
if not self.i_size:
|
||||
return
|
||||
elif not (self.i_mapping and self.i_mapping.nrpages > 0):
|
||||
return
|
||||
|
||||
page_cache = linux.PageCache(
|
||||
context=self._context,
|
||||
kernel_module_name="kernel",
|
||||
page_cache=self.i_mapping.dereference(),
|
||||
)
|
||||
yield from page_cache.get_cached_pages()
|
||||
|
||||
def get_contents(self):
|
||||
"""Get the inode cached pages from the page cache
|
||||
|
||||
Yields:
|
||||
page_index (int): The page index in the Tree. File offset is page_index * PAGE_SIZE.
|
||||
page_content (str): The page content
|
||||
"""
|
||||
for page_obj in self.get_pages():
|
||||
page_index = int(page_obj.index)
|
||||
page_content = page_obj.get_content()
|
||||
yield page_index, page_content
|
||||
|
||||
|
||||
class address_space(objects.StructType):
|
||||
@property
|
||||
def i_pages(self):
|
||||
"""Returns the appropriate member containing the page cache tree"""
|
||||
if self.has_member("i_pages"):
|
||||
# Kernel >= 4.17
|
||||
return self.member("i_pages")
|
||||
elif self.has_member("page_tree"):
|
||||
# Kernel < 4.17
|
||||
return self.member("page_tree")
|
||||
|
||||
raise exceptions.VolatilityException("Unsupported page cache tree")
|
||||
|
||||
|
||||
class page(objects.StructType):
|
||||
@property
|
||||
@functools.lru_cache()
|
||||
def pageflags_enum(self) -> Dict:
|
||||
"""Returns 'pageflags' enumeration key/values
|
||||
|
||||
Returns:
|
||||
A dictionary with the pageflags enumeration key/values
|
||||
"""
|
||||
# FIXME: It would be even better to use @functools.cached_property instead,
|
||||
# however, this requires Python +3.8
|
||||
try:
|
||||
pageflags_enum = self._context.symbol_space.get_enumeration(
|
||||
self.get_symbol_table_name() + constants.BANG + "pageflags"
|
||||
).choices
|
||||
except exceptions.SymbolError:
|
||||
vollog.debug(
|
||||
"Unable to find pageflags enum. This can happen in kernels < 2.6.26 or wrong ISF"
|
||||
)
|
||||
# set to empty dict to show that the enum was not found, and so shouldn't be searched for again
|
||||
pageflags_enum = {}
|
||||
|
||||
return pageflags_enum
|
||||
|
||||
def get_flags_list(self) -> List[str]:
|
||||
"""Returns a list of page flags
|
||||
|
||||
Returns:
|
||||
List of page flags
|
||||
"""
|
||||
flags = []
|
||||
for name, value in self.pageflags_enum.items():
|
||||
if self.flags & (1 << value) != 0:
|
||||
flags.append(name)
|
||||
|
||||
return flags
|
||||
|
||||
def _get_vmlinux(self):
|
||||
linuxutils_required_version = (2, 1, 1)
|
||||
linuxutils_current_version = linux.LinuxUtilities._version
|
||||
if not requirements.VersionRequirement.matches_required(
|
||||
linuxutils_required_version, linuxutils_current_version
|
||||
):
|
||||
raise exceptions.PluginRequirementException(
|
||||
f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}"
|
||||
)
|
||||
|
||||
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
|
||||
return vmlinux
|
||||
|
||||
def to_paddr(self) -> int:
|
||||
"""Converts a page's virtual address to its physical address using the current physical memory model.
|
||||
|
||||
Returns:
|
||||
int: page physical address
|
||||
"""
|
||||
vmlinux = self._get_vmlinux()
|
||||
vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name]
|
||||
|
||||
vmemmap_start = None
|
||||
if vmlinux.has_symbol("mem_section"):
|
||||
# SPARSEMEM_VMEMMAP physical memory model: memmap is virtually contiguous
|
||||
if vmlinux.has_symbol("vmemmap_base"):
|
||||
# CONFIG_DYNAMIC_MEMORY_LAYOUT - KASLR kernels >= 4.9
|
||||
vmemmap_start = vmlinux.object_from_symbol("vmemmap_base")
|
||||
else:
|
||||
# !CONFIG_DYNAMIC_MEMORY_LAYOUT
|
||||
if vmlinux_layer._maxvirtaddr < 57:
|
||||
# 4-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L4
|
||||
vmemmap_base_l4 = 0xFFFFEA0000000000
|
||||
vmemmap_start = vmemmap_base_l4
|
||||
else:
|
||||
# 5-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L5
|
||||
# FIXME: Once 5-level paging is supported, uncomment the following lines and remove the exception
|
||||
# vmemmap_base_l5 = 0xFFD4000000000000
|
||||
# vmemmap_start = vmemmap_base_l5
|
||||
raise exceptions.VolatilityException(
|
||||
"5-level paging is not yet supported"
|
||||
)
|
||||
|
||||
elif vmlinux.has_symbol("mem_map"):
|
||||
# FLATMEM physical memory model, typically 32bit
|
||||
vmemmap_start = vmlinux.object_from_symbol("mem_map")
|
||||
|
||||
elif vmlinux.has_symbol("node_data"):
|
||||
raise exceptions.VolatilityException("NUMA systems are not yet supported")
|
||||
else:
|
||||
raise exceptions.VolatilityException("Unsupported Linux memory model")
|
||||
|
||||
if not vmemmap_start:
|
||||
raise exceptions.VolatilityException(
|
||||
"Something went wrong, we shouldn't be here"
|
||||
)
|
||||
|
||||
page_type_size = vmlinux.get_type("page").size
|
||||
pagec = vmlinux_layer.canonicalize(self.vol.offset)
|
||||
pfn = (pagec - vmemmap_start) // page_type_size
|
||||
page_paddr = pfn * vmlinux_layer.page_size
|
||||
|
||||
return page_paddr
|
||||
|
||||
def get_content(self) -> Union[str, None]:
|
||||
"""Returns the page content
|
||||
|
||||
Returns:
|
||||
The page content
|
||||
"""
|
||||
vmlinux = self._get_vmlinux()
|
||||
vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name]
|
||||
physical_layer = vmlinux.context.layers["memory_layer"]
|
||||
page_paddr = self.to_paddr()
|
||||
if not page_paddr:
|
||||
return None
|
||||
|
||||
page_data = physical_layer.read(page_paddr, vmlinux_layer.page_size)
|
||||
return page_data
|
||||
|
||||
|
||||
class IDR(objects.StructType):
|
||||
IDR_BITS = 8
|
||||
IDR_MASK = (1 << IDR_BITS) - 1
|
||||
INT_SIZE = 4
|
||||
MAX_IDR_SHIFT = INT_SIZE * 8 - 1
|
||||
MAX_IDR_BIT = 1 << MAX_IDR_SHIFT
|
||||
|
||||
def _get_vmlinux(self):
|
||||
linuxutils_required_version = (2, 1, 1)
|
||||
linuxutils_current_version = linux.LinuxUtilities._version
|
||||
if not requirements.VersionRequirement.matches_required(
|
||||
linuxutils_required_version, linuxutils_current_version
|
||||
):
|
||||
raise exceptions.PluginRequirementException(
|
||||
f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}"
|
||||
)
|
||||
|
||||
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
|
||||
return vmlinux
|
||||
|
||||
def idr_max(self, num_layers: int) -> int:
|
||||
"""Returns the maximum ID which can be allocated given idr::layers
|
||||
|
||||
Args:
|
||||
num_layers: Number of layers
|
||||
|
||||
Returns:
|
||||
Maximum ID for a given number of layers
|
||||
"""
|
||||
# Kernel < 4.17
|
||||
bits = min([self.INT_SIZE, num_layers * self.IDR_BITS, self.MAX_IDR_SHIFT])
|
||||
|
||||
return (1 << bits) - 1
|
||||
|
||||
def idr_find(self, idr_id: int) -> int:
|
||||
"""Finds an ID within the IDR data structure.
|
||||
Based on idr_find_slowpath(), 3.9 <= Kernel < 4.11
|
||||
Args:
|
||||
idr_id: The IDR lookup ID
|
||||
|
||||
Returns:
|
||||
A pointer to the given ID element
|
||||
"""
|
||||
vmlinux = self._get_vmlinux()
|
||||
if not vmlinux.get_type("idr_layer").has_member("layer"):
|
||||
vollog.info(
|
||||
"Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6"
|
||||
)
|
||||
return None
|
||||
|
||||
if idr_id < 0:
|
||||
return None
|
||||
|
||||
idr_layer = self.top
|
||||
if not idr_layer:
|
||||
return None
|
||||
|
||||
n = (idr_layer.layer + 1) * self.IDR_BITS
|
||||
|
||||
if idr_id > self.idr_max(idr_layer.layer + 1):
|
||||
return None
|
||||
|
||||
assert n != 0
|
||||
|
||||
while n > 0 and idr_layer:
|
||||
n -= self.IDR_BITS
|
||||
assert n == idr_layer.layer * self.IDR_BITS
|
||||
idr_layer = idr_layer.ary[(idr_id >> n) & self.IDR_MASK]
|
||||
|
||||
return idr_layer
|
||||
|
||||
def _old_kernel_get_entries(self) -> int:
|
||||
# Kernels < 4.11
|
||||
cur = self.cur
|
||||
total = next_id = 0
|
||||
while next_id < cur:
|
||||
entry = self.idr_find(next_id)
|
||||
if entry:
|
||||
yield entry
|
||||
total += 1
|
||||
|
||||
next_id += 1
|
||||
|
||||
def _new_kernel_get_entries(self) -> int:
|
||||
# Kernels >= 4.11
|
||||
id_storage = linux.IDStorage.choose_id_storage(
|
||||
self._context, kernel_module_name="kernel"
|
||||
)
|
||||
for page_addr in id_storage.get_entries(root=self.idr_rt):
|
||||
yield page_addr
|
||||
|
||||
def get_entries(self) -> int:
|
||||
"""Walks the IDR and yield a pointer associated with each element.
|
||||
|
||||
Args:
|
||||
in_use (int, optional): _description_. Defaults to 0.
|
||||
|
||||
Yields:
|
||||
A pointer associated with each element.
|
||||
"""
|
||||
if self.has_member("idr_rt"):
|
||||
# Kernels >= 4.11
|
||||
get_entries_func = self._new_kernel_get_entries
|
||||
else:
|
||||
# Kernels < 4.11
|
||||
get_entries_func = self._old_kernel_get_entries
|
||||
|
||||
for page_addr in get_entries_func():
|
||||
yield page_addr
|
||||
|
||||
|
||||
class rb_root(objects.StructType):
|
||||
def _walk_nodes(self, root_node) -> Iterator[int]:
|
||||
|
||||
Reference in New Issue
Block a user