-
Notifications
You must be signed in to change notification settings - Fork 534
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
Changes from 26 commits
d5a0b93
e1b343a
c88ebe8
9440f53
9d08c46
b209ea3
485ef89
dd3542b
a388895
f3d7647
dda104b
2a5f38e
8bc6259
3105a31
6e4213e
0a35026
89b8da8
32a9d16
4a34b98
38c5cc1
a7b4e2f
2d262e7
5c70356
302f9fd
4115c26
bd82f4f
d956742
0e4e751
b447bfa
94704c6
cd8690a
ca98aa2
cc9486c
3b679cb
bb6556d
0b82f73
8095924
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
# 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 | ||
from volatility3.framework.symbols.linux.utilities import tainting | ||
|
||
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, 18, 0) | ||
|
||
@classmethod | ||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: | ||
return [ | ||
requirements.ModuleRequirement( | ||
name="kernel", | ||
description="Linux kernel", | ||
architectures=architectures.LINUX_ARCHS, | ||
), | ||
requirements.VersionRequirement( | ||
name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) | ||
), | ||
requirements.PluginRequirement( | ||
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) | ||
), | ||
requirements.PluginRequirement( | ||
name="check_modules", | ||
plugin=check_modules.Check_modules, | ||
version=(1, 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 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 = {} | ||
# lsmod | ||
run_results["lsmod"] = list(lsmod.Lsmod.list_modules(context, kernel_name)) | ||
# check_modules | ||
sysfs_modules: dict = check_modules.Check_modules.get_kset_modules( | ||
context, kernel_name | ||
) | ||
## Convert get_kset_modules() offsets back to module objects | ||
run_results["check_modules"] = [ | ||
kernel.object(object_type="module", offset=m_offset, absolute=True) | ||
for m_offset in sysfs_modules.values() | ||
] | ||
# hidden_modules | ||
if run_hidden_modules: | ||
known_modules_addresses = set( | ||
context.layers[kernel.layer_name].canonicalize(module.vol.offset) | ||
for module in run_results["lsmod"] + run_results["check_modules"] | ||
) | ||
modules_memory_boundaries = ( | ||
hidden_modules.Hidden_modules.get_modules_memory_boundaries( | ||
context, kernel_name | ||
) | ||
) | ||
run_results["hidden_modules"] = list( | ||
hidden_modules.Hidden_modules.get_hidden_modules( | ||
context, | ||
kernel_name, | ||
known_modules_addresses, | ||
modules_memory_boundaries, | ||
) | ||
) | ||
|
||
return run_results | ||
Abyss-W4tcher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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]) | ||
Abyss-W4tcher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
seen_addresses = set() | ||
for modules_list in run_results.values(): | ||
Abyss-W4tcher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(), | ||
) |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -19,7 +19,7 @@ | |||
from volatility3.framework.objects import utility | ||||
from volatility3.framework.symbols import generic, linux, intermed | ||||
from volatility3.framework.symbols.linux.extensions import elf | ||||
|
||||
from volatility3.framework.symbols.linux.utilities import tainting | ||||
|
||||
vollog = logging.getLogger(__name__) | ||||
|
||||
|
@@ -279,6 +279,30 @@ def get_symbol_by_address(self, wanted_sym_address): | |||
|
||||
return None | ||||
|
||||
def get_taints_as_plain_string(self) -> str: | ||||
Abyss-W4tcher marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
"""Convert the module's taints value to a 1-1 character mapping. | ||||
Convenient wrapper around framework's Tainting capabilities. | ||||
|
||||
Returns: | ||||
The raw taints string. | ||||
""" | ||||
return tainting.Tainting( | ||||
self._context, | ||||
linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name, | ||||
).get_taints_as_plain_string(self.taints, True) | ||||
|
||||
def get_taints_parsed(self) -> List[str]: | ||||
"""Convert the module's taints string to a 1-1 descriptor mapping. | ||||
Convenient wrapper around framework's Tainting capabilities. | ||||
|
||||
Returns: | ||||
A comprehensive (user-friendly) taint descriptor list. | ||||
""" | ||||
return tainting.Tainting( | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Straight pass throughs like this don't need adding, people should just depend directly on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These (new) straight pass through are convenient functions to allow plugins devs to do: my_module.get_taints_as_plain_string() instead of tainting.Tainting(context,vmlinux.name).get_taints_as_plain_string(my_module.taints, True) It also manages the correct attribute and parameters to pass to fit the module use case. This also applies to volshell Regarding the need for an API bump, even if it is a passthrough, it still is a non-private method addition to the API, no matter its content ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For some reason I can't answer the comment about API versioning so I'll do it here. The point of these new APIs is not only for modules, but for later plugins. We plan on doing a kernel tainting plugin, so putting these in a sub-LinuxUtilities is not beneficial right now but will be in the future. API minor bumps (re-counted after merge of develop into this branch):
Which gives : There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, sorry, I thought these were on LinuxUtilities, rather than on Having said that,
That leaves only the bump on module.get_trains_* which is needed so that a plugin being run under the framework fails early rather than trying to make calls to an API that doesn't exist. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, that proxy call also needs to check that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's noted for Removing this passthrough might prevent users/devs exploring the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ikelos, I made a PoC to set requirements on (unversionable internal) APIs with a wrapper: # Requirements can be stacked
# Raises an exception (at plugin runtime) on requirement failure
@requirements.VersionRequirement.version_requirement_wrapper(
name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0)
)
def get_taints_as_plain_string(self) -> str: Disregarding the underlying machinery, do you think this design could be an appropriate solution to flexibly emit requirements ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, you're probably right, it should get at least an additive MINOR version bump. I think we've managed ok before but for the sake of accuracy you're right. I'm not sure that a module not being exposed as a method on a class is that good an excuse to just keep heaping stuff on the module. Plus if we don't start now, we'll end up doing it for every new feature in existance, and that'll become unwieldy down the line (assuming it isn't already unwieldy with all the methods it has at the moment). If people don't know about tainting, they're not going to notice it in the module documentation, and if they know that's what they're looking for, they'll find it quickly enough (particularly with existing plugins in the framework that make use of it). I don't think that argument holds up as to why it needs to be part of the module interface. So at the moment, still not convinced it should be part of module. I'm actually not against the So tagging individual subfunctions within There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can understand that the extra-complexity for the sake of convenience might not be worthy, and as its counterpart is not that much longer I think it is best to stop here and move on 👍. I'll remove the proxies. Currently, internal APIs calling
To fix this, we would need to basically make all internal classes inherit from I think this versioning issue is already in the framework unfortunately, but I do not have an immediate way of solving it properly without updating each LinuxUtilities call. I opened an issue (#1554) as it starts to diverge from the original |
||||
self._context, | ||||
linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name, | ||||
).get_taints_parsed(self.taints, True) | ||||
|
||||
@property | ||||
def section_symtab(self): | ||||
if self.has_member("kallsyms"): | ||||
|
Uh oh!
There was an error while loading. Please reload this page.