mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-22 01:24:51 +02:00
Merge pull request #1845 from SolitudePy/categorize_linux_malware
Malware categorization: linux.check_afinfo & linux.hidden_modules
This commit is contained in:
@@ -36,7 +36,7 @@ For plugin requests, please create an issue with a description of the requested
|
||||
$ python3 vol.py --help | grep -i linux. | head -n 5
|
||||
banners.Banners Attempts to identify potential linux banners in an
|
||||
linux.bash.Bash Recovers bash command history from memory.
|
||||
linux.check_afinfo.Check_afinfo
|
||||
linux.malware.check_afinfo.Check_afinfo
|
||||
linux.check_creds.Check_creds
|
||||
linux.malware.check_idt.Check_idt
|
||||
|
||||
|
||||
@@ -426,7 +426,7 @@ class TestLinuxPageCacheInodepages:
|
||||
class TestLinuxCheckAfinfo:
|
||||
def test_linux_generic_check_afinfo(self, image, volatility, python):
|
||||
rc, out, _err = test_volatility.runvol_plugin(
|
||||
"linux.check_afinfo.Check_afinfo", image, volatility, python
|
||||
"linux.malware.check_afinfo.Check_afinfo", image, volatility, python
|
||||
)
|
||||
|
||||
# linux-sample-1.bin has no suspicious results.
|
||||
@@ -525,7 +525,7 @@ class TestLinuxHiddenModules:
|
||||
# TODO: this check should be specific, against a distinct infected sample
|
||||
image = LinuxSamples.LINUX_GENERIC.value.path
|
||||
rc, out, _err = test_volatility.runvol_plugin(
|
||||
"linux.hidden_modules.Hidden_modules", image, volatility, python
|
||||
"linux.malware.hidden_modules.Hidden_modules", image, volatility, python
|
||||
)
|
||||
|
||||
# linux-sample-1.bin has no hidden modules.
|
||||
|
||||
@@ -1,215 +1,20 @@
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
"""A module containing a plugin that verifies the operation function
|
||||
pointers of network protocols."""
|
||||
import logging
|
||||
from typing import List, Tuple, Generator
|
||||
|
||||
from volatility3.framework import exceptions, interfaces
|
||||
from volatility3.framework import renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.framework import interfaces, deprecation
|
||||
from volatility3.plugins.linux.malware import check_afinfo
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Check_afinfo(plugins.PluginInterface):
|
||||
"""Verifies the operation function pointers of network protocols."""
|
||||
class Check_afinfo(
|
||||
interfaces.plugins.PluginInterface,
|
||||
deprecation.PluginRenameClass,
|
||||
replacement_class=check_afinfo.Check_afinfo,
|
||||
removal_date="2026-06-07",
|
||||
):
|
||||
"""Verifies the operation function pointers of network protocols (deprecated)."""
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _check_members(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_name: str,
|
||||
var_ops: interfaces.objects.ObjectInterface,
|
||||
var_name: str,
|
||||
members: List[str],
|
||||
) -> Generator[Tuple[str, str, int], None, None]:
|
||||
"""
|
||||
Yields any members that are not pointing inside the kernel
|
||||
"""
|
||||
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
for check in members:
|
||||
# redhat-specific garbage
|
||||
if check.startswith("__UNIQUE_ID_rh_kabi_hide"):
|
||||
continue
|
||||
|
||||
# These structures have members like `write` and `next`, which are built in Python functions
|
||||
addr = var_ops.member(attr=check)
|
||||
|
||||
# Unimplemented handlers are set to 0
|
||||
if not addr:
|
||||
continue
|
||||
|
||||
if len(vmlinux.get_symbols_by_absolute_location(addr)) == 0:
|
||||
yield var_name, check, addr
|
||||
|
||||
@classmethod
|
||||
def _check_pre_4_18_ops(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_name: str,
|
||||
var_name: str,
|
||||
var: interfaces.objects.ObjectInterface,
|
||||
op_members: List[str],
|
||||
seq_members: List[str],
|
||||
):
|
||||
"""
|
||||
Finds the correct way to reference `op_members`
|
||||
"""
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
if var.has_member("seq_fops"):
|
||||
yield from cls._check_members(
|
||||
context, vmlinux_name, var.seq_fops, var_name, op_members
|
||||
)
|
||||
# newer kernels
|
||||
if var.has_member("seq_ops"):
|
||||
yield from cls._check_members(
|
||||
context, vmlinux_name, var.seq_ops, var_name, seq_members
|
||||
)
|
||||
|
||||
# this is the most commonly hooked member by rootkits, so a force a check on it
|
||||
elif var.has_member("seq_show"):
|
||||
if len(vmlinux.get_symbols_by_location(var.seq_show)) == 0:
|
||||
yield var_name, "show", var.seq_show
|
||||
else:
|
||||
raise exceptions.VolatilityException(
|
||||
"_check_afinfo_pre_4_18: Unable to find sequence operations members for checking."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_afinfo_pre_4_18(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_name: str,
|
||||
seq_members: str,
|
||||
) -> Generator[Tuple[str, str, int], None, None]:
|
||||
"""
|
||||
Checks the operations structures for network protocols of < 4.18 systems
|
||||
"""
|
||||
tcp = ("tcp_seq_afinfo", ["tcp6_seq_afinfo", "tcp4_seq_afinfo"])
|
||||
udp = (
|
||||
"udp_seq_afinfo",
|
||||
[
|
||||
"udplite6_seq_afinfo",
|
||||
"udp6_seq_afinfo",
|
||||
"udplite4_seq_afinfo",
|
||||
"udp4_seq_afinfo",
|
||||
],
|
||||
)
|
||||
protocols = [tcp, udp]
|
||||
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
op_members = vmlinux.get_type("file_operations").members
|
||||
|
||||
# loop through all symbols
|
||||
for struct_type, global_vars in protocols:
|
||||
for global_var_name in global_vars:
|
||||
# this will lookup fail for the IPv6 protocols on kernels without IPv6 support
|
||||
try:
|
||||
global_var = vmlinux.object_from_symbol(global_var_name)
|
||||
except exceptions.SymbolError:
|
||||
continue
|
||||
|
||||
yield from cls._check_pre_4_18_ops(
|
||||
context,
|
||||
vmlinux_name,
|
||||
global_var_name,
|
||||
global_var,
|
||||
op_members,
|
||||
seq_members,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_afinfo_post_4_18(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_name: str,
|
||||
seq_members: str,
|
||||
) -> Generator[Tuple[str, str, int], None, None]:
|
||||
"""
|
||||
Checks the operations structures for network protocols of >= 4.18 systems
|
||||
"""
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
ops_structs = [
|
||||
"raw_seq_ops",
|
||||
"udp_seq_ops",
|
||||
"arp_seq_ops",
|
||||
"unix_seq_ops",
|
||||
"udp6_seq_ops",
|
||||
"raw6_seq_ops",
|
||||
"tcp_seq_ops",
|
||||
"tcp4_seq_ops",
|
||||
"tcp6_seq_ops",
|
||||
"packet_seq_ops",
|
||||
]
|
||||
|
||||
for protocol_ops_var in ops_structs:
|
||||
# These will fail if the particular kernel doesn't have support for a protocol like IPv6
|
||||
try:
|
||||
protocol_ops = vmlinux.object_from_symbol(protocol_ops_var)
|
||||
except exceptions.SymbolError:
|
||||
continue
|
||||
|
||||
yield from cls._check_members(
|
||||
context, vmlinux_name, protocol_ops, protocol_ops_var, seq_members
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def check_afinfo(
|
||||
cls, context: interfaces.context.ContextInterface, vmlinux_name
|
||||
) -> Generator[Tuple[str, str, int], None, None]:
|
||||
"""
|
||||
Walks the network protocol operations structures for common network protocols.
|
||||
Reports any initialized operations members that do not point inside the kernel.
|
||||
"""
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
type_check = vmlinux.get_type("tcp_seq_afinfo")
|
||||
if type_check.has_member("seq_fops"):
|
||||
checker = cls._check_afinfo_pre_4_18
|
||||
else:
|
||||
checker = cls._check_afinfo_post_4_18
|
||||
|
||||
seq_members = vmlinux.get_type("seq_operations").members
|
||||
|
||||
yield from checker(context, vmlinux_name, seq_members)
|
||||
|
||||
def _generator(self):
|
||||
"""
|
||||
A simple wrapper around `check_afino`
|
||||
"""
|
||||
for name, member, address in self.check_afinfo(
|
||||
self.context, self.config["kernel"]
|
||||
):
|
||||
yield 0, (name, member, format_hints.Hex(address))
|
||||
|
||||
def run(self):
|
||||
return renderers.TreeGrid(
|
||||
[
|
||||
("Symbol Name", str),
|
||||
("Member", str),
|
||||
("Handler Address", format_hints.Hex),
|
||||
],
|
||||
self._generator(),
|
||||
)
|
||||
|
||||
@@ -1,197 +1,20 @@
|
||||
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# This file is Copyright 2025 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.symbols.linux.utilities import (
|
||||
modules as linux_utilities_modules,
|
||||
)
|
||||
from volatility3.framework import interfaces, exceptions, deprecation
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.symbols.linux import extensions
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework import interfaces, deprecation
|
||||
from volatility3.plugins.linux.malware import hidden_modules
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Hidden_modules(plugins.PluginInterface):
|
||||
"""Carves memory to find hidden kernel modules"""
|
||||
class Hidden_modules(
|
||||
interfaces.plugins.PluginInterface,
|
||||
deprecation.PluginRenameClass,
|
||||
replacement_class=hidden_modules.Hidden_modules,
|
||||
removal_date="2026-06-07",
|
||||
):
|
||||
"""Carves memory to find hidden kernel modules (deprecated)."""
|
||||
|
||||
_required_framework_version = (2, 25, 0)
|
||||
_version = (3, 0, 2)
|
||||
|
||||
@classmethod
|
||||
def find_hidden_modules(
|
||||
cls, context, vmlinux_module_name: str
|
||||
) -> extensions.module:
|
||||
if 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"
|
||||
)
|
||||
|
||||
known_module_addresses = cls.get_lsmod_module_addresses(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
modules_memory_boundaries = (
|
||||
linux_utilities_modules.Modules.get_modules_memory_boundaries(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
)
|
||||
|
||||
yield from linux_utilities_modules.Modules.get_hidden_modules(
|
||||
context,
|
||||
vmlinux_module_name,
|
||||
known_module_addresses,
|
||||
modules_memory_boundaries,
|
||||
)
|
||||
|
||||
@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
|
||||
"""
|
||||
return linux_utilities_modules.get_hidden_modules(
|
||||
vmlinux_module_name, known_module_addresses, modules_memory_boundaries
|
||||
)
|
||||
|
||||
run = linux_utilities_modules.ModuleDisplayPlugin.run
|
||||
_generator = linux_utilities_modules.ModuleDisplayPlugin.generator
|
||||
implementation = find_hidden_modules
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.VersionRequirement(
|
||||
name="linux_utilities_modules_module_display_plugin",
|
||||
component=linux_utilities_modules.ModuleDisplayPlugin,
|
||||
version=(1, 0, 0),
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linux_utilities_modules",
|
||||
component=linux_utilities_modules.Modules,
|
||||
version=(3, 0, 1),
|
||||
),
|
||||
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
|
||||
|
||||
@staticmethod
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries,
|
||||
removal_date="2025-09-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
def get_modules_memory_boundaries(
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_module_name: str,
|
||||
) -> Tuple[int, int]:
|
||||
return linux_utilities_modules.Modules.get_modules_memory_boundaries(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_module_address_alignment,
|
||||
removal_date="2025-09-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
@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
|
||||
"""
|
||||
return linux_utilities_modules.get_module_address_alignment(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_hidden_modules,
|
||||
removal_date="2025-09-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
@staticmethod
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.validate_alignment_patterns,
|
||||
removal_date="2025-09-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
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 linux_utilities_modules.validate_alignment_patterns(
|
||||
addresses, address_alignment
|
||||
)
|
||||
|
||||
@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 linux_utilities_modules.Modules.list_modules(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
}
|
||||
return known_module_addresses
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
"""A module containing a plugin that verifies the operation function
|
||||
pointers of network protocols."""
|
||||
import logging
|
||||
from typing import List, Tuple, Generator
|
||||
|
||||
from volatility3.framework import exceptions, interfaces
|
||||
from volatility3.framework import renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.renderers import format_hints
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Check_afinfo(plugins.PluginInterface):
|
||||
"""Verifies the operation function pointers of network protocols."""
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _check_members(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_name: str,
|
||||
var_ops: interfaces.objects.ObjectInterface,
|
||||
var_name: str,
|
||||
members: List[str],
|
||||
) -> Generator[Tuple[str, str, int], None, None]:
|
||||
"""
|
||||
Yields any members that are not pointing inside the kernel
|
||||
"""
|
||||
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
for check in members:
|
||||
# redhat-specific garbage
|
||||
if check.startswith("__UNIQUE_ID_rh_kabi_hide"):
|
||||
continue
|
||||
|
||||
# These structures have members like `write` and `next`, which are built in Python functions
|
||||
addr = var_ops.member(attr=check)
|
||||
|
||||
# Unimplemented handlers are set to 0
|
||||
if not addr:
|
||||
continue
|
||||
|
||||
if len(vmlinux.get_symbols_by_absolute_location(addr)) == 0:
|
||||
yield var_name, check, addr
|
||||
|
||||
@classmethod
|
||||
def _check_pre_4_18_ops(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_name: str,
|
||||
var_name: str,
|
||||
var: interfaces.objects.ObjectInterface,
|
||||
op_members: List[str],
|
||||
seq_members: List[str],
|
||||
):
|
||||
"""
|
||||
Finds the correct way to reference `op_members`
|
||||
"""
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
if var.has_member("seq_fops"):
|
||||
yield from cls._check_members(
|
||||
context, vmlinux_name, var.seq_fops, var_name, op_members
|
||||
)
|
||||
# newer kernels
|
||||
if var.has_member("seq_ops"):
|
||||
yield from cls._check_members(
|
||||
context, vmlinux_name, var.seq_ops, var_name, seq_members
|
||||
)
|
||||
|
||||
# this is the most commonly hooked member by rootkits, so a force a check on it
|
||||
elif var.has_member("seq_show"):
|
||||
if len(vmlinux.get_symbols_by_location(var.seq_show)) == 0:
|
||||
yield var_name, "show", var.seq_show
|
||||
else:
|
||||
raise exceptions.VolatilityException(
|
||||
"_check_afinfo_pre_4_18: Unable to find sequence operations members for checking."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_afinfo_pre_4_18(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_name: str,
|
||||
seq_members: str,
|
||||
) -> Generator[Tuple[str, str, int], None, None]:
|
||||
"""
|
||||
Checks the operations structures for network protocols of < 4.18 systems
|
||||
"""
|
||||
tcp = ("tcp_seq_afinfo", ["tcp6_seq_afinfo", "tcp4_seq_afinfo"])
|
||||
udp = (
|
||||
"udp_seq_afinfo",
|
||||
[
|
||||
"udplite6_seq_afinfo",
|
||||
"udp6_seq_afinfo",
|
||||
"udplite4_seq_afinfo",
|
||||
"udp4_seq_afinfo",
|
||||
],
|
||||
)
|
||||
protocols = [tcp, udp]
|
||||
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
op_members = vmlinux.get_type("file_operations").members
|
||||
|
||||
# loop through all symbols
|
||||
for struct_type, global_vars in protocols:
|
||||
for global_var_name in global_vars:
|
||||
# this will lookup fail for the IPv6 protocols on kernels without IPv6 support
|
||||
try:
|
||||
global_var = vmlinux.object_from_symbol(global_var_name)
|
||||
except exceptions.SymbolError:
|
||||
continue
|
||||
|
||||
yield from cls._check_pre_4_18_ops(
|
||||
context,
|
||||
vmlinux_name,
|
||||
global_var_name,
|
||||
global_var,
|
||||
op_members,
|
||||
seq_members,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_afinfo_post_4_18(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_name: str,
|
||||
seq_members: str,
|
||||
) -> Generator[Tuple[str, str, int], None, None]:
|
||||
"""
|
||||
Checks the operations structures for network protocols of >= 4.18 systems
|
||||
"""
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
ops_structs = [
|
||||
"raw_seq_ops",
|
||||
"udp_seq_ops",
|
||||
"arp_seq_ops",
|
||||
"unix_seq_ops",
|
||||
"udp6_seq_ops",
|
||||
"raw6_seq_ops",
|
||||
"tcp_seq_ops",
|
||||
"tcp4_seq_ops",
|
||||
"tcp6_seq_ops",
|
||||
"packet_seq_ops",
|
||||
]
|
||||
|
||||
for protocol_ops_var in ops_structs:
|
||||
# These will fail if the particular kernel doesn't have support for a protocol like IPv6
|
||||
try:
|
||||
protocol_ops = vmlinux.object_from_symbol(protocol_ops_var)
|
||||
except exceptions.SymbolError:
|
||||
continue
|
||||
|
||||
yield from cls._check_members(
|
||||
context, vmlinux_name, protocol_ops, protocol_ops_var, seq_members
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def check_afinfo(
|
||||
cls, context: interfaces.context.ContextInterface, vmlinux_name
|
||||
) -> Generator[Tuple[str, str, int], None, None]:
|
||||
"""
|
||||
Walks the network protocol operations structures for common network protocols.
|
||||
Reports any initialized operations members that do not point inside the kernel.
|
||||
"""
|
||||
vmlinux = context.modules[vmlinux_name]
|
||||
|
||||
type_check = vmlinux.get_type("tcp_seq_afinfo")
|
||||
if type_check.has_member("seq_fops"):
|
||||
checker = cls._check_afinfo_pre_4_18
|
||||
else:
|
||||
checker = cls._check_afinfo_post_4_18
|
||||
|
||||
seq_members = vmlinux.get_type("seq_operations").members
|
||||
|
||||
yield from checker(context, vmlinux_name, seq_members)
|
||||
|
||||
def _generator(self):
|
||||
"""
|
||||
A simple wrapper around `check_afino`
|
||||
"""
|
||||
for name, member, address in self.check_afinfo(
|
||||
self.context, self.config["kernel"]
|
||||
):
|
||||
yield 0, (name, member, format_hints.Hex(address))
|
||||
|
||||
def run(self):
|
||||
return renderers.TreeGrid(
|
||||
[
|
||||
("Symbol Name", str),
|
||||
("Member", str),
|
||||
("Handler Address", format_hints.Hex),
|
||||
],
|
||||
self._generator(),
|
||||
)
|
||||
@@ -0,0 +1,197 @@
|
||||
# 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.symbols.linux.utilities import (
|
||||
modules as linux_utilities_modules,
|
||||
)
|
||||
from volatility3.framework import interfaces, exceptions, deprecation
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.symbols.linux import extensions
|
||||
from volatility3.framework.interfaces import plugins
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Hidden_modules(plugins.PluginInterface):
|
||||
"""Carves memory to find hidden kernel modules"""
|
||||
|
||||
_required_framework_version = (2, 25, 0)
|
||||
_version = (3, 0, 2)
|
||||
|
||||
@classmethod
|
||||
def find_hidden_modules(
|
||||
cls, context, vmlinux_module_name: str
|
||||
) -> extensions.module:
|
||||
if 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"
|
||||
)
|
||||
|
||||
known_module_addresses = cls.get_lsmod_module_addresses(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
modules_memory_boundaries = (
|
||||
linux_utilities_modules.Modules.get_modules_memory_boundaries(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
)
|
||||
|
||||
yield from linux_utilities_modules.Modules.get_hidden_modules(
|
||||
context,
|
||||
vmlinux_module_name,
|
||||
known_module_addresses,
|
||||
modules_memory_boundaries,
|
||||
)
|
||||
|
||||
@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
|
||||
"""
|
||||
return linux_utilities_modules.get_hidden_modules(
|
||||
vmlinux_module_name, known_module_addresses, modules_memory_boundaries
|
||||
)
|
||||
|
||||
run = linux_utilities_modules.ModuleDisplayPlugin.run
|
||||
_generator = linux_utilities_modules.ModuleDisplayPlugin.generator
|
||||
implementation = find_hidden_modules
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.VersionRequirement(
|
||||
name="linux_utilities_modules_module_display_plugin",
|
||||
component=linux_utilities_modules.ModuleDisplayPlugin,
|
||||
version=(1, 0, 0),
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linux_utilities_modules",
|
||||
component=linux_utilities_modules.Modules,
|
||||
version=(3, 0, 1),
|
||||
),
|
||||
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
|
||||
|
||||
@staticmethod
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries,
|
||||
removal_date="2025-09-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
def get_modules_memory_boundaries(
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_module_name: str,
|
||||
) -> Tuple[int, int]:
|
||||
return linux_utilities_modules.Modules.get_modules_memory_boundaries(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_module_address_alignment,
|
||||
removal_date="2025-09-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
@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
|
||||
"""
|
||||
return linux_utilities_modules.get_module_address_alignment(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_hidden_modules,
|
||||
removal_date="2025-09-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
@staticmethod
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.validate_alignment_patterns,
|
||||
removal_date="2025-09-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
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 linux_utilities_modules.validate_alignment_patterns(
|
||||
addresses, address_alignment
|
||||
)
|
||||
|
||||
@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 linux_utilities_modules.Modules.list_modules(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
}
|
||||
return known_module_addresses
|
||||
Reference in New Issue
Block a user