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 @@ -352,3 +353,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(),
)
121 changes: 121 additions & 0 deletions volatility3/framework/symbols/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from volatility3.framework.objects import utility
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.constants import linux as linux_constants


class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
Expand Down Expand Up @@ -830,3 +831,123 @@ def get_cached_pages(self) -> Iterator[interfaces.objects.ObjectInterface]:
page = self.vmlinux.object("page", offset=page_addr, absolute=True)
if page:
yield page


class Tainting:
Copy link
Member

Choose a reason for hiding this comment

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

This might want to go in a tainting.py module, so it would become linux.tainting.Tainting (and so that the init doesn't get too full).

Copy link
Member

Choose a reason for hiding this comment

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

This also would need its own version number, and a required_framework_version for anything it made use of (and the __init__ would need to check those...

Copy link
Member

Choose a reason for hiding this comment

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

Yep, see my branch for an example.

"""Tainted kernel and modules parsing capabilities.

Relevant kernel functions:
- modules: module_flags_taint
- kernel: print_tainted
"""

def __init__(
Copy link
Member

Choose a reason for hiding this comment

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

I'd also expected these just to be function containers, rather than carrying state themselves. We can have them carry state I guess, I'm just not sure the extra complexity/separation of parameters is worth it?

self,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
):
self.kernel = context.modules[kernel_module_name]

@property
def kernel_taint_flags_list(
self,
) -> Optional[List[interfaces.objects.ObjectInterface]]:
if self.kernel.has_symbol("taint_flags"):
return list(self.kernel.object_from_symbol("taint_flags"))
return None

def _module_flags_taint_pre_4_10_rc1(
self, taints: int, is_module: bool = False
) -> str:
"""Convert the module's taints value to a 1-1 character mapping.
Relies on statically defined taints mappings in the framework.

Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module

Returns:
The raw taints string.
"""
taints_string = ""
for char, taint_flag in linux_constants.TAINT_FLAGS.items():
if is_module and is_module != taint_flag.module:
continue

if taints & taint_flag.shift:
taints_string += char

return taints_string

def _module_flags_taint_post_4_10_rc1(
self, taints: int, is_module: bool = False
) -> 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 */
};

Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module

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

return taints_string

def get_taints_as_plain_string(self, taints: int, is_module: bool = False) -> str:
"""Convert the taints value to a 1-1 character mapping.

Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module
s
Returns:
The raw taints string.

Documentation:
- module_flags_taint kernel function
"""

if self.kernel_taint_flags_list:
return self._module_flags_taint_post_4_10_rc1(taints, is_module)
return self._module_flags_taint_pre_4_10_rc1(taints, is_module)

def get_taints_parsed(self, taints: int, is_module: bool = False) -> List[str]:
"""Convert the taints string to a 1-1 descriptor mapping.

Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module

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

Documentation:
- module_flags_taint kernel function
"""
comprehensive_taints = []
for character in self.get_taints_as_plain_string(taints, is_module):
taint_flag = linux_constants.TAINT_FLAGS.get(character)
if not taint_flag:
comprehensive_taints.append(f"<UNKNOWN_TAINT_CHAR_{character}>")
elif taint_flag.when_present:
comprehensive_taints.append(taint_flag.desc)

return comprehensive_taints
Loading
Loading