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

New linux plugin: modxview #1330

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
55 changes: 55 additions & 0 deletions volatility3/framework/constants/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Linux-specific values that aren't found in debug symbols
"""
from enum import IntEnum, Flag
from dataclasses import dataclass

KERNEL_NAME = "__kernel__"

Expand Down Expand Up @@ -347,3 +348,57 @@ def flags(self) -> str:
MODULE_MAXIMUM_CORE_SIZE = 20000000
MODULE_MAXIMUM_CORE_TEXT_SIZE = 20000000
MODULE_MINIMUM_SIZE = 4096


@dataclass
class TaintFlag:
shift: int
desc: str
when_present: bool
module: bool


TAINT_FLAGS = {
Abyss-W4tcher marked this conversation as resolved.
Show resolved Hide resolved
"P": TaintFlag(
shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=True, module=True
),
"G": TaintFlag(
shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=False, module=True
),
"F": TaintFlag(shift=1 << 1, desc="FORCED_MODULE", when_present=True, module=False),
"S": TaintFlag(
shift=1 << 2, desc="CPU_OUT_OF_SPEC", when_present=True, module=False
),
"R": TaintFlag(shift=1 << 3, desc="FORCED_RMMOD", when_present=True, module=False),
"M": TaintFlag(shift=1 << 4, desc="MACHINE_CHECK", when_present=True, module=False),
"B": TaintFlag(shift=1 << 5, desc="BAD_PAGE", when_present=True, module=False),
"U": TaintFlag(shift=1 << 6, desc="USER", when_present=True, module=False),
"D": TaintFlag(shift=1 << 7, desc="DIE", when_present=True, module=False),
"A": TaintFlag(
shift=1 << 8, desc="OVERRIDDEN_ACPI_TABLE", when_present=True, module=False
),
"W": TaintFlag(shift=1 << 9, desc="WARN", when_present=True, module=False),
"C": TaintFlag(shift=1 << 10, desc="CRAP", when_present=True, module=True),
"I": TaintFlag(
shift=1 << 11, desc="FIRMWARE_WORKAROUND", when_present=True, module=False
),
"O": TaintFlag(shift=1 << 12, desc="OOT_MODULE", when_present=True, module=True),
"E": TaintFlag(
shift=1 << 13, desc="UNSIGNED_MODULE", when_present=True, module=True
),
"L": TaintFlag(shift=1 << 14, desc="SOFTLOCKUP", when_present=True, module=False),
"K": TaintFlag(shift=1 << 15, desc="LIVEPATCH", when_present=True, module=True),
"X": TaintFlag(shift=1 << 16, desc="AUX", when_present=True, module=True),
"T": TaintFlag(shift=1 << 17, desc="RANDSTRUCT", when_present=True, module=True),
"N": TaintFlag(shift=1 << 18, desc="TEST", when_present=True, module=True),
}
"""Flags used to taint kernel and modules, for debugging purposes.

Map based on 6.12-rc5.

Documentation :
- https://www.kernel.org/doc/Documentation/admin-guide/sysctl/kernel.rst#:~:text=guide/sysrq.rst.-,tainted,-%3D%3D%3D%3D%3D%3D%3D%0A%0ANon%2Dzero%20if
- https://www.kernel.org/doc/Documentation/admin-guide/tainted-kernels.rst#:~:text=More%20detailed%20explanation%20for%20tainting
- taint_flag kernel struct
- taint_flags kernel constant
"""
196 changes: 196 additions & 0 deletions volatility3/framework/plugins/linux/modxview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from typing import List, Dict, Set, Iterator
from volatility3.plugins.linux import lsmod, check_modules, hidden_modules
from volatility3.framework import interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints, TreeGrid, NotAvailableValue
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.constants import architectures

vollog = logging.getLogger(__name__)


class Modxview(interfaces.plugins.PluginInterface):
"""Centralize lsmod, check_modules and hidden_modules results to efficiently
spot modules presence and taints."""

_version = (1, 0, 0)
_required_framework_version = (2, 11, 0)

@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="check_modules",
plugin=check_modules.Check_modules,
version=(0, 0, 0),
),
requirements.PluginRequirement(
name="hidden_modules",
plugin=hidden_modules.Hidden_modules,
version=(1, 0, 0),
),
requirements.BooleanRequirement(
name="plain_taints",
description="Display the plain taints string for each module.",
optional=True,
default=False,
),
]

@classmethod
def run_lsmod(
cls, context: interfaces.context.ContextInterface, kernel_name: str
) -> List[extensions.module]:
"""Wrapper for the lsmod plugin."""
return list(lsmod.Lsmod.list_modules(context, kernel_name))

@classmethod
def run_check_modules(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
) -> List[extensions.module]:
"""Wrapper for the check_modules plugin.
Here, we extract the /sys/module/ list."""
kernel = context.modules[kernel_name]
sysfs_modules: dict = check_modules.Check_modules.get_kset_modules(
context, kernel_name
)

# Convert get_kset_modules() offsets back to module objects
return [
kernel.object(object_type="module", offset=m_offset, absolute=True)
for m_offset in sysfs_modules.values()
]

@classmethod
def run_hidden_modules(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
known_modules_addresses: Set[int],
) -> List[extensions.module]:
"""Wrapper for the hidden_modules plugin."""
modules_memory_boundaries = (
hidden_modules.Hidden_modules.get_modules_memory_boundaries(
context, kernel_name
)
)
return list(
hidden_modules.Hidden_modules.get_hidden_modules(
context,
kernel_name,
known_modules_addresses,
modules_memory_boundaries,
)
)

@classmethod
def flatten_run_modules_results(
cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True
) -> Iterator[extensions.module]:
"""Flatten a dictionary mapping plugin names and modules list, to a single merged list.
This is useful to get a generic lookup list of all the detected modules.

Args:
run_results: dictionary of plugin names mapping a list of detected modules
deduplicate: remove duplicate modules, based on their offsets

Returns:
Iterator of modules objects
"""
seen_addresses = set()
for modules in run_results.values():
for module in modules:
if deduplicate and module.vol.offset in seen_addresses:
continue
seen_addresses.add(module.vol.offset)
yield module
gcmoreira marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def run_modules_scanners(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
run_hidden_modules: bool = True,
) -> Dict[str, List[extensions.module]]:
"""Run module scanning plugins and aggregate the results.

Args:
run_hidden_modules: specify if the hidden_modules plugin should be run
Returns:
Dictionary mapping each plugin to its corresponding result
"""

kernel = context.modules[kernel_name]
run_results = {}
run_results["lsmod"] = cls.run_lsmod(context, kernel_name)
run_results["check_modules"] = cls.run_check_modules(context, kernel_name)
if run_hidden_modules:
known_module_addresses = set(
context.layers[kernel.layer_name].canonicalize(module.vol.offset)
for module in run_results["lsmod"] + run_results["check_modules"]
)
run_results["hidden_modules"] = cls.run_hidden_modules(
context, kernel_name, known_module_addresses
)

return run_results

def _generator(self):
kernel_name = self.config["kernel"]
run_results = self.run_modules_scanners(self.context, kernel_name)
modules_offsets = {}
for key in ["lsmod", "check_modules", "hidden_modules"]:
modules_offsets[key] = set(module.vol.offset for module in run_results[key])

seen_addresses = set()
for modules_list in run_results.values():
for module in modules_list:
if module.vol.offset in seen_addresses:
continue
seen_addresses.add(module.vol.offset)

if self.config.get("plain_taints"):
taints = module.get_taints_as_plain_string()
else:
taints = ",".join(module.get_taints_parsed())

yield (
0,
(
module.get_name() or NotAvailableValue(),
format_hints.Hex(module.vol.offset),
module.vol.offset in modules_offsets["lsmod"],
module.vol.offset in modules_offsets["check_modules"],
module.vol.offset in modules_offsets["hidden_modules"],
taints or NotAvailableValue(),
),
)

def run(self):
columns = [
("Name", str),
("Address", format_hints.Hex),
("In procfs", bool),
("In sysfs", bool),
("Hidden", bool),
("Taints", str),
]

return TreeGrid(
columns,
self._generator(),
)
82 changes: 82 additions & 0 deletions volatility3/framework/symbols/linux/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,77 @@ def get_symbol_by_address(self, wanted_sym_address):

return None

def _module_flags_taints_pre_4_10_rc1(self) -> str:
"""Convert the module's taints value to a 1-1 character mapping.
Relies on statically defined taints mappings in the framework.

Returns:
The raw taints string.
"""
taints_string = ""
for char, taint_flag in linux_constants.TAINT_FLAGS.items():
if taint_flag.module and self.taints_value & taint_flag.shift:
taints_string += char

return taints_string

def _module_flags_taints_post_4_10_rc1(self) -> str:
"""Convert the module's taints value to a 1-1 character mapping.
Relies on kernel symbol embedded taints definitions.

struct taint_flag {
char c_true; /* character printed when tainted */
char c_false; /* character printed when not tainted */
bool module; /* also show as a per-module taint flag */
};

Returns:
The raw taints string.
"""
taints_string = ""
for i, taint_flag in enumerate(self.taint_flags_list):
c_true = chr(taint_flag.c_true)
c_false = chr(taint_flag.c_false)
if taint_flag.module and (self.taints_value & (1 << i)):
taints_string += c_true
elif taint_flag.module and c_false != " ":
taints_string += c_false

return taints_string

def get_taints_as_plain_string(self) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are externally visible additions to the API, which means a MINOR version number somewhere, needs to go up. I suspect it may be the framework itself because I don't think anything further down is versioned?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, we want to include this API in a more generic place, but keep these convenient self-contained functions. So right now I'm not sure of how many bumps are needed.

"""Convert the module's taints value to a 1-1 character mapping.

Returns:
The raw taints string.

Documentation:
- module_flags_taint kernel function
"""

if self.taint_flags_list:
return self._module_flags_taints_post_4_10_rc1()
return self._module_flags_taints_pre_4_10_rc1()

def get_taints_parsed(self) -> List[str]:
"""Convert the module's taints string to a 1-1 descriptor mapping.

Returns:
A comprehensive (user-friendly) taint descriptor list.

Documentation:
- module_flags_taint kernel function
"""
comprehensive_taints = []
for c in self.get_taints_as_plain_string():
Abyss-W4tcher marked this conversation as resolved.
Show resolved Hide resolved
taint_flag = linux_constants.TAINT_FLAGS.get(c)
if not taint_flag:
comprehensive_taints.append(f"<UNKNOWN_TAINT_CHAR_{c}>")
elif taint_flag.when_present:
comprehensive_taints.append(taint_flag.desc)

return comprehensive_taints

@property
def section_symtab(self):
if self.has_member("kallsyms"):
Expand Down Expand Up @@ -307,6 +378,17 @@ def section_strtab(self):
return self.strtab
raise AttributeError("module -> strtab: Unable to get strtab")

@property
def taints_value(self) -> int:
return self.taints
Abyss-W4tcher marked this conversation as resolved.
Show resolved Hide resolved

@property
def taint_flags_list(self) -> Optional[List[interfaces.objects.ObjectInterface]]:
kernel = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
if kernel.has_symbol("taint_flags"):
return list(kernel.object_from_symbol("taint_flags"))
return None


class task_struct(generic.GenericIntelProcess):
def add_process_layer(
Expand Down
Loading