From 640b86cbc3d3946db3d152bc5c0606a95fca4955 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Thu, 31 Oct 2024 10:02:34 -0500 Subject: [PATCH] move directories --- .../plugins/linux/kernel_tracing}/ftrace.py | 0 .../linux/{ => kernel_tracing}/perf_events.py | 4 +- .../linux/kernel_tracing/tracepoints.py | 247 ++++++++++++++++++ 3 files changed, 250 insertions(+), 1 deletion(-) rename volatility3/{plugins/linux => framework/plugins/linux/kernel_tracing}/ftrace.py (100%) rename volatility3/framework/plugins/linux/{ => kernel_tracing}/perf_events.py (93%) create mode 100644 volatility3/framework/plugins/linux/kernel_tracing/tracepoints.py diff --git a/volatility3/plugins/linux/ftrace.py b/volatility3/framework/plugins/linux/kernel_tracing/ftrace.py similarity index 100% rename from volatility3/plugins/linux/ftrace.py rename to volatility3/framework/plugins/linux/kernel_tracing/ftrace.py diff --git a/volatility3/framework/plugins/linux/perf_events.py b/volatility3/framework/plugins/linux/kernel_tracing/perf_events.py similarity index 93% rename from volatility3/framework/plugins/linux/perf_events.py rename to volatility3/framework/plugins/linux/kernel_tracing/perf_events.py index 764cbae0c..2136de2d8 100644 --- a/volatility3/framework/plugins/linux/perf_events.py +++ b/volatility3/framework/plugins/linux/kernel_tracing/perf_events.py @@ -67,6 +67,7 @@ class PerfEvents(plugins.PluginInterface): # if the names are smeared then bail try: event_name = utility.pointer_to_string(event.pmu.name, count=64) + full_name = utility.array_to_string(event.prog.aux.ksym.name, count=512) program_name = utility.array_to_string(event.prog.aux.name) except exceptions.InvalidAddressException: continue @@ -84,7 +85,7 @@ class PerfEvents(plugins.PluginInterface): yield ( 0, - (task.pid, task_name, event_name, program_name, program_address), + (event.vol.offset, task_name, event_name, program_name, full_name, program_address), ) def run(self) -> renderers.TreeGrid: @@ -93,6 +94,7 @@ class PerfEvents(plugins.PluginInterface): ("PID", int), ("Process", str), ("Event Name", str), + ("Full Name", str), ("Program Name", str), ("Address", format_hints.Hex), ], diff --git a/volatility3/framework/plugins/linux/kernel_tracing/tracepoints.py b/volatility3/framework/plugins/linux/kernel_tracing/tracepoints.py new file mode 100644 index 000000000..cf9fe20b9 --- /dev/null +++ b/volatility3/framework/plugins/linux/kernel_tracing/tracepoints.py @@ -0,0 +1,247 @@ +# This file is Copyright 2023 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from typing import List +from volatility3.plugins.linux import lsmod, check_unlinked_modules +from volatility3.framework import constants, exceptions, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import TreeGrid +from volatility3.framework.symbols import linux +from volatility3.framework.objects import utility + +vollog = logging.getLogger(__name__) +UNKNOWN = "UNKNOWN" + + +class Check_tracepoints(interfaces.plugins.PluginInterface): + """Detect tracepoints hooking""" + + _version = (1, 0, 0) + _required_framework_version = (2, 5, 2) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64", "AArch64"], + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="check_unlinked_modules", + plugin=check_unlinked_modules.Check_unlinked_modules, + version=(1, 0, 0), + ), + requirements.PluginRequirement( + name="check_modules", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), + ] + + def run(self): + """Plugin output format : + + Tracepoint : "tracepoint offset" ["tracepoint name"] + Probe : "probe offset" ["probe name"] + Module : hex("module offset") ["associated module"] | "UNKNOWN" + Probe out of kernel .text : True | False + """ + + columns = [ + ("tracepoint", str), + ("Probe", str), + ("Module", str), + ("Probe out of kernel .text", bool), + ] + + return TreeGrid( + columns, + self._generator(), + ) + + def _generator(self): + self.vmlinux = self.context.modules[self.config["kernel"]] + if not self.vmlinux.has_symbol("__start___tracepoints_ptrs"): + raise exceptions.SymbolError( + "__start___tracepoints_ptrs", + self.vmlinux.symbol_table_name, + 'The provided symbol table does not include the "__start___tracepoints_ptrs" symbol. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt.', + ) + + self.checked_probes = {} + self.set_compiled_kernel_space_boundaries() + self.setup_modules_and_handlers() + + k_config_have_arch_prel32_relocations = False + tracepoints = [] + tracepoints_start = self.vmlinux.object_from_symbol( + "__start___tracepoints_ptrs" + ) + tracepoints_end = self.vmlinux.object_from_symbol("__stop___tracepoints_ptrs") + tracepoints_size_raw = tracepoints_end.vol.offset - tracepoints_start.vol.offset + + # Determine if tracepoints array contains a set of valid absolute pointers, or a set of relative pointers (represented by 32 bits integers). + # See https://elixir.bootlin.com/linux/v6.6/source/include/linux/tracepoint.h#L113 for references. + tracepoints_ptrs = utility.array_of_pointers( + tracepoints_start, + tracepoints_size_raw // 8, + self.vmlinux.symbol_table_name + constants.BANG + "tracepoint", + self.context, + ) + # Check two different pointers + if ( + not tracepoints_ptrs[0].is_readable() + or not tracepoints_ptrs[1].is_readable() + ): + k_config_have_arch_prel32_relocations = True + else: + for tracepoint_ptr in tracepoints_ptrs: + tracepoints.append(tracepoint_ptr.dereference()) + + vollog.debug( + f"CONFIG_HAVE_ARCH_PREL32_RELOCATIONS was determined to be {k_config_have_arch_prel32_relocations}" + ) + + if k_config_have_arch_prel32_relocations: + subtype_int = self.vmlinux.context.symbol_space.get_type( + self.vmlinux.symbol_table_name + constants.BANG + "int" + ) + tracepoints_relative_offsets = tracepoints_start.cast( + "array", count=tracepoints_size_raw // 4, subtype=subtype_int + ) + # Based on "offset_to_ptr()". See https://elixir.bootlin.com/linux/v6.6/source/include/linux/compiler.h#L223 for references. + for relative_offset in tracepoints_relative_offsets: + tracepoint = self.vmlinux.object( + "tracepoint", + relative_offset + relative_offset.vol.offset, + absolute=True, + ) + tracepoints.append(tracepoint) + + results = [] + for i, tracepoint in enumerate(tracepoints): + self._progress_callback( + (i / len(tracepoints)) * 100, f"Iterating over tracepoints..." + ) + # Ignore tracepoints without attached probes + if not tracepoint.funcs.is_readable(): + continue + + try: + parse_result = self.parse_tracepoint(tracepoint) + results.append((0, parse_result)) + except Exception as e: + vollog.exception(f"Unhandled exception : {e}") + + # Preferred to "yield", otherwise progress_callback and results overlap... + return results + + def parse_tracepoint(self, tracepoint): + tracepoint_name = utility.pointer_to_string(tracepoint.name, count=512) + tracepoint_offset = tracepoint.vol.offset + probe_handler_address = tracepoint.funcs.dereference().func + + # Avoid running the aggressive module finder twice for an address, if it wasn't found previously + if self.checked_probes.get(probe_handler_address): + module_name = self.checked_probes[probe_handler_address] + else: + module_name = self.wrapper_lookup_module_address(probe_handler_address) + self.checked_probes[probe_handler_address] = module_name + + # Useful information allowing to detect if a module was inserted dynamically or if it is part of the compiled kernel + probe_out_of_kernel_range = ( + probe_handler_address < self.kernel_space_start + or probe_handler_address > self.kernel_space_end + ) + + ### Format results ### + probe_handler_address_symbol = UNKNOWN + f_module = UNKNOWN + # Fetch more informations about the module + if module_name != UNKNOWN: + module_obj = get_module_object_from_name(module_name, self.modules) + module_address = module_obj.vol.offset + f_module = f"{hex(module_address)} [{module_name}]" + probe_handler_address_symbol = ( + module_obj.get_symbol_by_address(probe_handler_address) or UNKNOWN + ) + + results = ( + f"{hex(tracepoint_offset)} [{tracepoint_name}]", + f"{hex(probe_handler_address)} [{probe_handler_address_symbol}]", + f_module, + probe_out_of_kernel_range, + ) + return results + + def set_compiled_kernel_space_boundaries(self): + """Set compiler kernel address spaces. Preferred to linux.LinuxUtilities.generate_kernel_handler_info()[0] for convenience""" + self.kernel_space_start = self.vmlinux.get_absolute_symbol_address("_stext") + self.kernel_space_end = self.vmlinux.get_absolute_symbol_address("_etext") + + def get_all_handlers(self): + """Concatenate all handlers ("/proc/modules", "/sys/module/" and "unlinked kobject modules from sysfs hierarchy")""" + return self.proc_handlers + self.sysfs_handlers + self.sysfs_unlinked_handlers + + def wrapper_lookup_module_address(self, leaked_address: int): + # Detect module name based on leaked_address address + module_name, _ = linux.LinuxUtilities.lookup_module_address( + self.vmlinux, + self.get_all_handlers(), + leaked_address, + ) + # Aggressive module finder, for deeply hidden rootkits (try to detect usage of kobject_del) + if module_name == UNKNOWN: + sysfs_unlinked_modules = check_unlinked_modules.Check_unlinked_modules( + self.context, self.config_path + )._generator(self.sysfs_handlers, self.sysfs_modules, leaked_address) + if sysfs_unlinked_modules: + sysfs_unlinked_modules = [m[1] for m in sysfs_unlinked_modules] + self.sysfs_unlinked_handlers = ( + linux.LinuxUtilities.generate_kernel_handler_info( + self.context, self.vmlinux.name, sysfs_unlinked_modules + ) + ) + self.modules.extend(sysfs_unlinked_modules) + + # Search handlers again with the new informations, to see if leaked_address fits now + module_name, _ = linux.LinuxUtilities.lookup_module_address( + self.vmlinux, + self.get_all_handlers(), + leaked_address, + ) + + return module_name + + def setup_modules_and_handlers(self): + # Get /proc/modules and /sys/module/ listed modules + self.proc_modules = list( + lsmod.Lsmod.list_modules(self.context, self.vmlinux.name) + ) + self.sysfs_modules = list( + check_unlinked_modules.Check_unlinked_modules.wrapper_get_sysfs_modules( + self.context, self.config_path, self.vmlinux.name + ) + ) + # Calculate boundaries for each module + self.proc_handlers = linux.LinuxUtilities.generate_kernel_handler_info( + self.context, self.vmlinux.name, self.proc_modules + ) + self.sysfs_handlers = linux.LinuxUtilities.generate_kernel_handler_info( + self.context, self.vmlinux.name, self.sysfs_modules + ) + self.sysfs_unlinked_handlers = [] + self.modules = self.proc_modules + self.sysfs_modules + + +## UTILITIES ## +def get_module_object_from_name( + wanted_module_name: str, modules: list +) -> linux.extensions.module: + """Return a module object based on a module name""" + for m in modules: + if utility.array_to_string(m.name) == wanted_module_name: + return m