Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add linux hidden_modules plugin #1283

Merged
merged 19 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6cd39c0
Refactor of module object. Adding function helpers to simplify the co…
gcmoreira Sep 18, 2024
5dee3ae
Add linux.hidden_modules plugin
gcmoreira Oct 1, 2024
d5e6e7c
Allow any module state value in both traditional and fast scan methods
gcmoreira Oct 1, 2024
590aa9c
Make it callable from other plugins.
gcmoreira Oct 3, 2024
8d925bd
Added the --heuristic-mode option, which relaxes constraints to impro…
gcmoreira Oct 3, 2024
e8754fa
Fix typo in usage help
gcmoreira Oct 3, 2024
b5948d7
Linux: hidden_modules: Add @Abyss-W4tcher suggestion to optimize the …
gcmoreira Oct 3, 2024
f455c30
Linux: hidden_modules: remove missed optional heuristic_mode argument
gcmoreira Oct 3, 2024
d98c7eb
linux: hidden_modules: Make the fast method the default. Remove vol2 …
gcmoreira Oct 16, 2024
0ddd921
linux: hidden_modules: Remove unused module imports
gcmoreira Oct 16, 2024
526007f
Linux: hidden_modules: Use child_template
gcmoreira Oct 29, 2024
cbe071f
Linux: hidden_modules: Import the whole architectures module
gcmoreira Oct 29, 2024
dfd8a1f
Linux: hidden_modules: Include kernel version and commit details
gcmoreira Oct 29, 2024
1c6a548
Linux: hidden_modules: Simplify symbols type checks
gcmoreira Oct 29, 2024
8960bda
Linux: hidden_modules: Add a symbol table check for a recent dwarf2js…
gcmoreira Oct 29, 2024
f537c4a
Merge branch 'develop' into linux_hidden_modules
gcmoreira Oct 29, 2024
4b76b69
Linux: hidden_modules: Add docstrings and comments to enhance the doc…
gcmoreira Oct 29, 2024
722ccd5
Linux: Extensions: Clean up the Linux constants imports in the object…
gcmoreira Oct 30, 2024
4f86b3f
Merge branch 'develop' into linux_hidden_modules
gcmoreira Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions volatility3/framework/constants/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,11 @@ class PT_FLAGS(Flag):
def flags(self) -> str:
"""Returns the ptrace flags string"""
return str(self).replace(self.__class__.__name__ + ".", "")


# Valid sizes for modules. Note that the Linux kernel does not define these values; they
# are based on empirical observations of typical memory allocations for kernel modules.
# We use this to verify that the found module falls within reasonable limits.
MODULE_MAXIMUM_CORE_SIZE = 20000000
MODULE_MAXIMUM_CORE_TEXT_SIZE = 20000000
MODULE_MINIMUM_SIZE = 4096
246 changes: 246 additions & 0 deletions volatility3/framework/plugins/linux/hidden_modules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
# 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, Set, Tuple, Iterable
from volatility3.framework import renderers, interfaces, exceptions, objects
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__)


class Hidden_modules(interfaces.plugins.PluginInterface):
"""Carves memory to find hidden kernel modules"""

_required_framework_version = (2, 10, 0)

_version = (1, 0, 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)
),
]

@staticmethod
def get_modules_memory_boundaries(
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
) -> Tuple[int]:
"""Determine the boundaries of the module allocation area

Args:
context: The context to retrieve required elements (layers, symbol tables) from
vmlinux_module_name: The name of the kernel module on which to operate

Returns:
A tuple containing the minimum and maximum addresses for the module allocation area.
"""
vmlinux = context.modules[vmlinux_module_name]
if vmlinux.has_symbol("mod_tree"):
# Kernel >= 5.19 58d208de3e8d87dbe196caf0b57cc58c7a3836ca
mod_tree = vmlinux.object_from_symbol("mod_tree")
modules_addr_min = mod_tree.addr_min
modules_addr_max = mod_tree.addr_max
elif vmlinux.has_symbol("module_addr_min"):
# 2.6.27 <= kernel < 5.19 3a642e99babe0617febb6f402e1e063479f489db
modules_addr_min = vmlinux.object_from_symbol("module_addr_min")
modules_addr_max = vmlinux.object_from_symbol("module_addr_max")

if isinstance(modules_addr_min, objects.Void):
raise exceptions.VolatilityException(
"Your ISF symbols lack type information. You may need to update the"
"ISF using the latest version of dwarf2json"
)
else:
raise exceptions.VolatilityException(
"Cannot find the module memory allocation area. Unsupported kernel"
)

return modules_addr_min, modules_addr_max

@classmethod
def _get_module_address_alignment(
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
) -> int:
"""Obtain the module memory address alignment.

struct module is aligned to the L1 cache line, which is typically 64 bytes for most
common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this
will still work.

Args:
context: The context to retrieve required elements (layers, symbol tables) from
vmlinux_module_name: The name of the kernel module on which to operate

Returns:
The struct module alignment
"""
# FIXME: When dwarf2json/ISF supports type alignments. Read it directly from the type metadata
gcmoreira marked this conversation as resolved.
Show resolved Hide resolved
# Additionally, while 'context' and 'vmlinux_module_name' are currently unused, they will be
# essential for retrieving type metadata in the future.
return 64

@staticmethod
def _validate_alignment_patterns(
addresses: Iterable[int],
address_alignment: int,
) -> bool:
"""Check if the memory addresses meet our alignments patterns

Args:
addresses: Iterable with the address values
address_alignment: Number of bytes for alignment validation

Returns:
True if all the addresses meet the alignment
"""
return all(addr % address_alignment == 0 for addr in addresses)

@classmethod
def get_hidden_modules(
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
known_module_addresses: Set[int],
modules_memory_boundaries: Tuple,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Enumerate hidden modules by taking advantage of memory address alignment patterns

This technique is much faster and uses less memory than the traditional scan method
in Volatility2, but it doesn't work with older kernels.

From kernels 4.2 struct module allocation are aligned to the L1 cache line size.
In i386/amd64/arm64 this is typically 64 bytes. However, this can be changed in
the Linux kernel configuration via CONFIG_X86_L1_CACHE_SHIFT. The alignment can
also be obtained from the DWARF info i.e. DW_AT_alignment<64>, but dwarf2json
doesn't support this feature yet.
In kernels < 4.2, alignment attributes are absent in the struct module, meaning
alignment cannot be guaranteed. Therefore, for older kernels, it's better to use
the traditional scan technique.

Args:
context: The context to retrieve required elements (layers, symbol tables) from
vmlinux_module_name: The name of the kernel module on which to operate
known_module_addresses: Set with known module addresses
modules_memory_boundaries: Minimum and maximum address boundaries for module allocation.
Yields:
module objects
"""
vmlinux = context.modules[vmlinux_module_name]
vmlinux_layer = context.layers[vmlinux.layer_name]

module_addr_min, module_addr_max = modules_memory_boundaries
module_address_alignment = cls._get_module_address_alignment(
context, vmlinux_module_name
)
if not cls._validate_alignment_patterns(
known_module_addresses, module_address_alignment
):
vollog.warning(
f"Module addresses aren't aligned to {module_address_alignment} bytes. "
"Switching to 1 byte aligment scan method."
)
module_address_alignment = 1

mkobj_offset = vmlinux.get_type("module").relative_child_offset("mkobj")
mod_offset = vmlinux.get_type("module_kobject").relative_child_offset("mod")
offset_to_mkobj_mod = mkobj_offset + mod_offset
mod_member_template = vmlinux.get_type("module_kobject").child_template("mod")
mod_size = mod_member_template.size
mod_member_data_format = mod_member_template.data_format

for module_addr in range(
module_addr_min, module_addr_max, module_address_alignment
):
if module_addr in known_module_addresses:
continue

try:
# This is just a pre-filter. Module readability and consistency are verified in module.is_valid()
self_referential_bytes = vmlinux_layer.read(
module_addr + offset_to_mkobj_mod, mod_size
)
self_referential = objects.convert_data_to_value(
self_referential_bytes, int, mod_member_data_format
)
if self_referential != module_addr:
continue
except (
exceptions.PagedInvalidAddressException,
exceptions.InvalidAddressException,
):
continue

module = vmlinux.object("module", offset=module_addr, absolute=True)
if module and module.is_valid():
yield module

@classmethod
def get_lsmod_module_addresses(
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
) -> Set[int]:
"""Obtain a set the known module addresses from linux.lsmod plugin

Args:
context: The context to retrieve required elements (layers, symbol tables) from
vmlinux_module_name: The name of the kernel module on which to operate

Returns:
A set containing known kernel module addresses
"""
vmlinux = context.modules[vmlinux_module_name]
vmlinux_layer = context.layers[vmlinux.layer_name]

known_module_addresses = {
vmlinux_layer.canonicalize(module.vol.offset)
for module in lsmod.Lsmod.list_modules(context, vmlinux_module_name)
}
return known_module_addresses

def _generator(self):
vmlinux_module_name = self.config["kernel"]
known_module_addresses = self.get_lsmod_module_addresses(
self.context, vmlinux_module_name
)
modules_memory_boundaries = self.get_modules_memory_boundaries(
self.context, vmlinux_module_name
)
for module in self.get_hidden_modules(
self.context,
vmlinux_module_name,
known_module_addresses,
modules_memory_boundaries,
):
module_addr = module.vol.offset
module_name = module.get_name() or renderers.NotAvailableValue()
fields = (format_hints.Hex(module_addr), module_name)
yield (0, fields)

def run(self):
if self.context.symbol_space.verify_table_versions(
"dwarf2json", lambda version, _: (not version) or version < (0, 8, 0)
):
raise exceptions.SymbolSpaceError(
"Invalid symbol table, please ensure the ISF table produced by dwarf2json was created with version 0.8.0 or later"
)

headers = [
("Address", format_hints.Hex),
("Name", str),
]
return renderers.TreeGrid(headers, self._generator())
Loading
Loading