Skip to content

New linux plugin: modxview #1330

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

Merged
merged 37 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d5a0b93
add TAINT_FLAGS constant
Abyss-W4tcher Oct 31, 2024
e1b343a
add module taints parsing apis
Abyss-W4tcher Oct 31, 2024
c88ebe8
introduce modxview linux plugin
Abyss-W4tcher Oct 31, 2024
9440f53
use a dict of dataclasses for taint_flags
Abyss-W4tcher Nov 1, 2024
9d08c46
add module offset to seen_addresses
Abyss-W4tcher Nov 1, 2024
b209ea3
remove slashes in columns
Abyss-W4tcher Nov 1, 2024
485ef89
remove taints_value overload attr
Abyss-W4tcher Nov 8, 2024
dd3542b
explicit loop iterator
Abyss-W4tcher Nov 8, 2024
a388895
Merge branch 'volatilityfoundation:develop' into modxview_plugin
Abyss-W4tcher Dec 22, 2024
f3d7647
unify Tainting parsing capabilities
Abyss-W4tcher Dec 22, 2024
dda104b
move out Tainting capabilities
Abyss-W4tcher Jan 2, 2025
2a5f38e
introduce versioned Linux utilities
Abyss-W4tcher Jan 2, 2025
8bc6259
initial tainting utilities
Abyss-W4tcher Jan 2, 2025
3105a31
leverage Tainting from separated Linux utilities
Abyss-W4tcher Jan 2, 2025
6e4213e
update tainting requirements to new versioned utilities
Abyss-W4tcher Jan 2, 2025
0a35026
2.13.0 -> 2.14.0 bump
Abyss-W4tcher Jan 2, 2025
89b8da8
make self.kernel private and call parent __init__
Abyss-W4tcher Jan 2, 2025
32a9d16
Merge branch 'develop' into modxview_plugin
ikelos Jan 3, 2025
4a34b98
minor readability adjustments
Abyss-W4tcher Jan 3, 2025
38c5cc1
bump framework req to 2.16.0
Abyss-W4tcher Jan 3, 2025
a7b4e2f
version check_modules
Abyss-W4tcher Jan 5, 2025
2d262e7
cut unnecessary intermediate LinuxUtilityInterface
Abyss-W4tcher Jan 5, 2025
5c70356
version check_modules requirement
Abyss-W4tcher Jan 5, 2025
302f9fd
cut unnecessary plugin runner functions
Abyss-W4tcher Jan 5, 2025
4115c26
bump framework req to 2.18.0
Abyss-W4tcher Jan 5, 2025
bd82f4f
2.16.0 -> 2.18.0 bump
Abyss-W4tcher Jan 5, 2025
d956742
remove typing.Set import
Abyss-W4tcher Jan 5, 2025
0e4e751
stateless classmethods
Abyss-W4tcher Jan 11, 2025
b447bfa
remove module tainting proxies
Abyss-W4tcher Jan 16, 2025
94704c6
2.16.0 -> 2.17.0 bump
Abyss-W4tcher Jan 16, 2025
cd8690a
require framework version 2.17.0
Abyss-W4tcher Jan 16, 2025
ca98aa2
Merge branch 'develop' into modxview_plugin
Abyss-W4tcher Jan 18, 2025
cc9486c
pre-process module triaging to improve readability
Abyss-W4tcher Jan 18, 2025
3b679cb
explicit None check
Abyss-W4tcher Jan 18, 2025
bb6556d
correct arguments for pre_4_10_rc1
Abyss-W4tcher Jan 18, 2025
0b82f73
functools caching and doc.
Abyss-W4tcher Jan 18, 2025
8095924
typo
Abyss-W4tcher Jan 18, 2025
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
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 = {
"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

@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:
"""Tainted kernel and modules parsing capabilities.

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

def __init__(
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