From d5a0b93383fda59267bdd9b42e716b70ad66595c Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 31 Oct 2024 12:28:40 +0100 Subject: [PATCH 001/141] add TAINT_FLAGS constant --- .../framework/constants/linux/__init__.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 7c485d3c3..9f25c9225 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -347,3 +347,88 @@ class PT_FLAGS(Flag): MODULE_MAXIMUM_CORE_SIZE = 20000000 MODULE_MAXIMUM_CORE_TEXT_SIZE = 20000000 MODULE_MINIMUM_SIZE = 4096 + + +TAINT_FLAGS = { + "P": { + "shift": 1 << 0, + "desc": "PROPRIETARY_MODULE", + "when_present": True, + "module": True, + }, + "G": { + "shift": 1 << 0, + "desc": "PROPRIETARY_MODULE", + "when_present": False, + "module": True, + }, + "F": { + "shift": 1 << 1, + "desc": "FORCED_MODULE", + "when_present": True, + "module": False, + }, + # CPU_OUT_OF_SPEC was TAINT_UNSAFE_SMP on < 3.15-rc1 : https://lore.kernel.org/linux-kernel//20140303080432.GA25489@localhost/t/#:~:text=liked%20your%20proposal%3A-,%3E%20Right,-%2C%20I%20was%20about + "S": { + "shift": 1 << 2, + "desc": "CPU_OUT_OF_SPEC", + "when_present": True, + "module": False, + }, + "R": { + "shift": 1 << 3, + "desc": "FORCED_RMMOD", + "when_present": True, + "module": False, + }, + "M": { + "shift": 1 << 4, + "desc": "MACHINE_CHECK", + "when_present": True, + "module": False, + }, + "B": {"shift": 1 << 5, "desc": "BAD_PAGE", "when_present": True, "module": False}, + "U": {"shift": 1 << 6, "desc": "USER", "when_present": True, "module": False}, + "D": {"shift": 1 << 7, "desc": "DIE", "when_present": True, "module": False}, + "A": { + "shift": 1 << 8, + "desc": "OVERRIDDEN_ACPI_TABLE", + "when_present": True, + "module": False, + }, + "W": {"shift": 1 << 9, "desc": "WARN", "when_present": True, "module": False}, + "C": {"shift": 1 << 10, "desc": "CRAP", "when_present": True, "module": True}, + "I": { + "shift": 1 << 11, + "desc": "FIRMWARE_WORKAROUND", + "when_present": True, + "module": False, + }, + "O": {"shift": 1 << 12, "desc": "OOT_MODULE", "when_present": True, "module": True}, + "E": { + "shift": 1 << 13, + "desc": "UNSIGNED_MODULE", + "when_present": True, + "module": True, + }, + "L": { + "shift": 1 << 14, + "desc": "SOFTLOCKUP", + "when_present": True, + "module": False, + }, + "K": {"shift": 1 << 15, "desc": "LIVEPATCH", "when_present": True, "module": True}, + "X": {"shift": 1 << 16, "desc": "AUX", "when_present": True, "module": True}, + "T": {"shift": 1 << 17, "desc": "RANDSTRUCT", "when_present": True, "module": True}, + "N": {"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 +""" From e1b343a284436ee4d92a7b8a6daf0a98dd01fdeb Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 31 Oct 2024 12:33:54 +0100 Subject: [PATCH 002/141] add module taints parsing apis --- .../symbols/linux/extensions/__init__.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index aa3e8c675..89e2cde27 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -279,6 +279,77 @@ class module(generic.GenericIntelProcess): return None + def _module_flags_taints_pre_4_10_rc1(self) -> str: + """Convert the module's taints value to a 1-1 character mapping. + Relies on statically defined taints mappings in the framework. + + Returns: + The raw taints string. + """ + taints_string = "" + for char, infos in linux_constants.TAINT_FLAGS.items(): + if infos["module"] and self.taints_value & infos["shift"]: + taints_string += char + + return taints_string + + def _module_flags_taints_post_4_10_rc1(self) -> 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 */ + }; + + Returns: + The raw taints string. + """ + taints_string = "" + for i, taint_flag in enumerate(self.taint_flags_list): + c_true = chr(taint_flag.c_true) + c_false = chr(taint_flag.c_false) + if taint_flag.module and (self.taints_value & (1 << i)): + taints_string += c_true + elif taint_flag.module and c_false != " ": + taints_string += c_false + + return taints_string + + def get_taints_as_plain_string(self) -> str: + """Convert the module's taints value to a 1-1 character mapping. + + Returns: + The raw taints string. + + Documentation: + - module_flags_taint kernel function + """ + + if self.taint_flags_list: + return self._module_flags_taints_post_4_10_rc1() + return self._module_flags_taints_pre_4_10_rc1() + + def get_taints_parsed(self) -> List[str]: + """Convert the module's taints string to a 1-1 descriptor mapping. + + Returns: + A comprehensive (user-friendly) taint descriptor list. + + Documentation: + - module_flags_taint kernel function + """ + comprehensive_taints = [] + for c in self.get_taints_as_plain_string(): + infos = linux_constants.TAINT_FLAGS.get(c) + if not infos: + comprehensive_taints.append(f"") + elif infos["when_present"]: + comprehensive_taints.append(infos["desc"]) + + return comprehensive_taints + @property def section_symtab(self): if self.has_member("kallsyms"): @@ -307,6 +378,17 @@ class module(generic.GenericIntelProcess): return self.strtab raise AttributeError("module -> strtab: Unable to get strtab") + @property + def taints_value(self) -> int: + return self.taints + + @property + def taint_flags_list(self) -> Optional[List[interfaces.objects.ObjectInterface]]: + kernel = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + if kernel.has_symbol("taint_flags"): + return list(kernel.object_from_symbol("taint_flags")) + return None + class task_struct(generic.GenericIntelProcess): def add_process_layer( From c88ebe89270355d188770c74b39eb8acef1f3549 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 31 Oct 2024 12:35:54 +0100 Subject: [PATCH 003/141] introduce modxview linux plugin --- .../framework/plugins/linux/modxview.py | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 volatility3/framework/plugins/linux/modxview.py diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py new file mode 100644 index 000000000..66b644164 --- /dev/null +++ b/volatility3/framework/plugins/linux/modxview.py @@ -0,0 +1,195 @@ +# 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, Set, 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 + +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, 11, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=architectures.LINUX_ARCHS, + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="check_modules", + plugin=check_modules.Check_modules, + version=(0, 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 run_lsmod( + cls, context: interfaces.context.ContextInterface, kernel_name: str + ) -> List[extensions.module]: + """Wrapper for the lsmod plugin.""" + return list(lsmod.Lsmod.list_modules(context, kernel_name)) + + @classmethod + def run_check_modules( + cls, + context: interfaces.context.ContextInterface, + kernel_name: str, + ) -> List[extensions.module]: + """Wrapper for the check_modules plugin. + Here, we extract the /sys/module/ list.""" + kernel = context.modules[kernel_name] + sysfs_modules: dict = check_modules.Check_modules.get_kset_modules( + context, kernel_name + ) + + # Convert get_kset_modules() offsets back to module objects + return [ + kernel.object(object_type="module", offset=m_offset, absolute=True) + for m_offset in sysfs_modules.values() + ] + + @classmethod + def run_hidden_modules( + cls, + context: interfaces.context.ContextInterface, + kernel_name: str, + known_modules_addresses: Set[int], + ) -> List[extensions.module]: + """Wrapper for the hidden_modules plugin.""" + modules_memory_boundaries = ( + hidden_modules.Hidden_modules.get_modules_memory_boundaries( + context, kernel_name + ) + ) + return list( + hidden_modules.Hidden_modules.get_hidden_modules( + context, + kernel_name, + known_modules_addresses, + modules_memory_boundaries, + ) + ) + + @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 + 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. + + 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 = {} + run_results["lsmod"] = cls.run_lsmod(context, kernel_name) + run_results["check_modules"] = cls.run_check_modules(context, kernel_name) + if run_hidden_modules: + known_module_addresses = set( + context.layers[kernel.layer_name].canonicalize(module.vol.offset) + for module in run_results["lsmod"] + run_results["check_modules"] + ) + run_results["hidden_modules"] = cls.run_hidden_modules( + context, kernel_name, known_module_addresses + ) + + return run_results + + def _generator(self): + kernel_name = self.config["kernel"] + run_results = self.run_modules_scanners(self.context, kernel_name) + modules_offsets = {} + for key in ["lsmod", "check_modules", "hidden_modules"]: + modules_offsets[key] = set(module.vol.offset for module in run_results[key]) + + seen_addresses = set() + for modules_list in run_results.values(): + for module in modules_list: + if module.vol.offset in seen_addresses: + continue + seen_addresses.add(module.vol.offset) + + if self.config.get("plain_taints"): + taints = module.get_taints_as_plain_string() + else: + taints = ",".join(module.get_taints_parsed()) + + yield ( + 0, + ( + module.get_name() or NotAvailableValue(), + format_hints.Hex(module.vol.offset), + module.vol.offset in modules_offsets["lsmod"], + module.vol.offset in modules_offsets["check_modules"], + module.vol.offset in modules_offsets["hidden_modules"], + taints or NotAvailableValue(), + ), + ) + + def run(self): + columns = [ + ("Name", str), + ("Address", format_hints.Hex), + ("In /proc/modules", bool), + ("In /sys/module/", bool), + ("Hidden", bool), + ("Taints", str), + ] + + return TreeGrid( + columns, + self._generator(), + ) From 9440f53429a1f9c7d77d51eeb75c2b5938da040f Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 1 Nov 2024 15:05:25 +0100 Subject: [PATCH 004/141] use a dict of dataclasses for taint_flags --- .../framework/constants/linux/__init__.py | 112 +++++++----------- .../symbols/linux/extensions/__init__.py | 12 +- 2 files changed, 47 insertions(+), 77 deletions(-) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 9f25c9225..6cf8585f5 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -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__" @@ -349,78 +350,47 @@ 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": { - "shift": 1 << 0, - "desc": "PROPRIETARY_MODULE", - "when_present": True, - "module": True, - }, - "G": { - "shift": 1 << 0, - "desc": "PROPRIETARY_MODULE", - "when_present": False, - "module": True, - }, - "F": { - "shift": 1 << 1, - "desc": "FORCED_MODULE", - "when_present": True, - "module": False, - }, - # CPU_OUT_OF_SPEC was TAINT_UNSAFE_SMP on < 3.15-rc1 : https://lore.kernel.org/linux-kernel//20140303080432.GA25489@localhost/t/#:~:text=liked%20your%20proposal%3A-,%3E%20Right,-%2C%20I%20was%20about - "S": { - "shift": 1 << 2, - "desc": "CPU_OUT_OF_SPEC", - "when_present": True, - "module": False, - }, - "R": { - "shift": 1 << 3, - "desc": "FORCED_RMMOD", - "when_present": True, - "module": False, - }, - "M": { - "shift": 1 << 4, - "desc": "MACHINE_CHECK", - "when_present": True, - "module": False, - }, - "B": {"shift": 1 << 5, "desc": "BAD_PAGE", "when_present": True, "module": False}, - "U": {"shift": 1 << 6, "desc": "USER", "when_present": True, "module": False}, - "D": {"shift": 1 << 7, "desc": "DIE", "when_present": True, "module": False}, - "A": { - "shift": 1 << 8, - "desc": "OVERRIDDEN_ACPI_TABLE", - "when_present": True, - "module": False, - }, - "W": {"shift": 1 << 9, "desc": "WARN", "when_present": True, "module": False}, - "C": {"shift": 1 << 10, "desc": "CRAP", "when_present": True, "module": True}, - "I": { - "shift": 1 << 11, - "desc": "FIRMWARE_WORKAROUND", - "when_present": True, - "module": False, - }, - "O": {"shift": 1 << 12, "desc": "OOT_MODULE", "when_present": True, "module": True}, - "E": { - "shift": 1 << 13, - "desc": "UNSIGNED_MODULE", - "when_present": True, - "module": True, - }, - "L": { - "shift": 1 << 14, - "desc": "SOFTLOCKUP", - "when_present": True, - "module": False, - }, - "K": {"shift": 1 << 15, "desc": "LIVEPATCH", "when_present": True, "module": True}, - "X": {"shift": 1 << 16, "desc": "AUX", "when_present": True, "module": True}, - "T": {"shift": 1 << 17, "desc": "RANDSTRUCT", "when_present": True, "module": True}, - "N": {"shift": 1 << 18, "desc": "TEST", "when_present": True, "module": True}, + "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. diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 89e2cde27..42c1a470d 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -287,8 +287,8 @@ class module(generic.GenericIntelProcess): The raw taints string. """ taints_string = "" - for char, infos in linux_constants.TAINT_FLAGS.items(): - if infos["module"] and self.taints_value & infos["shift"]: + for char, taint_flag in linux_constants.TAINT_FLAGS.items(): + if taint_flag.module and self.taints_value & taint_flag.shift: taints_string += char return taints_string @@ -342,11 +342,11 @@ class module(generic.GenericIntelProcess): """ comprehensive_taints = [] for c in self.get_taints_as_plain_string(): - infos = linux_constants.TAINT_FLAGS.get(c) - if not infos: + taint_flag = linux_constants.TAINT_FLAGS.get(c) + if not taint_flag: comprehensive_taints.append(f"") - elif infos["when_present"]: - comprehensive_taints.append(infos["desc"]) + elif taint_flag.when_present: + comprehensive_taints.append(taint_flag.desc) return comprehensive_taints From 9d08c4681ae1cf18ddf4bd53ff970f6a9bc26573 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 1 Nov 2024 15:07:23 +0100 Subject: [PATCH 005/141] add module offset to seen_addresses --- volatility3/framework/plugins/linux/modxview.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index 66b644164..f44984926 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -116,6 +116,7 @@ class Modxview(interfaces.plugins.PluginInterface): for module in modules: if deduplicate and module.vol.offset in seen_addresses: continue + seen_addresses.add(module.vol.offset) yield module @classmethod From b209ea36a284ae1a75ba18222cbe8db1c1eede4f Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 1 Nov 2024 15:12:52 +0100 Subject: [PATCH 006/141] remove slashes in columns --- volatility3/framework/plugins/linux/modxview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index f44984926..d79f5e7a9 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -184,8 +184,8 @@ class Modxview(interfaces.plugins.PluginInterface): columns = [ ("Name", str), ("Address", format_hints.Hex), - ("In /proc/modules", bool), - ("In /sys/module/", bool), + ("In procfs", bool), + ("In sysfs", bool), ("Hidden", bool), ("Taints", str), ] From c7259037356fde4cf6120acddecbae4af16c9ea0 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 7 Nov 2024 18:08:58 +0100 Subject: [PATCH 007/141] introduce scatter-gather scatterlists --- .../framework/symbols/linux/__init__.py | 1 + .../symbols/linux/extensions/__init__.py | 104 ++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 3289775b6..e8a3d5240 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -43,6 +43,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) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index aa3e8c675..0dd657372 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2410,3 +2410,107 @@ 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) -> 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) -> 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, + ) -> Iterator[bytes]: + """Traverse a scatterlist to gather content located at each + dma_address position. + + Returns: + An iterator of bytes + """ + physical_layer = self._context.layers["memory_layer"] + for sg in self.for_each_sg(): + yield from physical_layer.read(sg.dma_address, sg._sg_dma_len()) From 8f33aaf5b4ee859b12ca35347144597bec59ee1b Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 8 Nov 2024 17:45:53 +0100 Subject: [PATCH 008/141] Optional type hints --- volatility3/framework/symbols/linux/extensions/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0dd657372..e5a074a49 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2445,7 +2445,7 @@ class scatterlist(objects.StructType): """ return self._context.layers[self.vol.layer_name].page_size // self.vol.size - def _sg_next(self) -> interfaces.objects.ObjectInterface: + def _sg_next(self) -> Optional[interfaces.objects.ObjectInterface]: """Get the next scatterlist struct from the list. Based on kernel's sg_next. @@ -2476,7 +2476,7 @@ class scatterlist(objects.StructType): ) return sg - def for_each_sg(self) -> Iterator[interfaces.objects.ObjectInterface]: + 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() @@ -2504,7 +2504,7 @@ class scatterlist(objects.StructType): def get_content( self, - ) -> Iterator[bytes]: + ) -> Optional[Iterator[bytes]]: """Traverse a scatterlist to gather content located at each dma_address position. From 485ef894e113cf68eb1acaef3c679b675639281d Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 8 Nov 2024 17:49:08 +0100 Subject: [PATCH 009/141] remove taints_value overload attr --- .../framework/symbols/linux/extensions/__init__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 42c1a470d..f9f72c161 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -288,7 +288,7 @@ class module(generic.GenericIntelProcess): """ taints_string = "" for char, taint_flag in linux_constants.TAINT_FLAGS.items(): - if taint_flag.module and self.taints_value & taint_flag.shift: + if taint_flag.module and self.taints & taint_flag.shift: taints_string += char return taints_string @@ -310,7 +310,7 @@ class module(generic.GenericIntelProcess): for i, taint_flag in enumerate(self.taint_flags_list): c_true = chr(taint_flag.c_true) c_false = chr(taint_flag.c_false) - if taint_flag.module and (self.taints_value & (1 << i)): + if taint_flag.module and (self.taints & (1 << i)): taints_string += c_true elif taint_flag.module and c_false != " ": taints_string += c_false @@ -378,10 +378,6 @@ class module(generic.GenericIntelProcess): return self.strtab raise AttributeError("module -> strtab: Unable to get strtab") - @property - def taints_value(self) -> int: - return self.taints - @property def taint_flags_list(self) -> Optional[List[interfaces.objects.ObjectInterface]]: kernel = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) From dd3542b127b751ad083c91be4e8ffd373a1c74f7 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 8 Nov 2024 17:51:48 +0100 Subject: [PATCH 010/141] explicit loop iterator --- volatility3/framework/symbols/linux/extensions/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index f9f72c161..402f8c9c6 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -341,10 +341,10 @@ class module(generic.GenericIntelProcess): - module_flags_taint kernel function """ comprehensive_taints = [] - for c in self.get_taints_as_plain_string(): - taint_flag = linux_constants.TAINT_FLAGS.get(c) + for character in self.get_taints_as_plain_string(): + taint_flag = linux_constants.TAINT_FLAGS.get(character) if not taint_flag: - comprehensive_taints.append(f"") + comprehensive_taints.append(f"") elif taint_flag.when_present: comprehensive_taints.append(taint_flag.desc) From 20f15d3591a5e6340ecd2d3628406c01aef71924 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 2 Dec 2024 11:34:49 +0100 Subject: [PATCH 011/141] modular physical_layer access --- volatility3/framework/symbols/linux/extensions/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index e5a074a49..2d77b8562 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2511,6 +2511,10 @@ class scatterlist(objects.StructType): Returns: An iterator of bytes """ - physical_layer = self._context.layers["memory_layer"] + # Either "physical" is layer-1 because this is a module layer, either "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()) From f0f3bb65581e433cc7b44c2e877a1e95c927e17e Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 17 Dec 2024 18:31:41 +0000 Subject: [PATCH 012/141] Core: Start to fix up the typing in ModuleCollection Fixes #1418 --- volatility3/framework/contexts/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index 6961d9328..1a55656b3 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -11,7 +11,7 @@ without them interfering with each other. import functools import hashlib import logging -from typing import Callable, Iterable, List, Optional, Set, Tuple, Union +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 @@ -386,10 +386,9 @@ 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: + def __init__(self, modules: Optional[List[SizedModule]] = None) -> None: self._prefix_count = {} + self._modules: Dict[str, SizedModule] = {} super().__init__(modules) def deduplicate(self) -> "ModuleCollection": @@ -402,9 +401,9 @@ 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: From a0b169cf6b1f0a5d8ced886ff855e7ad7dd791c0 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 17 Dec 2024 18:34:19 +0000 Subject: [PATCH 013/141] This PR does not strictly change any interfaces, just the inner workings of a function. --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 55ef19e4b..2ea034176 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change VERSION_MINOR = 12 # Number of changes that only add to the interface -VERSION_PATCH = 0 # Number of changes that do not change the interface +VERSION_PATCH = 1 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = ( From 1b8f831fda1fc0d47eaf81144dec81354dca8490 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 17 Dec 2024 18:37:39 +0000 Subject: [PATCH 014/141] Core: Also fix up the interface to match the concrete classes --- volatility3/framework/interfaces/context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index 8b5e816e8..e85429732 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -295,6 +295,7 @@ 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 def symbols(self) -> List: """Lists the symbols contained in the symbol table for this module""" From f3d7647433a727a5bb7bc8c91fa3803ad44a6bf4 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 22 Dec 2024 15:48:29 +0100 Subject: [PATCH 015/141] unify Tainting parsing capabilities --- .../framework/symbols/linux/__init__.py | 121 ++++++++++++++++++ .../symbols/linux/extensions/__init__.py | 74 ++--------- 2 files changed, 131 insertions(+), 64 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 0230a9c48..832b1de9b 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -11,6 +11,7 @@ from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.objects import utility from volatility3.framework.symbols import intermed from volatility3.framework.symbols.linux import extensions +from volatility3.framework.constants import linux as linux_constants class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): @@ -830,3 +831,123 @@ class PageCache: page = self.vmlinux.object("page", offset=page_addr, absolute=True) if page: yield page + + +class Tainting: + """Tainted kernel and modules parsing capabilities. + + Relevant kernel functions: + - modules: module_flags_taint + - kernel: print_tainted + """ + + def __init__( + self, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + ): + self.kernel = context.modules[kernel_module_name] + + @property + def kernel_taint_flags_list( + self, + ) -> Optional[List[interfaces.objects.ObjectInterface]]: + if self.kernel.has_symbol("taint_flags"): + return list(self.kernel.object_from_symbol("taint_flags")) + return None + + def _module_flags_taint_pre_4_10_rc1( + self, 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 is_module != taint_flag.module: + continue + + if taints & taint_flag.shift: + taints_string += char + + return taints_string + + def _module_flags_taint_post_4_10_rc1( + self, 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 i, taint_flag in enumerate(self.kernel_taint_flags_list): + if is_module and is_module != taint_flag.module: + continue + c_true = chr(taint_flag.c_true) + c_false = chr(taint_flag.c_false) + if taints & (1 << i): + taints_string += c_true + elif c_false != " ": + taints_string += c_false + + return taints_string + + def get_taints_as_plain_string(self, 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 + s + Returns: + The raw taints string. + + Documentation: + - module_flags_taint kernel function + """ + + if self.kernel_taint_flags_list: + return self._module_flags_taint_post_4_10_rc1(taints, is_module) + return self._module_flags_taint_pre_4_10_rc1(taints, is_module) + + def get_taints_parsed(self, 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 self.get_taints_as_plain_string(taints, is_module): + taint_flag = linux_constants.TAINT_FLAGS.get(character) + if not taint_flag: + comprehensive_taints.append(f"") + elif taint_flag.when_present: + comprehensive_taints.append(taint_flag.desc) + + return comprehensive_taints diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0ecf731f4..075a83ae8 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -279,76 +279,29 @@ class module(generic.GenericIntelProcess): return None - def _module_flags_taints_pre_4_10_rc1(self) -> str: - """Convert the module's taints value to a 1-1 character mapping. - Relies on statically defined taints mappings in the framework. - - Returns: - The raw taints string. - """ - taints_string = "" - for char, taint_flag in linux_constants.TAINT_FLAGS.items(): - if taint_flag.module and self.taints & taint_flag.shift: - taints_string += char - - return taints_string - - def _module_flags_taints_post_4_10_rc1(self) -> 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 */ - }; - - Returns: - The raw taints string. - """ - taints_string = "" - for i, taint_flag in enumerate(self.taint_flags_list): - c_true = chr(taint_flag.c_true) - c_false = chr(taint_flag.c_false) - if taint_flag.module and (self.taints & (1 << i)): - taints_string += c_true - elif taint_flag.module and c_false != " ": - taints_string += c_false - - return taints_string - def get_taints_as_plain_string(self) -> str: """Convert the module's taints value to a 1-1 character mapping. + Convenient wrapper around framework's Tainting capabilities. Returns: The raw taints string. - - Documentation: - - module_flags_taint kernel function """ - - if self.taint_flags_list: - return self._module_flags_taints_post_4_10_rc1() - return self._module_flags_taints_pre_4_10_rc1() + return linux.Tainting( + self._context, + linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name, + ).get_taints_as_plain_string(self.taints, True) def get_taints_parsed(self) -> List[str]: """Convert the module's taints string to a 1-1 descriptor mapping. + Convenient wrapper around framework's Tainting capabilities. Returns: A comprehensive (user-friendly) taint descriptor list. - - Documentation: - - module_flags_taint kernel function """ - comprehensive_taints = [] - for character in self.get_taints_as_plain_string(): - taint_flag = linux_constants.TAINT_FLAGS.get(character) - if not taint_flag: - comprehensive_taints.append(f"") - elif taint_flag.when_present: - comprehensive_taints.append(taint_flag.desc) - - return comprehensive_taints + return linux.Tainting( + self._context, + linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name, + ).get_taints_parsed(self.taints, True) @property def section_symtab(self): @@ -376,13 +329,6 @@ class module(generic.GenericIntelProcess): return self.strtab raise AttributeError("Unable to get strtab") - @property - def taint_flags_list(self) -> Optional[List[interfaces.objects.ObjectInterface]]: - kernel = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - if kernel.has_symbol("taint_flags"): - return list(kernel.object_from_symbol("taint_flags")) - return None - class task_struct(generic.GenericIntelProcess): def add_process_layer( From c6209800bdc810627f8757881f32e1cf0cfb6f17 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 1 Jan 2025 13:23:02 +0000 Subject: [PATCH 016/141] Core: Fix up issues when resolving merge --- volatility3/framework/constants/_version.py | 2 +- volatility3/framework/interfaces/context.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 02402c5c9..694375538 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -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 = 15 # Number of changes that only add to the interface +VERSION_MINOR = 14 # Number of changes that only add to the interface VERSION_PATCH = 1 # Number of changes that do not change the interface VERSION_SUFFIX = "" diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index 30840a5b9..0b2ae0cc9 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -306,6 +306,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface): @abstractmethod def symbols(self) -> List: """Lists 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]: From 443f7afc9c95c5738cd9eec841ca834860914759 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 1 Jan 2025 13:59:20 +0000 Subject: [PATCH 017/141] Core: Fix code scanning issue concerning equality --- volatility3/framework/contexts/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index a9ec4ac69..e1fb56d94 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -11,6 +11,7 @@ without them interfering with each other. import functools import hashlib import logging +import re from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union from volatility3.framework import constants, interfaces, symbols, exceptions @@ -387,7 +388,6 @@ class ModuleCollection(interfaces.context.ModuleContainer): contents.""" def __init__(self, modules: Optional[List[SizedModule]] = None) -> None: - self._prefix_count = {} self._modules: Dict[str, SizedModule] = {} super().__init__(modules) @@ -408,13 +408,12 @@ class ModuleCollection(interfaces.context.ModuleContainer): 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 From 6be039a7e5ddf23f918f073428cbab7e3604f4e7 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 1 Jan 2025 14:05:02 +0000 Subject: [PATCH 018/141] Core: Fix black error --- volatility3/framework/interfaces/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index 0b2ae0cc9..48c066e96 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -306,7 +306,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface): @abstractmethod def symbols(self) -> List: """Lists the symbols contained in the symbol table for this module""" - raise NotImplementedError("Symbols property has not been implemented.") + raise NotImplementedError("Symbols property has not been implemented.") @abstractmethod def get_symbols_by_absolute_location(self, offset: int, size: int = 0) -> List[str]: From dda104bd62b9f5f7b9c0208832c6d788c0ebd2ea Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 2 Jan 2025 16:37:14 +0100 Subject: [PATCH 019/141] move out Tainting capabilities --- .../framework/symbols/linux/__init__.py | 121 ------------------ 1 file changed, 121 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 832b1de9b..0230a9c48 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -11,7 +11,6 @@ from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.objects import utility from volatility3.framework.symbols import intermed from volatility3.framework.symbols.linux import extensions -from volatility3.framework.constants import linux as linux_constants class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): @@ -831,123 +830,3 @@ class PageCache: page = self.vmlinux.object("page", offset=page_addr, absolute=True) if page: yield page - - -class Tainting: - """Tainted kernel and modules parsing capabilities. - - Relevant kernel functions: - - modules: module_flags_taint - - kernel: print_tainted - """ - - def __init__( - self, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - ): - self.kernel = context.modules[kernel_module_name] - - @property - def kernel_taint_flags_list( - self, - ) -> Optional[List[interfaces.objects.ObjectInterface]]: - if self.kernel.has_symbol("taint_flags"): - return list(self.kernel.object_from_symbol("taint_flags")) - return None - - def _module_flags_taint_pre_4_10_rc1( - self, 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 is_module != taint_flag.module: - continue - - if taints & taint_flag.shift: - taints_string += char - - return taints_string - - def _module_flags_taint_post_4_10_rc1( - self, 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 i, taint_flag in enumerate(self.kernel_taint_flags_list): - if is_module and is_module != taint_flag.module: - continue - c_true = chr(taint_flag.c_true) - c_false = chr(taint_flag.c_false) - if taints & (1 << i): - taints_string += c_true - elif c_false != " ": - taints_string += c_false - - return taints_string - - def get_taints_as_plain_string(self, 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 - s - Returns: - The raw taints string. - - Documentation: - - module_flags_taint kernel function - """ - - if self.kernel_taint_flags_list: - return self._module_flags_taint_post_4_10_rc1(taints, is_module) - return self._module_flags_taint_pre_4_10_rc1(taints, is_module) - - def get_taints_parsed(self, 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 self.get_taints_as_plain_string(taints, is_module): - taint_flag = linux_constants.TAINT_FLAGS.get(character) - if not taint_flag: - comprehensive_taints.append(f"") - elif taint_flag.when_present: - comprehensive_taints.append(taint_flag.desc) - - return comprehensive_taints From 2a5f38ebad48e0d729b3b22caac84bd4209f20a2 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 2 Jan 2025 16:39:07 +0100 Subject: [PATCH 020/141] introduce versioned Linux utilities --- .../framework/symbols/linux/utilities/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 volatility3/framework/symbols/linux/utilities/__init__.py diff --git a/volatility3/framework/symbols/linux/utilities/__init__.py b/volatility3/framework/symbols/linux/utilities/__init__.py new file mode 100644 index 000000000..4225d444b --- /dev/null +++ b/volatility3/framework/symbols/linux/utilities/__init__.py @@ -0,0 +1,11 @@ +from volatility3 import framework +from volatility3.framework import interfaces + + +class LinuxUtilityInterface(interfaces.configuration.VersionableInterface): + """Class with multiple useful Linux functions surrounding a specific piece of functionality.""" + + _version = (2, 1, 1) + _required_framework_version = (2, 0, 0) + + framework.require_interface_version(*_required_framework_version) From 8bc62598f4530bdf2fb99aeb725e5b8f3e0d8cd5 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 2 Jan 2025 16:39:57 +0100 Subject: [PATCH 021/141] initial tainting utilities --- .../symbols/linux/utilities/tainting.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 volatility3/framework/symbols/linux/utilities/tainting.py diff --git a/volatility3/framework/symbols/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py new file mode 100644 index 000000000..e6d75a963 --- /dev/null +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -0,0 +1,130 @@ +from volatility3 import framework +from volatility3.framework import interfaces +from volatility3.framework.symbols.linux.utilities import LinuxUtilityInterface +from volatility3.framework.constants import linux as linux_constants +from typing import List, Optional + + +class Tainting(LinuxUtilityInterface): + """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, 14, 0) + + framework.require_interface_version(*_required_framework_version) + + def __init__( + self, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + ): + self.kernel = context.modules[kernel_module_name] + + @property + def _kernel_taint_flags_list( + self, + ) -> Optional[List[interfaces.objects.ObjectInterface]]: + if self.kernel.has_symbol("taint_flags"): + return list(self.kernel.object_from_symbol("taint_flags")) + return None + + def _module_flags_taint_pre_4_10_rc1( + self, 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 is_module != taint_flag.module: + continue + + if taints & taint_flag.shift: + taints_string += char + + return taints_string + + def _module_flags_taint_post_4_10_rc1( + self, 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 i, taint_flag in enumerate(self._kernel_taint_flags_list): + if is_module and is_module != taint_flag.module: + continue + c_true = chr(taint_flag.c_true) + c_false = chr(taint_flag.c_false) + if taints & (1 << i): + taints_string += c_true + elif c_false != " ": + taints_string += c_false + + return taints_string + + def get_taints_as_plain_string(self, 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 + s + Returns: + The raw taints string. + + Documentation: + - module_flags_taint kernel function + """ + + if self._kernel_taint_flags_list: + return self._module_flags_taint_post_4_10_rc1(taints, is_module) + return self._module_flags_taint_pre_4_10_rc1(taints, is_module) + + def get_taints_parsed(self, 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 self.get_taints_as_plain_string(taints, is_module): + taint_flag = linux_constants.TAINT_FLAGS.get(character) + if not taint_flag: + comprehensive_taints.append(f"") + elif taint_flag.when_present: + comprehensive_taints.append(taint_flag.desc) + + return comprehensive_taints From 3105a31964a1a36420281bd995d983a81b161e97 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 2 Jan 2025 16:40:49 +0100 Subject: [PATCH 022/141] leverage Tainting from separated Linux utilities --- volatility3/framework/symbols/linux/extensions/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 075a83ae8..ac07d2def 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -19,7 +19,7 @@ from volatility3.framework.layers import linear from volatility3.framework.objects import utility from volatility3.framework.symbols import generic, linux, intermed from volatility3.framework.symbols.linux.extensions import elf - +from volatility3.framework.symbols.linux.utilities import tainting vollog = logging.getLogger(__name__) @@ -286,7 +286,7 @@ class module(generic.GenericIntelProcess): Returns: The raw taints string. """ - return linux.Tainting( + return tainting.Tainting( self._context, linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name, ).get_taints_as_plain_string(self.taints, True) @@ -298,7 +298,7 @@ class module(generic.GenericIntelProcess): Returns: A comprehensive (user-friendly) taint descriptor list. """ - return linux.Tainting( + return tainting.Tainting( self._context, linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name, ).get_taints_parsed(self.taints, True) From 6e4213e321b96dd8f0b35df6c87aa8426698a42c Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 2 Jan 2025 16:41:37 +0100 Subject: [PATCH 023/141] update tainting requirements to new versioned utilities --- volatility3/framework/plugins/linux/modxview.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index d79f5e7a9..c97864a87 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -9,6 +9,7 @@ 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__) @@ -18,7 +19,7 @@ class Modxview(interfaces.plugins.PluginInterface): spot modules presence and taints.""" _version = (1, 0, 0) - _required_framework_version = (2, 11, 0) + _required_framework_version = (2, 14, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -28,6 +29,9 @@ class Modxview(interfaces.plugins.PluginInterface): 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) ), From 0a3502697cca4dac2ac2f39896ecfaaef507ac9b Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 2 Jan 2025 16:42:16 +0100 Subject: [PATCH 024/141] 2.13.0 -> 2.14.0 bump --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 11edc07d8..9ca2d0a5b 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -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 = 13 # Number of changes that only add to the interface +VERSION_MINOR = 14 # Number of changes that only add to the interface VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" From 89b8da8c39fe14f699d711c95a8311ec1e21331e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 2 Jan 2025 16:48:22 +0100 Subject: [PATCH 025/141] make self.kernel private and call parent __init__ --- .../framework/symbols/linux/utilities/tainting.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py index e6d75a963..603215961 100644 --- a/volatility3/framework/symbols/linux/utilities/tainting.py +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -22,15 +22,18 @@ class Tainting(LinuxUtilityInterface): self, context: interfaces.context.ContextInterface, kernel_module_name: str, + *args, + **kwargs, ): - self.kernel = context.modules[kernel_module_name] + super().__init__(*args, **kwargs) + self._kernel = context.modules[kernel_module_name] @property def _kernel_taint_flags_list( self, ) -> Optional[List[interfaces.objects.ObjectInterface]]: - if self.kernel.has_symbol("taint_flags"): - return list(self.kernel.object_from_symbol("taint_flags")) + if self._kernel.has_symbol("taint_flags"): + return list(self._kernel.object_from_symbol("taint_flags")) return None def _module_flags_taint_pre_4_10_rc1( From 4a34b988d1e4cb02e33e555c3e2ed63d808e5028 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 3 Jan 2025 13:21:09 +0100 Subject: [PATCH 026/141] minor readability adjustments --- .../framework/symbols/linux/utilities/tainting.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py index 603215961..f7f6c83ec 100644 --- a/volatility3/framework/symbols/linux/utilities/tainting.py +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -51,7 +51,7 @@ class Tainting(LinuxUtilityInterface): """ taints_string = "" for char, taint_flag in linux_constants.TAINT_FLAGS.items(): - if is_module and is_module != taint_flag.module: + if is_module and not taint_flag.module: continue if taints & taint_flag.shift: @@ -79,12 +79,12 @@ class Tainting(LinuxUtilityInterface): The raw taints string. """ taints_string = "" - for i, taint_flag in enumerate(self._kernel_taint_flags_list): - if is_module and is_module != taint_flag.module: + for taint_bit, taint_flag in enumerate(self._kernel_taint_flags_list): + 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 << i): + if taints & (1 << taint_bit): taints_string += c_true elif c_false != " ": taints_string += c_false @@ -97,7 +97,6 @@ class Tainting(LinuxUtilityInterface): Args: taints: The taints value, represented by an integer is_module: Indicates if the taints value is associated with a built-in/LKM module - s Returns: The raw taints string. From 38c5cc168f93a4d1a5cab2a6c9b071cf32e22fc2 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 3 Jan 2025 13:23:27 +0100 Subject: [PATCH 027/141] bump framework req to 2.16.0 --- volatility3/framework/plugins/linux/modxview.py | 2 +- volatility3/framework/symbols/linux/utilities/tainting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index c97864a87..042930740 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -19,7 +19,7 @@ class Modxview(interfaces.plugins.PluginInterface): spot modules presence and taints.""" _version = (1, 0, 0) - _required_framework_version = (2, 14, 0) + _required_framework_version = (2, 16, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: diff --git a/volatility3/framework/symbols/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py index f7f6c83ec..fc2f94109 100644 --- a/volatility3/framework/symbols/linux/utilities/tainting.py +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -14,7 +14,7 @@ class Tainting(LinuxUtilityInterface): """ _version = (1, 0, 0) - _required_framework_version = (2, 14, 0) + _required_framework_version = (2, 16, 0) framework.require_interface_version(*_required_framework_version) From a7b4e2fb45bef981eb54c44a5e0cef87b879058f Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 6 Jan 2025 00:02:15 +0100 Subject: [PATCH 028/141] version check_modules --- volatility3/framework/plugins/linux/check_modules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/linux/check_modules.py b/volatility3/framework/plugins/linux/check_modules.py index 9b3594c5e..0ed638d9c 100644 --- a/volatility3/framework/plugins/linux/check_modules.py +++ b/volatility3/framework/plugins/linux/check_modules.py @@ -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 From 2d262e7acf5c9aabb32240c01cd57890b7d57647 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 6 Jan 2025 00:20:56 +0100 Subject: [PATCH 029/141] cut unnecessary intermediate LinuxUtilityInterface --- .../framework/symbols/linux/utilities/__init__.py | 11 ----------- .../framework/symbols/linux/utilities/tainting.py | 5 ++--- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/__init__.py b/volatility3/framework/symbols/linux/utilities/__init__.py index 4225d444b..e69de29bb 100644 --- a/volatility3/framework/symbols/linux/utilities/__init__.py +++ b/volatility3/framework/symbols/linux/utilities/__init__.py @@ -1,11 +0,0 @@ -from volatility3 import framework -from volatility3.framework import interfaces - - -class LinuxUtilityInterface(interfaces.configuration.VersionableInterface): - """Class with multiple useful Linux functions surrounding a specific piece of functionality.""" - - _version = (2, 1, 1) - _required_framework_version = (2, 0, 0) - - framework.require_interface_version(*_required_framework_version) diff --git a/volatility3/framework/symbols/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py index fc2f94109..29d7d2b5b 100644 --- a/volatility3/framework/symbols/linux/utilities/tainting.py +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -1,11 +1,10 @@ from volatility3 import framework from volatility3.framework import interfaces -from volatility3.framework.symbols.linux.utilities import LinuxUtilityInterface from volatility3.framework.constants import linux as linux_constants from typing import List, Optional -class Tainting(LinuxUtilityInterface): +class Tainting(interfaces.configuration.VersionableInterface): """Tainted kernel and modules parsing capabilities. Relevant Linux kernel functions: @@ -14,7 +13,7 @@ class Tainting(LinuxUtilityInterface): """ _version = (1, 0, 0) - _required_framework_version = (2, 16, 0) + _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) From 5c70356c27aedc931c03a8e633952b126ef5254b Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 6 Jan 2025 00:23:16 +0100 Subject: [PATCH 030/141] version check_modules requirement --- volatility3/framework/plugins/linux/modxview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index 042930740..b44f84c7d 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -38,7 +38,7 @@ class Modxview(interfaces.plugins.PluginInterface): requirements.PluginRequirement( name="check_modules", plugin=check_modules.Check_modules, - version=(0, 0, 0), + version=(1, 0, 0), ), requirements.PluginRequirement( name="hidden_modules", From 302f9fdf5ba1c24d07d2fce3d0f7c87c3e6bd1f2 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 6 Jan 2025 00:24:18 +0100 Subject: [PATCH 031/141] cut unnecessary plugin runner functions --- .../framework/plugins/linux/modxview.py | 78 ++++++------------- 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index b44f84c7d..a247bc2cc 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -53,54 +53,6 @@ class Modxview(interfaces.plugins.PluginInterface): ), ] - @classmethod - def run_lsmod( - cls, context: interfaces.context.ContextInterface, kernel_name: str - ) -> List[extensions.module]: - """Wrapper for the lsmod plugin.""" - return list(lsmod.Lsmod.list_modules(context, kernel_name)) - - @classmethod - def run_check_modules( - cls, - context: interfaces.context.ContextInterface, - kernel_name: str, - ) -> List[extensions.module]: - """Wrapper for the check_modules plugin. - Here, we extract the /sys/module/ list.""" - kernel = context.modules[kernel_name] - sysfs_modules: dict = check_modules.Check_modules.get_kset_modules( - context, kernel_name - ) - - # Convert get_kset_modules() offsets back to module objects - return [ - kernel.object(object_type="module", offset=m_offset, absolute=True) - for m_offset in sysfs_modules.values() - ] - - @classmethod - def run_hidden_modules( - cls, - context: interfaces.context.ContextInterface, - kernel_name: str, - known_modules_addresses: Set[int], - ) -> List[extensions.module]: - """Wrapper for the hidden_modules plugin.""" - modules_memory_boundaries = ( - hidden_modules.Hidden_modules.get_modules_memory_boundaries( - context, kernel_name - ) - ) - return list( - hidden_modules.Hidden_modules.get_hidden_modules( - context, - kernel_name, - known_modules_addresses, - modules_memory_boundaries, - ) - ) - @classmethod def flatten_run_modules_results( cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True @@ -140,15 +92,35 @@ class Modxview(interfaces.plugins.PluginInterface): kernel = context.modules[kernel_name] run_results = {} - run_results["lsmod"] = cls.run_lsmod(context, kernel_name) - run_results["check_modules"] = cls.run_check_modules(context, kernel_name) + # 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_module_addresses = set( + known_modules_addresses = set( context.layers[kernel.layer_name].canonicalize(module.vol.offset) for module in run_results["lsmod"] + run_results["check_modules"] ) - run_results["hidden_modules"] = cls.run_hidden_modules( - context, kernel_name, known_module_addresses + 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 From 4115c26e7cc119a68aa33fff7f5b8a730b5b2c69 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 6 Jan 2025 00:24:44 +0100 Subject: [PATCH 032/141] bump framework req to 2.18.0 --- volatility3/framework/plugins/linux/modxview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index a247bc2cc..34f5bac8f 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -19,7 +19,7 @@ class Modxview(interfaces.plugins.PluginInterface): spot modules presence and taints.""" _version = (1, 0, 0) - _required_framework_version = (2, 16, 0) + _required_framework_version = (2, 18, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From bd82f4f33d860cb067e379600da5bbc74f9e2247 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 6 Jan 2025 00:25:07 +0100 Subject: [PATCH 033/141] 2.16.0 -> 2.18.0 bump --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 24f96fa89..832b2a5ba 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -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 = 18 # Number of changes that only add to the interface VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" From d956742db98610dfa94678ffb98d53c5b6bcd161 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 6 Jan 2025 00:31:39 +0100 Subject: [PATCH 034/141] remove typing.Set import --- volatility3/framework/plugins/linux/modxview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index 34f5bac8f..3655200e8 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Dict, Set, Iterator +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 0860441c2fc5a5a97902a7582473d8462c457bc3 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 9 Jan 2025 17:11:02 +0000 Subject: [PATCH 035/141] Core: Improve speed to JSONSchema validation --- pyproject.toml | 3 +-- volatility3/framework/plugins/isfinfo.py | 2 +- volatility3/schemas/__init__.py | 16 +++++++++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 86e3921d2..af22cbe0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,10 +33,9 @@ cloud = [ dev = [ "volatility3[full,cloud]", - "jsonschema>=4.23.0,<5", + "fastjsonschema>=2.21.1,<3", "pyinstaller>=6.11.0,<7", "pyinstaller-hooks-contrib>=2024.9", - "types-jsonschema>=4.23.0,<5", ] test = [ diff --git a/volatility3/framework/plugins/isfinfo.py b/volatility3/framework/plugins/isfinfo.py index 1c2ac52e9..34b0a5653 100644 --- a/volatility3/framework/plugins/isfinfo.py +++ b/volatility3/framework/plugins/isfinfo.py @@ -97,7 +97,7 @@ class IsfInfo(plugins.PluginInterface): if filter_item in isf_file: filtered_list.append(isf_file) - if find_spec("jsonschema") and self.config["validate"]: + if find_spec("fastjsonschema") and self.config["validate"]: def check_valid(data): return "True" if schemas.validate(data, True) else "False" diff --git a/volatility3/schemas/__init__.py b/volatility3/schemas/__init__.py index 90cfaba48..3964e29a6 100644 --- a/volatility3/schemas/__init__.py +++ b/volatility3/schemas/__init__.py @@ -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 @@ -92,7 +94,12 @@ def valid( if input_hash in cached_validations and use_cache: return True try: - import jsonschema + import fastjsonschema + + schema_key = json.dumps(schema, sort_keys=True) + if schema_key not in validators: + validator = fastjsonschema.compile(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,10 +107,13 @@ def valid( try: vollog.debug("Validating JSON against schema...") - jsonschema.validate(input, schema) + validators[schema_key](input) + import pdb + + pdb.set_trace() cached_validations.add(input_hash) vollog.debug("JSON validated against schema (result cached)") - except jsonschema.exceptions.SchemaError: + except fastjsonschema.JsonSchemaValueException: vollog.debug("Schema validation error", exc_info=True) return False From e676e6179a3cd9c8d6afb838560d7a9e4d3a5420 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 9 Jan 2025 18:08:22 +0000 Subject: [PATCH 036/141] Swap fastjsonschema for jsonschema because of date-time validation issues --- volatility3/schemas/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/volatility3/schemas/__init__.py b/volatility3/schemas/__init__.py index 3964e29a6..b94e0831d 100644 --- a/volatility3/schemas/__init__.py +++ b/volatility3/schemas/__init__.py @@ -94,11 +94,13 @@ def valid( if input_hash in cached_validations and use_cache: return True try: - import fastjsonschema + import jsonschema schema_key = json.dumps(schema, sort_keys=True) if schema_key not in validators: - validator = fastjsonschema.compile(schema) + 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") @@ -107,10 +109,7 @@ def valid( try: vollog.debug("Validating JSON against schema...") - validators[schema_key](input) - import pdb - - pdb.set_trace() + validators[schema_key].validate(input) cached_validations.add(input_hash) vollog.debug("JSON validated against schema (result cached)") except fastjsonschema.JsonSchemaValueException: From 21decf13708d882369cc1f8fa2884f5a8ae5494d Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 9 Jan 2025 18:12:36 +0000 Subject: [PATCH 037/141] Core: Put the dependencies back for jsonschema --- pyproject.toml | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index af22cbe0a..fbdbf0a8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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,16 +32,14 @@ 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]", - "fastjsonschema>=2.21.1,<3", + "jsonschema>=4.23.0,<5", "pyinstaller>=6.11.0,<7", "pyinstaller-hooks-contrib>=2024.9", + "types-jsonschema>=4.23.0,<5", ] test = [ @@ -78,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] From d07d31047b2ed49a570aa4b3698f6a64145c2d83 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 9 Jan 2025 18:24:48 +0000 Subject: [PATCH 038/141] Core: Revert the exception catching too --- volatility3/schemas/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/schemas/__init__.py b/volatility3/schemas/__init__.py index b94e0831d..e894def9f 100644 --- a/volatility3/schemas/__init__.py +++ b/volatility3/schemas/__init__.py @@ -112,7 +112,7 @@ def valid( validators[schema_key].validate(input) cached_validations.add(input_hash) vollog.debug("JSON validated against schema (result cached)") - except fastjsonschema.JsonSchemaValueException: + except jsonschema.exceptions.SchemaError: vollog.debug("Schema validation error", exc_info=True) return False From 0e4e7518447837b9c7f0f30203155b3a3fee0c3a Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 11 Jan 2025 14:05:36 +0100 Subject: [PATCH 039/141] stateless classmethods --- .../symbols/linux/utilities/tainting.py | 67 +++++++++++++------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py index 29d7d2b5b..552f51b98 100644 --- a/volatility3/framework/symbols/linux/utilities/tainting.py +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -17,26 +17,22 @@ class Tainting(interfaces.configuration.VersionableInterface): framework.require_interface_version(*_required_framework_version) - def __init__( - self, + @classmethod + def _get_kernel_taint_flags_list( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - *args, - **kwargs, - ): - super().__init__(*args, **kwargs) - self._kernel = context.modules[kernel_module_name] - - @property - def _kernel_taint_flags_list( - self, ) -> Optional[List[interfaces.objects.ObjectInterface]]: - if self._kernel.has_symbol("taint_flags"): - return list(self._kernel.object_from_symbol("taint_flags")) + 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( - self, taints: int, is_module: bool = False + 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. @@ -58,8 +54,13 @@ class Tainting(interfaces.configuration.VersionableInterface): return taints_string + @classmethod def _module_flags_taint_post_4_10_rc1( - self, taints: int, is_module: bool = False + 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. @@ -78,7 +79,9 @@ class Tainting(interfaces.configuration.VersionableInterface): The raw taints string. """ taints_string = "" - for taint_bit, taint_flag in enumerate(self._kernel_taint_flags_list): + 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) @@ -90,7 +93,14 @@ class Tainting(interfaces.configuration.VersionableInterface): return taints_string - def get_taints_as_plain_string(self, taints: int, is_module: bool = False) -> str: + @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: @@ -103,11 +113,22 @@ class Tainting(interfaces.configuration.VersionableInterface): - module_flags_taint kernel function """ - if self._kernel_taint_flags_list: - return self._module_flags_taint_post_4_10_rc1(taints, is_module) - return self._module_flags_taint_pre_4_10_rc1(taints, is_module) + 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( + context, kernel_module_name, taints, is_module + ) - def get_taints_parsed(self, taints: int, is_module: bool = False) -> List[str]: + @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: @@ -121,7 +142,9 @@ class Tainting(interfaces.configuration.VersionableInterface): - module_flags_taint kernel function """ comprehensive_taints = [] - for character in self.get_taints_as_plain_string(taints, is_module): + 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"") From 6817d2c765fb5117a8ec6adb92cf343d37a92595 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 13 Jan 2025 16:00:50 +1100 Subject: [PATCH 040/141] linux: ensure process listing functions yield only valid tasks --- volatility3/framework/plugins/linux/pslist.py | 5 ++- .../symbols/linux/extensions/__init__.py | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 37cf000fc..931acf29a 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -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]: @@ -250,6 +250,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 diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index df1c00e3d..a50b8ae09 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -307,6 +307,36 @@ 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: + return False + + if not (self.signal and self.signal.is_readable()): + return False + + if not (self.nsproxy and self.nsproxy.is_readable()): + return False + + if not (self.real_parent and self.real_parent.is_readable()): + return False + + if 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 +431,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 From 093b12b7cdf4a1623a5d534309f0673c0311cc6b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 13 Jan 2025 16:52:45 +1100 Subject: [PATCH 041/141] Linux and Windows: Ensure linked list object extensions consistently yield valid entries --- .../symbols/linux/extensions/__init__.py | 42 ++++++++++------- .../symbols/windows/extensions/__init__.py | 47 +++++++++---------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index df1c00e3d..2065b3bb4 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1209,35 +1209,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) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index f12fd3f5b..214002f49 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -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) From 0d9715136cc9cc96637996b2bb027a76b8b5e87a Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 13 Jan 2025 17:13:44 +1100 Subject: [PATCH 042/141] Linux: Ensure VMA enumration functions yield only valid objects consistently --- .../symbols/linux/extensions/__init__.py | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index df1c00e3d..61562270a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -811,23 +811,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 +849,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 +861,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") From 8bc04529350c3ce5a927099a72bc4e419a049db5 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 14 Jan 2025 11:21:23 +1100 Subject: [PATCH 043/141] linux: Improve compatibility with ancient kernels --- .../symbols/linux/extensions/__init__.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index a50b8ae09..4a1a263dd 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -316,16 +316,26 @@ class task_struct(generic.GenericIntelProcess): if self.pid < 0: return False - if not (self.signal and self.signal.is_readable()): + if self.has_member("signal") and not ( + self.signal and self.signal.is_readable() + ): return False - if not (self.nsproxy and self.nsproxy.is_readable()): + if self.has_member("nsproxy") and not ( + self.nsproxy and self.nsproxy.is_readable() + ): return False - if not (self.real_parent and self.real_parent.is_readable()): + if self.has_member("real_parent") and not ( + self.real_parent and self.real_parent.is_readable() + ): return False - if self.active_mm and not self.active_mm.is_readable(): + if ( + self.has_member("active_mm") + and self.active_mm + and not self.active_mm.is_readable() + ): return False if self.mm: From 7fc2af5b4ecf4b1ced5c71357d164981b05ed309 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 14 Jan 2025 11:39:48 +1100 Subject: [PATCH 044/141] linux: Add an additional quick check before validating pointer readability --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 4a1a263dd..e2c9454f6 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -313,7 +313,7 @@ class task_struct(generic.GenericIntelProcess): if not layer.is_valid(self.vol.offset, self.vol.size): return False - if self.pid < 0: + if self.pid < 0 or self.tgid < 0: return False if self.has_member("signal") and not ( From d21fdb4211c800404acd863b71b8302022d090e4 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 13 Jan 2025 16:52:45 +1100 Subject: [PATCH 045/141] Linux: pagecache: Harden Page Cache API to consistently yield valid entries --- .../framework/plugins/linux/pagecache.py | 14 ++++++-- .../framework/symbols/linux/__init__.py | 9 +++--- .../symbols/linux/extensions/__init__.py | 32 +++++++++++++------ 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 382268515..430190970 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -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]: @@ -253,6 +253,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 +288,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 +324,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 +399,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]: diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 5aa27b964..3265dfd37 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -838,11 +838,12 @@ 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 - page = self.vmlinux.object("page", offset=page_addr, absolute=True) - if page: - yield page + if not layer.is_valid(page_addr): + continue + + yield self.vmlinux.object("page", offset=page_addr, absolute=True) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index df1c00e3d..187b1e280 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2489,7 +2489,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 +2502,21 @@ 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(): 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): @@ -2625,7 +2632,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 +2648,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 +2767,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 From 529b67fc96d7e975bdff83edd7096c4c0cd8db80 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 14 Jan 2025 12:31:25 +1100 Subject: [PATCH 046/141] Linux: pagecache: Fix issue reported in #1527 --- volatility3/framework/plugins/linux/pagecache.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 430190970..9aff0e4f9 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -469,6 +469,8 @@ class InodePages(plugins.PluginInterface): inode_size, page_idx, ) + continue + f.seek(current_fp) f.write(page_bytes) From 1a84f96c70060bc09aab3c1b3348d980f8b9bc0e Mon Sep 17 00:00:00 2001 From: Kerry Goodwine Date: Thu, 9 Jan 2025 15:49:24 -0500 Subject: [PATCH 047/141] Actions: Add new workflow for generating windows EXEs with pyinstaller --- .github/workflows/build-pyinstaller.yml | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/build-pyinstaller.yml diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml new file mode 100644 index 000000000..bcba95403 --- /dev/null +++ b/.github/workflows/build-pyinstaller.yml @@ -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 From 9f08af47b161579bf31f9d45c8b248c23861388a Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 15 Jan 2025 12:51:52 +1100 Subject: [PATCH 048/141] Linux: Add support for Intel 32bit with PAE --- volatility3/framework/automagic/linux.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/volatility3/framework/automagic/linux.py b/volatility3/framework/automagic/linux.py index f22cae012..542d26a8d 100644 --- a/volatility3/framework/automagic/linux.py +++ b/volatility3/framework/automagic/linux.py @@ -71,6 +71,12 @@ 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 == 512 + ): + layer_class = intel.LinuxIntelPAE + dtb_symbol_name = "swapper_pg_dir" else: layer_class = intel.LinuxIntel dtb_symbol_name = "swapper_pg_dir" From 28c74f8c1b853df3680de14f6fdc22958516a9c2 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 15 Jan 2025 13:23:39 +1100 Subject: [PATCH 049/141] Linux: Add support for Intel 32bit with PAE in early kernels, including versions 2.3.27 and 2.3.28. --- volatility3/framework/automagic/linux.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/automagic/linux.py b/volatility3/framework/automagic/linux.py index 542d26a8d..cb4f3cc64 100644 --- a/volatility3/framework/automagic/linux.py +++ b/volatility3/framework/automagic/linux.py @@ -71,10 +71,9 @@ 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 == 512 - ): + 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: From b27f98fed258b597e043a5c80304d8489da27b32 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 15 Jan 2025 14:37:35 +1100 Subject: [PATCH 050/141] linux: pslist: fix task credentials rendering --- volatility3/framework/plugins/linux/pslist.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 37cf000fc..77b57e000 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -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, ) From 66084878627f636d0bebabbecc79842ac954d209 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 15 Jan 2025 15:08:57 +1100 Subject: [PATCH 051/141] Linux: pagecache: Fix issue with incosistent inode page caches --- volatility3/framework/plugins/linux/pagecache.py | 6 +++++- volatility3/framework/symbols/linux/__init__.py | 14 ++++++++++++-- .../framework/symbols/linux/extensions/__init__.py | 9 +++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 9aff0e4f9..b2766be8d 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -520,7 +520,11 @@ class InodePages(plugins.PluginInterface): 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 + 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 = ( diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 3265dfd37..08f69c326 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -3,6 +3,7 @@ # import math import contextlib +import logging from abc import ABC, abstractmethod from typing import Iterator, List, Tuple, Optional, Union @@ -12,6 +13,8 @@ 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"} @@ -612,7 +615,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 @@ -846,4 +849,11 @@ class PageCache: if not layer.is_valid(page_addr): continue - yield self.vmlinux.object("page", offset=page_addr, absolute=True) + page = self.vmlinux.object("page", offset=page_addr, absolute=True) + if not page.is_valid(): + vollog.error( + f"Invalid cached page at {page.vol.offset:#x}, aborting", + ) + break + + yield page diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 187b1e280..6ae022923 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2534,6 +2534,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 From ce40659d8728f57f9b6e63709af4b7a17588a309 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 16 Jan 2025 19:19:47 +1100 Subject: [PATCH 052/141] linux: radix_tree: Fix various issues, enhance early inconsistency detection, and improve compatibility with older kernel versions --- volatility3/framework/exceptions.py | 4 ++ .../framework/symbols/linux/__init__.py | 62 ++++++++++++------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index c44fb4f2e..41c67b88d 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -130,3 +130,7 @@ 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""" diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 08f69c326..4c9934439 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -3,6 +3,7 @@ # import math import contextlib +import functools import logging from abc import ABC, abstractmethod from typing import Iterator, List, Tuple, Optional, Union @@ -662,7 +663,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 @@ -697,7 +698,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 @@ -720,6 +721,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 @@ -756,43 +758,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 @@ -805,14 +821,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: From 3c92d7f7b7d3d7d9548275e960571d4ac37a6a21 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 16 Jan 2025 19:22:46 +1100 Subject: [PATCH 053/141] linux: page_cache: enhance early inconsistency detection --- volatility3/framework/symbols/linux/__init__.py | 14 ++++++-------- .../framework/symbols/linux/extensions/__init__.py | 7 ++++++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 4c9934439..a7e6ef405 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -861,17 +861,15 @@ class PageCache: """ 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): - continue + 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 not page.is_valid(): - vollog.error( - f"Invalid cached page at {page.vol.offset:#x}, aborting", - ) - break + error_msg = f"Invalid cached page at {page_addr:#x}, aborting" + vollog.error(error_msg) + raise exceptions.LinuxPageCacheException(error_msg) yield page diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 6ae022923..997370bbb 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2513,6 +2513,11 @@ class inode(objects.StructType): 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() if page_content: @@ -2524,7 +2529,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 From c1497410b4721703ebbb9f30f2ab3db497a4438b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 16 Jan 2025 19:28:25 +1100 Subject: [PATCH 054/141] linux: page_cache plugin: lazy file initialization and avoid redundant inode page cache walk during dumps not showing output with dumping to file. --- .../framework/plugins/linux/pagecache.py | 89 ++++++++++++------- 1 file changed, 57 insertions(+), 32 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index b2766be8d..7ad0f5a8f 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -8,7 +8,7 @@ import datetime from dataclasses import dataclass, astuple from typing import List, Set, Type, Iterable -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 @@ -453,16 +453,18 @@ 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, @@ -470,10 +472,20 @@ class InodePages(plugins.PluginInterface): page_idx, ) continue + page_bytes = page_content[:page_bytes_len] - f.seek(current_fp) - f.write(page_bytes) + 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) @@ -514,31 +526,44 @@ class InodePages(plugins.PluginInterface): 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 - 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, - ) + if not self.config["dump"]: + 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 = int(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 + yield 0, fields + except exceptions.LinuxPageCacheException: + vollog.warning( + f"Page cache for inode at {inode.vol.offset:#x} is corrupt" + ) - if self.config["dump"]: + else: open_method = self.open inode_address = inode.vol.offset filename = open_method.sanitize_filename(f"inode_0x{inode_address:x}.dmp") From 5502a54198fc7617bb22e2e96519114c59704ab0 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 16 Jan 2025 19:38:41 +1100 Subject: [PATCH 055/141] linux: page_cache plugin: Refactor to make _generator more readable --- .../framework/plugins/linux/pagecache.py | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 7ad0f5a8f..39ed60486 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -6,7 +6,7 @@ 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, exceptions from volatility3.framework.renderers import format_hints @@ -489,6 +489,44 @@ class InodePages(plugins.PluginInterface): 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 = int(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] @@ -510,7 +548,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: @@ -525,45 +562,7 @@ class InodePages(plugins.PluginInterface): vollog.error("The inode is not a regular file") return None - inode_size = inode.i_size - if not self.config["dump"]: - 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 = int(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" - ) - - else: + if self.config["dump"]: open_method = self.open inode_address = inode.vol.offset filename = open_method.sanitize_filename(f"inode_0x{inode_address:x}.dmp") @@ -571,6 +570,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 = [ From 9944fcc61f179a0d949266a90d3e0e290d5017c6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 16 Jan 2025 21:15:00 +1100 Subject: [PATCH 056/141] linux: page_cache test case: Since the --dump no longer generate output, we test both modes, listing and dumping. --- test/test_volatility.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/test_volatility.py b/test/test_volatility.py index bb7c9a851..e25e8278b 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -714,7 +714,7 @@ def test_linux_page_cache_inodepages(image, volatility, python): image, volatility, python, - pluginargs=["--inode", inode_address, "--dump"], + pluginargs=["--inode", inode_address], ) assert rc == 0 @@ -725,6 +725,14 @@ def test_linux_page_cache_inodepages(image, volatility, python): rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True", out, ) + + rc, out, _err = runvol_plugin( + "linux.pagecache.InodePages", + image, + volatility, + python, + pluginargs=["--inode", inode_address, "--dump"], + ) assert os.path.exists(inode_dump_filename) with open(inode_dump_filename, "rb") as fp: inode_contents = fp.read() From d8254b63735388b9ef6be27009474ccea5726650 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 16 Jan 2025 21:39:37 +1100 Subject: [PATCH 057/141] linux: page_cache test case: Improve test --- test/test_volatility.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/test/test_volatility.py b/test/test_volatility.py index e25e8278b..8676d1f3e 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -708,24 +708,25 @@ 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", - 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, - ) - rc, out, _err = runvol_plugin( "linux.pagecache.InodePages", image, @@ -733,6 +734,10 @@ def test_linux_page_cache_inodepages(image, volatility, python): python, pluginargs=["--inode", inode_address, "--dump"], ) + + assert rc == 0 + assert out.count(b"\n") >= 4 + assert os.path.exists(inode_dump_filename) with open(inode_dump_filename, "rb") as fp: inode_contents = fp.read() From 749a0f9c656d8da0f5087faf25f225a6097ac8b4 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 16 Jan 2025 12:06:22 +0000 Subject: [PATCH 058/141] Core: Fix up ISFinfo looking for fastjsonschema --- volatility3/framework/plugins/isfinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/isfinfo.py b/volatility3/framework/plugins/isfinfo.py index 34b0a5653..1c2ac52e9 100644 --- a/volatility3/framework/plugins/isfinfo.py +++ b/volatility3/framework/plugins/isfinfo.py @@ -97,7 +97,7 @@ class IsfInfo(plugins.PluginInterface): if filter_item in isf_file: filtered_list.append(isf_file) - if find_spec("fastjsonschema") and self.config["validate"]: + if find_spec("jsonschema") and self.config["validate"]: def check_valid(data): return "True" if schemas.validate(data, True) else "False" From b447bfa81c36e91c3cf30bdc432e6eba48afbc53 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 16 Jan 2025 16:24:38 +0100 Subject: [PATCH 059/141] remove module tainting proxies --- .../framework/plugins/linux/modxview.py | 16 ++++++++++-- .../symbols/linux/extensions/__init__.py | 25 ------------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index 3655200e8..69c6ac8bb 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -140,9 +140,21 @@ class Modxview(interfaces.plugins.PluginInterface): seen_addresses.add(module.vol.offset) if self.config.get("plain_taints"): - taints = module.get_taints_as_plain_string() + taints = tainting.Tainting.get_taints_as_plain_string( + self.context, + kernel_name, + module.taints, + True, + ) else: - taints = ",".join(module.get_taints_parsed()) + taints = ",".join( + tainting.Tainting.get_taints_parsed( + self.context, + kernel_name, + module.taints, + True, + ) + ) yield ( 0, diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index ac2f87df0..289d6c0a4 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -19,7 +19,6 @@ from volatility3.framework.layers import linear, intel from volatility3.framework.objects import utility from volatility3.framework.symbols import generic, linux, intermed from volatility3.framework.symbols.linux.extensions import elf -from volatility3.framework.symbols.linux.utilities import tainting vollog = logging.getLogger(__name__) @@ -279,30 +278,6 @@ class module(generic.GenericIntelProcess): return None - def get_taints_as_plain_string(self) -> str: - """Convert the module's taints value to a 1-1 character mapping. - Convenient wrapper around framework's Tainting capabilities. - - Returns: - The raw taints string. - """ - return tainting.Tainting( - self._context, - linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name, - ).get_taints_as_plain_string(self.taints, True) - - def get_taints_parsed(self) -> List[str]: - """Convert the module's taints string to a 1-1 descriptor mapping. - Convenient wrapper around framework's Tainting capabilities. - - Returns: - A comprehensive (user-friendly) taint descriptor list. - """ - return tainting.Tainting( - self._context, - linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name, - ).get_taints_parsed(self.taints, True) - @property def section_symtab(self): if self.has_member("kallsyms"): From 94704c6674d7f5fb9d57698faa0d9ed943c6158c Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 16 Jan 2025 16:26:57 +0100 Subject: [PATCH 060/141] 2.16.0 -> 2.17.0 bump --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 832b2a5ba..3d68ab810 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -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 = 18 # Number of changes that only add to the interface +VERSION_MINOR = 17 # Number of changes that only add to the interface VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" From cd8690a8059836f7e216c211c4397924ae311c84 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 16 Jan 2025 16:27:22 +0100 Subject: [PATCH 061/141] require framework version 2.17.0 --- volatility3/framework/plugins/linux/modxview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index 69c6ac8bb..3c2c5f05e 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -19,7 +19,7 @@ class Modxview(interfaces.plugins.PluginInterface): spot modules presence and taints.""" _version = (1, 0, 0) - _required_framework_version = (2, 18, 0) + _required_framework_version = (2, 17, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From adf81bc74a388d6ff5bffabe588bc5ca72147506 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 16 Jan 2025 19:59:31 +0000 Subject: [PATCH 062/141] Update copyright dates --- README.md | 2 +- doc/source/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cc33d3cc4..b74bdab0b 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ The latest generated copy of the documentation can be found at: Date: Fri, 17 Jan 2025 16:03:47 +0000 Subject: [PATCH 063/141] Core: Correct version dependencies to avoid conflicts Fixes #1546 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 86e3921d2..542a1480a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ cloud = [ 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,7 +48,7 @@ test = [ docs = [ "volatility3[dev]", - "sphinx>=8.0.0,<7", + "sphinx>=8.0.0,<9", "sphinx-autodoc-typehints>=2.5.0,<3", "sphinx-rtd-theme>=3.0.1,<4", ] From c4430cda8d6b13b0d69a787fe32e8158ae471c3a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Fri, 17 Jan 2025 16:09:35 +0000 Subject: [PATCH 064/141] Core: Try to maintain python-3.8 support for the documentation --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 542a1480a..8944bb058 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ test = [ docs = [ "volatility3[dev]", "sphinx>=8.0.0,<9", - "sphinx-autodoc-typehints>=2.5.0,<3", + "sphinx-autodoc-typehints>=2.0.0,<3", "sphinx-rtd-theme>=3.0.1,<4", ] From 13a8c53f7b64bc7180b665e363b2a3f0348e8b04 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Fri, 17 Jan 2025 16:13:35 +0000 Subject: [PATCH 065/141] Core: There was no clear reason to stop supporting older versions of sphinx --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8944bb058..3f16eeece 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ test = [ docs = [ "volatility3[dev]", - "sphinx>=8.0.0,<9", + "sphinx>=4.0.0,<9", "sphinx-autodoc-typehints>=2.0.0,<3", "sphinx-rtd-theme>=3.0.1,<4", ] From cc9486cf03f6f8b8035069f02702ce30a589cb7e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 02:19:11 +0100 Subject: [PATCH 066/141] pre-process module triaging to improve readability --- .../framework/plugins/linux/modxview.py | 71 ++++++++++--------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index 3c2c5f05e..125c1cc33 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -82,7 +82,8 @@ class Modxview(interfaces.plugins.PluginInterface): kernel_name: str, run_hidden_modules: bool = True, ) -> Dict[str, List[extensions.module]]: - """Run module scanning plugins and aggregate the results. + """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 @@ -128,46 +129,50 @@ class Modxview(interfaces.plugins.PluginInterface): def _generator(self): kernel_name = self.config["kernel"] run_results = self.run_modules_scanners(self.context, kernel_name) - modules_offsets = {} - for key in ["lsmod", "check_modules", "hidden_modules"]: - modules_offsets[key] = set(module.vol.offset for module in run_results[key]) + 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): + # 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]) - seen_addresses = set() - for modules_list in run_results.values(): - for module in modules_list: - if module.vol.offset in seen_addresses: - continue - seen_addresses.add(module.vol.offset) - - if self.config.get("plain_taints"): - taints = tainting.Tainting.get_taints_as_plain_string( + 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, ) - 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.vol.offset), - module.vol.offset in modules_offsets["lsmod"], - module.vol.offset in modules_offsets["check_modules"], - module.vol.offset in modules_offsets["hidden_modules"], - taints or NotAvailableValue(), - ), ) + 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), From 3b679cbafbb50a2c986a63efd223cf9088bbc330 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 13:26:43 +0100 Subject: [PATCH 067/141] explicit None check --- volatility3/framework/plugins/linux/modxview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index 125c1cc33..c74bf28e8 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -136,7 +136,7 @@ class Modxview(interfaces.plugins.PluginInterface): 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): + 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: From bb6556dbc0145682866d56bb2608b5b841e381e8 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 13:37:52 +0100 Subject: [PATCH 068/141] correct arguments for pre_4_10_rc1 --- volatility3/framework/symbols/linux/utilities/tainting.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py index 552f51b98..14b69d3d6 100644 --- a/volatility3/framework/symbols/linux/utilities/tainting.py +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -117,9 +117,7 @@ class Tainting(interfaces.configuration.VersionableInterface): 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( - context, kernel_module_name, taints, is_module - ) + return cls._module_flags_taint_pre_4_10_rc1(taints, is_module) @classmethod def get_taints_parsed( From 0b82f731375583076abdfabd332ce067612d69f5 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 13:45:30 +0100 Subject: [PATCH 069/141] functools caching and doc. --- .../framework/symbols/linux/utilities/tainting.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/volatility3/framework/symbols/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py index 14b69d3d6..c1136436e 100644 --- a/volatility3/framework/symbols/linux/utilities/tainting.py +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -1,3 +1,5 @@ +import functools + from volatility3 import framework from volatility3.framework import interfaces from volatility3.framework.constants import linux as linux_constants @@ -18,11 +20,18 @@ class Tainting(interfaces.configuration.VersionableInterface): 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 symbok exists + """ kernel = context.modules[kernel_module_name] if kernel.has_symbol("taint_flags"): return list(kernel.object_from_symbol("taint_flags")) From 8095924e8a926990f6002f16d2c7259c5c750980 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 13:46:13 +0100 Subject: [PATCH 070/141] typo --- volatility3/framework/symbols/linux/utilities/tainting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py index c1136436e..2360401d5 100644 --- a/volatility3/framework/symbols/linux/utilities/tainting.py +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -30,7 +30,7 @@ class Tainting(interfaces.configuration.VersionableInterface): in-memory or not. Returns: - A list of "taint_flag" kernel objects if taint_flags symbok exists + A list of "taint_flag" kernel objects if taint_flags symbol exists """ kernel = context.modules[kernel_module_name] if kernel.has_symbol("taint_flags"): From 2fe8ee5983bd5faf8a89db5712512aa411329dbc Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 18 Jan 2025 13:18:53 +0000 Subject: [PATCH 071/141] Layers: Update LeechCore RawIO with better error handling for readlines Fixes #1419 --- volatility3/framework/layers/leechcore.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volatility3/framework/layers/leechcore.py b/volatility3/framework/layers/leechcore.py index eeede1673..06c359203 100644 --- a/volatility3/framework/layers/leechcore.py +++ b/volatility3/framework/layers/leechcore.py @@ -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") From 0849c163a1c517fa8595f9cc7610a737d1904fc2 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 14:48:37 +0100 Subject: [PATCH 072/141] appropriate symbols type hinting --- volatility3/framework/contexts/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index f527544c0..17a91e827 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -337,7 +337,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") From d46cb3328d07ae2216045ba3fc33679c2ab13fbc Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 14:48:46 +0100 Subject: [PATCH 073/141] appropriate symbols type hinting --- volatility3/framework/interfaces/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index a87e0f1e8..2b95a18ad 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -303,8 +303,8 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface): """Determines whether an enumeration is present in the module's symbol table.""" @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""" @abstractmethod def get_symbols_by_absolute_location(self, offset: int, size: int = 0) -> List[str]: From fb93d2333b8d3854d348548decc76ac67f358699 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 14:50:14 +0100 Subject: [PATCH 074/141] improve comments --- volatility3/framework/interfaces/symbols.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/interfaces/symbols.py b/volatility3/framework/interfaces/symbols.py index b8712e38d..c0bebe1e2 100644 --- a/volatility3/framework/interfaces/symbols.py +++ b/volatility3/framework/interfaces/symbols.py @@ -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 names.""" 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: From 1e9551b0530be824ab8d9a40db57cbd813d48136 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 14:50:28 +0100 Subject: [PATCH 075/141] types base class and comments improvements --- volatility3/framework/interfaces/symbols.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/symbols.py b/volatility3/framework/interfaces/symbols.py index c0bebe1e2..752d288f7 100644 --- a/volatility3/framework/interfaces/symbols.py +++ b/volatility3/framework/interfaces/symbols.py @@ -375,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 [] From aa99410dd2c50ee556293db959c469739c882684 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 14:51:31 +0100 Subject: [PATCH 076/141] prefer KeysView iterable to lists --- volatility3/framework/symbols/intermed.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/volatility3/framework/symbols/intermed.py b/volatility3/framework/symbols/intermed.py index 6802af7d6..0a30148aa 100644 --- a/volatility3/framework/symbols/intermed.py +++ b/volatility3/framework/symbols/intermed.py @@ -411,18 +411,23 @@ 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) + def types(self): + """Returns an iterable (KeysView) of the available symbol type names.""" + # self.natives.types (set) is generally very small compared to user_types, + # so the dict conversion overhead can be neglected + 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) From 3a2933155b6f92a8585666f611cd2069424ce5a9 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 14:51:52 +0100 Subject: [PATCH 077/141] improve comments --- volatility3/framework/symbols/native.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/native.py b/volatility3/framework/symbols/native.py index 7c3e1b312..61417532e 100644 --- a/volatility3/framework/symbols/native.py +++ b/volatility3/framework/symbols/native.py @@ -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: From ba09db6952d37590f62a647265c2bb5bb903ec3c Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 14:54:04 +0100 Subject: [PATCH 078/141] improve comments --- volatility3/framework/interfaces/symbols.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/symbols.py b/volatility3/framework/interfaces/symbols.py index 752d288f7..2d142de9a 100644 --- a/volatility3/framework/interfaces/symbols.py +++ b/volatility3/framework/interfaces/symbols.py @@ -149,7 +149,7 @@ class BaseSymbolTableInterface: @property def enumerations(self) -> Iterable[Any]: - """Returns an iterable of the available enumerations names.""" + """Returns an iterable of the available enumerations.""" raise NotImplementedError( "Abstract property enumerations not implemented by subclass." ) From 4b3d93b0f0637c7d41acc545f397e3522a913978 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 15:00:03 +0100 Subject: [PATCH 079/141] 2.17.0 -> 2.18.0 bump --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 3d68ab810..832b2a5ba 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -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 = 17 # Number of changes that only add to the interface +VERSION_MINOR = 18 # Number of changes that only add to the interface VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" From c5628c5d79496ae051942598bab08c19d3632a18 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 15:48:44 +0100 Subject: [PATCH 080/141] revert the mistakenly removed types type hinting --- volatility3/framework/symbols/intermed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/intermed.py b/volatility3/framework/symbols/intermed.py index 0a30148aa..9ece69d8b 100644 --- a/volatility3/framework/symbols/intermed.py +++ b/volatility3/framework/symbols/intermed.py @@ -420,7 +420,7 @@ class Version1Format(ISFormatTable): return self._json_object.get("enums", {}).keys() @property - def types(self): + def types(self) -> Iterable[str]: """Returns an iterable (KeysView) of the available symbol type names.""" # self.natives.types (set) is generally very small compared to user_types, # so the dict conversion overhead can be neglected From 0ee016e65554539318705a5b7c292864fcc2f436 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 15:50:28 +0100 Subject: [PATCH 081/141] 2.17.0 -> 2.17.1 bump --- volatility3/framework/constants/_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 832b2a5ba..041439909 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 18 # Number of changes that only add to the interface -VERSION_PATCH = 0 # Number of changes that do not change the interface +VERSION_MINOR = 17 # Number of changes that only add to the interface +VERSION_PATCH = 1 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = ( From c2ef3c2fe575f2c3ea49541b7e1207e7d93884f1 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 18 Jan 2025 15:59:51 +0100 Subject: [PATCH 082/141] add fixme about merge operator --- volatility3/framework/symbols/intermed.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/intermed.py b/volatility3/framework/symbols/intermed.py index 9ece69d8b..cb0b67969 100644 --- a/volatility3/framework/symbols/intermed.py +++ b/volatility3/framework/symbols/intermed.py @@ -422,8 +422,12 @@ class Version1Format(ISFormatTable): @property def types(self) -> Iterable[str]: """Returns an iterable (KeysView) of the available symbol type names.""" - # self.natives.types (set) is generally very small compared to user_types, - # so the dict conversion overhead can be neglected + # 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), From 726fbe6ccec8480c1baa74fadcc7fc4e87377bcd Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 13:01:14 +0100 Subject: [PATCH 083/141] 2.17.1 -> 2.18.0 bump --- volatility3/framework/constants/_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 041439909..832b2a5ba 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 17 # Number of changes that only add to the interface -VERSION_PATCH = 1 # Number of changes that do not change the interface +VERSION_MINOR = 18 # Number of changes that only add to the interface +VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = ( From 08830fee05c30adfc5ea4e997ce317f6f6927033 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 15:06:42 +0100 Subject: [PATCH 084/141] use architectures.LINUX_ARCHS --- volatility3/framework/plugins/linux/pagecache.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 382268515..408a9b98a 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -8,6 +8,7 @@ import datetime from dataclasses import dataclass, astuple from typing import List, Set, Type, Iterable +from volatility3.framework.constants import architectures from volatility3.framework import renderers, interfaces from volatility3.framework.renderers import format_hints from volatility3.framework.interfaces import plugins @@ -112,7 +113,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): requirements.ModuleRequirement( name="kernel", description="Linux kernel", - architectures=["Intel32", "Intel64"], + architectures=architectures.LINUX_ARCHS, ), requirements.PluginRequirement( name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0) @@ -397,7 +398,7 @@ class InodePages(plugins.PluginInterface): requirements.ModuleRequirement( name="kernel", description="Linux kernel", - architectures=["Intel32", "Intel64"], + architectures=architectures.LINUX_ARCHS, ), requirements.PluginRequirement( name="files", plugin=Files, version=(1, 0, 0) From d325e1ca176b55f68de03f462ca855631736ef6c Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 15:08:27 +0100 Subject: [PATCH 085/141] add inode_size and format_symlink to Inode* dataclasses --- volatility3/framework/plugins/linux/pagecache.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 408a9b98a..c86664f3d 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -38,6 +38,11 @@ class InodeUser: modification_time: str change_time: str path: str + inode_size: int + + @staticmethod + def format_symlink(symlink_source: str, symlink_dest: str): + return f"{symlink_source} -> {symlink_dest}" @dataclass @@ -81,6 +86,7 @@ class InodeInternal: access_time_dt = self.inode.get_access_time() modification_time_dt = self.inode.get_modification_time() change_time_dt = self.inode.get_change_time() + inode_size = int(self.inode.i_size) inode_user = InodeUser( superblock_addr=superblock_addr, @@ -96,6 +102,7 @@ class InodeInternal: modification_time=modification_time_dt, change_time=change_time_dt, path=self.path, + inode_size=inode_size, ) return inode_user From 1d0159325fbf04bb5029a9ed4ae2ea1acc770cee Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 15:09:20 +0100 Subject: [PATCH 086/141] switch to InodeUser.format_symlink --- volatility3/framework/plugins/linux/pagecache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index c86664f3d..a0dd8efe4 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -156,10 +156,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): """ # i_link (fast symlinks) were introduced in 4.2 if inode and inode.is_link and inode.has_member("i_link") and inode.i_link: - i_link_str = inode.i_link.dereference().cast( + symlink_dest = inode.i_link.dereference().cast( "string", max_length=255, encoding="utf-8", errors="replace" ) - symlink_path = f"{symlink_path} -> {i_link_str}" + symlink_path = InodeUser.format_symlink(symlink_path, symlink_dest) return symlink_path From 48a8f3929bbe992ff687a494e3ea0e6a7446f42c Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 15:10:32 +0100 Subject: [PATCH 087/141] add and leverage follow_symlinks parameter --- volatility3/framework/plugins/linux/pagecache.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index a0dd8efe4..4871d0d0f 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -220,12 +220,14 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str, + follow_symlinks: bool = True, ) -> Iterable[InodeInternal]: """Retrieves the inodes from the superblocks Args: context: The context that the plugin will operate within vmlinux_module_name: The name of the kernel module on which to operate + follow_symlinks: Whether to follow symlinks or not Yields: An InodeInternal object @@ -297,7 +299,9 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): continue seen_inodes.add(file_inode_ptr) - file_path = cls._follow_symlink(file_inode_ptr, file_path) + if follow_symlinks: + file_path = cls._follow_symlink(file_inode_ptr, file_path) + inode_in = InodeInternal( superblock=superblock, mountpoint=mountpoint, From 6f2ff4f7c6f702c11bf7d75b5ab78cb8f5881a20 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 15:11:03 +0100 Subject: [PATCH 088/141] add InodeSize column to Files --- volatility3/framework/plugins/linux/pagecache.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 4871d0d0f..770c6391a 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -389,6 +389,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): ("ModificationTime", datetime.datetime), ("ChangeTime", datetime.datetime), ("FilePath", str), + ("InodeSize", int), ] return renderers.TreeGrid( From 85941060051b530350ad8e770de4c1f2d3edefee Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 15:12:10 +0100 Subject: [PATCH 089/141] 1.0.1 -> 1.2.0 Files bump --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 770c6391a..d57bd77f8 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -112,7 +112,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 1) + _version = (1, 2, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From e8b44efc17dbfd6e412436d324702ed3e4cc7c7f Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 15:13:26 +0100 Subject: [PATCH 090/141] add and leverage write_inode_content_to_stream --- .../framework/plugins/linux/pagecache.py | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index d57bd77f8..42fa7538f 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -6,7 +6,7 @@ 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, IO from volatility3.framework.constants import architectures from volatility3.framework import renderers, interfaces @@ -452,31 +452,47 @@ class InodePages(plugins.PluginInterface): vollog.error("The inode is not a regular file") return None - # By using truncate/seek, provided the filesystem supports it, a sparse file will be + try: + with open_method(filename) as f: + InodePages.write_inode_content_to_stream(inode, f, vmlinux_layer) + except OSError as e: + vollog.error("Unable to write to file (%s): %s", filename, e) + + @staticmethod + def write_inode_content_to_stream( + inode: interfaces.objects.ObjectInterface, + stream: IO, + vmlinux_layer: interfaces.layers.TranslationLayerInterface, + ) -> None: + """Extracts the inode's contents from the page cache and saves them to a stream + + Args: + inode: The inode to dump + stream: A IO steam to write to, typically FileHandlerInterface or BytesIO + vmlinux_layer: The kernel layer to obtain the page size + """ + + # By using truncate/seek, provided the filesystem supports it, and the + # stream is a File interface, a sparse file will be # created, saving both disk space and I/O time. # Additionally, using the page index will guarantee that each page is written at the # appropriate file position. - try: - with open_method(filename) as f: - inode_size = inode.i_size - f.truncate(inode_size) + inode_size = inode.i_size + stream.truncate(inode_size) - for page_idx, page_content in inode.get_contents(): - current_fp = page_idx * vmlinux_layer.page_size - max_length = inode_size - current_fp - page_bytes = page_content[:max_length] - if current_fp + len(page_bytes) > inode_size: - vollog.error( - "Page out of file bounds: inode 0x%x, inode size %d, page index %d", - inode.vol.offset, - inode_size, - page_idx, - ) - f.seek(current_fp) - f.write(page_bytes) - - except OSError as e: - vollog.error("Unable to write to file (%s): %s", filename, e) + for page_idx, page_content in inode.get_contents(): + current_fp = page_idx * vmlinux_layer.page_size + max_length = inode_size - current_fp + page_bytes = page_content[:max_length] + if current_fp + len(page_bytes) > inode_size: + vollog.error( + "Page out of file bounds: inode 0x%x, inode size %d, page index %d", + inode.vol.offset, + inode_size, + page_idx, + ) + stream.seek(current_fp) + stream.write(page_bytes) def _generator(self): vmlinux_module_name = self.config["kernel"] From 818ddb746bb5bcbf45fe22fb54e8022d75d5fedd Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 15:15:55 +0100 Subject: [PATCH 091/141] 2.0.0 -> 2.1.0 InodePages bump --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 42fa7538f..feac31bb7 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -402,7 +402,7 @@ class InodePages(plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _version = (2, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From da068676d13eeefae033e46f8a916d91881bc2a6 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 15:16:09 +0100 Subject: [PATCH 092/141] typo --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index feac31bb7..e87bc2c9d 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -468,7 +468,7 @@ class InodePages(plugins.PluginInterface): Args: inode: The inode to dump - stream: A IO steam to write to, typically FileHandlerInterface or BytesIO + stream: An IO steam to write to, typically FileHandlerInterface or BytesIO vmlinux_layer: The kernel layer to obtain the page size """ From 452d6e705b973213238becd22d736f6ee1cb45e0 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 16:26:25 +0100 Subject: [PATCH 093/141] 1.0.1 -> 1.1.0 Files bump --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index e87bc2c9d..89d30a9eb 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -112,7 +112,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): _required_framework_version = (2, 0, 0) - _version = (1, 2, 0) + _version = (1, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From ecae545f5897e7c9a59d231d0b3f04b195cbf169 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 17:05:52 +0100 Subject: [PATCH 094/141] typo --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 7d211150a..850045244 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2948,7 +2948,7 @@ class scatterlist(objects.StructType): Returns: An iterator of bytes """ - # Either "physical" is layer-1 because this is a module layer, either "physical" is the current layer + # 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 ) From eddba98ef7c8c72162233dd19332e770d2a916d7 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 17:09:59 +0100 Subject: [PATCH 095/141] type hint format_symlink --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 89d30a9eb..d98103368 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -41,7 +41,7 @@ class InodeUser: inode_size: int @staticmethod - def format_symlink(symlink_source: str, symlink_dest: str): + def format_symlink(symlink_source: str, symlink_dest: str) -> str: return f"{symlink_source} -> {symlink_dest}" From 59703045c78941a12087a295b18cc8bc98414a06 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 17:26:42 +0100 Subject: [PATCH 096/141] switch calling convention to context and layer name --- .../framework/plugins/linux/pagecache.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index d98103368..d5297ab10 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -435,18 +435,20 @@ class InodePages(plugins.PluginInterface): @staticmethod def write_inode_content_to_file( + context: interfaces.context.ContextInterface, + layer_name: str, inode: interfaces.objects.ObjectInterface, filename: str, open_method: Type[interfaces.plugins.FileHandlerInterface], - vmlinux_layer: interfaces.layers.TranslationLayerInterface, ) -> None: """Extracts the inode's contents from the page cache and saves them to a file Args: + context: The context on which to operate + layer_name: The name of the layer on which to operate inode: The inode to dump filename: Filename for writing the inode content open_method: class for constructing output files - vmlinux_layer: The kernel layer to obtain the page size """ if not inode.is_reg: vollog.error("The inode is not a regular file") @@ -454,24 +456,26 @@ class InodePages(plugins.PluginInterface): try: with open_method(filename) as f: - InodePages.write_inode_content_to_stream(inode, f, vmlinux_layer) + InodePages.write_inode_content_to_stream(context, layer_name, inode, f) except OSError as e: vollog.error("Unable to write to file (%s): %s", filename, e) @staticmethod def write_inode_content_to_stream( + context: interfaces.context.ContextInterface, + layer_name: str, inode: interfaces.objects.ObjectInterface, stream: IO, - vmlinux_layer: interfaces.layers.TranslationLayerInterface, ) -> None: """Extracts the inode's contents from the page cache and saves them to a stream Args: + context: The context on which to operate + layer_name: The name of the layer on which to operate inode: The inode to dump stream: An IO steam to write to, typically FileHandlerInterface or BytesIO - vmlinux_layer: The kernel layer to obtain the page size """ - + layer = context.layers[layer_name] # By using truncate/seek, provided the filesystem supports it, and the # stream is a File interface, a sparse file will be # created, saving both disk space and I/O time. @@ -481,7 +485,7 @@ class InodePages(plugins.PluginInterface): stream.truncate(inode_size) for page_idx, page_content in inode.get_contents(): - current_fp = page_idx * vmlinux_layer.page_size + current_fp = page_idx * layer.page_size max_length = inode_size - current_fp page_bytes = page_content[:max_length] if current_fp + len(page_bytes) > inode_size: @@ -557,7 +561,7 @@ class InodePages(plugins.PluginInterface): filename = open_method.sanitize_filename(f"inode_0x{inode_address:x}.dmp") vollog.info("[*] Writing inode at 0x%x to '%s'", inode_address, filename) self.write_inode_content_to_file( - inode, filename, open_method, vmlinux_layer + self.context, vmlinux_layer.name, inode, filename, open_method ) def run(self): From a02243bb3c4a14076cda7a516c7499e1734f19d0 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 17:27:27 +0100 Subject: [PATCH 097/141] 2.1.0 -> 3.0.0 InodePages bump --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index d5297ab10..a86c1b936 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -402,7 +402,7 @@ class InodePages(plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (2, 1, 0) + _version = (3, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From d42ffc01e22672602a346b21f90b3733cd02db3e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 19 Jan 2025 17:33:47 +0100 Subject: [PATCH 098/141] typo --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index a86c1b936..77aa42338 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -473,7 +473,7 @@ class InodePages(plugins.PluginInterface): context: The context on which to operate layer_name: The name of the layer on which to operate inode: The inode to dump - stream: An IO steam to write to, typically FileHandlerInterface or BytesIO + stream: An IO stream to write to, typically FileHandlerInterface or BytesIO """ layer = context.layers[layer_name] # By using truncate/seek, provided the filesystem supports it, and the From 74b98e62c7aa4ca721f48651243122fa222548a5 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 19 Jan 2025 23:42:41 +0000 Subject: [PATCH 099/141] Revert "Add missing exception handling in env var recovery. Prevent backtraces" --- volatility3/framework/plugins/linux/envars.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/linux/envars.py b/volatility3/framework/plugins/linux/envars.py index 04b75c8a8..8cdbfe493 100644 --- a/volatility3/framework/plugins/linux/envars.py +++ b/volatility3/framework/plugins/linux/envars.py @@ -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( From e18bffdde95e4adb0ee89aeb28b3845c53b37687 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 19 Jan 2025 23:51:28 +0000 Subject: [PATCH 100/141] Revert "Pre linux.pagecache.recoverfs support" --- .../framework/plugins/linux/pagecache.py | 97 ++++++------------- 1 file changed, 32 insertions(+), 65 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 77aa42338..382268515 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -6,9 +6,8 @@ import math import logging import datetime from dataclasses import dataclass, astuple -from typing import List, Set, Type, Iterable, IO +from typing import List, Set, Type, Iterable -from volatility3.framework.constants import architectures from volatility3.framework import renderers, interfaces from volatility3.framework.renderers import format_hints from volatility3.framework.interfaces import plugins @@ -38,11 +37,6 @@ class InodeUser: modification_time: str change_time: str path: str - inode_size: int - - @staticmethod - def format_symlink(symlink_source: str, symlink_dest: str) -> str: - return f"{symlink_source} -> {symlink_dest}" @dataclass @@ -86,7 +80,6 @@ class InodeInternal: access_time_dt = self.inode.get_access_time() modification_time_dt = self.inode.get_modification_time() change_time_dt = self.inode.get_change_time() - inode_size = int(self.inode.i_size) inode_user = InodeUser( superblock_addr=superblock_addr, @@ -102,7 +95,6 @@ class InodeInternal: modification_time=modification_time_dt, change_time=change_time_dt, path=self.path, - inode_size=inode_size, ) return inode_user @@ -112,7 +104,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): _required_framework_version = (2, 0, 0) - _version = (1, 1, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -120,7 +112,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): requirements.ModuleRequirement( name="kernel", description="Linux kernel", - architectures=architectures.LINUX_ARCHS, + architectures=["Intel32", "Intel64"], ), requirements.PluginRequirement( name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0) @@ -156,10 +148,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): """ # 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: - symlink_dest = inode.i_link.dereference().cast( + i_link_str = inode.i_link.dereference().cast( "string", max_length=255, encoding="utf-8", errors="replace" ) - symlink_path = InodeUser.format_symlink(symlink_path, symlink_dest) + symlink_path = f"{symlink_path} -> {i_link_str}" return symlink_path @@ -220,14 +212,12 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str, - follow_symlinks: bool = True, ) -> Iterable[InodeInternal]: """Retrieves the inodes from the superblocks Args: context: The context that the plugin will operate within vmlinux_module_name: The name of the kernel module on which to operate - follow_symlinks: Whether to follow symlinks or not Yields: An InodeInternal object @@ -299,9 +289,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): continue seen_inodes.add(file_inode_ptr) - if follow_symlinks: - file_path = cls._follow_symlink(file_inode_ptr, file_path) - + file_path = cls._follow_symlink(file_inode_ptr, file_path) inode_in = InodeInternal( superblock=superblock, mountpoint=mountpoint, @@ -389,7 +377,6 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): ("ModificationTime", datetime.datetime), ("ChangeTime", datetime.datetime), ("FilePath", str), - ("InodeSize", int), ] return renderers.TreeGrid( @@ -402,7 +389,7 @@ class InodePages(plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (3, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -410,7 +397,7 @@ class InodePages(plugins.PluginInterface): requirements.ModuleRequirement( name="kernel", description="Linux kernel", - architectures=architectures.LINUX_ARCHS, + architectures=["Intel32", "Intel64"], ), requirements.PluginRequirement( name="files", plugin=Files, version=(1, 0, 0) @@ -435,68 +422,48 @@ class InodePages(plugins.PluginInterface): @staticmethod def write_inode_content_to_file( - context: interfaces.context.ContextInterface, - layer_name: str, inode: interfaces.objects.ObjectInterface, filename: str, open_method: Type[interfaces.plugins.FileHandlerInterface], + vmlinux_layer: interfaces.layers.TranslationLayerInterface, ) -> None: """Extracts the inode's contents from the page cache and saves them to a file Args: - context: The context on which to operate - layer_name: The name of the layer on which to operate inode: The inode to dump filename: Filename for writing the inode content open_method: class for constructing output files + vmlinux_layer: The kernel layer to obtain the page size """ if not inode.is_reg: vollog.error("The inode is not a regular file") return None - try: - with open_method(filename) as f: - InodePages.write_inode_content_to_stream(context, layer_name, inode, f) - except OSError as e: - vollog.error("Unable to write to file (%s): %s", filename, e) - - @staticmethod - def write_inode_content_to_stream( - context: interfaces.context.ContextInterface, - layer_name: str, - inode: interfaces.objects.ObjectInterface, - stream: IO, - ) -> None: - """Extracts the inode's contents from the page cache and saves them to a stream - - Args: - context: The context on which to operate - layer_name: The name of the layer on which to operate - inode: The inode to dump - stream: An IO stream to write to, typically FileHandlerInterface or BytesIO - """ - layer = context.layers[layer_name] - # By using truncate/seek, provided the filesystem supports it, and the - # stream is a File interface, a sparse file will be + # By using truncate/seek, provided the filesystem supports it, a sparse file will be # created, saving both disk space and I/O time. # Additionally, using the page index will guarantee that each page is written at the # appropriate file position. - inode_size = inode.i_size - stream.truncate(inode_size) + try: + with open_method(filename) as f: + inode_size = inode.i_size + f.truncate(inode_size) - for page_idx, page_content in inode.get_contents(): - current_fp = page_idx * layer.page_size - max_length = inode_size - current_fp - page_bytes = page_content[:max_length] - if current_fp + len(page_bytes) > inode_size: - vollog.error( - "Page out of file bounds: inode 0x%x, inode size %d, page index %d", - inode.vol.offset, - inode_size, - page_idx, - ) - stream.seek(current_fp) - stream.write(page_bytes) + for page_idx, page_content in inode.get_contents(): + current_fp = page_idx * vmlinux_layer.page_size + max_length = inode_size - current_fp + page_bytes = page_content[:max_length] + if current_fp + len(page_bytes) > inode_size: + vollog.error( + "Page out of file bounds: inode 0x%x, inode size %d, page index %d", + inode.vol.offset, + inode_size, + page_idx, + ) + f.seek(current_fp) + f.write(page_bytes) + + except OSError as e: + vollog.error("Unable to write to file (%s): %s", filename, e) def _generator(self): vmlinux_module_name = self.config["kernel"] @@ -561,7 +528,7 @@ class InodePages(plugins.PluginInterface): filename = open_method.sanitize_filename(f"inode_0x{inode_address:x}.dmp") vollog.info("[*] Writing inode at 0x%x to '%s'", inode_address, filename) self.write_inode_content_to_file( - self.context, vmlinux_layer.name, inode, filename, open_method + inode, filename, open_method, vmlinux_layer ) def run(self): From bf76aad1e2367e2db4d561d9cc1cd76a42162420 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 20 Jan 2025 10:28:46 +1100 Subject: [PATCH 101/141] linux: page_cache.Files plugin: Ensure the inode's i_link pointer is readable --- volatility3/framework/plugins/linux/pagecache.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 39ed60486..f265241b6 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -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" ) From ec7a101eb92499741cafeb4dea1ab1ef1683a27c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 20 Jan 2025 11:05:09 +1100 Subject: [PATCH 102/141] linux: page_cache.InodePages plugin: Remove unnecesary casting --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index f265241b6..32b176b72 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -511,7 +511,7 @@ class InodePages(plugins.PluginInterface): 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_index = page_obj.index page_file_offset = page_index * vmlinux_layer.page_size dump_safe = ( page_file_offset < inode_size From dfe3d255c064b9c78edf4f5f58eff6c15cc56486 Mon Sep 17 00:00:00 2001 From: Odysseas Stavrou Date: Mon, 20 Jan 2025 22:25:01 +0200 Subject: [PATCH 103/141] Volshell: Update Process retrieval methods with virtual/physical offsets --- volatility3/cli/volshell/linux.py | 58 +++++++++++++++++++++++++++++ volatility3/cli/volshell/windows.py | 47 +++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/volatility3/cli/volshell/linux.py b/volatility3/cli/volshell/linux.py index cc58fa1c2..9ea3ea1f5 100644 --- a/volatility3/cli/volshell/linux.py +++ b/volatility3/cli/volshell/linux.py @@ -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,52 @@ class Volshell(generic.Volshell): return None print(f"No task with task ID {pid} found") + def get_process(self, pid=None, offset=None): + """Get Task based on a process ID. Does not retrieve the layer, to change layer use the .pid attribute. The offset argument can be used both for physical or virtual offsets""" + + if pid is not None and offset is not None: + print("Only one parameter is accepted") + return None + + if offset is not 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] + + ptask = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "task_struct", + layer_name=memory_layer_name, + offset=offset, + native_layer_name=kernel_layer_name, + ) + + try: + DescExitStateEnum(ptask.exit_state) + except ValueError: + print( + f"task_struct @ {hex(ptask.vol.offset)} as exit_state {ptask.exit_state} is likely not valid" + ) + + if not (0 < ptask.pid < 65535): + print( + f"task_struct @ {hex(ptask.vol.offset)} as pid {ptask.pid} is likely not valid" + ) + + return ptask + + 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 +107,7 @@ class Volshell(generic.Volshell): result += [ (["ct", "change_task", "cp"], self.change_task), (["lt", "list_tasks", "ps"], self.list_tasks), + (["gp", "get_process"], self.get_process), (["symbols"], self.context.symbol_space[self.current_symbol_table]), ] if self.config.get("pid", None) is not None: diff --git a/volatility3/cli/volshell/windows.py b/volatility3/cli/volshell/windows.py index 303d4d5c3..a77392561 100644 --- a/volatility3/cli/volshell/windows.py +++ b/volatility3/cli/volshell/windows.py @@ -44,11 +44,58 @@ class Volshell(generic.Volshell): ) ) + def get_process(self, pid=None, v_offset=None, p_offset=None): + """Returns the EPROCESS object that matches the pid. If v_offset/p_offset is provided, construct the EPROCESS object at the provided address. Only one parameter is allowed.""" + + if sum(1 if x is not None else 0 for x in [pid, v_offset, p_offset]) != 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 v_offset is not None: + eproc = self.context.object( + eprocess_symbol, + layer_name=kernel_layer_name, + offset=v_offset, + ) + + return eproc + + if p_offset is not None: + eproc = self.context.object( + eprocess_symbol, + layer_name=memory_layer_name, + offset=p_offset, + 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: From b59f051353cd58b7d6e4bfda2f07746820f4f32a Mon Sep 17 00:00:00 2001 From: Odysseas Stavrou Date: Wed, 22 Jan 2025 02:39:35 +0200 Subject: [PATCH 104/141] Volshell: Updates to the get_process() methods --- volatility3/cli/volshell/linux.py | 55 +++++++++++++++++++---------- volatility3/cli/volshell/windows.py | 23 ++++++++---- 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/volatility3/cli/volshell/linux.py b/volatility3/cli/volshell/linux.py index 9ea3ea1f5..b3689c3ae 100644 --- a/volatility3/cli/volshell/linux.py +++ b/volatility3/cli/volshell/linux.py @@ -51,42 +51,61 @@ class Volshell(generic.Volshell): return None print(f"No task with task ID {pid} found") - def get_process(self, pid=None, offset=None): - """Get Task based on a process ID. Does not retrieve the layer, to change layer use the .pid attribute. The offset argument can be used both for physical or virtual offsets""" + 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. - if pid is not None and offset is not None: + 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 - if offset is not None: - vmlinux_module_name = self.config["kernel"] - vmlinux = self.context.modules[vmlinux_module_name] + 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] + kernel_layer_name = vmlinux.layer_name + kernel_layer = self.context.layers[kernel_layer_name] - memory_layer_name = kernel_layer.dependencies[0] + memory_layer_name = kernel_layer.dependencies[0] - ptask = self.context.object( - vmlinux.symbol_table_name + constants.BANG + "task_struct", + 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=offset, + offset=physaddr, native_layer_name=kernel_layer_name, ) + if physaddr is not None or virtaddr is not None: try: - DescExitStateEnum(ptask.exit_state) + DescExitStateEnum(task.exit_state) except ValueError: print( - f"task_struct @ {hex(ptask.vol.offset)} as exit_state {ptask.exit_state} is likely not valid" + f"task_struct @ {hex(task.vol.offset)} as exit_state {task.exit_state} is likely not valid" ) - if not (0 < ptask.pid < 65535): + if not (0 < task.pid < 65535): print( - f"task_struct @ {hex(ptask.vol.offset)} as pid {ptask.pid} is likely not valid" + f"task_struct @ {hex(task.vol.offset)} as pid {task.pid} is likely not valid" ) - return ptask + return task if pid is not None: tasks = self.list_tasks() @@ -107,7 +126,7 @@ class Volshell(generic.Volshell): result += [ (["ct", "change_task", "cp"], self.change_task), (["lt", "list_tasks", "ps"], self.list_tasks), - (["gp", "get_process"], self.get_process), + (["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: diff --git a/volatility3/cli/volshell/windows.py b/volatility3/cli/volshell/windows.py index a77392561..9b89a8b81 100644 --- a/volatility3/cli/volshell/windows.py +++ b/volatility3/cli/volshell/windows.py @@ -44,10 +44,19 @@ class Volshell(generic.Volshell): ) ) - def get_process(self, pid=None, v_offset=None, p_offset=None): - """Returns the EPROCESS object that matches the pid. If v_offset/p_offset is provided, construct the EPROCESS object at the provided address. Only one parameter is allowed.""" + 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. - if sum(1 if x is not None else 0 for x in [pid, v_offset, p_offset]) != 1: + 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 @@ -61,20 +70,20 @@ class Volshell(generic.Volshell): eprocess_symbol = kernel.symbol_table_name + constants.BANG + "_EPROCESS" - if v_offset is not None: + if virtaddr is not None: eproc = self.context.object( eprocess_symbol, layer_name=kernel_layer_name, - offset=v_offset, + offset=virtaddr, ) return eproc - if p_offset is not None: + if physaddr is not None: eproc = self.context.object( eprocess_symbol, layer_name=memory_layer_name, - offset=p_offset, + offset=physaddr, native_layer_name=kernel_layer_name, ) From c10572905f3f3760594db07575534a5805a10fe3 Mon Sep 17 00:00:00 2001 From: Daniel Davidov <35842733+Danking555@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:45:52 +0200 Subject: [PATCH 105/141] Add low stub offset kernel detection reference: Memprocfs and https://www.youtube.com/watch?v=_ShCSth6dWM --- volatility3/framework/automagic/pdbscan.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 729c48063..1ccecf97a 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -11,6 +11,7 @@ import contextlib import logging import math import os +import struct from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union from volatility3.framework import constants, exceptions, interfaces, layers @@ -376,8 +377,43 @@ 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]: + 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): + if 0xffffffffffff00ff & int.from_bytes(physical_layer.read(offset, 0x8), "little") != 0x00000001000600E9: + continue # not _PROCESSOR_START_BLOCK->Jmp + potential_kernel_hint = int.from_bytes(physical_layer.read(offset + 0x70, 0x8), "little") + if (0xfffff80000000003 & potential_kernel_hint) != 0xfffff80000000000: + continue # not _PROCESSOR_START_BLOCK->LmTarget + kernel_hint = potential_kernel_hint & 0xffffffffffff + kernel_base = kernel_hint & (~0x1fffff) & 0xffffffffffff + break + + 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, From 34a7dfc72fb3dced313e9a52b73b96dff31b778a Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 23 Jan 2025 10:42:52 +0100 Subject: [PATCH 106/141] split linux modules utilities --- .../symbols/linux/utilities/modules.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 volatility3/framework/symbols/linux/utilities/modules.py diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py new file mode 100644 index 000000000..ac9b2afaf --- /dev/null +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -0,0 +1,68 @@ +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) + + @staticmethod + def mask_mods_list( + 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 + ] + + @staticmethod + def lookup_module_address( + 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 From 43ab0b0c314832742c00f7821cd6f3327529894e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 23 Jan 2025 10:43:14 +0100 Subject: [PATCH 107/141] add deprecation decorator --- .../framework/configuration/requirements.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 3e3608000..3af5601dc 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -11,6 +11,7 @@ expect to be in the context (such as particular layers or symboltables). import abc import logging import os +import functools from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type from urllib import parse, request @@ -723,3 +724,25 @@ class ModuleRequirement( """Builds the appropriate configuration for the specified requirement.""" return context.modules[value].build_configuration() + + +def deprecated_method(replacement: str, additional_information: str = ""): + """A decorator for marking functions as deprecated. + + Args: + replacement: The replacement function overriding the deprecated API (full path preferred, starting from "volatility3."). String was preferred, for convenience and to prevent import conflicts on caller side. + 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, additional_information + deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__name__}\" is deprecated, use \"{replacement}\" instead. {additional_information}" + vollog.warning(deprecation_msg) + # Return the wrapped function with its original arguments + return deprecated_func(*args, **kwargs) + + return wrapper + + return decorator From 7575aa5d6354419b51d1ac563c1de4db62da0ca1 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 23 Jan 2025 10:44:20 +0100 Subject: [PATCH 108/141] deprecate lookup_module_address and mask_mods_list --- .../framework/symbols/linux/__init__.py | 101 ++++++++---------- 1 file changed, 44 insertions(+), 57 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 423284b03..3dc744f78 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -8,11 +8,13 @@ import logging from abc import ABC, abstractmethod from typing import Iterator, List, Tuple, Optional, Union +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.objects import utility from volatility3.framework.symbols import intermed from volatility3.framework.symbols.linux import extensions +from volatility3.framework.configuration import requirements vollog = logging.getLogger(__name__) @@ -81,7 +83,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 2, 0) + _version = (2, 2, 1) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -338,27 +340,6 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): yield fd_num, filp, full_path - @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 generate_kernel_handler_info( cls, @@ -382,41 +363,9 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return [ (constants.linux.KERNEL_NAME, start_addr, end_addr) - ] + LinuxUtilities.mask_mods_list(context, kernel.layer_name, mods_list) - - @classmethod - def lookup_module_address( - cls, - kernel_module: interfaces.context.ModuleInterface, - handlers: List[Tuple[str, int, int]], - target_address: int, - ): - """ - 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 + ] + linux_utilities_modules.Modules.mask_mods_list( + context, kernel.layer_name, mods_list + ) @classmethod def walk_internal_list(cls, vmlinux, struct_name, list_member, list_start): @@ -504,6 +453,44 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): [chr((code >> (i * 8)) & 0xFF) for i in range(code_bytes_length)] ) + ## Deprecated APIs ## + @classmethod + @requirements.deprecated_method( + replacement="volatility3.framework.symbols.linux.utilities.modules.Modules.mask_mods_list" + ) + def mask_mods_list( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + 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 + """ + return linux_utilities_modules.Modules.mask_mods_list(context, layer_name, mods) + + @classmethod + @requirements.deprecated_method( + replacement="volatility3.framework.symbols.linux.utilities.modules.Modules.lookup_module_address" + ) + 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. + """ + return linux_utilities_modules.Modules.lookup_module_address( + kernel_module.context, kernel_module.name, handlers, target_address + ) + class IDStorage(ABC): """Abstraction to support both XArray and RadixTree""" From fd77c041537b40f38db7428ce05a876ad5b1e08b Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 23 Jan 2025 10:46:42 +0100 Subject: [PATCH 109/141] move to linux_utilities_modules APIs --- volatility3/framework/plugins/linux/check_idt.py | 7 +++++-- .../framework/plugins/linux/keyboard_notifiers.py | 7 +++++-- volatility3/framework/plugins/linux/kthreads.py | 9 ++++++--- volatility3/framework/plugins/linux/netfilter.py | 7 +++++-- volatility3/framework/plugins/linux/tty_check.py | 7 +++++-- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/plugins/linux/check_idt.py b/volatility3/framework/plugins/linux/check_idt.py index 07582e2c1..5859e73d6 100644 --- a/volatility3/framework/plugins/linux/check_idt.py +++ b/volatility3/framework/plugins/linux/check_idt.py @@ -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 @@ -99,8 +100,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 ( diff --git a/volatility3/framework/plugins/linux/keyboard_notifiers.py b/volatility3/framework/plugins/linux/keyboard_notifiers.py index 72273a77b..c1b7572c6 100644 --- a/volatility3/framework/plugins/linux/keyboard_notifiers.py +++ b/volatility3/framework/plugins/linux/keyboard_notifiers.py @@ -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 @@ -66,8 +67,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]) diff --git a/volatility3/framework/plugins/linux/kthreads.py b/volatility3/framework/plugins/linux/kthreads.py index 40e992069..2e1bbed47 100644 --- a/volatility3/framework/plugins/linux/kthreads.py +++ b/volatility3/framework/plugins/linux/kthreads.py @@ -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]: @@ -88,8 +89,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 = [ diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index 73496dfd9..ccb831b61 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -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 ( @@ -263,8 +264,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": diff --git a/volatility3/framework/plugins/linux/tty_check.py b/volatility3/framework/plugins/linux/tty_check.py index 45238ef8c..f375968a4 100644 --- a/volatility3/framework/plugins/linux/tty_check.py +++ b/volatility3/framework/plugins/linux/tty_check.py @@ -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 @@ -79,8 +80,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)) From b8d9c7b88311016cea97b8b60afeec4d47558af0 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Thu, 23 Jan 2025 11:57:43 -0600 Subject: [PATCH 110/141] #1473 - add missing exception handling for get_key --- volatility3/framework/plugins/windows/envars.py | 12 ++++++------ .../framework/plugins/windows/getservicesids.py | 5 +++-- .../framework/plugins/windows/getsids.py | 2 +- .../plugins/windows/registry/userassist.py | 17 ++++++++++++----- .../framework/plugins/windows/svcscan.py | 6 +++--- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/volatility3/framework/plugins/windows/envars.py b/volatility3/framework/plugins/windows/envars.py index cac4ecf40..48e1ef671 100644 --- a/volatility3/framework/plugins/windows/envars.py +++ b/volatility3/framework/plugins/windows/envars.py @@ -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(): diff --git a/volatility3/framework/plugins/windows/getservicesids.py b/volatility3/framework/plugins/windows/getservicesids.py index eece7fb6c..b97d2bb46 100644 --- a/volatility3/framework/plugins/windows/getservicesids.py +++ b/volatility3/framework/plugins/windows/getservicesids.py @@ -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,10 @@ 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: diff --git a/volatility3/framework/plugins/windows/getsids.py b/volatility3/framework/plugins/windows/getsids.py index df0c7a835..00c78e1cf 100644 --- a/volatility3/framework/plugins/windows/getsids.py +++ b/volatility3/framework/plugins/windows/getsids.py @@ -158,7 +158,7 @@ class GetSIDs(interfaces.plugins.PluginInterface): layers.registry.RegistryFormatException, ): continue - except (KeyError, exceptions.InvalidAddressException): + except (KeyError, exceptions.InvalidAddressException, layers.registry.RegistryFormatException): continue return sids diff --git a/volatility3/framework/plugins/windows/registry/userassist.py b/volatility3/framework/plugins/windows/registry/userassist.py index 932ee9d6f..646fb1d7f 100644 --- a/volatility3/framework/plugins/windows/registry/userassist.py +++ b/volatility3/framework/plugins/windows/registry/userassist.py @@ -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,17 @@ 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}") + 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)") diff --git a/volatility3/framework/plugins/windows/svcscan.py b/volatility3/framework/plugins/windows/svcscan.py index bd477ba27..93087f352 100644 --- a/volatility3/framework/plugins/windows/svcscan.py +++ b/volatility3/framework/plugins/windows/svcscan.py @@ -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,12 @@ 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", From 6c4cafa64f68e6b001cd1ed32e8e5fb3d9993f30 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Thu, 23 Jan 2025 11:59:37 -0600 Subject: [PATCH 111/141] #1473 - black fixes --- .../framework/plugins/windows/getservicesids.py | 12 ++++++++++-- volatility3/framework/plugins/windows/getsids.py | 6 +++++- .../framework/plugins/windows/registry/userassist.py | 8 ++++++-- volatility3/framework/plugins/windows/svcscan.py | 12 ++++++++++-- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/windows/getservicesids.py b/volatility3/framework/plugins/windows/getservicesids.py index b97d2bb46..207d0e2ad 100644 --- a/volatility3/framework/plugins/windows/getservicesids.py +++ b/volatility3/framework/plugins/windows/getservicesids.py @@ -87,10 +87,18 @@ class GetServiceSIDs(interfaces.plugins.PluginInterface): # Get ControlSet\Services. try: services = hive.get_key(r"CurrentControlSet\Services") - except (KeyError, exceptions.InvalidAddressException, registry.RegistryFormatException): + except ( + KeyError, + exceptions.InvalidAddressException, + registry.RegistryFormatException, + ): try: services = hive.get_key(r"ControlSet001\Services") - except (KeyError, exceptions.InvalidAddressException, registry.RegistryFormatException): + except ( + KeyError, + exceptions.InvalidAddressException, + registry.RegistryFormatException, + ): continue if services: diff --git a/volatility3/framework/plugins/windows/getsids.py b/volatility3/framework/plugins/windows/getsids.py index 00c78e1cf..a75bbe7ea 100644 --- a/volatility3/framework/plugins/windows/getsids.py +++ b/volatility3/framework/plugins/windows/getsids.py @@ -158,7 +158,11 @@ class GetSIDs(interfaces.plugins.PluginInterface): layers.registry.RegistryFormatException, ): continue - except (KeyError, exceptions.InvalidAddressException, layers.registry.RegistryFormatException): + except ( + KeyError, + exceptions.InvalidAddressException, + layers.registry.RegistryFormatException, + ): continue return sids diff --git a/volatility3/framework/plugins/windows/registry/userassist.py b/volatility3/framework/plugins/windows/registry/userassist.py index 646fb1d7f..d50b5216e 100644 --- a/volatility3/framework/plugins/windows/registry/userassist.py +++ b/volatility3/framework/plugins/windows/registry/userassist.py @@ -173,10 +173,14 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac return_list=True, ) except RegistryFormatException as e: - vollog.warning(f"Error accessing UserAssist key in {hive_name} at {hive.hive_offset:#x}") + vollog.warning( + f"Error accessing UserAssist key in {hive_name} at {hive.hive_offset:#x}" + ) return None except KeyError: - vollog.warning(f"UserAssist key not found in {hive_name} at {hive.hive_offset:#x}") + vollog.warning( + f"UserAssist key not found in {hive_name} at {hive.hive_offset:#x}" + ) return None if not userassist_node_path: diff --git a/volatility3/framework/plugins/windows/svcscan.py b/volatility3/framework/plugins/windows/svcscan.py index 93087f352..17baac5b0 100644 --- a/volatility3/framework/plugins/windows/svcscan.py +++ b/volatility3/framework/plugins/windows/svcscan.py @@ -159,12 +159,20 @@ class SvcScan(interfaces.plugins.PluginInterface): return cast( objects.StructType, hive.get_key(r"CurrentControlSet\Services") ) - except (KeyError, exceptions.InvalidAddressException, registry.RegistryFormatException): + except ( + KeyError, + exceptions.InvalidAddressException, + registry.RegistryFormatException, + ): try: return cast( objects.StructType, hive.get_key(r"ControlSet001\Services") ) - except (KeyError, exceptions.InvalidAddressException, registry.RegistryFormatException): + except ( + KeyError, + exceptions.InvalidAddressException, + registry.RegistryFormatException, + ): vollog.log( constants.LOGLEVEL_VVVV, "Could not retrieve any control set from SYSTEM hive", From 81ba89eef663727608886e66595dfd7b3dcd9831 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Thu, 23 Jan 2025 12:20:14 -0600 Subject: [PATCH 112/141] #1473 - update exception message --- volatility3/framework/plugins/windows/registry/userassist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/registry/userassist.py b/volatility3/framework/plugins/windows/registry/userassist.py index d50b5216e..87016553a 100644 --- a/volatility3/framework/plugins/windows/registry/userassist.py +++ b/volatility3/framework/plugins/windows/registry/userassist.py @@ -174,7 +174,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac ) except RegistryFormatException as e: vollog.warning( - f"Error accessing UserAssist key in {hive_name} at {hive.hive_offset:#x}" + f"Error accessing UserAssist key in {hive_name} at {hive.hive_offset:#x}: {e}" ) return None except KeyError: From a68be50798254cbadc490393721e74180b4117cc Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 23 Jan 2025 20:30:21 +0000 Subject: [PATCH 113/141] Revert "Typing fix" This reverts commit c82d432b10258136ff0777dfec1fbf5844316132. --- volatility3/framework/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index a1925faef..754939460 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -5,6 +5,7 @@ # Check the python version to ensure it's suitable import glob import sys +from volatility3.framework import check_python_version as check_python_version import zipfile import importlib import inspect @@ -57,7 +58,7 @@ class NonInheritable: 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) From 61f60ac464ba01885216704faf37ed6b6fc4beb6 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 23 Jan 2025 20:52:08 +0000 Subject: [PATCH 114/141] Core: Move the python check somewhere it can't accidentally be removed --- volatility3/framework/__init__.py | 12 +++++++++++- volatility3/framework/check_python_version.py | 14 -------------- volatility3/framework/constants/__init__.py | 2 ++ 3 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 volatility3/framework/check_python_version.py diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 754939460..466e697bb 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -5,7 +5,6 @@ # Check the python version to ensure it's suitable import glob import sys -from volatility3.framework import check_python_version as check_python_version import zipfile import importlib import inspect @@ -16,6 +15,17 @@ from typing import Any, Dict, Generator, List, Optional, Tuple, Type, TypeVar from volatility3.framework import constants, interfaces +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" + ) # ## # diff --git a/volatility3/framework/check_python_version.py b/volatility3/framework/check_python_version.py deleted file mode 100644 index f2d284f2a..000000000 --- a/volatility3/framework/check_python_version.py +++ /dev/null @@ -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" - ) diff --git a/volatility3/framework/constants/__init__.py b/volatility3/framework/constants/__init__.py index 23cc2dde5..2e6ae0261 100644 --- a/volatility3/framework/constants/__init__.py +++ b/volatility3/framework/constants/__init__.py @@ -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")), From 128e1be154cc5a9853da3413565685689bf702e5 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 23 Jan 2025 20:57:44 +0000 Subject: [PATCH 115/141] Core: Fix f-string quotes --- volatility3/framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 466e697bb..0bbdefa43 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -24,7 +24,7 @@ if ( ) ): raise RuntimeError( - f"Volatility framework requires python version {".".join(str(x) for x in constants.REQUIRED_PYTHON_VERSION)} or greater" + f"Volatility framework requires python version {'.'.join(str(x) for x in constants.REQUIRED_PYTHON_VERSION)} or greater" ) # ## From 17d52c27bfc31ad9ecad7fb0b8604586039ce2b0 Mon Sep 17 00:00:00 2001 From: Daniel Davidov <35842733+Danking555@users.noreply.github.com> Date: Fri, 24 Jan 2025 21:09:28 +0200 Subject: [PATCH 116/141] Update method_low_stub_offset & run ruff & black * Eliminate unnecessary scanning for 32 bit processors where the structure PROCESSOR_START_BLOCK doesn't exist * Put offsets as values of constants in a class - LowStubLayout. * Add documentation in the class and in the function method_low_stub_offset * Run "ruff check --fix" and "black ." * Checked the method works on 3 physical machines --- volatility3/framework/automagic/pdbscan.py | 74 ++++++++++++++++++---- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 1ccecf97a..7d289bcb6 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -11,7 +11,6 @@ import contextlib import logging import math import os -import struct from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union from volatility3.framework import constants, exceptions, interfaces, layers @@ -377,25 +376,73 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): valid_kernel = (virtual_layer_name, address, res[0]) return valid_kernel - def method_low_stub_offset(self, + class LowStubLayout: + """ + Represents 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 + + 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') + 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): - if 0xffffffffffff00ff & int.from_bytes(physical_layer.read(offset, 0x8), "little") != 0x00000001000600E9: - continue # not _PROCESSOR_START_BLOCK->Jmp - potential_kernel_hint = int.from_bytes(physical_layer.read(offset + 0x70, 0x8), "little") - if (0xfffff80000000003 & potential_kernel_hint) != 0xfffff80000000000: - continue # not _PROCESSOR_START_BLOCK->LmTarget - kernel_hint = potential_kernel_hint & 0xffffffffffff - kernel_base = kernel_hint & (~0x1fffff) & 0xffffffffffff + # 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): + jmp_and_completion_values = int.from_bytes( + physical_layer.read(offset, 0x8), "little" + ) + if ( + 0xFFFFFFFFFFFF00FF & jmp_and_completion_values + != self.LowStubLayout.JMP_AND_COMPLETION_SIGNATURE + ): + continue + cr3_value = int.from_bytes( + physical_layer.read( + offset + self.LowStubLayout.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 + self.LowStubLayout.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 if kernel_base: @@ -408,7 +455,6 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): if valid_kernel: return valid_kernel kernel_base -= 0x200000 - return None # List of methods to be run, in order, to determine the valid kernels From e9088be0d86fa2f68774d47371ebd2736287f1c5 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sat, 28 Dec 2024 22:46:39 +0000 Subject: [PATCH 117/141] Prevent infinite looping and out of memory errors #1482 --- .../framework/symbols/windows/extensions/registry.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/volatility3/framework/symbols/windows/extensions/registry.py b/volatility3/framework/symbols/windows/extensions/registry.py index c9544a8ba..b282b13cf 100644 --- a/volatility3/framework/symbols/windows/extensions/registry.py +++ b/volatility3/framework/symbols/windows/extensions/registry.py @@ -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 "" + seen.add(kcb.ParentKcb.vol.offset) + + if len(output) > 128: + return "" + if kcb.NameBlock.Name is None: break From 506a61d8846e6a3399ab1f964d41b09a197592a6 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 24 Jan 2025 22:09:24 +0000 Subject: [PATCH 118/141] Address feedback --- volatility3/framework/plugins/windows/handles.py | 4 ++-- volatility3/framework/symbols/windows/extensions/registry.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/handles.py b/volatility3/framework/plugins/windows/handles.py index 38ccfbfbc..6a391fe35 100644 --- a/volatility3/framework/plugins/windows/handles.py +++ b/volatility3/framework/plugins/windows/handles.py @@ -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(), ), ) diff --git a/volatility3/framework/symbols/windows/extensions/registry.py b/volatility3/framework/symbols/windows/extensions/registry.py index b282b13cf..a8cc7703c 100644 --- a/volatility3/framework/symbols/windows/extensions/registry.py +++ b/volatility3/framework/symbols/windows/extensions/registry.py @@ -138,11 +138,11 @@ class CM_KEY_BODY(objects.StructType): kcb = self.KeyControlBlock while kcb.ParentKcb: if kcb.ParentKcb.vol.offset in seen: - return "" + return None seen.add(kcb.ParentKcb.vol.offset) if len(output) > 128: - return "" + return None if kcb.NameBlock.Name is None: break From 8b0165b6f6ea7a6ebb70ddb01b2de29b627ddcfd Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 25 Jan 2025 14:45:21 +0100 Subject: [PATCH 119/141] add linux_utilities_modules requirement --- volatility3/framework/plugins/linux/check_idt.py | 5 +++++ volatility3/framework/plugins/linux/keyboard_notifiers.py | 5 +++++ volatility3/framework/plugins/linux/kthreads.py | 5 +++++ volatility3/framework/plugins/linux/netfilter.py | 5 +++++ volatility3/framework/plugins/linux/tty_check.py | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/volatility3/framework/plugins/linux/check_idt.py b/volatility3/framework/plugins/linux/check_idt.py index 5859e73d6..ffb707af5 100644 --- a/volatility3/framework/plugins/linux/check_idt.py +++ b/volatility3/framework/plugins/linux/check_idt.py @@ -28,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) ), diff --git a/volatility3/framework/plugins/linux/keyboard_notifiers.py b/volatility3/framework/plugins/linux/keyboard_notifiers.py index c1b7572c6..8577de848 100644 --- a/volatility3/framework/plugins/linux/keyboard_notifiers.py +++ b/volatility3/framework/plugins/linux/keyboard_notifiers.py @@ -27,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) ), diff --git a/volatility3/framework/plugins/linux/kthreads.py b/volatility3/framework/plugins/linux/kthreads.py index 2e1bbed47..bd0e895a4 100644 --- a/volatility3/framework/plugins/linux/kthreads.py +++ b/volatility3/framework/plugins/linux/kthreads.py @@ -31,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) ), diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index ccb831b61..33a8ca7cc 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -691,6 +691,11 @@ class Netfilter(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=cls._required_lsmod_version ), diff --git a/volatility3/framework/plugins/linux/tty_check.py b/volatility3/framework/plugins/linux/tty_check.py index f375968a4..9bbca246c 100644 --- a/volatility3/framework/plugins/linux/tty_check.py +++ b/volatility3/framework/plugins/linux/tty_check.py @@ -30,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) ), From ce671bb2fa3a7d03042a993814b4c11f19650cf4 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 25 Jan 2025 15:02:25 +0100 Subject: [PATCH 120/141] transfer deprecated_method into framework module --- volatility3/framework/__init__.py | 29 ++++++++++++++++++- .../framework/configuration/requirements.py | 23 --------------- .../framework/symbols/linux/__init__.py | 17 +++++++---- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index a1925faef..12254ca77 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -11,7 +11,8 @@ import inspect import logging import os import traceback -from typing import Any, Dict, Generator, List, Optional, Tuple, Type, TypeVar +import functools +from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, TypeVar from volatility3.framework import constants, interfaces @@ -52,6 +53,32 @@ def require_interface_version(*args) -> None: ) +class Deprecation: + """Deprecation related methods.""" + + @staticmethod + def deprecated_method(replacement: Callable, 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) + 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, additional_information + deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__name__}\" is deprecated, use \"{replacement.__module__ + '.' + replacement.__name__}\" instead. {additional_information}" + vollog.warning(deprecation_msg) + # 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 diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 3af5601dc..3e3608000 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -11,7 +11,6 @@ expect to be in the context (such as particular layers or symboltables). import abc import logging import os -import functools from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type from urllib import parse, request @@ -724,25 +723,3 @@ class ModuleRequirement( """Builds the appropriate configuration for the specified requirement.""" return context.modules[value].build_configuration() - - -def deprecated_method(replacement: str, additional_information: str = ""): - """A decorator for marking functions as deprecated. - - Args: - replacement: The replacement function overriding the deprecated API (full path preferred, starting from "volatility3."). String was preferred, for convenience and to prevent import conflicts on caller side. - 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, additional_information - deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__name__}\" is deprecated, use \"{replacement}\" instead. {additional_information}" - vollog.warning(deprecation_msg) - # Return the wrapped function with its original arguments - return deprecated_func(*args, **kwargs) - - return wrapper - - return decorator diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 3dc744f78..0b7ef751c 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -10,11 +10,16 @@ from typing import Iterator, List, Tuple, Optional, Union 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 -from volatility3.framework.configuration import requirements vollog = logging.getLogger(__name__) @@ -455,8 +460,8 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): ## Deprecated APIs ## @classmethod - @requirements.deprecated_method( - replacement="volatility3.framework.symbols.linux.utilities.modules.Modules.mask_mods_list" + @Deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.mask_mods_list ) def mask_mods_list( cls, @@ -472,8 +477,8 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return linux_utilities_modules.Modules.mask_mods_list(context, layer_name, mods) @classmethod - @requirements.deprecated_method( - replacement="volatility3.framework.symbols.linux.utilities.modules.Modules.lookup_module_address" + @Deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.lookup_module_address ) def lookup_module_address( cls, From 2e1b77f4b3186bae3dfbc7e51e2ba51e9fd850e3 Mon Sep 17 00:00:00 2001 From: Daniel Davidov <35842733+Danking555@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:22:49 +0200 Subject: [PATCH 121/141] Move LowStubLayout constants to windows.constants * Moved constants out of the class and moved to constants.windows * Applied ruff and black --- volatility3/framework/automagic/pdbscan.py | 26 +++---------------- .../framework/constants/windows/__init__.py | 18 +++++++++++++ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 7d289bcb6..f9c0d853d 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -376,26 +376,6 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): valid_kernel = (virtual_layer_name, address, res[0]) return valid_kernel - class LowStubLayout: - """ - Represents 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 - def method_low_stub_offset( self, context: interfaces.context.ContextInterface, @@ -417,12 +397,12 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): ) if ( 0xFFFFFFFFFFFF00FF & jmp_and_completion_values - != self.LowStubLayout.JMP_AND_COMPLETION_SIGNATURE + != constants.windows.JMP_AND_COMPLETION_SIGNATURE ): continue cr3_value = int.from_bytes( physical_layer.read( - offset + self.LowStubLayout.PROCESSOR_START_BLOCK_CR3_OFFSET, 0x8 + offset + constants.windows.PROCESSOR_START_BLOCK_CR3_OFFSET, 0x8 ), "little", ) @@ -434,7 +414,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): continue potential_kernel_hint = int.from_bytes( physical_layer.read( - offset + self.LowStubLayout.PROCESSOR_START_BLOCK_LM_TARGET_OFFSET, + offset + constants.windows.PROCESSOR_START_BLOCK_LM_TARGET_OFFSET, 0x8, ), "little", diff --git a/volatility3/framework/constants/windows/__init__.py b/volatility3/framework/constants/windows/__init__.py index 7face984a..6f37acd2d 100644 --- a/volatility3/framework/constants/windows/__init__.py +++ b/volatility3/framework/constants/windows/__init__.py @@ -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 From d8658f0729f7abbd8410f76571aad6369deabee6 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 25 Jan 2025 17:08:27 +0100 Subject: [PATCH 122/141] put deprecated functions order back --- .../framework/symbols/linux/__init__.py | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 0b7ef751c..bc2492f7a 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -345,6 +345,23 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): yield fd_num, filp, full_path + @classmethod + @Deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.mask_mods_list + ) + def mask_mods_list( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + 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 + """ + return linux_utilities_modules.Modules.mask_mods_list(context, layer_name, mods) + @classmethod def generate_kernel_handler_info( cls, @@ -372,6 +389,26 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): context, kernel.layer_name, mods_list ) + @classmethod + @Deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.lookup_module_address + ) + 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. + """ + 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): while list_start: @@ -458,44 +495,6 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): [chr((code >> (i * 8)) & 0xFF) for i in range(code_bytes_length)] ) - ## Deprecated APIs ## - @classmethod - @Deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.mask_mods_list - ) - def mask_mods_list( - cls, - context: interfaces.context.ContextInterface, - layer_name: str, - 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 - """ - return linux_utilities_modules.Modules.mask_mods_list(context, layer_name, mods) - - @classmethod - @Deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.lookup_module_address - ) - 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. - """ - return linux_utilities_modules.Modules.lookup_module_address( - kernel_module.context, kernel_module.name, handlers, target_address - ) - class IDStorage(ABC): """Abstraction to support both XArray and RadixTree""" From 825720ed5d8f290b244735c758149e5b9d208c12 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 25 Jan 2025 19:48:31 +0100 Subject: [PATCH 123/141] catch UnsatisfiedException at plugin runtime --- volatility3/cli/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 82a2a4205..d3ce74847 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -500,6 +500,16 @@ class CommandLine: renderer.filter = text_filter.CLIFilter(grid, args.filters) renderer.column_hide_list = args.hide_columns renderer.render(grid) + except exceptions.UnsatisfiedException as excp: + output = sys.stderr + output.write( + "An unsatisfied framework exception was encountered post plugin construction:\n" + ) + self.process_unsatisfied_exceptions(excp) + output.write( + f"Unable to validate the requirements: {[x for x in excp.unsatisfied]}\n", + ) + sys.exit(1) except exceptions.VolatilityException as excp: self.process_exceptions(excp) From 2ed00cc91a6764d05162c01055fc17c18124c6aa Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 25 Jan 2025 19:52:47 +0100 Subject: [PATCH 124/141] add optional version requirement to deprecated_method --- volatility3/framework/__init__.py | 56 ++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 12254ca77..bf4ec4447 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -12,9 +12,11 @@ import logging import os import traceback 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 # ## @@ -57,20 +59,64 @@ class Deprecation: """Deprecation related methods.""" @staticmethod - def deprecated_method(replacement: Callable, additional_information: str = ""): + def deprecated_method( + replacement: Callable, + replacement_base_class_required_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_base_class_required_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, additional_information - deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__name__}\" is deprecated, use \"{replacement.__module__ + '.' + replacement.__name__}\" instead. {additional_information}" - vollog.warning(deprecation_msg) + nonlocal replacement, replacement_base_class_required_version, additional_information + # Prevent version mismatches between deprecated (proxy) methods and the ones they proxy + if replacement_base_class_required_version is not None and callable( + replacement + ): + # example: replacement = volatility3.MyClass.my_dummy_function + # "MyClass.my_dummy_function" -> "MyClass" + replacement_base_class_name = replacement.__qualname__.split(".")[0] + # replacement.__globals__ example: {'MyClass': } + replacement_base_class = replacement.__globals__.get( + replacement_base_class_name + ) + + # Verify that the base class inherits from VersionableInterface + if inspect.isclass(replacement_base_class) and issubclass( + replacement_base_class, + interfaces.configuration.VersionableInterface, + ): + # Construct a requirement + req = requirements.VersionRequirement( + name=replacement_base_class.__name__, + component=replacement_base_class, + version=replacement_base_class_required_version, + ) + # Verify the requirement + if not req.matches_required( + req._version, req._component.version + ): + full_unsat_req_path = ( + deprecated_func.__module__ + + "." + + deprecated_func.__qualname__ + + "." + + req.name + ) + # Catched by the cli and redirected to process_unsatisfied_exceptions + raise exceptions.UnsatisfiedException( + {full_unsat_req_path: req} + ) + + 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) From 3657c6fe5e8db92b1a6364a962b778a89e802624 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 25 Jan 2025 19:54:20 +0100 Subject: [PATCH 125/141] require Modules >= 1.0.0 on deprecated methods --- volatility3/framework/symbols/linux/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index bc2492f7a..6a01efc3a 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -347,7 +347,8 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): @classmethod @Deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.mask_mods_list + replacement=linux_utilities_modules.Modules.mask_mods_list, + replacement_base_class_required_version=(1, 0, 0), ) def mask_mods_list( cls, @@ -391,7 +392,8 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): @classmethod @Deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.lookup_module_address + replacement=linux_utilities_modules.Modules.lookup_module_address, + replacement_base_class_required_version=(1, 0, 0), ) def lookup_module_address( cls, From 4262eff8898b3fbc017abe3c1d1e4f13fa6fb189 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sat, 25 Jan 2025 20:04:51 +0100 Subject: [PATCH 126/141] adhere to AbstractNetfilter requirement checking --- .../framework/plugins/linux/netfilter.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index 33a8ca7cc..ccb7509aa 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -99,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 @@ -680,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) @@ -694,7 +709,7 @@ class Netfilter(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(1, 0, 0), + version=cls._required_linux_utilities_modules_version, ), requirements.PluginRequirement( name="lsmod", plugin=lsmod.Lsmod, version=cls._required_lsmod_version From 6d43dcd3a842d308705f3ecd3c023c94be473846 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 26 Jan 2025 10:31:46 +0100 Subject: [PATCH 127/141] add VersionMismatchException --- volatility3/framework/exceptions.py | 31 ++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index 41c67b88d..0409ae5f0 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -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): @@ -134,3 +135,31 @@ class RenderException(VolatilityException): 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 enhande 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.__qualname__} {self.target_component.version} unmet." From 4bd385cc9dadbfe8800fddc2bce9cef1ae4dadd5 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 26 Jan 2025 10:33:02 +0100 Subject: [PATCH 128/141] handle VersionMismatchException --- volatility3/cli/__init__.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index d3ce74847..b57d9a3f3 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -500,16 +500,6 @@ class CommandLine: renderer.filter = text_filter.CLIFilter(grid, args.filters) renderer.column_hide_list = args.hide_columns renderer.render(grid) - except exceptions.UnsatisfiedException as excp: - output = sys.stderr - output.write( - "An unsatisfied framework exception was encountered post plugin construction:\n" - ) - self.process_unsatisfied_exceptions(excp) - output.write( - f"Unable to validate the requirements: {[x for x in excp.unsatisfied]}\n", - ) - sys.exit(1) except exceptions.VolatilityException as excp: self.process_exceptions(excp) @@ -583,6 +573,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): @@ -627,9 +619,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}" @@ -640,13 +630,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 From ba6b709aba9496f75a61a806f06719189ac97491 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 26 Jan 2025 10:34:18 +0100 Subject: [PATCH 129/141] use VersionMismatchException --- volatility3/framework/__init__.py | 37 ++++++++++--------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index bf4ec4447..5d4e0f3c4 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -61,25 +61,23 @@ class Deprecation: @staticmethod def deprecated_method( replacement: Callable, - replacement_base_class_required_version: Tuple[int, int, int] = None, + 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_base_class_required_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. + 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_base_class_required_version, additional_information + nonlocal replacement, replacement_version, additional_information # Prevent version mismatches between deprecated (proxy) methods and the ones they proxy - if replacement_base_class_required_version is not None and callable( - replacement - ): + if replacement_version is not None and callable(replacement): # example: replacement = volatility3.MyClass.my_dummy_function # "MyClass.my_dummy_function" -> "MyClass" replacement_base_class_name = replacement.__qualname__.split(".")[0] @@ -93,26 +91,15 @@ class Deprecation: replacement_base_class, interfaces.configuration.VersionableInterface, ): - # Construct a requirement - req = requirements.VersionRequirement( - name=replacement_base_class.__name__, - component=replacement_base_class, - version=replacement_base_class_required_version, - ) - # Verify the requirement - if not req.matches_required( - req._version, req._component.version + # SemVer check + if not requirements.VersionRequirement.matches_required( + replacement_version, replacement_base_class.version ): - full_unsat_req_path = ( - deprecated_func.__module__ - + "." - + deprecated_func.__qualname__ - + "." - + req.name - ) - # Catched by the cli and redirected to process_unsatisfied_exceptions - raise exceptions.UnsatisfiedException( - {full_unsat_req_path: req} + raise exceptions.VersionMismatchException( + deprecated_func, + replacement_base_class, + replacement_version, + "A deprecated method was unable to proxy the call to its replacement", ) deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__qualname__}\" is deprecated, use \"{replacement.__module__ + '.' + replacement.__qualname__}\" instead. {additional_information}" From 28081ed989599bc395b190cf503e8d83ef5b8fed Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 26 Jan 2025 10:35:25 +0100 Subject: [PATCH 130/141] tidy up replacement_base_class_required_version --- volatility3/framework/symbols/linux/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 6a01efc3a..397c36c01 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -348,7 +348,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): @classmethod @Deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.mask_mods_list, - replacement_base_class_required_version=(1, 0, 0), + replacement_version=(1, 0, 0), ) def mask_mods_list( cls, @@ -393,7 +393,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): @classmethod @Deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.lookup_module_address, - replacement_base_class_required_version=(1, 0, 0), + replacement_version=(1, 0, 0), ) def lookup_module_address( cls, From 459483651b847241cb4189c4d1449fd18c0bdca7 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 26 Jan 2025 10:53:33 +0100 Subject: [PATCH 131/141] typo --- volatility3/framework/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index 0409ae5f0..99c5f155e 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -153,7 +153,7 @@ class VersionMismatchException(VolatilityException): 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 enhande debugging and bug tracking + failure_reason: A detailed failure reason to enhance debugging and bug tracking """ super().__init__(*args) self.source_component = source_component From 80eebd2f49bee665f194e4adba92cf12a192a997 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 26 Jan 2025 13:18:30 +0100 Subject: [PATCH 132/141] use classmethod instead of staticmethod --- volatility3/framework/symbols/linux/utilities/modules.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index ac9b2afaf..82c63fc18 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -13,8 +13,9 @@ class Modules(interfaces.configuration.VersionableInterface): framework.require_interface_version(*_required_framework_version) - @staticmethod + @classmethod def mask_mods_list( + cls, context: interfaces.context.ContextInterface, layer_name: str, mods: Iterator[interfaces.objects.ObjectInterface], @@ -33,8 +34,9 @@ class Modules(interfaces.configuration.VersionableInterface): for mod in mods ] - @staticmethod + @classmethod def lookup_module_address( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str, handlers: List[Tuple[str, int, int]], From 5d58ba63dbd6a60fd36faca6f23a3a1b4a11e724 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 26 Jan 2025 13:19:01 +0100 Subject: [PATCH 133/141] use __name__ instead of overkill __qualname__ --- volatility3/framework/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index 99c5f155e..a3d660444 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -162,4 +162,4 @@ class VersionMismatchException(VolatilityException): 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.__qualname__} {self.target_component.version} unmet." + 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." From 4cb386858ebe7c62f05b16c9f869fdf762084121 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 26 Jan 2025 13:20:48 +0100 Subject: [PATCH 134/141] use __self__ and enhance exception msg --- volatility3/framework/__init__.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 5d4e0f3c4..aa93340cd 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -77,14 +77,12 @@ class Deprecation: 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): - # example: replacement = volatility3.MyClass.my_dummy_function - # "MyClass.my_dummy_function" -> "MyClass" - replacement_base_class_name = replacement.__qualname__.split(".")[0] - # replacement.__globals__ example: {'MyClass': } - replacement_base_class = replacement.__globals__.get( - replacement_base_class_name - ) + 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( @@ -99,7 +97,7 @@ class Deprecation: deprecated_func, replacement_base_class, replacement_version, - "A deprecated method was unable to proxy the call to its replacement", + "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}" From a25165d935c02c07510b8b4721b074d7bed21258 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Sun, 26 Jan 2025 13:44:23 +0100 Subject: [PATCH 135/141] 2.18.1 -> 2.19.0 bump --- volatility3/framework/constants/_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 689e39664..f2403cf4a 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 18 # Number of changes that only add to the interface -VERSION_PATCH = 1 # Number of changes that do not change 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 = "" PACKAGE_VERSION = ( From 8b31ae612f8458e349cf0a063a578b97ec7ef8e9 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 27 Jan 2025 09:32:33 +0000 Subject: [PATCH 136/141] Layers: Make the low-stub method less brittle --- volatility3/framework/automagic/pdbscan.py | 64 ++++++++++++---------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index f9c0d853d..dd2ad0683 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -392,38 +392,42 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): # 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): - 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", - ) + 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: + # 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 - 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 if kernel_base: # Scanning 32mb in 2mb chunks for the 'ntoskrnl' base address From 7d14f4c778d1f624a35a6db103f99b9c00cfd8b0 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 27 Jan 2025 14:57:39 +0100 Subject: [PATCH 137/141] remove additional_description in favor of the docstring implementation --- volatility3/framework/interfaces/plugins.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/volatility3/framework/interfaces/plugins.py b/volatility3/framework/interfaces/plugins.py index 7ad78d0ba..f763815a6 100644 --- a/volatility3/framework/interfaces/plugins.py +++ b/volatility3/framework/interfaces/plugins.py @@ -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, From d2859f2390070111f42e470dc9e1c6d6ad38a802 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 27 Jan 2025 15:00:03 +0100 Subject: [PATCH 138/141] remove additional_description in favor of the docstring implementation --- volatility3/cli/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index b57d9a3f3..020dac2d2 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -363,11 +363,20 @@ class CommandLine: metavar="PLUGIN", ) for plugin in sorted(plugin_list): + # First line of a plugin docstring will be the short description for -h + # Following 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__.strip().split("\n", 1) + short_help = doc_split[0] + 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]) From 08f03642807056c0f3857eaa78cd4053fc7f391b Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 27 Jan 2025 15:02:02 +0100 Subject: [PATCH 139/141] adapt plugins docstring to fit short and additional help format --- volatility3/framework/plugins/configwriter.py | 4 ++-- volatility3/framework/plugins/linux/modxview.py | 4 ++-- volatility3/framework/plugins/linux/pstree.py | 3 +-- volatility3/framework/plugins/mac/mount.py | 4 ++-- volatility3/framework/plugins/mac/pstree.py | 3 +-- volatility3/framework/plugins/timeliner.py | 4 ++-- volatility3/framework/plugins/windows/pstree.py | 3 +-- volatility3/framework/plugins/windows/psxview.py | 7 ++++--- volatility3/framework/plugins/windows/registry/hivescan.py | 3 +-- volatility3/framework/plugins/windows/scheduled_tasks.py | 5 ++--- 10 files changed, 18 insertions(+), 22 deletions(-) diff --git a/volatility3/framework/plugins/configwriter.py b/volatility3/framework/plugins/configwriter.py index eca01a84a..a567a6acd 100644 --- a/volatility3/framework/plugins/configwriter.py +++ b/volatility3/framework/plugins/configwriter.py @@ -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) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index c74bf28e8..0dd503829 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -15,8 +15,8 @@ vollog = logging.getLogger(__name__) class Modxview(interfaces.plugins.PluginInterface): - """Centralize lsmod, check_modules and hidden_modules results to efficiently - spot modules presence and taints.""" + """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) diff --git a/volatility3/framework/plugins/linux/pstree.py b/volatility3/framework/plugins/linux/pstree.py index 74e172139..fd28fcbbd 100644 --- a/volatility3/framework/plugins/linux/pstree.py +++ b/volatility3/framework/plugins/linux/pstree.py @@ -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) diff --git a/volatility3/framework/plugins/mac/mount.py b/volatility3/framework/plugins/mac/mount.py index 1a1e33571..0f3aa745c 100644 --- a/volatility3/framework/plugins/mac/mount.py +++ b/volatility3/framework/plugins/mac/mount.py @@ -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) diff --git a/volatility3/framework/plugins/mac/pstree.py b/volatility3/framework/plugins/mac/pstree.py index e62d5eb72..ad5bb309b 100644 --- a/volatility3/framework/plugins/mac/pstree.py +++ b/volatility3/framework/plugins/mac/pstree.py @@ -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) diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index 0f4064d79..6000704eb 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -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) diff --git a/volatility3/framework/plugins/windows/pstree.py b/volatility3/framework/plugins/windows/pstree.py index 2be96277c..4f3fe0455 100644 --- a/volatility3/framework/plugins/windows/pstree.py +++ b/volatility3/framework/plugins/windows/pstree.py @@ -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) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index b5ddd2ee5..aa379bdc5 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -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. diff --git a/volatility3/framework/plugins/windows/registry/hivescan.py b/volatility3/framework/plugins/windows/registry/hivescan.py index 7b3c0b622..6e0171a78 100644 --- a/volatility3/framework/plugins/windows/registry/hivescan.py +++ b/volatility3/framework/plugins/windows/registry/hivescan.py @@ -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) diff --git a/volatility3/framework/plugins/windows/scheduled_tasks.py b/volatility3/framework/plugins/windows/scheduled_tasks.py index 6dd5613c4..31aaec4f0 100644 --- a/volatility3/framework/plugins/windows/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/scheduled_tasks.py @@ -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) From f64291faec4991bbe13f0e5cda655ae463b1f3bb Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 27 Jan 2025 17:44:18 +0100 Subject: [PATCH 140/141] split help on two consecutive newlines --- volatility3/cli/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 020dac2d2..a41cf95a3 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -363,12 +363,13 @@ class CommandLine: metavar="PLUGIN", ) for plugin in sorted(plugin_list): - # First line of a plugin docstring will be the short description for -h - # Following lines will be the additional description (argparse epilog) + # 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__.strip().split("\n", 1) - short_help = doc_split[0] + 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() From 105df4e140767045e51c69c9a24ad17d231564bc Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 27 Jan 2025 22:29:59 +0000 Subject: [PATCH 141/141] Windows: Fix vadyarascan typo --- volatility3/framework/plugins/windows/vadyarascan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 2e9cc44ea..9758c5994 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -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