Merge branch 'develop' into linux_parity_release_harden_mountinfo_api_and_related_fixes

This commit is contained in:
Gustavo Moreira
2025-01-30 08:39:44 +11:00
committed by GitHub
58 changed files with 1575 additions and 315 deletions
+50
View File
@@ -0,0 +1,50 @@
name: build-pyinstaller
on:
push:
branches:
- stable
- develop
- 'release/**'
pull_request:
branches:
- stable
- 'release/**'
jobs:
exe:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller
- name: Pyinstall executable
run: |
pyinstaller --clean -y vol.spec
pyinstaller --clean -y volshell.spec
- name: Move files
run: |
mv dist/vol.exe vol.exe
mv dist/volshell.exe volshell.exe
- name: Archive
uses: actions/upload-artifact@v4
with:
name: volatility3-pyinstaller
path: |
vol.exe
volshell.exe
README.md
LICENSE.txt
+1 -1
View File
@@ -88,7 +88,7 @@ The latest generated copy of the documentation can be found at: <https://volatil
## Licensing and Copyright
Copyright (C) 2007-2024 Volatility Foundation
Copyright (C) 2007-2025 Volatility Foundation
All Rights Reserved
+1 -1
View File
@@ -167,7 +167,7 @@ master_doc = "index"
# General information about the project.
project = "Volatility 3"
copyright = "2012-2024, Volatility Foundation"
copyright = "2012-2025, Volatility Foundation"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
+21 -18
View File
@@ -1,7 +1,15 @@
[project]
name = "volatility3"
description = "Memory forensics framework"
keywords = ["volatility", "memory", "forensics", "framework", "windows", "linux", "volshell"]
keywords = [
"volatility",
"memory",
"forensics",
"framework",
"windows",
"linux",
"volshell",
]
readme = "README.md"
authors = [
{ name = "Volatility Foundation", email = "volatility@volatilityfoundation.org" },
@@ -10,9 +18,7 @@ requires-python = ">=3.8.0"
license = { text = "VSL" }
dynamic = ["version"]
dependencies = [
"pefile>=2024.8.26",
]
dependencies = ["pefile>=2024.8.26"]
[project.optional-dependencies]
full = [
@@ -26,15 +32,12 @@ full = [
"pillow>=10.0.0,<11.0.0",
]
cloud = [
"gcsfs>=2024.10.0",
"s3fs>=2024.10.0",
]
cloud = ["gcsfs>=2024.10.0", "s3fs>=2024.10.0"]
dev = [
"volatility3[full,cloud]",
"jsonschema>=4.23.0,<5",
"pyinstaller>=6.11.0,<7",
"pyinstaller>=6.5.0,<7",
"pyinstaller-hooks-contrib>=2024.9",
"types-jsonschema>=4.23.0,<5",
]
@@ -48,8 +51,8 @@ test = [
docs = [
"volatility3[dev]",
"sphinx>=8.0.0,<7",
"sphinx-autodoc-typehints>=2.5.0,<3",
"sphinx>=4.0.0,<9",
"sphinx-autodoc-typehints>=2.0.0,<3",
"sphinx-rtd-theme>=3.0.1,<4",
]
@@ -79,16 +82,16 @@ target-version = "py38"
[tool.ruff.lint]
select = [
"F", # pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"G", # flake8-logging-format
"PIE", # flake8-pie
"UP", # pyupgrade
"F", # pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"G", # flake8-logging-format
"PIE", # flake8-pie
"UP", # pyupgrade
]
ignore = [
"E501", # ignore due to conflict with formatter
"E501", # ignore due to conflict with formatter
]
[build-system]
+19 -6
View File
@@ -708,6 +708,24 @@ def test_linux_page_cache_inodepages(image, volatility, python):
inode_address = hex(0x88001AB5C270)
inode_dump_filename = f"inode_{inode_address}.dmp"
rc, out, _err = runvol_plugin(
"linux.pagecache.InodePages",
image,
volatility,
python,
pluginargs=["--inode", inode_address],
)
assert rc == 0
assert out.count(b"\n") > 4
# PageVAddr PagePAddr MappingAddr .. DumpSafe
assert re.search(
rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
out,
)
try:
rc, out, _err = runvol_plugin(
"linux.pagecache.InodePages",
@@ -718,13 +736,8 @@ def test_linux_page_cache_inodepages(image, volatility, python):
)
assert rc == 0
assert out.count(b"\n") > 4
assert out.count(b"\n") >= 4
# PageVAddr PagePAddr MappingAddr .. DumpSafe
assert re.search(
rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
out,
)
assert os.path.exists(inode_dump_filename)
with open(inode_dump_filename, "rb") as fp:
inode_contents = fp.read()
+24 -10
View File
@@ -363,11 +363,21 @@ class CommandLine:
metavar="PLUGIN",
)
for plugin in sorted(plugin_list):
# First line of a plugin docstring will be the short description for -h.
# Text after the first two consecutive new lines will be
# the additional description (argparse epilog).
short_help = additional_help = None
if plugin_list[plugin].__doc__ is not None:
doc_split = plugin_list[plugin].__doc__.split("\n\n", 1)
short_help = doc_split[0].strip()
if len(doc_split) > 1:
additional_help = doc_split[1].strip()
plugin_parser = subparser.add_parser(
plugin,
help=plugin_list[plugin].__doc__,
description=plugin_list[plugin].__doc__,
epilog=plugin_list[plugin].additional_description,
help=short_help,
description=short_help,
epilog=additional_help,
)
self.populate_requirements_argparse(plugin_parser, plugin_list[plugin])
@@ -573,6 +583,8 @@ class CommandLine:
fulltrace = traceback.TracebackException.from_exception(excp).format(chain=True)
vollog.debug("".join(fulltrace))
file_a_bug_msg = f"Please re-run with -vvv and file a bug with the output at {constants.BUG_URL}"
if isinstance(excp, exceptions.InvalidAddressException):
general = "Volatility was unable to read a requested page:"
if isinstance(excp, exceptions.SwappedInvalidAddressException):
@@ -617,9 +629,7 @@ class CommandLine:
elif isinstance(excp, exceptions.LayerException):
general = f"Volatility experienced a layer-related issue: {excp.layer_name}"
detail = f"{excp}"
caused_by = [
"A faulty layer implementation (re-run with -vvv and file a bug)"
]
caused_by = [f"A faulty layer implementation. {file_a_bug_msg}"]
elif isinstance(excp, exceptions.MissingModuleException):
general = f"Volatility could not import a necessary module: {excp.module}"
detail = f"{excp}"
@@ -630,13 +640,17 @@ class CommandLine:
general = "Volatility experienced an issue when rendering the output:"
detail = f"{excp}"
caused_by = ["An invalid renderer option, such as no visible columns"]
elif isinstance(excp, exceptions.VersionMismatchException):
general = "A version mismatch was detected between two components:"
detail = f"{excp}"
caused_by = [
excp.failure_reason or "An outdated API caller, such as a method.",
file_a_bug_msg,
]
else:
general = "Volatility encountered an unexpected situation."
detail = ""
caused_by = [
"Please re-run using with -vvv and file a bug with the output",
f"at {constants.BUG_URL}",
]
caused_by = [file_a_bug_msg]
# Code that actually renders the exception
output = sys.stderr
+77
View File
@@ -3,6 +3,7 @@
#
from typing import Any, List, Optional, Tuple, Union
from enum import Enum
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -10,6 +11,16 @@ from volatility3.framework.configuration import requirements
from volatility3.plugins.linux import pslist
# Could import the enum from psscan.py to avoid code duplication
class DescExitStateEnum(Enum):
"""Enum for linux task exit_state as defined in include/linux/sched.h"""
TASK_RUNNING = 0x00000000
EXIT_DEAD = 0x00000010
EXIT_ZOMBIE = 0x00000020
EXIT_TRACE = EXIT_ZOMBIE | EXIT_DEAD
class Volshell(generic.Volshell):
"""Shell environment to directly interact with a linux memory image."""
@@ -40,6 +51,71 @@ class Volshell(generic.Volshell):
return None
print(f"No task with task ID {pid} found")
def get_process(self, pid=None, virtaddr=None, physaddr=None):
"""Return the task_struct object that matches the pid. If a physical or a virtual address is provided, construct the task_struct object at said address. Only one parameter is allowed.
Args:
pid (int, optional): PID to search for
virtaddr (int, optional): Virtual address to construct object at
physaddr (int, optional): Physical address to construct object at
Returns:
ObjectInterface: task_struct Object
"""
if sum(1 if x is not None else 0 for x in [pid, virtaddr, physaddr]) != 1:
print("Only one parameter is accepted")
return None
vmlinux_module_name = self.config["kernel"]
vmlinux = self.context.modules[vmlinux_module_name]
kernel_layer_name = vmlinux.layer_name
kernel_layer = self.context.layers[kernel_layer_name]
memory_layer_name = kernel_layer.dependencies[0]
task_struct_symbol = vmlinux.symbol_table_name + constants.BANG + "task_struct"
if virtaddr is not None:
task = self.context.object(
task_struct_symbol,
layer_name=kernel_layer_name,
offset=virtaddr,
)
if physaddr is not None:
task = self.context.object(
task_struct_symbol,
layer_name=memory_layer_name,
offset=physaddr,
native_layer_name=kernel_layer_name,
)
if physaddr is not None or virtaddr is not None:
try:
DescExitStateEnum(task.exit_state)
except ValueError:
print(
f"task_struct @ {hex(task.vol.offset)} as exit_state {task.exit_state} is likely not valid"
)
if not (0 < task.pid < 65535):
print(
f"task_struct @ {hex(task.vol.offset)} as pid {task.pid} is likely not valid"
)
return task
if pid is not None:
tasks = self.list_tasks()
for task in tasks:
if task.pid == pid:
return task
print(f"No task with task ID {pid} found")
return None
def list_tasks(self):
"""Returns a list of task objects from the primary layer"""
# We always use the main kernel memory and associated symbols
@@ -50,6 +126,7 @@ class Volshell(generic.Volshell):
result += [
(["ct", "change_task", "cp"], self.change_task),
(["lt", "list_tasks", "ps"], self.list_tasks),
(["gp", "get_process", "get_task"], self.get_process),
(["symbols"], self.context.symbol_space[self.current_symbol_table]),
]
if self.config.get("pid", None) is not None:
+56
View File
@@ -44,11 +44,67 @@ class Volshell(generic.Volshell):
)
)
def get_process(self, pid=None, virtaddr=None, physaddr=None):
"""Returns the _EPROCESS object that matches the pid. If a physical or a virtual address is provided, construct the _EPROCESS object at said address. Only one parameter is allowed.
Args:
pid (int, optional): PID / UniqueProcessId to search for.
virtaddr (int, optional): Virtual address to construct object at
physaddr (int, optional): Physical address to construct object at
Returns:
ObjectInterface: _EPROCESS Object
"""
if sum(1 if x is not None else 0 for x in [pid, virtaddr, physaddr]) != 1:
print("Only one parameter is accepted")
return None
kernel_name = self.config["kernel"]
kernel = self.context.modules[kernel_name]
kernel_layer_name = kernel.layer_name
kernel_layer = self.context.layers[kernel_layer_name]
memory_layer_name = kernel_layer.dependencies[0]
eprocess_symbol = kernel.symbol_table_name + constants.BANG + "_EPROCESS"
if virtaddr is not None:
eproc = self.context.object(
eprocess_symbol,
layer_name=kernel_layer_name,
offset=virtaddr,
)
return eproc
if physaddr is not None:
eproc = self.context.object(
eprocess_symbol,
layer_name=memory_layer_name,
offset=physaddr,
native_layer_name=kernel_layer_name,
)
return eproc
if pid is not None:
processes = self.list_processes()
for process in processes:
if process.UniqueProcessId == pid:
return process
print(f"No process with process ID {pid} found")
return None
return None
def construct_locals(self) -> List[Tuple[List[str], Any]]:
result = super().construct_locals()
result += [
(["cp", "change_process"], self.change_process),
(["lp", "list_processes", "ps"], self.list_processes),
(["gp", "get_process"], self.get_process),
(["symbols"], self.context.symbol_space[self.current_symbol_table]),
]
if self.config.get("pid", None) is not None:
+72 -3
View File
@@ -11,10 +11,24 @@ import inspect
import logging
import os
import traceback
from typing import Any, Dict, Generator, List, Optional, Tuple, Type, TypeVar
import functools
import warnings
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, TypeVar
from volatility3.framework import constants, interfaces
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
if (
sys.version_info.major != constants.REQUIRED_PYTHON_VERSION[0]
or sys.version_info.minor < constants.REQUIRED_PYTHON_VERSION[1]
or (
sys.version_info.minor == constants.REQUIRED_PYTHON_VERSION[1]
and sys.version_info.micro < constants.REQUIRED_PYTHON_VERSION[2]
)
):
raise RuntimeError(
f"Volatility framework requires python version {'.'.join(str(x) for x in constants.REQUIRED_PYTHON_VERSION)} or greater"
)
# ##
#
@@ -52,12 +66,67 @@ def require_interface_version(*args) -> None:
)
class Deprecation:
"""Deprecation related methods."""
@staticmethod
def deprecated_method(
replacement: Callable,
replacement_version: Tuple[int, int, int] = None,
additional_information: str = "",
):
"""A decorator for marking functions as deprecated.
Args:
replacement: The replacement function overriding the deprecated API, in the form of a Callable (typically a method)
replacement_version: The "replacement" base class version that the deprecated method expects before proxying to it. This implies that "replacement" is a method from a class that inherits from VersionableInterface.
additional_information: Information appended at the end of the deprecation message
"""
def decorator(deprecated_func):
@functools.wraps(deprecated_func)
def wrapper(*args, **kwargs):
nonlocal replacement, replacement_version, additional_information
# Prevent version mismatches between deprecated (proxy) methods and the ones they proxy
if (
replacement_version is not None
and callable(replacement)
and hasattr(replacement, "__self__")
):
replacement_base_class = replacement.__self__
# Verify that the base class inherits from VersionableInterface
if inspect.isclass(replacement_base_class) and issubclass(
replacement_base_class,
interfaces.configuration.VersionableInterface,
):
# SemVer check
if not requirements.VersionRequirement.matches_required(
replacement_version, replacement_base_class.version
):
raise exceptions.VersionMismatchException(
deprecated_func,
replacement_base_class,
replacement_version,
"This is a bug, the deprecated call needs to be removed and the caller needs to update their code to use the new method.",
)
deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__qualname__}\" is deprecated, use \"{replacement.__module__ + '.' + replacement.__qualname__}\" instead. {additional_information}"
warnings.warn(deprecation_msg, FutureWarning)
# Return the wrapped function with its original arguments
return deprecated_func(*args, **kwargs)
return wrapper
return decorator
class NonInheritable:
def __init__(self, value: Any, cls: Type) -> None:
self.default_value = value
self.cls = cls
def __get__(self, obj: Any, get_type: Optional[Type] = None) -> Any:
def __get__(self, obj: Any, get_type: Type = Optional[None]) -> Any:
if type is self.cls:
if hasattr(self.default_value, "__get__"):
return self.default_value.__get__(obj, get_type)
+5
View File
@@ -71,6 +71,11 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
elif "init_level4_pgt" in table.symbols:
layer_class = intel.LinuxIntel32e
dtb_symbol_name = "init_level4_pgt"
elif "pkmap_count" in table.symbols and table.get_symbol(
"pkmap_count"
).type.count in (512, 2048):
layer_class = intel.LinuxIntelPAE
dtb_symbol_name = "swapper_pg_dir"
else:
layer_class = intel.LinuxIntel
dtb_symbol_name = "swapper_pg_dir"
@@ -376,8 +376,74 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
valid_kernel = (virtual_layer_name, address, res[0])
return valid_kernel
def method_low_stub_offset(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
# This method is only valid for x64 systems
if not isinstance(vlayer, intel.Intel32e):
return None
kernel_hint = 0
kernel_base = 0
physical_layer = context.layers.get("memory_layer")
# Try locating kernel base via x64 Low Stub in lower 1MB starting from second page (4KB)
# If "Discard Low Memory" setting is disabled in BIOS, the Low Stub may be at the third/fourth or further pages
for offset in range(0x1000, 0x100000, 0x1000):
try:
jmp_and_completion_values = int.from_bytes(
physical_layer.read(offset, 0x8), "little"
)
if (
0xFFFFFFFFFFFF00FF & jmp_and_completion_values
!= constants.windows.JMP_AND_COMPLETION_SIGNATURE
):
continue
cr3_value = int.from_bytes(
physical_layer.read(
offset + constants.windows.PROCESSOR_START_BLOCK_CR3_OFFSET, 0x8
),
"little",
)
# Compare previously observed valid page table address that's stored in vlayer._initial_entry
# with PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3
# which was observed to be an invalid page address, so add 1 (to make it valid too)
if (cr3_value + 1) != vlayer._initial_entry:
continue
potential_kernel_hint = int.from_bytes(
physical_layer.read(
offset
+ constants.windows.PROCESSOR_START_BLOCK_LM_TARGET_OFFSET,
0x8,
),
"little",
)
if 0x3 & potential_kernel_hint:
continue
kernel_hint = potential_kernel_hint & 0xFFFFFFFFFFFF
kernel_base = kernel_hint & (~0x1FFFFF) & 0xFFFFFFFFFFFF
break
except exceptions.InvalidAddressException:
continue
if kernel_base:
# Scanning 32mb in 2mb chunks for the 'ntoskrnl' base address
while (kernel_base + 0x2000000) > kernel_hint:
for i in range(0, 0x200000, 0x1000):
valid_kernel = self.check_kernel_offset(
context, vlayer, kernel_base, progress_callback
)
if valid_kernel:
return valid_kernel
kernel_base -= 0x200000
return None
# List of methods to be run, in order, to determine the valid kernels
methods = [
method_low_stub_offset,
method_kdbg_offset,
method_module_offset,
method_fixed_mapping,
@@ -1,14 +0,0 @@
import sys
required_python_version = (3, 8, 0)
if (
sys.version_info.major != required_python_version[0]
or sys.version_info.minor < required_python_version[1]
or (
sys.version_info.minor == required_python_version[1]
and sys.version_info.micro < required_python_version[2]
)
):
raise RuntimeError(
f"Volatility framework requires python version {required_python_version[0]}.{required_python_version[1]}.{required_python_version[2]} or greater"
)
@@ -23,6 +23,8 @@ from volatility3.framework.constants._version import (
VERSION_SUFFIX as VERSION_SUFFIX,
)
REQUIRED_PYTHON_VERSION = (3, 8, 0)
PLUGINS_PATH = [
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "plugins")),
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "plugins")),
+1 -1
View File
@@ -1,6 +1,6 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 16 # Number of changes that only add to the interface
VERSION_MINOR = 19 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
@@ -6,6 +6,7 @@
Linux-specific values that aren't found in debug symbols
"""
from enum import IntEnum, Flag
from dataclasses import dataclass
KERNEL_NAME = "__kernel__"
@@ -352,3 +353,57 @@ NSEC_PER_SEC = 1e9
MODULE_MAXIMUM_CORE_SIZE = 20000000
MODULE_MAXIMUM_CORE_TEXT_SIZE = 20000000
MODULE_MINIMUM_SIZE = 4096
@dataclass
class TaintFlag:
shift: int
desc: str
when_present: bool
module: bool
TAINT_FLAGS = {
"P": TaintFlag(
shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=True, module=True
),
"G": TaintFlag(
shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=False, module=True
),
"F": TaintFlag(shift=1 << 1, desc="FORCED_MODULE", when_present=True, module=False),
"S": TaintFlag(
shift=1 << 2, desc="CPU_OUT_OF_SPEC", when_present=True, module=False
),
"R": TaintFlag(shift=1 << 3, desc="FORCED_RMMOD", when_present=True, module=False),
"M": TaintFlag(shift=1 << 4, desc="MACHINE_CHECK", when_present=True, module=False),
"B": TaintFlag(shift=1 << 5, desc="BAD_PAGE", when_present=True, module=False),
"U": TaintFlag(shift=1 << 6, desc="USER", when_present=True, module=False),
"D": TaintFlag(shift=1 << 7, desc="DIE", when_present=True, module=False),
"A": TaintFlag(
shift=1 << 8, desc="OVERRIDDEN_ACPI_TABLE", when_present=True, module=False
),
"W": TaintFlag(shift=1 << 9, desc="WARN", when_present=True, module=False),
"C": TaintFlag(shift=1 << 10, desc="CRAP", when_present=True, module=True),
"I": TaintFlag(
shift=1 << 11, desc="FIRMWARE_WORKAROUND", when_present=True, module=False
),
"O": TaintFlag(shift=1 << 12, desc="OOT_MODULE", when_present=True, module=True),
"E": TaintFlag(
shift=1 << 13, desc="UNSIGNED_MODULE", when_present=True, module=True
),
"L": TaintFlag(shift=1 << 14, desc="SOFTLOCKUP", when_present=True, module=False),
"K": TaintFlag(shift=1 << 15, desc="LIVEPATCH", when_present=True, module=True),
"X": TaintFlag(shift=1 << 16, desc="AUX", when_present=True, module=True),
"T": TaintFlag(shift=1 << 17, desc="RANDSTRUCT", when_present=True, module=True),
"N": TaintFlag(shift=1 << 18, desc="TEST", when_present=True, module=True),
}
"""Flags used to taint kernel and modules, for debugging purposes.
Map based on 6.12-rc5.
Documentation :
- https://www.kernel.org/doc/Documentation/admin-guide/sysctl/kernel.rst#:~:text=guide/sysrq.rst.-,tainted,-%3D%3D%3D%3D%3D%3D%3D%0A%0ANon%2Dzero%20if
- https://www.kernel.org/doc/Documentation/admin-guide/tainted-kernels.rst#:~:text=More%20detailed%20explanation%20for%20tainting
- taint_flag kernel struct
- taint_flags kernel constant
"""
@@ -10,3 +10,21 @@ KERNEL_MODULE_NAMES = ["ntkrnlmp", "ntkrnlpa", "ntkrpamp", "ntoskrnl"]
"""The list of names that kernel modules can have within the windows OS"""
PE_MAX_EXTRACTION_SIZE = 1024 * 1024 * 256
"""
The following constants represent the layout of the Low Stub which exists only on x64 machines with no virtualization/emulation,
responsible for transitioning from Real Mode(16 bit) to Protected Mode(32 bit) and Long Mode(64 bit) on boot/return from sleep.
Contains offsets to fields and structures within the undocumented structure _PROCESSOR_START_BLOCK.
Here's a reference: https://github.com/mic101/windows/blob/master/WRK-v1.2/base/ntos/inc/amd64.h#L3334
"""
# Expected signature for validation, constructed from:
# PROCESSOR_START_BLOCK->Jmp->OpCode | PROCESSOR_START_BLOCK->Jmp->Offset | PROCESSOR_START_BLOCK->CompletionFlag
JMP_AND_COMPLETION_SIGNATURE = 0x00000001000600E9
# Address of LmTarget (Long Mode target)
PROCESSOR_START_BLOCK_LM_TARGET_OFFSET = (
0x70 # PROCESSOR_START_BLOCK->LmTarget, PVOID 8 bytes
)
# CR3 register within structures describing initial processor state to be started
PROCESSOR_START_BLOCK_CR3_OFFSET = 0xA0 # PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3, ULONG64 8 bytes
+10 -12
View File
@@ -11,7 +11,8 @@ without them interfering with each other.
import functools
import hashlib
import logging
from typing import Callable, Iterable, List, Optional, Set, Tuple, Union
import re
from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
from volatility3.framework import constants, interfaces, symbols, exceptions
from volatility3.framework.objects import templates
@@ -337,7 +338,7 @@ class Module(interfaces.context.ModuleInterface):
)
@property
def symbols(self):
def symbols(self) -> Iterable[str]:
return self.context.symbol_space[self.symbol_table_name].symbols
get_symbol = get_module_wrapper("get_symbol")
@@ -386,10 +387,8 @@ class ModuleCollection(interfaces.context.ModuleContainer):
"""Class to contain a collection of SizedModules and reason about their
contents."""
def __init__(
self, modules: Optional[List[interfaces.context.ModuleInterface]] = None
) -> None:
self._prefix_count = {}
def __init__(self, modules: Optional[List[SizedModule]] = None) -> None:
self._modules: Dict[str, SizedModule] = {}
super().__init__(modules)
def deduplicate(self) -> "ModuleCollection":
@@ -402,20 +401,19 @@ class ModuleCollection(interfaces.context.ModuleContainer):
new_modules = []
seen: Set[str] = set()
for mod in self._modules:
if mod.hash not in seen or mod.size == 0:
if self._modules[mod].hash not in seen or self._modules[mod].size == 0:
new_modules.append(mod)
seen.add(mod.hash) # type: ignore # FIXME: mypy #5107
seen.add(self._modules[mod].hash)
return ModuleCollection(new_modules)
def free_module_name(self, prefix: str = "module") -> str:
"""Returns an unused module name"""
if prefix not in self._prefix_count:
self._prefix_count[prefix] = 1
existing_names = [name for name in self if re.match(rf"^{prefix}[0-9]*$", name)]
if not existing_names:
return prefix
count = self._prefix_count[prefix]
count = len(existing_names)
while prefix + str(count) in self:
count += 1
self._prefix_count[prefix] = count
return prefix + str(count)
@property
+34 -1
View File
@@ -8,9 +8,10 @@ space or symbol tables, and by layers when an address is invalid. The
:class:`PagedInvalidAddressException` contains information about the
size of the invalid page.
"""
from typing import Dict, Optional
from typing import Callable, Dict, Optional, Tuple
from volatility3.framework import interfaces
from volatility3.framework.interfaces.configuration import VersionableInterface
class VolatilityException(Exception):
@@ -130,3 +131,35 @@ class OfflineException(VolatilityException):
class RenderException(VolatilityException):
"""Thrown if there is an error during rendering"""
class LinuxPageCacheException(VolatilityException):
"""Thrown if there is an error during Linux Page Cache processing"""
class VersionMismatchException(VolatilityException):
"""Thrown if a version mismatch has been encountered between two components."""
def __init__(
self,
source_component: Callable,
target_component: VersionableInterface,
target_version: Tuple[int, int, int],
failure_reason: str = None,
*args,
):
"""
Args:
source_component: The component that required the target component
target_component: The component that is required. Must inherit from VersionableInterface
target_version: The version of the target component that was required, and ultimately was not satisfied
failure_reason: A detailed failure reason to enhance debugging and bug tracking
"""
super().__init__(*args)
self.source_component = source_component
self.target_component = target_component
self.target_version = target_version
self.failure_reason = failure_reason
def __str__(self):
return f"{self.source_component.__module__+ '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__+ '.' + self.target_component.__name__} {self.target_component.version} unmet."
+4 -2
View File
@@ -302,9 +302,11 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
def has_enumeration(self, name: str) -> bool:
"""Determines whether an enumeration is present in the module's symbol table."""
@property
@abstractmethod
def symbols(self) -> List:
"""Lists the symbols contained in the symbol table for this module"""
def symbols(self) -> Iterable[str]:
"""Returns an iterable of the symbols contained in the symbol table for this module"""
raise NotImplementedError("Symbols property has not been implemented.")
@abstractmethod
def get_symbols_by_absolute_location(self, offset: int, size: int = 0) -> List[str]:
@@ -112,8 +112,6 @@ class PluginInterface(
# Be careful with inheritance around this (We default to requiring a version which doesn't exist, so it must be set)
_required_framework_version: Tuple[int, int, int] = (0, 0, 0)
"""The _version variable is a quick way for plugins to define their current interface, it should follow SemVer rules"""
additional_description: str = None
"""Display additional description of the plugin after the description of the arguments. See: https://docs.python.org/3/library/argparse.html#epilog"""
def __init__(
self,
+11 -4
View File
@@ -122,7 +122,7 @@ class BaseSymbolTableInterface:
@property
def symbols(self) -> Iterable[str]:
"""Returns an iterator of the Symbol names."""
"""Returns an iterable of the available symbol names."""
raise NotImplementedError(
"Abstract property symbols not implemented by subclass."
)
@@ -131,7 +131,7 @@ class BaseSymbolTableInterface:
@property
def types(self) -> Iterable[str]:
"""Returns an iterator of the Symbol type names."""
"""Returns an iterable of the available symbol type names."""
raise NotImplementedError(
"Abstract property types not implemented by subclass."
)
@@ -149,7 +149,7 @@ class BaseSymbolTableInterface:
@property
def enumerations(self) -> Iterable[Any]:
"""Returns an iterator of the Enumeration names."""
"""Returns an iterable of the available enumerations."""
raise NotImplementedError(
"Abstract property enumerations not implemented by subclass."
)
@@ -366,6 +366,7 @@ class NativeTableInterface(BaseSymbolTableInterface):
@property
def symbols(self) -> Iterable[str]:
"""Returns an iterable of the available symbol names."""
return []
def get_enumeration(self, name: str) -> objects.Template:
@@ -374,7 +375,13 @@ class NativeTableInterface(BaseSymbolTableInterface):
)
@property
def enumerations(self) -> Iterable[str]:
def enumerations(self) -> Iterable[Any]:
"""Returns an iterable of the available enumerations."""
return []
@property
def types(self) -> Iterable[str]:
"""Returns an iterable of the available symbol type names."""
return []
@@ -129,6 +129,8 @@ if HAS_LEECHCORE:
def readline(self, __size: Optional[int] = ...) -> bytes:
data = b""
if not __size:
__size = 0
while __size > self._chunk_size or __size < 0:
data += self.read(self._chunk_size)
index = data.find(b"\n")
@@ -14,8 +14,8 @@ vollog = logging.getLogger(__name__)
class ConfigWriter(plugins.PluginInterface):
"""Runs the automagics and both prints and outputs configuration in the
output directory."""
"""Runs the automagics and both prints and outputs configuration in the \
output directory."""
_required_framework_version = (2, 0, 0)
@@ -5,6 +5,7 @@
import logging
from typing import List
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, renderers, symbols
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
@@ -27,6 +28,11 @@ class Check_idt(interfaces.plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
),
@@ -99,8 +105,10 @@ class Check_idt(interfaces.plugins.PluginInterface):
idt_addr = idt_addr & address_mask
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, idt_addr
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self.context, vmlinux.name, handlers, idt_addr
)
)
yield (
@@ -18,6 +18,7 @@ vollog = logging.getLogger(__name__)
class Check_modules(plugins.PluginInterface):
"""Compares module list to sysfs info, if available"""
_version = (1, 0, 0)
_required_framework_version = (2, 0, 0)
@classmethod
+4 -10
View File
@@ -5,7 +5,7 @@
import logging
from typing import Iterable, Tuple
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework import renderers, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.objects import utility
@@ -58,16 +58,10 @@ class Envars(plugins.PluginInterface):
Tuples of (key, value) representing each environment variable.
"""
# This ensures the `task` is valid as well as its
# memory mapping structures
try:
task_name = utility.array_to_string(task.comm)
env_start = task.mm.env_start
env_end = task.mm.env_end
except exceptions.InvalidAddressException:
return None
task_name = utility.array_to_string(task.comm)
task_pid = task.pid
env_start = task.mm.env_start
env_end = task.mm.env_end
env_area_size = env_end - env_start
if not (0 < env_area_size <= env_area_max_size):
vollog.debug(
@@ -4,6 +4,7 @@
import logging
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
@@ -26,6 +27,11 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 0, 0),
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
@@ -66,8 +72,10 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface):
):
call_addr = call_back.notifier_call
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, call_addr
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self.context, vmlinux.name, handlers, call_addr
)
)
yield (0, [format_hints.Hex(call_addr), module_name, symbol_name])
@@ -4,6 +4,7 @@
import logging
from typing import List
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
@@ -20,7 +21,7 @@ class Kthreads(plugins.PluginInterface):
"""Enumerates kthread functions"""
_required_framework_version = (2, 11, 0)
_version = (1, 0, 2)
_version = (1, 0, 3)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -30,6 +31,11 @@ class Kthreads(plugins.PluginInterface):
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
),
@@ -88,8 +94,10 @@ class Kthreads(plugins.PluginInterface):
if kthread.has_member("full_name")
else task_name
)
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, threadfn
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self.context, vmlinux.name, handlers, threadfn
)
)
fields = [
@@ -0,0 +1,189 @@
# 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, Dict, Iterator
from volatility3.plugins.linux import lsmod, check_modules, hidden_modules
from volatility3.framework import interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints, TreeGrid, NotAvailableValue
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.constants import architectures
from volatility3.framework.symbols.linux.utilities import tainting
vollog = logging.getLogger(__name__)
class Modxview(interfaces.plugins.PluginInterface):
"""Centralize lsmod, check_modules and hidden_modules results to efficiently \
spot modules presence and taints."""
_version = (1, 0, 0)
_required_framework_version = (2, 17, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="check_modules",
plugin=check_modules.Check_modules,
version=(1, 0, 0),
),
requirements.PluginRequirement(
name="hidden_modules",
plugin=hidden_modules.Hidden_modules,
version=(1, 0, 0),
),
requirements.BooleanRequirement(
name="plain_taints",
description="Display the plain taints string for each module.",
optional=True,
default=False,
),
]
@classmethod
def flatten_run_modules_results(
cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True
) -> Iterator[extensions.module]:
"""Flatten a dictionary mapping plugin names and modules list, to a single merged list.
This is useful to get a generic lookup list of all the detected modules.
Args:
run_results: dictionary of plugin names mapping a list of detected modules
deduplicate: remove duplicate modules, based on their offsets
Returns:
Iterator of modules objects
"""
seen_addresses = set()
for modules in run_results.values():
for module in modules:
if deduplicate and module.vol.offset in seen_addresses:
continue
seen_addresses.add(module.vol.offset)
yield module
@classmethod
def run_modules_scanners(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
run_hidden_modules: bool = True,
) -> Dict[str, List[extensions.module]]:
"""Run module scanning plugins and aggregate the results. It is designed
to not operate any inter-plugin results triage.
Args:
run_hidden_modules: specify if the hidden_modules plugin should be run
Returns:
Dictionary mapping each plugin to its corresponding result
"""
kernel = context.modules[kernel_name]
run_results = {}
# lsmod
run_results["lsmod"] = list(lsmod.Lsmod.list_modules(context, kernel_name))
# check_modules
sysfs_modules: dict = check_modules.Check_modules.get_kset_modules(
context, kernel_name
)
## Convert get_kset_modules() offsets back to module objects
run_results["check_modules"] = [
kernel.object(object_type="module", offset=m_offset, absolute=True)
for m_offset in sysfs_modules.values()
]
# hidden_modules
if run_hidden_modules:
known_modules_addresses = set(
context.layers[kernel.layer_name].canonicalize(module.vol.offset)
for module in run_results["lsmod"] + run_results["check_modules"]
)
modules_memory_boundaries = (
hidden_modules.Hidden_modules.get_modules_memory_boundaries(
context, kernel_name
)
)
run_results["hidden_modules"] = list(
hidden_modules.Hidden_modules.get_hidden_modules(
context,
kernel_name,
known_modules_addresses,
modules_memory_boundaries,
)
)
return run_results
def _generator(self):
kernel_name = self.config["kernel"]
run_results = self.run_modules_scanners(self.context, kernel_name)
aggregated_modules = {}
# We want to be explicit on the plugins results we are interested in
for plugin_name in ["lsmod", "check_modules", "hidden_modules"]:
# Iterate over each recovered module
for module in run_results[plugin_name]:
# Use offsets as unique keys, whether a module
# appears in many plugin runs or not
if aggregated_modules.get(module.vol.offset, None) is not None:
# Append the plugin to the list of originating plugins
aggregated_modules[module.vol.offset][1].append(plugin_name)
else:
aggregated_modules[module.vol.offset] = (module, [plugin_name])
for module_offset, (module, originating_plugins) in aggregated_modules.items():
# Tainting parsing capabilities applied to the module
if self.config.get("plain_taints"):
taints = tainting.Tainting.get_taints_as_plain_string(
self.context,
kernel_name,
module.taints,
True,
)
else:
taints = ",".join(
tainting.Tainting.get_taints_parsed(
self.context,
kernel_name,
module.taints,
True,
)
)
yield (
0,
(
module.get_name() or NotAvailableValue(),
format_hints.Hex(module_offset),
"lsmod" in originating_plugins,
"check_modules" in originating_plugins,
"hidden_modules" in originating_plugins,
taints or NotAvailableValue(),
),
)
def run(self):
columns = [
("Name", str),
("Address", format_hints.Hex),
("In procfs", bool),
("In sysfs", bool),
("Hidden", bool),
("Taints", str),
]
return TreeGrid(
columns,
self._generator(),
)
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
from abc import ABC, abstractmethod
import logging
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from typing import Iterator, List, Tuple
from volatility3 import framework
from volatility3.framework import (
@@ -98,6 +99,20 @@ class AbstractNetfilter(ABC):
f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}"
)
linux_utilities_modules_required_version = (
Netfilter._required_linux_utilities_modules_version
)
linux_utilities_modules_current_version = (
linux_utilities_modules.Modules._version
)
if not requirements.VersionRequirement.matches_required(
linux_utilities_modules_required_version,
linux_utilities_modules_current_version,
):
raise exceptions.PluginRequirementException(
f"linux_utilities_modules.Modules version not suitable: required {linux_utilities_modules_required_version} found {linux_utilities_modules_current_version}"
)
modules = lsmod.Lsmod.list_modules(context, kernel_module_name)
self.handlers = linux.LinuxUtilities.generate_kernel_handler_info(
context, kernel_module_name, modules
@@ -263,8 +278,10 @@ class AbstractNetfilter(ABC):
"""Helper to obtain the module and symbol name in the format needed for the
output of this plugin.
"""
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
self.vmlinux, self.handlers, addr
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self._context, self.vmlinux.name, self.handlers, addr
)
)
if module_name == "UNKNOWN":
@@ -677,6 +694,7 @@ class Netfilter(interfaces.plugins.PluginInterface):
_version = (1, 1, 0)
_required_linux_utilities_modules_version = (1, 0, 0)
_required_linuxutils_version = (2, 1, 0)
_required_lsmod_version = (2, 0, 0)
@@ -688,6 +706,11 @@ class Netfilter(interfaces.plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=cls._required_linux_utilities_modules_version,
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=cls._required_lsmod_version
),
@@ -6,9 +6,9 @@ import math
import logging
import datetime
from dataclasses import dataclass, astuple
from typing import List, Set, Type, Iterable
from typing import List, Set, Type, Iterable, Tuple
from volatility3.framework import renderers, interfaces
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
@@ -104,7 +104,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -147,7 +147,13 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
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:
if (
inode
and inode.is_link
and inode.has_member("i_link")
and inode.i_link
and inode.i_link.is_readable()
):
i_link_str = inode.i_link.dereference().cast(
"string", max_length=255, encoding="utf-8", errors="replace"
)
@@ -253,6 +259,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
if not root_inode.is_valid():
continue
if not (root_inode.i_mapping and root_inode.i_mapping.is_readable()):
# Retrieving data from the page cache requires a valid address space
continue
# Inode already processed?
if root_inode_ptr in seen_inodes:
continue
@@ -284,6 +294,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
if not file_inode.is_valid():
continue
if not (file_inode.i_mapping and file_inode.i_mapping.is_readable()):
# Retrieving data from the page cache requires a valid address space
continue
# Inode already processed?
if file_inode_ptr in seen_inodes:
continue
@@ -316,10 +330,12 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
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):
@@ -389,7 +405,7 @@ class InodePages(plugins.PluginInterface):
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -443,28 +459,80 @@ class InodePages(plugins.PluginInterface):
# 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:
with open_method(filename) as f:
inode_size = inode.i_size
f.truncate(inode_size)
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 = page_content[:max_length]
if current_fp + len(page_bytes) > inode_size:
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,
)
f.seek(current_fp)
f.write(page_bytes)
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
file_obj.seek(current_fp)
file_obj.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,
inode: interfaces.objects.ObjectInterface,
vmlinux_layer: interfaces.layers.TranslationLayerInterface,
) -> Iterable[Tuple[int, int, int, int, bool, str]]:
inode_size = inode.i_size
try:
for page_obj in inode.get_pages():
if page_obj.mapping != inode.i_mapping:
vollog.warning(
f"Cached page at {page_obj.vol.offset:#x} has a mismatched address space with the inode. Skipping page"
)
continue
page_vaddr = page_obj.vol.offset
page_paddr = page_obj.to_paddr()
page_mapping_addr = page_obj.mapping
page_index = page_obj.index
page_file_offset = page_index * vmlinux_layer.page_size
dump_safe = (
page_file_offset < inode_size
and page_mapping_addr
and page_mapping_addr.is_readable()
)
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
except exceptions.LinuxPageCacheException:
vollog.warning(f"Page cache for inode at {inode.vol.offset:#x} is corrupt")
def _generator(self):
vmlinux_module_name = self.config["kernel"]
vmlinux = self.context.modules[vmlinux_module_name]
@@ -486,7 +554,6 @@ class InodePages(plugins.PluginInterface):
else:
vollog.error("Unable to find inode with path %s", self.config["find"])
return None
elif self.config["inode"]:
inode = vmlinux.object("inode", self.config["inode"], absolute=True)
else:
@@ -501,27 +568,6 @@ class InodePages(plugins.PluginInterface):
vollog.error("The inode is not a regular file")
return None
inode_size = inode.i_size
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"]:
open_method = self.open
inode_address = inode.vol.offset
@@ -530,6 +576,8 @@ class InodePages(plugins.PluginInterface):
self.write_inode_content_to_file(
inode, filename, open_method, vmlinux_layer
)
else:
yield from self._generate_inode_fields(inode, vmlinux_layer)
def run(self):
headers = [
+17 -5
View File
@@ -34,7 +34,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists the processes present in a particular linux memory image."""
_required_framework_version = (2, 13, 0)
_version = (4, 0, 0)
_version = (4, 1, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -179,6 +179,10 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
file_output = "VMA start matching task start_code not found"
return file_output
@staticmethod
def _format_cred(cred):
return renderers.NotAvailableValue() if cred is None else cred
def _generator(
self,
pid_filter: Callable[[Any], bool],
@@ -212,16 +216,21 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
task_fields = self.get_task_fields(task, decorate_comm)
task_uid = self._format_cred(task_fields.uid)
task_gid = self._format_cred(task_fields.gid)
task_euid = self._format_cred(task_fields.euid)
task_egid = self._format_cred(task_fields.egid)
yield 0, (
format_hints.Hex(task_fields.offset),
task_fields.user_pid,
task_fields.user_tid,
task_fields.user_ppid,
task_fields.name,
task_fields.uid or renderers.NotAvailableValue(),
task_fields.gid or renderers.NotAvailableValue(),
task_fields.euid or renderers.NotAvailableValue(),
task_fields.egid or renderers.NotAvailableValue(),
task_uid,
task_gid,
task_euid,
task_egid,
task_fields.creation_time or renderers.NotAvailableValue(),
file_output,
)
@@ -250,6 +259,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
# Note that the init_task itself is not yielded, since "ps" also never shows it.
for task in init_task.tasks:
if not task.is_valid():
continue
if filter_func(task):
continue
@@ -9,8 +9,7 @@ from volatility3.plugins.linux import pslist
class PsTree(interfaces.plugins.PluginInterface):
"""Plugin for listing processes in a tree based on their parent process
ID."""
"""Plugin for listing processes in a tree based on their parent process ID."""
_required_framework_version = (2, 13, 0)
_version = (1, 1, 1)
@@ -5,6 +5,7 @@
import logging
from typing import List
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, renderers, exceptions, constants
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
@@ -29,6 +30,11 @@ class tty_check(plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 0, 0),
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
@@ -79,8 +85,10 @@ class tty_check(plugins.PluginInterface):
recv_buf = tty_dev.ldisc.ops.receive_buf
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, recv_buf
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self.context, vmlinux.name, handlers, recv_buf
)
)
yield (0, (name, format_hints.Hex(recv_buf), module_name, symbol_name))
+2 -2
View File
@@ -11,8 +11,8 @@ from volatility3.framework.symbols import mac
class Mount(plugins.PluginInterface):
"""A module containing a collection of plugins that produce data typically
found in Mac's mount command"""
"""A module containing a collection of plugins that produce data typically \
found in Mac's mount command"""
_required_framework_version = (2, 0, 0)
+1 -2
View File
@@ -10,8 +10,7 @@ from volatility3.plugins.mac import pslist
class PsTree(plugins.PluginInterface):
"""Plugin for listing processes in a tree based on their parent process
ID."""
"""Plugin for listing processes in a tree based on their parent process ID."""
_required_framework_version = (2, 0, 0)
+2 -2
View File
@@ -41,8 +41,8 @@ class TimeLinerInterface(metaclass=abc.ABCMeta):
class Timeliner(interfaces.plugins.PluginInterface):
"""Runs all relevant plugins that provide time related information and
orders the results by time."""
"""Runs all relevant plugins that provide time related information and \
orders the results by time."""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
@@ -76,14 +76,14 @@ class Envars(interfaces.plugins.PluginInterface):
"CurrentControlSet\\Control\\Session Manager\\Environment"
)
sys = True
except KeyError:
with contextlib.suppress(KeyError):
except (KeyError, registry.RegistryFormatException):
with contextlib.suppress(KeyError, registry.RegistryFormatException):
key = hive.get_key(
"ControlSet001\\Control\\Session Manager\\Environment"
)
sys = True
if sys:
with contextlib.suppress(KeyError):
with contextlib.suppress(KeyError, registry.RegistryFormatException):
for node in key.get_values():
try:
value_node_name = node.get_name()
@@ -100,11 +100,11 @@ class Envars(interfaces.plugins.PluginInterface):
continue
## The user-specific variables
with contextlib.suppress(KeyError):
with contextlib.suppress(KeyError, registry.RegistryFormatException):
key = hive.get_key("Environment")
ntuser = True
if ntuser:
with contextlib.suppress(KeyError):
with contextlib.suppress(KeyError, registry.RegistryFormatException):
for node in key.get_values():
try:
value_node_name = node.get_name()
@@ -123,7 +123,7 @@ class Envars(interfaces.plugins.PluginInterface):
## The volatile user variables
try:
key = hive.get_key("Volatile Environment")
except KeyError:
except (KeyError, registry.RegistryFormatException):
continue
try:
for node in key.get_values():
@@ -10,6 +10,7 @@ from typing import List
from volatility3.framework import renderers, interfaces, constants, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import registry
from volatility3.plugins.windows.registry import hivelist
vollog = logging.getLogger(__name__)
@@ -86,10 +87,18 @@ class GetServiceSIDs(interfaces.plugins.PluginInterface):
# Get ControlSet\Services.
try:
services = hive.get_key(r"CurrentControlSet\Services")
except (KeyError, exceptions.InvalidAddressException):
except (
KeyError,
exceptions.InvalidAddressException,
registry.RegistryFormatException,
):
try:
services = hive.get_key(r"ControlSet001\Services")
except (KeyError, exceptions.InvalidAddressException):
except (
KeyError,
exceptions.InvalidAddressException,
registry.RegistryFormatException,
):
continue
if services:
@@ -158,7 +158,11 @@ class GetSIDs(interfaces.plugins.PluginInterface):
layers.registry.RegistryFormatException,
):
continue
except (KeyError, exceptions.InvalidAddressException):
except (
KeyError,
exceptions.InvalidAddressException,
layers.registry.RegistryFormatException,
):
continue
return sids
@@ -341,7 +341,7 @@ class Handles(interfaces.plugins.PluginInterface):
try:
obj_name = entry.NameInfo.Name.String
except (ValueError, exceptions.InvalidAddressException):
obj_name = ""
obj_name = None
except exceptions.InvalidAddressException:
vollog.log(
@@ -359,7 +359,7 @@ class Handles(interfaces.plugins.PluginInterface):
format_hints.Hex(entry.HandleValue),
obj_type,
format_hints.Hex(entry.GrantedAccess),
obj_name,
obj_name or renderers.NotAvailableValue(),
),
)
@@ -14,8 +14,7 @@ vollog = logging.getLogger(__name__)
class PsTree(interfaces.plugins.PluginInterface):
"""Plugin for listing processes in a tree based on their parent process
ID."""
"""Plugin for listing processes in a tree based on their parent process ID."""
_required_framework_version = (2, 0, 0)
@@ -21,9 +21,10 @@ vollog = logging.getLogger(__name__)
class PsXView(plugins.PluginInterface):
"""Lists all processes found via four of the methods described in \"The Art of Memory Forensics,\" which may help
identify processes that are trying to hide themselves. I recommend using -r pretty if you are looking at this
plugin's output in a terminal."""
"""Lists all processes found via four of the methods described in \"The Art of Memory Forensics\" which may help \
identify processes that are trying to hide themselves.
We recommend using -r pretty if you are looking at this plugin's output in a terminal."""
# I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the functionality
# which the original plugin used to do it.
@@ -12,8 +12,7 @@ from volatility3.plugins.windows import poolscanner, bigpools
class HiveScan(interfaces.plugins.PluginInterface):
"""Scans for registry hives present in a particular windows memory
image."""
"""Scans for registry hives present in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@@ -13,7 +13,7 @@ from typing import Any, Generator, List, Tuple
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.layers.physical import BufferDataLayer
from volatility3.framework.layers.registry import RegistryHive
from volatility3.framework.layers.registry import RegistryHive, RegistryFormatException
from volatility3.framework.renderers import conversion, format_hints
from volatility3.framework.symbols import intermed
from volatility3.plugins.windows.registry import hivelist
@@ -167,10 +167,21 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac
self._determine_userassist_type()
userassist_node_path = hive.get_key(
"software\\microsoft\\windows\\currentversion\\explorer\\userassist",
return_list=True,
)
try:
userassist_node_path = hive.get_key(
"software\\microsoft\\windows\\currentversion\\explorer\\userassist",
return_list=True,
)
except RegistryFormatException as e:
vollog.warning(
f"Error accessing UserAssist key in {hive_name} at {hive.hive_offset:#x}: {e}"
)
return None
except KeyError:
vollog.warning(
f"UserAssist key not found in {hive_name} at {hive.hive_offset:#x}"
)
return None
if not userassist_node_path:
vollog.warning("list_userassist did not find a valid node_path (or None)")
@@ -1099,9 +1099,8 @@ class DynamicInfo:
class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Decodes scheduled task information from the Windows registry, including
information about triggers, actions, run times, and creation times.
"""
"""Decodes scheduled task information from the Windows registry, including \
information about triggers, actions, run times, and creation times."""
_required_framework_version = (2, 11, 0)
_version = (1, 0, 0)
@@ -15,7 +15,7 @@ from volatility3.framework import (
symbols,
)
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import scanners
from volatility3.framework.layers import scanners, registry
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows import versions
@@ -159,12 +159,20 @@ class SvcScan(interfaces.plugins.PluginInterface):
return cast(
objects.StructType, hive.get_key(r"CurrentControlSet\Services")
)
except (KeyError, exceptions.InvalidAddressException):
except (
KeyError,
exceptions.InvalidAddressException,
registry.RegistryFormatException,
):
try:
return cast(
objects.StructType, hive.get_key(r"ControlSet001\Services")
)
except (KeyError, exceptions.InvalidAddressException):
except (
KeyError,
exceptions.InvalidAddressException,
registry.RegistryFormatException,
):
vollog.log(
constants.LOGLEVEL_VVVV,
"Could not retrieve any control set from SYSTEM hive",
@@ -84,7 +84,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
if not vad_maps_to_scan:
vollog.warning(
f"No VADs were found for task {task.UniqueProcessID}, not scanning"
f"No VADs were found for task {task.UniqueProcessId}, not scanning"
)
continue
+16 -7
View File
@@ -411,18 +411,27 @@ class Version1Format(ISFormatTable):
@property
def symbols(self) -> Iterable[str]:
"""Returns an iterator of the symbol names."""
return list(self._json_object.get("symbols", {}))
"""Returns an iterable (KeysView) of the available symbol names."""
return self._json_object.get("symbols", {}).keys()
@property
def enumerations(self) -> Iterable[str]:
"""Returns an iterator of the available enumerations."""
return list(self._json_object.get("enums", {}))
def enumerations(self) -> Iterable[Any]:
"""Returns an iterable (KeysView) of the available enumerations."""
return self._json_object.get("enums", {}).keys()
@property
def types(self) -> Iterable[str]:
"""Returns an iterator of the symbol type names."""
return list(self._json_object.get("user_types", {})) + list(self.natives.types)
"""Returns an iterable (KeysView) of the available symbol type names."""
# We use ** instead of
# `set(self._json_object.get("user_types", {}).keys()).union(self.natives.types)`
# because converting user_types dict to a set is costly.
# It is more efficient to convert the (very small) self.natives.types set to a dict.
# FIXME: On Python3.8 support drop, merge the two dicts using the merge operator:
# (self._json_object.get("user_types", {}) | dict.fromkeys(self.natives.types)).keys()
return {
**self._json_object.get("user_types", {}),
**dict.fromkeys(self.natives.types),
}.keys()
def get_type_class(self, name: str) -> Type[interfaces.objects.ObjectInterface]:
return self._overrides.get(name, objects.AggregateType)
+84 -63
View File
@@ -3,15 +3,26 @@
#
import math
import contextlib
import functools
import logging
from abc import ABC, abstractmethod
from typing import Iterator, List, Tuple, Optional
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3 import framework
from volatility3.framework import constants, exceptions, interfaces, objects
from volatility3.framework import (
constants,
exceptions,
interfaces,
objects,
Deprecation,
)
from volatility3.framework.objects import utility
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux import extensions
vollog = logging.getLogger(__name__)
class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
provides = {"type": "interface"}
@@ -43,6 +54,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
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)
self.optional_set_type_class("scatterlist", extensions.scatterlist)
# kernels >= 4.18
self.optional_set_type_class("timespec64", extensions.timespec64)
@@ -348,6 +360,10 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
yield fd_num, filp, full_path
@classmethod
@Deprecation.deprecated_method(
replacement=linux_utilities_modules.Modules.mask_mods_list,
replacement_version=(1, 0, 0),
)
def mask_mods_list(
cls,
context: interfaces.context.ContextInterface,
@@ -355,18 +371,11 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
mods: Iterator[interfaces.objects.ObjectInterface],
) -> List[Tuple[str, int, int]]:
"""
DEPRECATED: use "volatility3.framework.symbols.linux.utilities.modules.Modules.mask_mods_list" instead.
A helper function to mask the starting and end address of kernel modules
"""
mask = context.layers[layer_name].address_mask
return [
(
utility.array_to_string(mod.name),
mod.get_module_base() & mask,
(mod.get_module_base() & mask) + mod.get_core_size(),
)
for mod in mods
]
return linux_utilities_modules.Modules.mask_mods_list(context, layer_name, mods)
@classmethod
def generate_kernel_handler_info(
@@ -391,41 +400,30 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
return [
(constants.linux.KERNEL_NAME, start_addr, end_addr)
] + LinuxUtilities.mask_mods_list(context, kernel.layer_name, mods_list)
] + linux_utilities_modules.Modules.mask_mods_list(
context, kernel.layer_name, mods_list
)
@classmethod
@Deprecation.deprecated_method(
replacement=linux_utilities_modules.Modules.lookup_module_address,
replacement_version=(1, 0, 0),
)
def lookup_module_address(
cls,
kernel_module: interfaces.context.ModuleInterface,
handlers: List[Tuple[str, int, int]],
target_address: int,
):
) -> Tuple[str, str]:
"""
DEPRECATED: use "volatility3.framework.symbols.linux.utilities.modules.Modules.lookup_module_address" instead.
Searches between the start and end address of the kernel module using target_address.
Returns the module and symbol name of the address provided.
"""
mod_name = "UNKNOWN"
symbol_name = "N/A"
for name, start, end in handlers:
if start <= target_address <= end:
mod_name = name
if name == constants.linux.KERNEL_NAME:
symbols = list(
kernel_module.get_symbols_by_absolute_location(target_address)
)
if len(symbols):
symbol_name = (
symbols[0].split(constants.BANG)[1]
if constants.BANG in symbols[0]
else symbols[0]
)
break
return mod_name, symbol_name
return linux_utilities_modules.Modules.lookup_module_address(
kernel_module.context, kernel_module.name, handlers, target_address
)
@classmethod
def walk_internal_list(cls, vmlinux, struct_name, list_member, list_start):
@@ -630,7 +628,7 @@ class IDStorage(ABC):
raise NotImplementedError
def nodep_to_node(self, nodep) -> interfaces.objects.ObjectInterface:
"""Instanciates a tree node from its pointer
"""Instantiates a tree node from its pointer
Args:
nodep: Pointer to the XArray/RadixTree node
@@ -677,7 +675,7 @@ class IDStorage(ABC):
height = self.get_tree_height(root.vol.offset)
nodep = self.get_head_node(root)
if not nodep:
if not (nodep and nodep.is_readable()):
return
# Keep the internal flag before untagging it
@@ -712,7 +710,7 @@ class XArray(IDStorage):
def get_node_height(self, nodep) -> int:
node = self.nodep_to_node(nodep)
return (node.shift / self.CHUNK_SHIFT) + 1
return (node.shift // self.CHUNK_SHIFT) + 1
def get_head_node(self, tree) -> int:
return tree.xa_head
@@ -735,6 +733,7 @@ class RadixTree(IDStorage):
RADIX_TREE_INTERNAL_NODE = 1
RADIX_TREE_EXCEPTIONAL_ENTRY = 2
RADIX_TREE_ENTRY_MASK = 3
RADIX_TREE_MAP_SHIFT = 6 # CONFIG_BASE_FULL
# Dynamic values. These will be initialized later
RADIX_TREE_INDEX_BITS = None
@@ -771,43 +770,57 @@ class RadixTree(IDStorage):
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
# kernels < 4.7 d0891265bbc988dc91ed8580b38eb3dac128581b
radix_tree_root = self.vmlinux.object(
"radix_tree_root", offset=treep, absolute=True
)
return radix_tree_root.height
# kernels >= 4.7.10
# kernels >= 4.7
return 0
@functools.cached_property
def _max_height_array(self):
if self.vmlinux.has_symbol("height_to_maxindex"):
# 2.6.24 26fb1589cb0aaec3a0b4418c54f30c1a2b1781f6 <= Kernels < 4.7 d0891265bbc988dc91ed8580b38eb3dac128581b
return self.vmlinux.object_from_symbol("height_to_maxindex")
elif self.vmlinux.has_symbol("height_to_maxnodes"):
# 4.8 c78c66d1ddfdbd2353f3fcfeba0268524537b096 <= kernels < 4.20 8cf2f98411e3a0865026a1061af637161b16d32b
return self.vmlinux.object_from_symbol("height_to_maxnodes")
return None
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
if self._max_height_array:
# 2.6.24 <= kernels <= 4.20 See _max_height_array()
return self._max_height_array[height]
else:
# Kernels < 4.7
height_to_maxindex_array = self.vmlinux.object_from_symbol(
"height_to_maxindex"
)
maxindex = height_to_maxindex_array[height]
return maxindex
# Kernels >= 4.20
return (self.CHUNK_SIZE << node.shift) - 1
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
height = (node.shift // self.CHUNK_SHIFT) + 1
elif hasattr(node, "path"):
# 3.15 <= Kernels < 4.7
return node.path & self.RADIX_TREE_HEIGHT_MASK
height = node.path & self.RADIX_TREE_HEIGHT_MASK
elif hasattr(node, "height"):
# Kernels < 3.15
return node.height
height = node.height
else:
raise exceptions.VolatilityException("Cannot find radix-tree node height")
if self._max_height_array and not (0 <= height < self._max_height_array.count):
error_msg = f"Radix Tree node {node.vol.offset:#x} height {height} exceeds max height of {self._max_height_array.count}"
vollog.error(error_msg)
raise exceptions.LinuxPageCacheException(error_msg)
return height
def get_head_node(self, tree) -> int:
return tree.rnode
@@ -820,14 +833,16 @@ class RadixTree(IDStorage):
def untag_node(self, nodep) -> int:
return nodep & (~self.RADIX_TREE_ENTRY_MASK)
def is_valid_node(self, nodep) -> bool:
def _is_exceptional_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 (
self.vmlinux.has_type("radix_tree_root")
and (nodep & self.RADIX_TREE_ENTRY_MASK)
== self.RADIX_TREE_EXCEPTIONAL_ENTRY
)
return True
def is_valid_node(self, nodep) -> bool:
return not self._is_exceptional_node(nodep)
class PageCache:
@@ -856,11 +871,17 @@ class PageCache:
Yields:
Page objects
"""
layer = self.vmlinux.context.layers[self.vmlinux.layer_name]
for page_addr in self._idstorage.get_entries(self._page_cache.i_pages):
if not page_addr:
continue
if not layer.is_valid(page_addr):
error_msg = f"Invalid cached page address at {page_addr:#x}, aborting"
vollog.error(error_msg)
raise exceptions.LinuxPageCacheException(error_msg)
page = self.vmlinux.object("page", offset=page_addr, absolute=True)
if page:
yield page
if not page.is_valid():
error_msg = f"Invalid cached page at {page_addr:#x}, aborting"
vollog.error(error_msg)
raise exceptions.LinuxPageCacheException(error_msg)
yield page
@@ -20,7 +20,6 @@ from volatility3.framework.objects import utility
from volatility3.framework.symbols import generic, linux, intermed
from volatility3.framework.symbols.linux.extensions import elf
vollog = logging.getLogger(__name__)
# Keep these in a basic module, to prevent import cycles when symbol providers require them
@@ -307,6 +306,46 @@ class module(generic.GenericIntelProcess):
class task_struct(generic.GenericIntelProcess):
def is_valid(self) -> bool:
layer = self._context.layers[self.vol.layer_name]
# Make sure the entire task content is readable
if not layer.is_valid(self.vol.offset, self.vol.size):
return False
if self.pid < 0 or self.tgid < 0:
return False
if self.has_member("signal") and not (
self.signal and self.signal.is_readable()
):
return False
if self.has_member("nsproxy") and not (
self.nsproxy and self.nsproxy.is_readable()
):
return False
if self.has_member("real_parent") and not (
self.real_parent and self.real_parent.is_readable()
):
return False
if (
self.has_member("active_mm")
and self.active_mm
and not self.active_mm.is_readable()
):
return False
if self.mm:
if not self.mm.is_readable():
return False
if self.mm != self.active_mm:
return False
return True
def add_process_layer(
self, config_prefix: Optional[str] = None, preferred_name: Optional[str] = None
) -> Optional[str]:
@@ -401,6 +440,8 @@ class task_struct(generic.GenericIntelProcess):
tasks_iterable = self._get_tasks_iterable()
threads_seen = set([self.vol.offset])
for task in tasks_iterable:
if not task.is_valid():
continue
if task.vol.offset not in threads_seen:
threads_seen.add(task.vol.offset)
yield task
@@ -811,23 +852,30 @@ class mm_struct(objects.StructType):
def _get_mmap_iter(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Returns an iterator for the mmap list member of an mm_struct. Use this only if
required, get_vma_iter() will choose the correct _get_maple_tree_iter() or
_get_mmap_iter() automatically as required."""
_get_mmap_iter() automatically as required.
Yields:
vm_area_struct objects
"""
if not self.has_member("mmap"):
raise AttributeError(
"_get_mmap_iter called on mm_struct where no mmap member exists."
)
if not self.mmap:
vma_pointer = self.mmap
if not (vma_pointer and vma_pointer.is_readable()):
return None
yield self.mmap
vma_object = vma_pointer.dereference()
yield vma_object
seen = {self.mmap.vol.offset}
link = self.mmap.vm_next
seen = {vma_pointer}
vma_pointer = vma_pointer.vm_next
while link != 0 and link.vol.offset not in seen:
yield link
seen.add(link.vol.offset)
link = link.vm_next
while vma_pointer and vma_pointer.is_readable() and vma_pointer not in seen:
vma_object = vma_pointer.dereference()
yield vma_object
seen.add(vma_pointer)
vma_pointer = vma_pointer.vm_next
# TODO: As of version 3.0.0 this method should be removed
def get_maple_tree_iter(self) -> Iterable[interfaces.objects.ObjectInterface]:
@@ -842,7 +890,11 @@ class mm_struct(objects.StructType):
def _get_maple_tree_iter(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Returns an iterator for the mm_mt member of an mm_struct. Use this only if
required, get_vma_iter() will choose the correct _get_maple_tree_iter() or
get_mmap_iter() automatically as required."""
get_mmap_iter() automatically as required.
Yields:
vm_area_struct objects
"""
if not self.has_member("mm_mt"):
raise AttributeError(
@@ -850,20 +902,27 @@ class mm_struct(objects.StructType):
)
symbol_table_name = self.get_symbol_table_name()
for vma_pointer in self.mm_mt.get_slot_iter():
# convert pointer to vm_area_struct and yield
vma = self._context.object(
# Convert pointer to vm_area_struct and yield
vma_object = self._context.object(
symbol_table_name + constants.BANG + "vm_area_struct",
layer_name=self.vol.native_layer_name,
offset=vma_pointer,
)
yield vma
yield vma_object
def get_vma_iter(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Returns an iterator for the VMAs in an mm_struct. Automatically choosing the mmap or mm_mt as required."""
"""Returns an iterator for the VMAs in an mm_struct.
Automatically choosing the mmap or mm_mt as required.
Yields:
vm_area_struct objects
"""
if self.has_member("mmap"):
# kernels < 6.1
yield from self._get_mmap_iter()
elif self.has_member("mm_mt"):
# kernels >= 6.1 d4af56c5c7c6781ca6ca8075e2cf5bc119ed33d1
yield from self._get_maple_tree_iter()
else:
raise AttributeError("Unable to find mmap or mm_mt in mm_struct")
@@ -1205,35 +1264,43 @@ class list_head(objects.StructType, collections.abc.Iterable):
Objects of the type specified via the "symbol_type" argument.
"""
layer = layer or self.vol.layer_name
layer_name = layer or self.vol.layer_name
trans_layer = self._context.layers[layer_name]
if not trans_layer.is_valid(self.vol.offset):
return None
relative_offset = self._context.symbol_space.get_type(
symbol_type
).relative_child_offset(member)
direction = "prev"
if forward:
direction = "next"
try:
link = getattr(self, direction).dereference()
except exceptions.InvalidAddressException:
direction = "next" if forward else "prev"
link_ptr = getattr(self, direction)
if not (link_ptr and link_ptr.is_readable()):
return None
link = link_ptr.dereference()
if not sentinel:
yield self._context.object(
symbol_type, layer, offset=self.vol.offset - relative_offset
)
obj_offset = self.vol.offset - relative_offset
if not trans_layer.is_valid(obj_offset):
return None
yield self._context.object(symbol_type, layer_name, offset=obj_offset)
seen = {self.vol.offset}
while link.vol.offset not in seen:
obj = self._context.object(
symbol_type, layer, offset=link.vol.offset - relative_offset
)
yield obj
obj_offset = link.vol.offset - relative_offset
if not trans_layer.is_valid(obj_offset):
return None
yield self._context.object(symbol_type, layer_name, offset=obj_offset)
seen.add(link.vol.offset)
try:
link = getattr(link, direction).dereference()
except exceptions.InvalidAddressException:
link_ptr = getattr(link, direction)
if not (link_ptr and link_ptr.is_readable()):
break
link = link_ptr.dereference()
def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]:
return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name)
@@ -2489,7 +2556,12 @@ class inode(objects.StructType):
"""
if not self.i_size:
return
elif not (self.i_mapping and self.i_mapping.nrpages > 0):
if not (
self.i_mapping
and self.i_mapping.is_readable()
and self.i_mapping.nrpages > 0
):
return
page_cache = linux.PageCache(
@@ -2497,19 +2569,26 @@ class inode(objects.StructType):
kernel_module_name="kernel",
page_cache=self.i_mapping.dereference(),
)
yield from page_cache.get_cached_pages()
def get_contents(self):
def get_contents(self) -> Iterable[Tuple[int, bytes]]:
"""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
page_content (bytes): The page content
"""
for page_obj in self.get_pages():
if page_obj.mapping != self.i_mapping:
vollog.warning(
f"Cached page at {page_obj.vol.offset:#x} has a mismatched address space with the inode. Skipping page"
)
continue
page_index = int(page_obj.index)
page_content = page_obj.get_content()
yield page_index, page_content
if page_content:
yield page_index, page_content
class address_space(objects.StructType):
@@ -2517,7 +2596,7 @@ class address_space(objects.StructType):
def i_pages(self):
"""Returns the appropriate member containing the page cache tree"""
if self.has_member("i_pages"):
# Kernel >= 4.17
# Kernel >= 4.17 b93b016313b3ba8003c3b8bb71f569af91f19fc7
return self.member("i_pages")
elif self.has_member("page_tree"):
# Kernel < 4.17
@@ -2527,6 +2606,15 @@ class address_space(objects.StructType):
class page(objects.StructType):
def is_valid(self) -> bool:
if self.mapping and not self.mapping.is_readable():
return False
if self.to_paddr() < 0:
return False
return True
@functools.cached_property
def pageflags_enum(self) -> Dict:
"""Returns 'pageflags' enumeration key/values
@@ -2625,7 +2713,7 @@ class page(objects.StructType):
return page_paddr
def get_content(self) -> Union[str, None]:
def get_content(self) -> Union[bytes, None]:
"""Returns the page content
Returns:
@@ -2641,8 +2729,13 @@ class page(objects.StructType):
if not page_paddr:
return None
page_data = physical_layer.read(page_paddr, vmlinux_layer.page_size)
return page_data
if not physical_layer.is_valid(page_paddr, length=vmlinux_layer.page_size):
vollog.debug(
"Unable to read page 0x%x content at 0x%x", self.vol.offset, page_paddr
)
return None
return physical_layer.read(page_paddr, vmlinux_layer.page_size)
def get_flags_list(self) -> List[str]:
"""Returns a list of page flags
@@ -2755,17 +2848,17 @@ class IDR(objects.StructType):
class rb_root(objects.StructType):
def _walk_nodes(self, root_node) -> Iterator[int]:
def _walk_nodes(self, root_node: int) -> Iterator[int]:
"""Traverses the Red-Black tree from the root node and yields a pointer to each
node in this tree.
Args:
root_node: A Red-Black tree node from which to start descending
root_node: A Red-Black tree node pointer from which to start descending
Yields:
A pointer to every node descending from the specified root node
"""
if not root_node:
if not (root_node and root_node.is_readable()):
return
yield root_node
@@ -2780,3 +2873,111 @@ class rb_root(objects.StructType):
"""
yield from self._walk_nodes(root_node=self.rb_node)
class scatterlist(objects.StructType):
SG_CHAIN = 0x01
SG_END = 0x02
SG_PAGE_LINK_MASK = SG_CHAIN | SG_END
def _sg_flags(self) -> int:
return self.page_link & self.SG_PAGE_LINK_MASK
def _sg_is_chain(self) -> int:
return self._sg_flags() & self.SG_CHAIN
def _sg_is_last(self) -> int:
return self._sg_flags() & self.SG_END
def _sg_chain_ptr(self) -> int:
"""Clears the last two bits basically."""
return self.page_link & ~self.SG_PAGE_LINK_MASK
def _sg_dma_len(self) -> int:
# Depends on CONFIG_NEED_SG_DMA_LENGTH
if self.has_member("dma_length"):
return self.dma_length
return self.length
def _get_sg_max_single_alloc(self) -> int:
"""Based on kernel's SG_MAX_SINGLE_ALLOC.
Doc. from kernel source :
* Maximum number of entries that will be allocated in one piece, if
* a list larger than this is required then chaining will be utilized.
"""
return self._context.layers[self.vol.layer_name].page_size // self.vol.size
def _sg_next(self) -> Optional[interfaces.objects.ObjectInterface]:
"""Get the next scatterlist struct from the list.
Based on kernel's sg_next.
Doc. from kernel source :
* Notes on SG table design.
*
* We use the unsigned long page_link field in the scatterlist struct to place
* the page pointer AND encode information about the sg table as well. The two
* lower bits are reserved for this information.
*
* If bit 0 is set, then the page_link contains a pointer to the next sg
* table list. Otherwise the next entry is at sg + 1.
*
* If bit 1 is set, then this sg entry is the last element in a list.
"""
if self._sg_is_last():
return None
if self._sg_is_chain():
next_address = self._sg_chain_ptr()
else:
next_address = self.vol.offset + self.vol.size
sg = self._context.object(
self.get_symbol_table_name() + constants.BANG + "scatterlist",
self.vol.layer_name,
next_address,
)
return sg
def for_each_sg(self) -> Optional[Iterator[interfaces.objects.ObjectInterface]]:
"""Iterate over each struct in the scatterlist."""
sg = self
sg_max_single_alloc = self._get_sg_max_single_alloc()
# Empty scatterlists protection
if sg.page_link == 0 and sg._sg_dma_len() == 0 and sg.dma_address == 0:
return None
else:
# Yield itself first
yield sg
entries_count = 1
# entries_count <= sg_max_single_alloc should always be true if the
# scatterlists were correctly chained.
while entries_count <= sg_max_single_alloc:
sg = sg._sg_next()
if sg is None:
break
# Points to a new scatterlist
elif sg._sg_is_chain():
entries_count = 0
else:
entries_count += 1
yield sg
def get_content(
self,
) -> Optional[Iterator[bytes]]:
"""Traverse a scatterlist to gather content located at each
dma_address position.
Returns:
An iterator of bytes
"""
# Either "physical" is layer-1 because this is a module layer, or "physical" is the current layer
physical_layer_name = self._context.layers[self.vol.layer_name].config.get(
"memory_layer", self.vol.layer_name
)
physical_layer = self._context.layers[physical_layer_name]
for sg in self.for_each_sg():
yield from physical_layer.read(sg.dma_address, sg._sg_dma_len())
@@ -0,0 +1,70 @@
from typing import Iterator, List, Tuple
from volatility3 import framework
from volatility3.framework import constants, interfaces
from volatility3.framework.objects import utility
class Modules(interfaces.configuration.VersionableInterface):
"""Kernel modules related utilities."""
_version = (1, 0, 0)
_required_framework_version = (2, 0, 0)
framework.require_interface_version(*_required_framework_version)
@classmethod
def mask_mods_list(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
mods: Iterator[interfaces.objects.ObjectInterface],
) -> List[Tuple[str, int, int]]:
"""
A helper function to mask the starting and end address of kernel modules
"""
mask = context.layers[layer_name].address_mask
return [
(
utility.array_to_string(mod.name),
mod.get_module_base() & mask,
(mod.get_module_base() & mask) + mod.get_core_size(),
)
for mod in mods
]
@classmethod
def lookup_module_address(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
handlers: List[Tuple[str, int, int]],
target_address: int,
) -> Tuple[str, str]:
"""
Searches between the start and end address of the kernel module using target_address.
Returns the module and symbol name of the address provided.
"""
kernel_module = context.modules[kernel_module_name]
mod_name = "UNKNOWN"
symbol_name = "N/A"
for name, start, end in handlers:
if start <= target_address <= end:
mod_name = name
if name == constants.linux.KERNEL_NAME:
symbols = list(
kernel_module.get_symbols_by_absolute_location(target_address)
)
if len(symbols):
symbol_name = (
symbols[0].split(constants.BANG)[1]
if constants.BANG in symbols[0]
else symbols[0]
)
break
return mod_name, symbol_name
@@ -0,0 +1,161 @@
import functools
from volatility3 import framework
from volatility3.framework import interfaces
from volatility3.framework.constants import linux as linux_constants
from typing import List, Optional
class Tainting(interfaces.configuration.VersionableInterface):
"""Tainted kernel and modules parsing capabilities.
Relevant Linux kernel functions:
- modules: module_flags_taint
- kernel: print_tainted
"""
_version = (1, 0, 0)
_required_framework_version = (2, 0, 0)
framework.require_interface_version(*_required_framework_version)
@classmethod
@functools.lru_cache
def _get_kernel_taint_flags_list(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
) -> Optional[List[interfaces.objects.ObjectInterface]]:
"""Determine whether the kernel embeds taint flags definition
in-memory or not.
Returns:
A list of "taint_flag" kernel objects if taint_flags symbol exists
"""
kernel = context.modules[kernel_module_name]
if kernel.has_symbol("taint_flags"):
return list(kernel.object_from_symbol("taint_flags"))
return None
@classmethod
def _module_flags_taint_pre_4_10_rc1(
cls,
taints: int,
is_module: bool = False,
) -> str:
"""Convert the module's taints value to a 1-1 character mapping.
Relies on statically defined taints mappings in the framework.
Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module
Returns:
The raw taints string.
"""
taints_string = ""
for char, taint_flag in linux_constants.TAINT_FLAGS.items():
if is_module and not taint_flag.module:
continue
if taints & taint_flag.shift:
taints_string += char
return taints_string
@classmethod
def _module_flags_taint_post_4_10_rc1(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
taints: int,
is_module: bool = False,
) -> str:
"""Convert the module's taints value to a 1-1 character mapping.
Relies on kernel symbol embedded taints definitions.
struct taint_flag {
char c_true; /* character printed when tainted */
char c_false; /* character printed when not tainted */
bool module; /* also show as a per-module taint flag */
};
Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module
Returns:
The raw taints string.
"""
taints_string = ""
for taint_bit, taint_flag in enumerate(
cls._get_kernel_taint_flags_list(context, kernel_module_name)
):
if is_module and not taint_flag.module:
continue
c_true = chr(taint_flag.c_true)
c_false = chr(taint_flag.c_false)
if taints & (1 << taint_bit):
taints_string += c_true
elif c_false != " ":
taints_string += c_false
return taints_string
@classmethod
def get_taints_as_plain_string(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
taints: int,
is_module: bool = False,
) -> str:
"""Convert the taints value to a 1-1 character mapping.
Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module
Returns:
The raw taints string.
Documentation:
- module_flags_taint kernel function
"""
if cls._get_kernel_taint_flags_list(context, kernel_module_name):
return cls._module_flags_taint_post_4_10_rc1(
context, kernel_module_name, taints, is_module
)
return cls._module_flags_taint_pre_4_10_rc1(taints, is_module)
@classmethod
def get_taints_parsed(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
taints: int,
is_module: bool = False,
) -> List[str]:
"""Convert the taints string to a 1-1 descriptor mapping.
Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module
Returns:
A comprehensive (user-friendly) taint descriptor list.
Documentation:
- module_flags_taint kernel function
"""
comprehensive_taints = []
for character in cls.get_taints_as_plain_string(
context, kernel_module_name, taints, is_module
):
taint_flag = linux_constants.TAINT_FLAGS.get(character)
if not taint_flag:
comprehensive_taints.append(f"<UNKNOWN_TAINT_CHAR_{character}>")
elif taint_flag.when_present:
comprehensive_taints.append(taint_flag.desc)
return comprehensive_taints
+1 -1
View File
@@ -30,7 +30,7 @@ class NativeTable(interfaces.symbols.NativeTableInterface):
@property
def types(self) -> Iterable[str]:
"""Returns an iterator of the symbol type names."""
"""Returns an iterable (set) of the available symbol type names."""
return self._types
def get_type(self, type_name: str) -> interfaces.objects.Template:
@@ -962,56 +962,55 @@ class LIST_ENTRY(objects.StructType, collections.abc.Iterable):
) -> Iterator[interfaces.objects.ObjectInterface]:
"""Returns an iterator of the entries in the list."""
layer = layer or self.vol.layer_name
layer_name = layer or self.vol.layer_name
native_layer_name = layer_name or self.vol.native_layer_name
trans_layer = self._context.layers[layer_name]
if not trans_layer.is_valid(self.vol.offset):
return None
relative_offset = self._context.symbol_space.get_type(
symbol_type
).relative_child_offset(member)
direction = "Blink"
if forward:
direction = "Flink"
direction = "Flink" if forward else "Blink"
trans_layer = self._context.layers[layer]
try:
is_valid = trans_layer.is_valid(self.vol.offset)
if not is_valid:
return None
link = getattr(self, direction).dereference()
except exceptions.InvalidAddressException:
link_ptr = getattr(self, direction)
if not (link_ptr and link_ptr.is_readable()):
return None
link = link_ptr.dereference()
if not sentinel:
obj_offset = self.vol.offset - relative_offset
if not trans_layer.is_valid(obj_offset):
return None
yield self._context.object(
symbol_type,
layer,
offset=self.vol.offset - relative_offset,
native_layer_name=layer or self.vol.native_layer_name,
layer_name,
offset=obj_offset,
native_layer_name=native_layer_name,
)
seen = {self.vol.offset}
while link.vol.offset not in seen:
obj_offset = link.vol.offset - relative_offset
if not trans_layer.is_valid(obj_offset):
return None
obj = self._context.object(
yield self._context.object(
symbol_type,
layer,
layer_name,
offset=obj_offset,
native_layer_name=layer or self.vol.native_layer_name,
native_layer_name=native_layer_name,
)
yield obj
seen.add(link.vol.offset)
try:
link = getattr(link, direction).dereference()
except exceptions.InvalidAddressException:
link_ptr = getattr(link, direction)
if not (link_ptr and link_ptr.is_readable()):
return None
link = link_ptr.dereference()
def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]:
return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name)
@@ -133,8 +133,17 @@ class CM_KEY_BODY(objects.StructType):
def get_full_key_name(self) -> str:
output = []
seen = set()
kcb = self.KeyControlBlock
while kcb.ParentKcb:
if kcb.ParentKcb.vol.offset in seen:
return None
seen.add(kcb.ParentKcb.vol.offset)
if len(output) > 128:
return None
if kcb.NameBlock.Name is None:
break
+10 -1
View File
@@ -14,6 +14,8 @@ vollog = logging.getLogger(__name__)
cached_validation_filepath = os.path.join(constants.CACHE_PATH, "valid_isf.hashcache")
validators = {}
def load_cached_validations() -> Set[str]:
"""Loads up the list of successfully cached json objects, so we don't need
@@ -93,6 +95,13 @@ def valid(
return True
try:
import jsonschema
schema_key = json.dumps(schema, sort_keys=True)
if schema_key not in validators:
validator_class = jsonschema.validators.validator_for(schema)
validator_class.check_schema(schema)
validator = validator_class(schema)
validators[schema_key] = validator
except ImportError:
vollog.info("Dependency for validation unavailable: jsonschema")
vollog.debug("All validations will report success, even with malformed input")
@@ -100,7 +109,7 @@ def valid(
try:
vollog.debug("Validating JSON against schema...")
jsonschema.validate(input, schema)
validators[schema_key].validate(input)
cached_validations.add(input_hash)
vollog.debug("JSON validated against schema (result cached)")
except jsonschema.exceptions.SchemaError: