From d76288f99407c93b719c130b9da6ba86471446db Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Thu, 13 Mar 2025 22:53:51 +0000 Subject: [PATCH 01/14] Update nearly all callers of now deprecated Linux kernel APIs --- .../framework/plugins/linux/check_idt.py | 23 ++++++------ .../framework/plugins/linux/hidden_modules.py | 8 ++-- .../plugins/linux/keyboard_notifiers.py | 34 ++++++++++------- .../framework/plugins/linux/kthreads.py | 37 +++++++++---------- .../framework/plugins/linux/tty_check.py | 32 +++++++++------- 5 files changed, 69 insertions(+), 65 deletions(-) diff --git a/volatility3/framework/plugins/linux/check_idt.py b/volatility3/framework/plugins/linux/check_idt.py index dbdb0e9be..c85f291cc 100644 --- a/volatility3/framework/plugins/linux/check_idt.py +++ b/volatility3/framework/plugins/linux/check_idt.py @@ -10,7 +10,6 @@ from volatility3.framework import interfaces, renderers, symbols from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import linux -from volatility3.plugins.linux import lsmod vollog = logging.getLogger(__name__) @@ -39,9 +38,6 @@ class Check_idt(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), ] @staticmethod @@ -82,10 +78,8 @@ class Check_idt(interfaces.plugins.PluginInterface): vmlinux = self.context.modules[self.config["kernel"]] - modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - - handlers = linux.LinuxUtilities.generate_kernel_handler_info( - self.context, vmlinux.name, modules + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + self.context, self.config["kernel"], run_hidden_modules=True ) idt_table_size = 256 @@ -134,19 +128,24 @@ class Check_idt(interfaces.plugins.PluginInterface): module_name = renderers.NotAvailableValue() symbol_name = renderers.NotAvailableValue() else: - module_name, symbol_name = ( - linux_utilities_modules.Modules.lookup_module_address( - self.context, vmlinux.name, handlers, idt_addr + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, idt_addr ) ) + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + yield ( 0, [ format_hints.Hex(i), format_hints.Hex(idt_addr), module_name, - symbol_name, + symbol_name or renderers.NotAvailableValue(), ], ) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 9891bf138..8999126b1 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -10,7 +10,6 @@ from volatility3.framework import renderers, interfaces, exceptions, deprecation from volatility3.framework.constants import architectures from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements -from volatility3.plugins.linux import lsmod vollog = logging.getLogger(__name__) @@ -29,9 +28,6 @@ class Hidden_modules(interfaces.plugins.PluginInterface): description="Linux kernel", architectures=architectures.LINUX_ARCHS, ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, @@ -163,7 +159,9 @@ class Hidden_modules(interfaces.plugins.PluginInterface): known_module_addresses = { vmlinux_layer.canonicalize(module.vol.offset) - for module in lsmod.Lsmod.list_modules(context, vmlinux_module_name) + for module in linux_utilities_modules.Modules.list_modules( + context, vmlinux_module_name + ) } return known_module_addresses diff --git a/volatility3/framework/plugins/linux/keyboard_notifiers.py b/volatility3/framework/plugins/linux/keyboard_notifiers.py index 8fd2846c1..beebc4248 100644 --- a/volatility3/framework/plugins/linux/keyboard_notifiers.py +++ b/volatility3/framework/plugins/linux/keyboard_notifiers.py @@ -9,7 +9,6 @@ from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import linux -from volatility3.plugins.linux import lsmod vollog = logging.getLogger(__name__) @@ -32,9 +31,6 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): component=linux_utilities_modules.Modules, version=(2, 0, 0), ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) ), @@ -43,12 +39,6 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] - modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - - handlers = linux.LinuxUtilities.generate_kernel_handler_info( - self.context, vmlinux.name, modules - ) - try: knl_addr = vmlinux.object_from_symbol("keyboard_notifier_list") except exceptions.SymbolError: @@ -65,6 +55,10 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): vollog.error("The head of the keyboard notifier list is paged out.") return + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + self.context, self.config["kernel"], run_hidden_modules=True + ) + knl = vmlinux.object( object_type="atomic_notifier_head", offset=knl_addr.vol.offset, @@ -76,13 +70,25 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): ): call_addr = call_back.notifier_call - module_name, symbol_name = ( - linux_utilities_modules.Modules.lookup_module_address( - self.context, vmlinux.name, handlers, call_addr + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, call_addr ) ) - yield (0, [format_hints.Hex(call_addr), module_name, symbol_name]) + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield ( + 0, + [ + format_hints.Hex(call_addr), + module_name, + symbol_name or renderers.NotAvailableValue(), + ], + ) def run(self): return renderers.TreeGrid( diff --git a/volatility3/framework/plugins/linux/kthreads.py b/volatility3/framework/plugins/linux/kthreads.py index 60d24f06e..99a1b57a9 100644 --- a/volatility3/framework/plugins/linux/kthreads.py +++ b/volatility3/framework/plugins/linux/kthreads.py @@ -5,14 +5,14 @@ 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 import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import linux from volatility3.framework.constants import architectures from volatility3.framework.objects import utility -from volatility3.plugins.linux import pslist, lsmod +from volatility3.plugins.linux import pslist vollog = logging.getLogger(__name__) @@ -42,28 +42,22 @@ class Kthreads(plugins.PluginInterface): requirements.PluginRequirement( name="pslist", plugin=pslist.PsList, version=(4, 0, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), ] def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] - modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - handlers = linux.LinuxUtilities.generate_kernel_handler_info( - self.context, vmlinux.name, modules - ) - - kthread_type = vmlinux.get_type( - vmlinux.symbol_table_name + constants.BANG + "kthread" - ) + kthread_type = vmlinux.get_type("kthread") if not kthread_type.has_member("threadfn"): raise exceptions.VolatilityException( "Unsupported kthread implementation. This plugin only works with kernels >= 5.8" ) + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + self.context, self.config["kernel"], run_hidden_modules=True + ) + for task in pslist.PsList.list_tasks( self.context, vmlinux.name, include_threads=True ): @@ -86,9 +80,7 @@ class Kthreads(plugins.PluginInterface): if not (threadfn and threadfn.is_readable()): continue - task_name = utility.array_to_string(task.comm) - - thread_name = task_name + thread_name = utility.array_to_string(task.comm) # kernels >= 5.17 in d6986ce24fc00b0638bd29efe8fb7ba7619ed2aa full_name was added to kthread if kthread.has_member("full_name"): @@ -101,18 +93,23 @@ class Kthreads(plugins.PluginInterface): f"full_name pointer for thread at {kthread.vol.offset:#x} is paged out." ) - module_name, symbol_name = ( - linux_utilities_modules.Modules.lookup_module_address( - self.context, vmlinux.name, handlers, threadfn + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, threadfn ) ) + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + fields = [ task.pid, thread_name, format_hints.Hex(threadfn), module_name, - symbol_name, + symbol_name or renderers.NotAvailableValue(), ] yield 0, fields diff --git a/volatility3/framework/plugins/linux/tty_check.py b/volatility3/framework/plugins/linux/tty_check.py index 281d46eda..b45272eb6 100644 --- a/volatility3/framework/plugins/linux/tty_check.py +++ b/volatility3/framework/plugins/linux/tty_check.py @@ -12,7 +12,6 @@ from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import linux -from volatility3.plugins.linux import lsmod vollog = logging.getLogger(__name__) @@ -35,9 +34,6 @@ class tty_check(plugins.PluginInterface): component=linux_utilities_modules.Modules, version=(2, 0, 0), ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) ), @@ -46,12 +42,6 @@ class tty_check(plugins.PluginInterface): def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] - modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - - handlers = linux.LinuxUtilities.generate_kernel_handler_info( - self.context, vmlinux.name, modules - ) - try: tty_drivers = vmlinux.object_from_symbol("tty_drivers").cast("list_head") except exceptions.SymbolError: @@ -64,6 +54,10 @@ class tty_check(plugins.PluginInterface): "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." ) + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + self.context, self.config["kernel"], run_hidden_modules=True + ) + for tty in tty_drivers.to_list( vmlinux.symbol_table_name + constants.BANG + "tty_driver", "tty_drivers" ): @@ -87,13 +81,23 @@ class tty_check(plugins.PluginInterface): except exceptions.InvalidAddressException: continue - module_name, symbol_name = ( - linux_utilities_modules.Modules.lookup_module_address( - self.context, vmlinux.name, handlers, recv_buf + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, recv_buf ) ) - yield (0, (name, format_hints.Hex(recv_buf), module_name, symbol_name)) + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield 0, ( + name, + format_hints.Hex(recv_buf), + module_name, + symbol_name or renderers.NotAvailableValue(), + ) def run(self): return renderers.TreeGrid( From c318e7c32664352f036aeed1453a8ed40ad6c90f Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 14 Mar 2025 01:52:16 +0000 Subject: [PATCH 02/14] Make configurable to sources --- .../framework/plugins/linux/check_idt.py | 6 +- .../framework/plugins/linux/check_modules.py | 2 +- .../framework/plugins/linux/hidden_modules.py | 2 +- .../plugins/linux/keyboard_notifiers.py | 6 +- .../framework/plugins/linux/kthreads.py | 6 +- volatility3/framework/plugins/linux/lsmod.py | 2 +- .../framework/plugins/linux/modxview.py | 15 +- .../framework/plugins/linux/netfilter.py | 2 +- .../framework/plugins/linux/tracing/ftrace.py | 6 +- .../plugins/linux/tracing/tracepoints.py | 6 +- .../framework/plugins/linux/tty_check.py | 6 +- .../symbols/linux/utilities/modules.py | 207 ++++++++++++++---- 12 files changed, 199 insertions(+), 67 deletions(-) diff --git a/volatility3/framework/plugins/linux/check_idt.py b/volatility3/framework/plugins/linux/check_idt.py index c85f291cc..119531836 100644 --- a/volatility3/framework/plugins/linux/check_idt.py +++ b/volatility3/framework/plugins/linux/check_idt.py @@ -33,7 +33,7 @@ class Check_idt(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) @@ -79,7 +79,9 @@ class Check_idt(interfaces.plugins.PluginInterface): vmlinux = self.context.modules[self.config["kernel"]] known_modules = linux_utilities_modules.Modules.run_modules_scanners( - self.context, self.config["kernel"], run_hidden_modules=True + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, ) idt_table_size = 256 diff --git a/volatility3/framework/plugins/linux/check_modules.py b/volatility3/framework/plugins/linux/check_modules.py index 76f75ec50..40f3f638c 100644 --- a/volatility3/framework/plugins/linux/check_modules.py +++ b/volatility3/framework/plugins/linux/check_modules.py @@ -33,7 +33,7 @@ class Check_modules(plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), ] diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 8999126b1..5be8e7174 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -31,7 +31,7 @@ class Hidden_modules(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), ] diff --git a/volatility3/framework/plugins/linux/keyboard_notifiers.py b/volatility3/framework/plugins/linux/keyboard_notifiers.py index beebc4248..b4f9dd3ca 100644 --- a/volatility3/framework/plugins/linux/keyboard_notifiers.py +++ b/volatility3/framework/plugins/linux/keyboard_notifiers.py @@ -29,7 +29,7 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) @@ -56,7 +56,9 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): return known_modules = linux_utilities_modules.Modules.run_modules_scanners( - self.context, self.config["kernel"], run_hidden_modules=True + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, ) knl = vmlinux.object( diff --git a/volatility3/framework/plugins/linux/kthreads.py b/volatility3/framework/plugins/linux/kthreads.py index 99a1b57a9..7d6abcecc 100644 --- a/volatility3/framework/plugins/linux/kthreads.py +++ b/volatility3/framework/plugins/linux/kthreads.py @@ -34,7 +34,7 @@ class Kthreads(plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) @@ -55,7 +55,9 @@ class Kthreads(plugins.PluginInterface): ) known_modules = linux_utilities_modules.Modules.run_modules_scanners( - self.context, self.config["kernel"], run_hidden_modules=True + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, ) for task in pslist.PsList.list_tasks( diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index b4f881801..a3ace9a24 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -33,7 +33,7 @@ class Lsmod(plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), ] diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index f6a6f7727..4373ffb7c 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -34,7 +34,7 @@ spot modules presence and taints.""" requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), requirements.VersionRequirement( name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) @@ -93,13 +93,22 @@ spot modules presence and taints.""" kernel = self.context.modules[kernel_name] + wanted_sources = [ + linux_utilities_modules.Modules.source_lsmod_identifier, + linux_utilities_modules.Modules.source_sysfs_identifier, + linux_utilities_modules.Modules.source_hidden_identifier, + ] + run_results = linux_utilities_modules.Modules.run_modules_scanners( - self.context, kernel_name, flatten=False + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_sources=wanted_sources, + flatten=False, ) aggregated_modules = {} # We want to be explicit on the plugins results we are interested in - for plugin_name in ["lsmod", "check_modules", "hidden_modules"]: + for plugin_name in wanted_sources: # Iterate over each recovered module for mod_info in run_results[plugin_name]: # Use offsets as unique keys, whether a module diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index 9c0055a54..e8a33be61 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -726,7 +726,7 @@ class Netfilter(interfaces.plugins.PluginInterface): _version = (1, 1, 1) - _required_linux_utilities_modules_version = (2, 0, 0) + _required_linux_utilities_modules_version = (3, 0, 0) _required_linuxutils_version = (2, 1, 0) _required_lsmod_version = (2, 0, 0) _required_linuxnet_version = (1, 0, 0) diff --git a/volatility3/framework/plugins/linux/tracing/ftrace.py b/volatility3/framework/plugins/linux/tracing/ftrace.py index 17766cc74..146230f02 100644 --- a/volatility3/framework/plugins/linux/tracing/ftrace.py +++ b/volatility3/framework/plugins/linux/tracing/ftrace.py @@ -79,7 +79,7 @@ class CheckFtrace(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), requirements.BooleanRequirement( name="show_ftrace_flags", @@ -223,7 +223,9 @@ class CheckFtrace(interfaces.plugins.PluginInterface): return known_modules = linux_utilities_modules.Modules.run_modules_scanners( - self.context, kernel_name, run_hidden_modules=True + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, ) for ftrace_ops in self.iterate_ftrace_ops_list(self.context, kernel_name): diff --git a/volatility3/framework/plugins/linux/tracing/tracepoints.py b/volatility3/framework/plugins/linux/tracing/tracepoints.py index fe6d11af9..83903b19b 100644 --- a/volatility3/framework/plugins/linux/tracing/tracepoints.py +++ b/volatility3/framework/plugins/linux/tracing/tracepoints.py @@ -52,7 +52,7 @@ class CheckTracepoints(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), ] @@ -229,7 +229,9 @@ class CheckTracepoints(interfaces.plugins.PluginInterface): return known_modules = linux_utilities_modules.Modules.run_modules_scanners( - self.context, kernel_name, run_hidden_modules=False + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, ) tracepoints = self.iterate_tracepoints_array(self.context, kernel_name) diff --git a/volatility3/framework/plugins/linux/tty_check.py b/volatility3/framework/plugins/linux/tty_check.py index b45272eb6..f4b581756 100644 --- a/volatility3/framework/plugins/linux/tty_check.py +++ b/volatility3/framework/plugins/linux/tty_check.py @@ -32,7 +32,7 @@ class tty_check(plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, - version=(2, 0, 0), + version=(3, 0, 0), ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) @@ -55,7 +55,9 @@ class tty_check(plugins.PluginInterface): ) known_modules = linux_utilities_modules.Modules.run_modules_scanners( - self.context, self.config["kernel"], run_hidden_modules=True + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, ) for tty in tty_drivers.to_list( diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 0eeb2b33c..dac52dce3 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -19,11 +19,26 @@ vollog = logging.getLogger(__name__) class Modules(interfaces.configuration.VersionableInterface): """Kernel modules related utilities.""" - _version = (2, 0, 0) + _version = (3, 0, 0) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) + # Valid sources of kernel modules to send to `run_module_scanners` + source_kernel_identifier = "kernel" + source_lsmod_identifier = "lsmod" + source_sysfs_identifier = "check_modules" + source_hidden_identifier = "hidden_modules" + + # With few exceptions, rootkit checking plugins want all sources + # This provides a stable identifier as new sources are added over time + all_sources_identifier = [ + source_kernel_identifier, + source_lsmod_identifier, + source_sysfs_identifier, + source_hidden_identifier, + ] + class ModuleInfo(NamedTuple): """ Used to track the name and boundary of a kernel module @@ -34,13 +49,13 @@ class Modules(interfaces.configuration.VersionableInterface): start: int end: int - @staticmethod + @classmethod def module_lookup_by_address( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str, modules: Iterable[ModuleInfo], target_address: int, - run_hidden_modules: bool = True, ) -> Optional[Tuple[ModuleInfo, Optional[str]]]: """ Determine if a target address lies in a module memory space. @@ -195,7 +210,7 @@ class Modules(interfaces.configuration.VersionableInterface): def get_kernel_module_info( context: interfaces.context.ContextInterface, kernel_module_name: str, - ) -> ModuleInfo: + ) -> Iterator[ModuleInfo]: """ Returns a ModuleInfo instance that encodes the kernel This is required to map function pointers to the kerenl executable @@ -210,77 +225,173 @@ class Modules(interfaces.configuration.VersionableInterface): end_addr = kernel.object_from_symbol("_etext") end_addr = end_addr.vol.offset & address_mask - return Modules.ModuleInfo( - start_addr, constants.linux.KERNEL_NAME, start_addr, end_addr + return [ + Modules.ModuleInfo( + start_addr, constants.linux.KERNEL_NAME, start_addr, end_addr + ) + ] + + @classmethod + def _get_hidden_modules_results( + cls, + context: str, + kernel_module_name: str, + run_results: Dict[str, List[ModuleInfo]], + ): + known_modules_addresses = set() + + kernel = context.modules[kernel_module_name] + + # Walk each sources' results + for results in run_results.values(): + for modinfo in results: + address = context.layers[kernel.layer_name].canonicalize(modinfo.start) + known_modules_addresses.add(address) + + modules_memory_boundaries = cls.get_modules_memory_boundaries( + context, kernel_module_name ) + hidden_results = [] + + address_mask = context.layers[kernel.layer_name].address_mask + + for module in cls.get_hidden_modules( + context, + kernel_module_name, + known_modules_addresses, + modules_memory_boundaries, + ): + modinfo = cls.get_module_info_for_module(address_mask, module) + if modinfo: + hidden_results.append(modinfo) + + return hidden_results + + @classmethod + def _get_list_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> List[ModuleInfo]: + """ + Gather `module` instances from lsmod + """ + yield from cls.list_modules(context, kernel_module_name) + + @classmethod + def _get_sysfs_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> List[ModuleInfo]: + """ + Gather the `module` instances from sysfs + """ + kernel = context.modules[kernel_module_name] + + sysfs_modules: dict = cls.get_kset_modules(context, kernel_module_name) + + for m_offset in sysfs_modules.values(): + yield kernel.object(object_type="module", offset=m_offset, absolute=True) + + @classmethod + def _validated_sources(cls, caller_wanted_sources) -> List[str]: + """ + Called by `run_modules_scanners` to validate the caller supplied sources list + An exception is thrown if an empty source list is given or a list containing an invalid source + """ + if not caller_wanted_sources: + raise ValueError("`caller_wanted_sources` must have at least one source.") + + if ( + len(caller_wanted_sources) == 1 + and caller_wanted_sources[0] == Modules.source_hidden_identifier + ): + raise ValueError( + f"{Modules.source_hidden_identifier} cannot be the only source or there is nothing to compare against." + ) + + wanted_sources = [] + + for source in caller_wanted_sources: + if source not in Modules.all_sources_identifier: + raise ValueError( + f"Invalid source sent through `caller_wanted_sources`: {source}" + ) + + wanted_sources.append(source) + + return wanted_sources + @classmethod def run_modules_scanners( cls, context: interfaces.context.ContextInterface, - kernel_name: str, - run_hidden_modules: bool = True, + kernel_module_name: str, + caller_wanted_sources: List[str], flatten: bool = True, ) -> Dict[str, List[ModuleInfo]]: """Run module scanning plugins and aggregate the results. It is designed to not operate any inter-plugin results triage. + Rules for `caller_wanted_sources`: + + If `Modules.all_sources_identifier` is specified then every source will be populated + + If `Modules.source_hidden_identifier` is in the list, then at least one other sources must be + specified so a comparison will be populated + + If empty or an invalid source is specified then a ValueError is thrown + Args: - run_hidden_modules: specify if the hidden_modules plugin should be run + called_wanted_sources: The list of sources to gather modules. + flatten: Whether to de-duplicate modules across sources Returns: Dictionary mapping each plugin to its corresponding result """ - kernel = context.modules[kernel_name] + module_gatherers = { + Modules.source_kernel_identifier: cls.get_kernel_module_info, + Modules.source_lsmod_identifier: cls._get_list_modules, + Modules.source_sysfs_identifier: cls._get_sysfs_modules, + } + + kernel = context.modules[kernel_module_name] address_mask = context.layers[kernel.layer_name].address_mask + wanted_sources = Modules._validated_sources(caller_wanted_sources) + run_results = {} - # the kernel module boundaries - run_results["kernel"] = [cls.get_kernel_module_info(context, kernel_name)] + run_hidden_modules = False - # lsmod - run_results["lsmod"] = [] + # Special case hidden modules since it gathers modules on its own + if Modules.source_hidden_identifier in wanted_sources: + run_hidden_modules = True + wanted_sources.remove(Modules.source_hidden_identifier) - for module in cls.list_modules(context, kernel_name): - modinfo = cls.get_module_info_for_module(address_mask, module) - if modinfo: - run_results["lsmod"].append(modinfo) + # Walk each source, gathering modules + for wanted_source in wanted_sources: + run_results[wanted_source] = [] - # check_modules - run_results["check_modules"] = [] + gatherer = module_gatherers[wanted_source] - sysfs_modules: dict = cls.get_kset_modules(context, kernel_name) + # process each module coming from back the current source + for module in gatherer(context, kernel_module_name): + # the kernel sends back a ModuleInfo directly + if wanted_source == Modules.source_kernel_identifier: + modinfo = module + else: + modinfo = cls.get_module_info_for_module(address_mask, module) - for m_offset in sysfs_modules.values(): - module = kernel.object(object_type="module", offset=m_offset, absolute=True) - modinfo = cls.get_module_info_for_module(address_mask, module) - if modinfo: - run_results["check_modules"].append(modinfo) - - # hidden_modules - if run_hidden_modules: - known_modules_addresses = set( - context.layers[kernel.layer_name].canonicalize(modinfo.start) - for modinfo in run_results["kernel"] - + run_results["lsmod"] - + run_results["check_modules"] - ) - modules_memory_boundaries = cls.get_modules_memory_boundaries( - context, kernel_name - ) - run_results["hidden_modules"] = [] - - for module in cls.get_hidden_modules( - context, - kernel_name, - known_modules_addresses, - modules_memory_boundaries, - ): - modinfo = cls.get_module_info_for_module(address_mask, module) if modinfo: - run_results["hidden_modules"].append(modinfo) + run_results[wanted_source].append(modinfo) + + # run hidden modules against the other sources + if run_hidden_modules: + run_results[Modules.source_hidden_identifier] = ( + cls._get_hidden_modules_results( + context, kernel_module_name, run_results + ) + ) if flatten: return cls.flatten_run_modules_results(run_results) From 4606495ded4f71c2bdd9066b260556947729e618 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 14 Mar 2025 02:09:08 +0000 Subject: [PATCH 03/14] Bump dep versions --- volatility3/framework/plugins/linux/check_modules.py | 2 +- volatility3/framework/plugins/linux/hidden_modules.py | 8 ++++---- volatility3/framework/plugins/linux/lsmod.py | 2 +- volatility3/framework/plugins/linux/modxview.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/volatility3/framework/plugins/linux/check_modules.py b/volatility3/framework/plugins/linux/check_modules.py index 40f3f638c..44cb568e6 100644 --- a/volatility3/framework/plugins/linux/check_modules.py +++ b/volatility3/framework/plugins/linux/check_modules.py @@ -41,7 +41,7 @@ class Check_modules(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_kset_modules, removal_date="2025-09-25", - replacement_version=(2, 0, 0), + replacement_version=(3, 0, 0), ) def get_kset_modules( cls, context: interfaces.context.ContextInterface, vmlinux_name: str diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 5be8e7174..985d4cfcb 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -39,7 +39,7 @@ class Hidden_modules(interfaces.plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries, removal_date="2025-09-25", - replacement_version=(2, 0, 0), + replacement_version=(3, 0, 0), ) def get_modules_memory_boundaries( context: interfaces.context.ContextInterface, @@ -52,7 +52,7 @@ class Hidden_modules(interfaces.plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_module_address_alignment, removal_date="2025-09-25", - replacement_version=(2, 0, 0), + replacement_version=(3, 0, 0), ) @classmethod def _get_module_address_alignment( @@ -80,7 +80,7 @@ class Hidden_modules(interfaces.plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_hidden_modules, removal_date="2025-09-25", - replacement_version=(2, 0, 0), + replacement_version=(3, 0, 0), ) @classmethod def get_hidden_modules( @@ -120,7 +120,7 @@ class Hidden_modules(interfaces.plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.validate_alignment_patterns, removal_date="2025-09-25", - replacement_version=(2, 0, 0), + replacement_version=(3, 0, 0), ) def _validate_alignment_patterns( addresses: Iterable[int], diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index a3ace9a24..466bfa0b4 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -40,7 +40,7 @@ class Lsmod(plugins.PluginInterface): @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.list_modules, - replacement_version=(2, 0, 0), + replacement_version=(3, 0, 0), removal_date="2025-09-25", ) def list_modules( diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index 4373ffb7c..e43672974 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -50,7 +50,7 @@ spot modules presence and taints.""" @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.flatten_run_modules_results, - replacement_version=(2, 0, 0), + replacement_version=(3, 0, 0), removal_date="2025-09-25", ) def flatten_run_modules_results( @@ -73,7 +73,7 @@ spot modules presence and taints.""" @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.run_modules_scanners, - replacement_version=(2, 0, 0), + replacement_version=(3, 0, 0), removal_date="2025-09-25", ) def run_modules_scanners( From cf8bb3dfbc12897445220d24912a43bc1cea75ac Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 14 Mar 2025 20:27:25 +0000 Subject: [PATCH 04/14] Adjust run_modules_scanners to avoid special handling of hidden modules, adjust modxview to new API, add classes and version requirements on module gathering interface --- .../framework/plugins/linux/check_idt.py | 7 +- .../plugins/linux/keyboard_notifiers.py | 7 +- .../framework/plugins/linux/kthreads.py | 7 +- .../framework/plugins/linux/modxview.py | 43 +-- .../framework/plugins/linux/tracing/ftrace.py | 16 +- .../plugins/linux/tracing/tracepoints.py | 13 +- .../framework/plugins/linux/tty_check.py | 7 +- .../symbols/linux/utilities/modules.py | 333 +++++++++--------- 8 files changed, 229 insertions(+), 204 deletions(-) diff --git a/volatility3/framework/plugins/linux/check_idt.py b/volatility3/framework/plugins/linux/check_idt.py index 119531836..e199d98d3 100644 --- a/volatility3/framework/plugins/linux/check_idt.py +++ b/volatility3/framework/plugins/linux/check_idt.py @@ -35,6 +35,11 @@ class Check_idt(interfaces.plugins.PluginInterface): component=linux_utilities_modules.Modules, version=(3, 0, 0), ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) ), @@ -81,7 +86,7 @@ class Check_idt(interfaces.plugins.PluginInterface): known_modules = linux_utilities_modules.Modules.run_modules_scanners( context=self.context, kernel_module_name=self.config["kernel"], - caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, ) idt_table_size = 256 diff --git a/volatility3/framework/plugins/linux/keyboard_notifiers.py b/volatility3/framework/plugins/linux/keyboard_notifiers.py index b4f9dd3ca..215704350 100644 --- a/volatility3/framework/plugins/linux/keyboard_notifiers.py +++ b/volatility3/framework/plugins/linux/keyboard_notifiers.py @@ -31,6 +31,11 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): component=linux_utilities_modules.Modules, version=(3, 0, 0), ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) ), @@ -58,7 +63,7 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): known_modules = linux_utilities_modules.Modules.run_modules_scanners( context=self.context, kernel_module_name=self.config["kernel"], - caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, ) knl = vmlinux.object( diff --git a/volatility3/framework/plugins/linux/kthreads.py b/volatility3/framework/plugins/linux/kthreads.py index 7d6abcecc..06e94b221 100644 --- a/volatility3/framework/plugins/linux/kthreads.py +++ b/volatility3/framework/plugins/linux/kthreads.py @@ -36,6 +36,11 @@ class Kthreads(plugins.PluginInterface): component=linux_utilities_modules.Modules, version=(3, 0, 0), ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) ), @@ -57,7 +62,7 @@ class Kthreads(plugins.PluginInterface): known_modules = linux_utilities_modules.Modules.run_modules_scanners( context=self.context, kernel_module_name=self.config["kernel"], - caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, ) for task in pslist.PsList.list_tasks( diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index e43672974..b04eb74ca 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -36,6 +36,11 @@ spot modules presence and taints.""" component=linux_utilities_modules.Modules, version=(3, 0, 0), ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), requirements.VersionRequirement( name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) ), @@ -89,44 +94,42 @@ spot modules presence and taints.""" ) def _generator(self): - kernel_name = self.config["kernel"] + kernel = self.context.modules[self.config["kernel"]] - kernel = self.context.modules[kernel_name] - - wanted_sources = [ - linux_utilities_modules.Modules.source_lsmod_identifier, - linux_utilities_modules.Modules.source_sysfs_identifier, - linux_utilities_modules.Modules.source_hidden_identifier, + wanted_gatherers = [ + linux_utilities_modules.ModuleGathererLsmod, + linux_utilities_modules.ModuleGathererSysFs, + linux_utilities_modules.ModuleGathererScanner, ] run_results = linux_utilities_modules.Modules.run_modules_scanners( context=self.context, kernel_module_name=self.config["kernel"], - caller_wanted_sources=wanted_sources, + caller_wanted_gatherers=wanted_gatherers, flatten=False, ) aggregated_modules = {} # We want to be explicit on the plugins results we are interested in - for plugin_name in wanted_sources: + for gatherer in wanted_gatherers: # Iterate over each recovered module - for mod_info in run_results[plugin_name]: + for mod_info in run_results[gatherer]: # Use offsets as unique keys, whether a module # appears in many plugin runs or not if aggregated_modules.get(mod_info.offset, None) is not None: # Append the plugin to the list of originating plugins - aggregated_modules[mod_info.offset].append(plugin_name) + aggregated_modules[mod_info.offset].append(gatherer) else: - aggregated_modules[mod_info.offset] = [plugin_name] + aggregated_modules[mod_info.offset] = [gatherer] - for module_offset, originating_plugins in aggregated_modules.items(): - # Tainting parsing capabilities applied to the module + for module_offset, gatherers in aggregated_modules.items(): module = kernel.object("module", offset=module_offset, absolute=True) + # 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, + self.config["kernel"], module.taints, True, ) @@ -134,7 +137,7 @@ spot modules presence and taints.""" taints = ",".join( tainting.Tainting.get_taints_parsed( self.context, - kernel_name, + self.config["kernel"], module.taints, True, ) @@ -145,9 +148,9 @@ spot modules presence and taints.""" ( module.get_name() or NotAvailableValue(), format_hints.Hex(module_offset), - "lsmod" in originating_plugins, - "check_modules" in originating_plugins, - "hidden_modules" in originating_plugins, + linux_utilities_modules.ModuleGathererLsmod in gatherers, + linux_utilities_modules.ModuleGathererSysFs in gatherers, + linux_utilities_modules.ModuleGathererScanner in gatherers, taints or NotAvailableValue(), ), ) @@ -158,7 +161,7 @@ spot modules presence and taints.""" ("Address", format_hints.Hex), ("In procfs", bool), ("In sysfs", bool), - ("Hidden", bool), + ("In scan", bool), ("Taints", str), ] diff --git a/volatility3/framework/plugins/linux/tracing/ftrace.py b/volatility3/framework/plugins/linux/tracing/ftrace.py index 146230f02..c5e4f9ef8 100644 --- a/volatility3/framework/plugins/linux/tracing/ftrace.py +++ b/volatility3/framework/plugins/linux/tracing/ftrace.py @@ -5,7 +5,7 @@ # Public researches: https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Fixing-A-Memory-Forensics-Blind-Spot-Linux-Kernel-Tracing-wp.pdf import logging -from typing import Dict, List, Generator +from typing import List, Generator from enum import Enum from dataclasses import dataclass @@ -65,7 +65,7 @@ class CheckFtrace(interfaces.plugins.PluginInterface): Investigate the ftrace infrastructure to uncover kernel attached callbacks, which can be leveraged to hook kernel functions and modify their behaviour.""" - _version = (3, 0, 0) + _version = (4, 0, 0) _required_framework_version = (2, 19, 0) @classmethod @@ -81,6 +81,11 @@ class CheckFtrace(interfaces.plugins.PluginInterface): component=linux_utilities_modules.Modules, version=(3, 0, 0), ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), requirements.BooleanRequirement( name="show_ftrace_flags", description="Show ftrace flags associated with an ftrace_ops struct", @@ -127,9 +132,8 @@ class CheckFtrace(interfaces.plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - known_modules: Dict[str, List[linux_utilities_modules.Modules.ModuleInfo]], + known_modules: List[linux_utilities_modules.ModuleInfo], ftrace_ops: interfaces.objects.ObjectInterface, - run_hidden_modules: bool = True, ) -> Generator[ParsedFtraceOps, None, None]: """Parse an ftrace_ops struct to highlight ftrace kernel hooking. Iterates over embedded ftrace_func_entry entries, which point to hooked memory areas. @@ -137,8 +141,6 @@ class CheckFtrace(interfaces.plugins.PluginInterface): Args: known_modules: A dict of known modules, used to locate callbacks origin. Typically obtained through run_modules_scanners(). ftrace_ops: The ftrace_ops struct to parse - run_hidden_modules: Whether to run the hidden_modules plugin or not. Note: it won't be run, even if specified, \ - if the "hidden_modules" key is present in known_modules. Yields: An iterable of ParsedFtraceOps dataclasses, containing a selection of useful fields (callback, hook, module) related to an ftrace_ops struct @@ -225,7 +227,7 @@ class CheckFtrace(interfaces.plugins.PluginInterface): known_modules = linux_utilities_modules.Modules.run_modules_scanners( context=self.context, kernel_module_name=self.config["kernel"], - caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, ) for ftrace_ops in self.iterate_ftrace_ops_list(self.context, kernel_name): diff --git a/volatility3/framework/plugins/linux/tracing/tracepoints.py b/volatility3/framework/plugins/linux/tracing/tracepoints.py index 83903b19b..9d4a4a2e3 100644 --- a/volatility3/framework/plugins/linux/tracing/tracepoints.py +++ b/volatility3/framework/plugins/linux/tracing/tracepoints.py @@ -5,7 +5,7 @@ # Public researches: https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Fixing-A-Memory-Forensics-Blind-Spot-Linux-Kernel-Tracing-wp.pdf import logging -from typing import Dict, Iterable, List, Optional +from typing import Iterable, List, Optional from dataclasses import dataclass import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules @@ -38,7 +38,7 @@ class CheckTracepoints(interfaces.plugins.PluginInterface): Investigate the tracepoints subsystem to uncover kernel attached probes, which can be leveraged to hook kernel functions and modify their behaviour.""" - _version = (1, 0, 0) + _version = (2, 0, 0) _required_framework_version = (2, 19, 0) @classmethod @@ -54,6 +54,11 @@ class CheckTracepoints(interfaces.plugins.PluginInterface): component=linux_utilities_modules.Modules, version=(3, 0, 0), ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), ] @classmethod @@ -96,7 +101,7 @@ class CheckTracepoints(interfaces.plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - known_modules: Dict[str, List[linux_utilities_modules.Modules.ModuleInfo]], + known_modules: List[linux_utilities_modules.ModuleInfo], tracepoint: interfaces.objects.ObjectInterface, run_hidden_modules: bool = True, ) -> Optional[Iterable[ParsedTracepointFunc]]: @@ -231,7 +236,7 @@ class CheckTracepoints(interfaces.plugins.PluginInterface): known_modules = linux_utilities_modules.Modules.run_modules_scanners( context=self.context, kernel_module_name=self.config["kernel"], - caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, ) tracepoints = self.iterate_tracepoints_array(self.context, kernel_name) diff --git a/volatility3/framework/plugins/linux/tty_check.py b/volatility3/framework/plugins/linux/tty_check.py index f4b581756..7d30b84ee 100644 --- a/volatility3/framework/plugins/linux/tty_check.py +++ b/volatility3/framework/plugins/linux/tty_check.py @@ -34,6 +34,11 @@ class tty_check(plugins.PluginInterface): component=linux_utilities_modules.Modules, version=(3, 0, 0), ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) ), @@ -57,7 +62,7 @@ class tty_check(plugins.PluginInterface): known_modules = linux_utilities_modules.Modules.run_modules_scanners( context=self.context, kernel_module_name=self.config["kernel"], - caller_wanted_sources=linux_utilities_modules.Modules.all_sources_identifier, + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, ) for tty in tty_drivers.to_list( diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index dac52dce3..a930bef20 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1,6 +1,18 @@ import logging import warnings -from typing import Iterable, Iterator, List, Optional, Tuple, NamedTuple, Dict, Set +from typing import ( + Iterable, + Iterator, + List, + Optional, + Tuple, + NamedTuple, + Dict, + Set, + Generator, + Union, +) +from abc import ABCMeta, abstractmethod from volatility3 import framework from volatility3.framework import ( @@ -10,12 +22,44 @@ from volatility3.framework import ( exceptions, objects, ) + from volatility3.framework.objects import utility from volatility3.framework.symbols.linux import extensions vollog = logging.getLogger(__name__) +class ModuleInfo(NamedTuple): + """ + Used to track the name and boundary of a kernel module + """ + + offset: int + name: str + start: int + end: int + + +class ModuleGathererInterface( + interfaces.configuration.VersionableInterface, metaclass=ABCMeta +): + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + framework.require_interface_version(*_required_framework_version) + + gatherer_return_type = Generator[Union[ModuleInfo, "extensions.module"], None, None] + + @classmethod + @abstractmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> gatherer_return_type: + """ + This method must return a generator (yield) of each `gatherer_return_type` found from its source + """ + + class Modules(interfaces.configuration.VersionableInterface): """Kernel modules related utilities.""" @@ -24,31 +68,6 @@ class Modules(interfaces.configuration.VersionableInterface): framework.require_interface_version(*_required_framework_version) - # Valid sources of kernel modules to send to `run_module_scanners` - source_kernel_identifier = "kernel" - source_lsmod_identifier = "lsmod" - source_sysfs_identifier = "check_modules" - source_hidden_identifier = "hidden_modules" - - # With few exceptions, rootkit checking plugins want all sources - # This provides a stable identifier as new sources are added over time - all_sources_identifier = [ - source_kernel_identifier, - source_lsmod_identifier, - source_sysfs_identifier, - source_hidden_identifier, - ] - - class ModuleInfo(NamedTuple): - """ - Used to track the name and boundary of a kernel module - """ - - offset: int - name: str - start: int - end: int - @classmethod def module_lookup_by_address( cls, @@ -204,138 +223,41 @@ class Modules(interfaces.configuration.VersionableInterface): end = start + module.get_core_size() - return Modules.ModuleInfo(module.vol.offset, mod_name, start, end) - - @staticmethod - def get_kernel_module_info( - context: interfaces.context.ContextInterface, - kernel_module_name: str, - ) -> Iterator[ModuleInfo]: - """ - Returns a ModuleInfo instance that encodes the kernel - This is required to map function pointers to the kerenl executable - """ - kernel = context.modules[kernel_module_name] - - address_mask = context.layers[kernel.layer_name].address_mask - - start_addr = kernel.object_from_symbol("_text") - start_addr = start_addr.vol.offset & address_mask - - end_addr = kernel.object_from_symbol("_etext") - end_addr = end_addr.vol.offset & address_mask - - return [ - Modules.ModuleInfo( - start_addr, constants.linux.KERNEL_NAME, start_addr, end_addr - ) - ] + return ModuleInfo(module.vol.offset, mod_name, start, end) @classmethod - def _get_hidden_modules_results( - cls, - context: str, - kernel_module_name: str, - run_results: Dict[str, List[ModuleInfo]], - ): - known_modules_addresses = set() - - kernel = context.modules[kernel_module_name] - - # Walk each sources' results - for results in run_results.values(): - for modinfo in results: - address = context.layers[kernel.layer_name].canonicalize(modinfo.start) - known_modules_addresses.add(address) - - modules_memory_boundaries = cls.get_modules_memory_boundaries( - context, kernel_module_name - ) - - hidden_results = [] - - address_mask = context.layers[kernel.layer_name].address_mask - - for module in cls.get_hidden_modules( - context, - kernel_module_name, - known_modules_addresses, - modules_memory_boundaries, - ): - modinfo = cls.get_module_info_for_module(address_mask, module) - if modinfo: - hidden_results.append(modinfo) - - return hidden_results - - @classmethod - def _get_list_modules( - cls, context: interfaces.context.ContextInterface, kernel_module_name: str - ) -> List[ModuleInfo]: + def _validate_gatherers(cls, caller_wanted_gatherers) -> List[str]: """ - Gather `module` instances from lsmod + Called by `run_modules_scanners` to validate the caller supplied gatherers list + An exception is thrown if an empty gatherers list is given or a list containing an invalid source """ - yield from cls.list_modules(context, kernel_module_name) - - @classmethod - def _get_sysfs_modules( - cls, context: interfaces.context.ContextInterface, kernel_module_name: str - ) -> List[ModuleInfo]: - """ - Gather the `module` instances from sysfs - """ - kernel = context.modules[kernel_module_name] - - sysfs_modules: dict = cls.get_kset_modules(context, kernel_module_name) - - for m_offset in sysfs_modules.values(): - yield kernel.object(object_type="module", offset=m_offset, absolute=True) - - @classmethod - def _validated_sources(cls, caller_wanted_sources) -> List[str]: - """ - Called by `run_modules_scanners` to validate the caller supplied sources list - An exception is thrown if an empty source list is given or a list containing an invalid source - """ - if not caller_wanted_sources: - raise ValueError("`caller_wanted_sources` must have at least one source.") - - if ( - len(caller_wanted_sources) == 1 - and caller_wanted_sources[0] == Modules.source_hidden_identifier - ): + if not caller_wanted_gatherers: raise ValueError( - f"{Modules.source_hidden_identifier} cannot be the only source or there is nothing to compare against." + "`caller_wanted_gatherers` must have at least one gatherer." ) - wanted_sources = [] - - for source in caller_wanted_sources: - if source not in Modules.all_sources_identifier: + for gatherer in caller_wanted_gatherers: + if gatherer not in ModuleGatherers.all_gatherers_identifier: raise ValueError( - f"Invalid source sent through `caller_wanted_sources`: {source}" + f"Invalid gatherer sent through `caller_wanted_gatherers`: {gatherer}" ) - wanted_sources.append(source) - - return wanted_sources - @classmethod def run_modules_scanners( cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - caller_wanted_sources: List[str], + caller_wanted_gatherers: List[ModuleGathererInterface], flatten: bool = True, - ) -> Dict[str, List[ModuleInfo]]: + ) -> Dict[ModuleGathererInterface, List[ModuleInfo]]: """Run module scanning plugins and aggregate the results. It is designed to not operate any inter-plugin results triage. Rules for `caller_wanted_sources`: - If `Modules.all_sources_identifier` is specified then every source will be populated + If `ModuleGathers.all_gathers_identifier` is specified then every source will be populated - If `Modules.source_hidden_identifier` is in the list, then at least one other sources must be + If `ModuleGathers.Scanner` is in the list, then at least one other sources must be specified so a comparison will be populated If empty or an invalid source is specified then a ValueError is thrown @@ -346,52 +268,30 @@ class Modules(interfaces.configuration.VersionableInterface): Returns: Dictionary mapping each plugin to its corresponding result """ - - module_gatherers = { - Modules.source_kernel_identifier: cls.get_kernel_module_info, - Modules.source_lsmod_identifier: cls._get_list_modules, - Modules.source_sysfs_identifier: cls._get_sysfs_modules, - } + # Throws ValueError if invalid gatherers sent in + Modules._validate_gatherers(caller_wanted_gatherers) kernel = context.modules[kernel_module_name] address_mask = context.layers[kernel.layer_name].address_mask - wanted_sources = Modules._validated_sources(caller_wanted_sources) + run_results: Dict[ModuleGathererInterface, List[ModuleInfo]] = {} - run_results = {} - - run_hidden_modules = False - - # Special case hidden modules since it gathers modules on its own - if Modules.source_hidden_identifier in wanted_sources: - run_hidden_modules = True - wanted_sources.remove(Modules.source_hidden_identifier) - - # Walk each source, gathering modules - for wanted_source in wanted_sources: - run_results[wanted_source] = [] - - gatherer = module_gatherers[wanted_source] + # Walk each source gathering modules + for gatherer in caller_wanted_gatherers: + run_results[gatherer] = [] # process each module coming from back the current source - for module in gatherer(context, kernel_module_name): + for module in gatherer.gather_modules(context, kernel_module_name): + # the kernel sends back a ModuleInfo directly - if wanted_source == Modules.source_kernel_identifier: + if gatherer == ModuleGathererKernel: modinfo = module else: modinfo = cls.get_module_info_for_module(address_mask, module) if modinfo: - run_results[wanted_source].append(modinfo) - - # run hidden modules against the other sources - if run_hidden_modules: - run_results[Modules.source_hidden_identifier] = ( - cls._get_hidden_modules_results( - context, kernel_module_name, run_results - ) - ) + run_results[gatherer].append(modinfo) if flatten: return cls.flatten_run_modules_results(run_results) @@ -449,7 +349,7 @@ class Modules(interfaces.configuration.VersionableInterface): Returns: List of ModuleInfo objects """ - uniq_modules: List[Modules.ModuleInfo] = [] + uniq_modules: List[ModuleInfo] = [] seen_addresses: int = set() @@ -645,3 +545,98 @@ class Modules(interfaces.configuration.VersionableInterface): True if all the addresses meet the alignment """ return all(addr % address_alignment == 0 for addr in addresses) + + +class ModuleGathererLsmod(ModuleGathererInterface): + """ + Gathers modules from the main kernel list + """ + + @classmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> ModuleGathererInterface.gatherer_return_type: + yield from Modules.list_modules(context, kernel_module_name) + + +class ModuleGathererSysFs(ModuleGathererInterface): + """ + Gathers modules from the sysfs /sys/modules objects + """ + + @classmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> ModuleGathererInterface.gatherer_return_type: + kernel = context.modules[kernel_module_name] + + sysfs_modules: dict = Modules.get_kset_modules(context, kernel_module_name) + + for m_offset in sysfs_modules.values(): + yield kernel.object(object_type="module", offset=m_offset, absolute=True) + + +class ModuleGathererScanner(ModuleGathererInterface): + """ + Gathers modules by scanning memory + """ + + @classmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> ModuleGathererInterface.gatherer_return_type: + modules_memory_boundaries = Modules.get_modules_memory_boundaries( + context, kernel_module_name + ) + + # Send in an empty list to not filter on any modules + yield from Modules.get_hidden_modules( + context=context, + vmlinux_module_name=kernel_module_name, + known_module_addresses=[], + modules_memory_boundaries=modules_memory_boundaries, + ) + + +class ModuleGathererKernel(ModuleGathererInterface): + """ + Creates a ModuleInfo instance for the kernel so that plugins + can determine when function pointers reference the kernel + """ + + @classmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> ModuleGathererInterface.gatherer_return_type: + """ + Returns a ModuleInfo instance that encodes the kernel + This is required to map function pointers to the kerenl executable + """ + kernel = context.modules[kernel_module_name] + + address_mask = context.layers[kernel.layer_name].address_mask + + start_addr = kernel.object_from_symbol("_text") + start_addr = start_addr.vol.offset & address_mask + + end_addr = kernel.object_from_symbol("_etext") + end_addr = end_addr.vol.offset & address_mask + + yield ModuleInfo(start_addr, constants.linux.KERNEL_NAME, start_addr, end_addr) + + +class ModuleGatherers(interfaces.configuration.VersionableInterface): + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + framework.require_interface_version(*_required_framework_version) + + # Valid sources of cores kernel module gatherers to send to `run_module_scanners` + # With few exceptions, rootkit checking plugins want all sources + # This provides a stable identifier as new sources are added over time + all_gatherers_identifier = [ + ModuleGathererLsmod, + ModuleGathererSysFs, + ModuleGathererScanner, + ModuleGathererKernel, + ] From 2e93b8ed0982de47d0745975fb516b1b580f84b2 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 14 Mar 2025 20:43:12 +0000 Subject: [PATCH 05/14] Prevent CodeQL from losing its mind. Properly checking for instances of the interface --- .../symbols/linux/utilities/modules.py | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index a930bef20..ee48ebc28 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -225,23 +225,6 @@ class Modules(interfaces.configuration.VersionableInterface): return ModuleInfo(module.vol.offset, mod_name, start, end) - @classmethod - def _validate_gatherers(cls, caller_wanted_gatherers) -> List[str]: - """ - Called by `run_modules_scanners` to validate the caller supplied gatherers list - An exception is thrown if an empty gatherers list is given or a list containing an invalid source - """ - if not caller_wanted_gatherers: - raise ValueError( - "`caller_wanted_gatherers` must have at least one gatherer." - ) - - for gatherer in caller_wanted_gatherers: - if gatherer not in ModuleGatherers.all_gatherers_identifier: - raise ValueError( - f"Invalid gatherer sent through `caller_wanted_gatherers`: {gatherer}" - ) - @classmethod def run_modules_scanners( cls, @@ -268,8 +251,20 @@ class Modules(interfaces.configuration.VersionableInterface): Returns: Dictionary mapping each plugin to its corresponding result """ - # Throws ValueError if invalid gatherers sent in - Modules._validate_gatherers(caller_wanted_gatherers) + if not caller_wanted_gatherers: + raise ValueError( + "`caller_wanted_gatherers` must have at least one gatherer." + ) + + if not isinstance(caller_wanted_gatherers, Iterable): + raise ValueError("`caller_wanted_gatherers` must be iterable") + + for gatherer in caller_wanted_gatherers: + if not issubclass(gatherer, ModuleGathererInterface): + raise ValueError( + f"Invalid gatherer sent through `caller_wanted_gatherers`: {gatherer}" + ) + kernel = context.modules[kernel_module_name] From f5ae7928a300e1eeec2c19fd67b9d7d883a9d2e1 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 14 Mar 2025 20:45:14 +0000 Subject: [PATCH 06/14] Black fix --- volatility3/framework/symbols/linux/utilities/modules.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index ee48ebc28..c3700ae60 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -265,7 +265,6 @@ class Modules(interfaces.configuration.VersionableInterface): f"Invalid gatherer sent through `caller_wanted_gatherers`: {gatherer}" ) - kernel = context.modules[kernel_module_name] address_mask = context.layers[kernel.layer_name].address_mask From a0f3cba6f6d71648a4feb884502491b773815255 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 14 Mar 2025 21:08:15 +0000 Subject: [PATCH 07/14] Fix stale comments --- volatility3/framework/symbols/linux/utilities/modules.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index c3700ae60..957d7b5b4 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -237,14 +237,9 @@ class Modules(interfaces.configuration.VersionableInterface): to not operate any inter-plugin results triage. Rules for `caller_wanted_sources`: - - If `ModuleGathers.all_gathers_identifier` is specified then every source will be populated - - If `ModuleGathers.Scanner` is in the list, then at least one other sources must be - specified so a comparison will be populated + If `ModuleGatherers.all_gathers_identifier` is specified then every source will be populated If empty or an invalid source is specified then a ValueError is thrown - Args: called_wanted_sources: The list of sources to gather modules. flatten: Whether to de-duplicate modules across sources From 53e32e36f6128fa3ec53e77f3e7fb859cbc1d173 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 14 Mar 2025 22:34:06 +0000 Subject: [PATCH 08/14] Add versioning on gatherers, make check non-specific to kernel gatherer, add requirements to ModuleGatherers --- .../symbols/linux/utilities/modules.py | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 957d7b5b4..70b45fbd9 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -23,6 +23,7 @@ from volatility3.framework import ( objects, ) +from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.symbols.linux import extensions @@ -274,7 +275,7 @@ class Modules(interfaces.configuration.VersionableInterface): for module in gatherer.gather_modules(context, kernel_module_name): # the kernel sends back a ModuleInfo directly - if gatherer == ModuleGathererKernel: + if isinstance(module, ModuleInfo): modinfo = module else: modinfo = cls.get_module_info_for_module(address_mask, module) @@ -541,6 +542,10 @@ class ModuleGathererLsmod(ModuleGathererInterface): Gathers modules from the main kernel list """ + _version = (1, 0, 0) + + name = "Lsmod" + @classmethod def gather_modules( cls, context: interfaces.context.ContextInterface, kernel_module_name: str @@ -553,6 +558,10 @@ class ModuleGathererSysFs(ModuleGathererInterface): Gathers modules from the sysfs /sys/modules objects """ + _version = (1, 0, 0) + + name = "SysFs" + @classmethod def gather_modules( cls, context: interfaces.context.ContextInterface, kernel_module_name: str @@ -570,6 +579,10 @@ class ModuleGathererScanner(ModuleGathererInterface): Gathers modules by scanning memory """ + _version = (1, 0, 0) + + name = "Scanner" + @classmethod def gather_modules( cls, context: interfaces.context.ContextInterface, kernel_module_name: str @@ -593,6 +606,10 @@ class ModuleGathererKernel(ModuleGathererInterface): can determine when function pointers reference the kernel """ + _version = (1, 0, 0) + + name = "kernel" + @classmethod def gather_modules( cls, context: interfaces.context.ContextInterface, kernel_module_name: str @@ -614,7 +631,10 @@ class ModuleGathererKernel(ModuleGathererInterface): yield ModuleInfo(start_addr, constants.linux.KERNEL_NAME, start_addr, end_addr) -class ModuleGatherers(interfaces.configuration.VersionableInterface): +class ModuleGatherers( + interfaces.configuration.VersionableInterface, + interfaces.configuration.ConfigurableInterface, +): _version = (1, 0, 0) _required_framework_version = (2, 0, 0) @@ -629,3 +649,19 @@ class ModuleGatherers(interfaces.configuration.VersionableInterface): ModuleGathererScanner, ModuleGathererKernel, ] + + @classmethod + def get_requirements(cls): + reqs = [] + + # for now, all versions are 1, this will be broken out if/when that changes + for gatherer in ModuleGatherers.all_gatherers_identifier: + reqs.append( + requirements.VersionRequirement( + name=gatherer.name.replace(" ", ""), + component=gatherer, + version=(1, 0, 0), + ) + ) + + return reqs From 674cc045d21694647857f9d47ca3ff94f49a8c51 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sat, 15 Mar 2025 17:22:31 +0000 Subject: [PATCH 09/14] Update to using str dictionary key. Add requirements for each gatherer in modxview. Validate names are unique while sanity checking. --- .../framework/plugins/linux/modxview.py | 24 ++++++++++----- .../symbols/linux/utilities/modules.py | 30 ++++++++++++++----- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index b04eb74ca..cf31a3a33 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -38,7 +38,17 @@ spot modules presence and taints.""" ), requirements.VersionRequirement( name="linux_utilities_module_gatherers", - component=linux_utilities_modules.ModuleGatherers, + component=linux_utilities_modules.ModuleGathererLsmod, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGathererSysFs, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGathererScanner, version=(1, 0, 0), ), requirements.VersionRequirement( @@ -113,14 +123,14 @@ spot modules presence and taints.""" # We want to be explicit on the plugins results we are interested in for gatherer in wanted_gatherers: # Iterate over each recovered module - for mod_info in run_results[gatherer]: + for mod_info in run_results[gatherer.name]: # Use offsets as unique keys, whether a module # appears in many plugin runs or not if aggregated_modules.get(mod_info.offset, None) is not None: # Append the plugin to the list of originating plugins - aggregated_modules[mod_info.offset].append(gatherer) + aggregated_modules[mod_info.offset].append(gatherer.name) else: - aggregated_modules[mod_info.offset] = [gatherer] + aggregated_modules[mod_info.offset] = [gatherer.name] for module_offset, gatherers in aggregated_modules.items(): module = kernel.object("module", offset=module_offset, absolute=True) @@ -148,9 +158,9 @@ spot modules presence and taints.""" ( module.get_name() or NotAvailableValue(), format_hints.Hex(module_offset), - linux_utilities_modules.ModuleGathererLsmod in gatherers, - linux_utilities_modules.ModuleGathererSysFs in gatherers, - linux_utilities_modules.ModuleGathererScanner in gatherers, + linux_utilities_modules.ModuleGathererLsmod.name in gatherers, + linux_utilities_modules.ModuleGathererSysFs.name in gatherers, + linux_utilities_modules.ModuleGathererScanner.name in gatherers, taints or NotAvailableValue(), ), ) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 70b45fbd9..eeee98a01 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -233,19 +233,21 @@ class Modules(interfaces.configuration.VersionableInterface): kernel_module_name: str, caller_wanted_gatherers: List[ModuleGathererInterface], flatten: bool = True, - ) -> Dict[ModuleGathererInterface, List[ModuleInfo]]: + ) -> Dict[str, List[ModuleInfo]]: """Run module scanning plugins and aggregate the results. It is designed to not operate any inter-plugin results triage. - Rules for `caller_wanted_sources`: + Rules for `caller_wanted_gatherers`: If `ModuleGatherers.all_gathers_identifier` is specified then every source will be populated - If empty or an invalid source is specified then a ValueError is thrown + If empty or an invalid gatherer is specified then a ValueError is thrown + + All gatherer names must be unique Args: called_wanted_sources: The list of sources to gather modules. - flatten: Whether to de-duplicate modules across sources + flatten: Whether to de-duplicate modules across gatherers Returns: - Dictionary mapping each plugin to its corresponding result + Dictionary mapping each gatherer to its corresponding result """ if not caller_wanted_gatherers: raise ValueError( @@ -255,12 +257,26 @@ class Modules(interfaces.configuration.VersionableInterface): if not isinstance(caller_wanted_gatherers, Iterable): raise ValueError("`caller_wanted_gatherers` must be iterable") + seen_names = set() + for gatherer in caller_wanted_gatherers: if not issubclass(gatherer, ModuleGathererInterface): raise ValueError( f"Invalid gatherer sent through `caller_wanted_gatherers`: {gatherer}" ) + if not hasattr(gatherer, "name"): + raise ValueError( + f"{gatherer} does not have a name attribute, which is required." + ) + + if gatherer.name in seen_names: + raise ValueError( + f"{gatherer} has a name {gatherer.name} which has already been processed. Names must be unique." + ) + + seen_names.add(gatherer.name) + kernel = context.modules[kernel_module_name] address_mask = context.layers[kernel.layer_name].address_mask @@ -269,7 +285,7 @@ class Modules(interfaces.configuration.VersionableInterface): # Walk each source gathering modules for gatherer in caller_wanted_gatherers: - run_results[gatherer] = [] + run_results[gatherer.name] = [] # process each module coming from back the current source for module in gatherer.gather_modules(context, kernel_module_name): @@ -281,7 +297,7 @@ class Modules(interfaces.configuration.VersionableInterface): modinfo = cls.get_module_info_for_module(address_mask, module) if modinfo: - run_results[gatherer].append(modinfo) + run_results[gatherer.name].append(modinfo) if flatten: return cls.flatten_run_modules_results(run_results) From 0f5dd10aa9ba8370c8153dd6d36fb600dd03f888 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sat, 15 Mar 2025 13:23:01 -0500 Subject: [PATCH 10/14] Apply suggestions from code review Co-authored-by: ikelos --- volatility3/framework/plugins/linux/modxview.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index cf31a3a33..ed21acfd1 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -37,17 +37,17 @@ spot modules presence and taints.""" version=(3, 0, 0), ), requirements.VersionRequirement( - name="linux_utilities_module_gatherers", + name="linux_utilities_module_gatherer_lsmod", component=linux_utilities_modules.ModuleGathererLsmod, version=(1, 0, 0), ), requirements.VersionRequirement( - name="linux_utilities_module_gatherers", + name="linux_utilities_module_gatherer_sysfs", component=linux_utilities_modules.ModuleGathererSysFs, version=(1, 0, 0), ), requirements.VersionRequirement( - name="linux_utilities_module_gatherers", + name="linux_utilities_module_gatherer_scanner", component=linux_utilities_modules.ModuleGathererScanner, version=(1, 0, 0), ), From e79f03691a1aa84afa3e2a1fe5fc9746709a5d70 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sat, 15 Mar 2025 19:01:52 +0000 Subject: [PATCH 11/14] Add name to interface and validate it in processing loop --- volatility3/framework/symbols/linux/utilities/modules.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index eeee98a01..b711f4e2c 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -51,6 +51,9 @@ class ModuleGathererInterface( gatherer_return_type = Generator[Union[ModuleInfo, "extensions.module"], None, None] + # Must be set to a unique, descriptive name of the gathering technique or data structure source + name = None + @classmethod @abstractmethod def gather_modules( @@ -265,9 +268,9 @@ class Modules(interfaces.configuration.VersionableInterface): f"Invalid gatherer sent through `caller_wanted_gatherers`: {gatherer}" ) - if not hasattr(gatherer, "name"): + if gatherer.name is None or len(gatherer.name) == 0: raise ValueError( - f"{gatherer} does not have a name attribute, which is required." + f"{gatherer} does not have a valid name attribute, which is required. It must be a non-zero length string." ) if gatherer.name in seen_names: From 9da147df1b6b79a5c9ab0a67ed3593829bb6c598 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sat, 15 Mar 2025 19:10:00 +0000 Subject: [PATCH 12/14] simplify check --- volatility3/framework/symbols/linux/utilities/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index b711f4e2c..52dfe77e4 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -268,7 +268,7 @@ class Modules(interfaces.configuration.VersionableInterface): f"Invalid gatherer sent through `caller_wanted_gatherers`: {gatherer}" ) - if gatherer.name is None or len(gatherer.name) == 0: + if not gatherer.name: raise ValueError( f"{gatherer} does not have a valid name attribute, which is required. It must be a non-zero length string." )