-
Notifications
You must be signed in to change notification settings - Fork 484
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: linux.tracing.ftrace #1564
Open
Abyss-W4tcher
wants to merge
20
commits into
volatilityfoundation:develop
Choose a base branch
from
Abyss-W4tcher:linux_tracing_ftrace
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
b8e8fb6
initial split linux modules utilities
Abyss-W4tcher f007a28
initial linux.tracing.ftrace.Check_ftrace
Abyss-W4tcher 414cab1
2.18.0 -> 2.19.0 bump
Abyss-W4tcher d5a2134
modules utilities __init__.py
Abyss-W4tcher 37b792b
ruff fix
Abyss-W4tcher 614ac50
explicit returns and extra Optional type hinting
Abyss-W4tcher d297693
correct required framework version
Abyss-W4tcher 3ba60d5
remove self-contained hidden_modules check, switch to dataclass
Abyss-W4tcher e7b51bd
prefer staticmethod when cls is not needed
Abyss-W4tcher 3b05ed7
Merge remote-tracking branch 'origin/develop' into linux_tracing_ftrace
Abyss-W4tcher f75a4be
rename modules_utilities to linux_utilities_modules
Abyss-W4tcher 48eca36
1.0.0 -> 1.1.0 Modules bump
Abyss-W4tcher c57f607
require Modules >= 1.1.0
Abyss-W4tcher f2ac621
use classmethod instead of staticmethod
Abyss-W4tcher 22a2fe1
clarify generator variable
Abyss-W4tcher 03cf84f
assign kernel layer to variable early
Abyss-W4tcher 9d11c1f
prevent modules memory space overlap scenario
Abyss-W4tcher d777ab3
Merge branch 'volatilityfoundation:develop' into linux_tracing_ftrace
Abyss-W4tcher c000e81
tune with the new additional_description mechanism
Abyss-W4tcher b055848
explicit powers of two instead of auto()
Abyss-W4tcher File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,310 @@ | ||
# 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 | ||
# | ||
|
||
# Public researches: https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Fixing-A-Memory-Forensics-Blind-Spot-Linux-Kernel-Tracing-wp.pdf | ||
|
||
import logging | ||
from typing import Dict, List, Iterable, Optional | ||
from enum import auto, IntFlag | ||
from dataclasses import dataclass | ||
|
||
from volatility3.plugins.linux import hidden_modules, modxview | ||
from volatility3.framework import constants, exceptions, 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.symbols.linux.utilities import modules as modules_utilities | ||
from volatility3.framework.constants import architectures | ||
|
||
vollog = logging.getLogger(__name__) | ||
|
||
|
||
# https://docs.python.org/3.13/library/enum.html#enum.IntFlag | ||
class FtraceOpsFlags(IntFlag): | ||
"""Denote the state of an ftrace_ops struct. | ||
Based on https://elixir.bootlin.com/linux/v6.13-rc3/source/include/linux/ftrace.h#L255. | ||
""" | ||
|
||
FTRACE_OPS_FL_ENABLED = auto() | ||
FTRACE_OPS_FL_DYNAMIC = auto() | ||
FTRACE_OPS_FL_SAVE_REGS = auto() | ||
FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED = auto() | ||
FTRACE_OPS_FL_RECURSION = auto() | ||
FTRACE_OPS_FL_STUB = auto() | ||
FTRACE_OPS_FL_INITIALIZED = auto() | ||
FTRACE_OPS_FL_DELETED = auto() | ||
FTRACE_OPS_FL_ADDING = auto() | ||
FTRACE_OPS_FL_REMOVING = auto() | ||
FTRACE_OPS_FL_MODIFYING = auto() | ||
FTRACE_OPS_FL_ALLOC_TRAMP = auto() | ||
FTRACE_OPS_FL_IPMODIFY = auto() | ||
FTRACE_OPS_FL_PID = auto() | ||
FTRACE_OPS_FL_RCU = auto() | ||
FTRACE_OPS_FL_TRACE_ARRAY = auto() | ||
FTRACE_OPS_FL_PERMANENT = auto() | ||
FTRACE_OPS_FL_DIRECT = auto() | ||
FTRACE_OPS_FL_SUBOP = auto() | ||
|
||
|
||
@dataclass | ||
class ParsedFtraceOps: | ||
"""Parsed ftrace_ops struct representation, containing a selection of forensics valuable | ||
informations.""" | ||
|
||
ftrace_ops_offset: int | ||
callback_symbol: str | ||
callback_address: int | ||
hooked_symbols: str | ||
module_name: str | ||
module_address: int | ||
flags: str | ||
|
||
|
||
class CheckFtrace(interfaces.plugins.PluginInterface): | ||
"""Detect ftrace hooking""" | ||
|
||
_version = (1, 0, 0) | ||
_required_framework_version = (2, 19, 0) | ||
additional_description = """Investigate the ftrace infrastructure to uncover kernel attached callbacks, which can be leveraged | ||
Abyss-W4tcher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
to hook kernel functions and modify their behaviour.""" | ||
|
||
@staticmethod | ||
def get_requirements() -> List[interfaces.configuration.RequirementInterface]: | ||
return [ | ||
requirements.ModuleRequirement( | ||
name="kernel", | ||
description="Linux kernel", | ||
architectures=architectures.LINUX_ARCHS, | ||
), | ||
requirements.VersionRequirement( | ||
name="modules_utilities", | ||
component=modules_utilities.Modules, | ||
version=(1, 0, 0), | ||
), | ||
requirements.PluginRequirement( | ||
name="modxview", plugin=modxview.Modxview, version=(1, 0, 0) | ||
), | ||
requirements.PluginRequirement( | ||
name="hidden_modules", | ||
plugin=hidden_modules.Hidden_modules, | ||
version=(1, 0, 0), | ||
), | ||
requirements.BooleanRequirement( | ||
name="show_ftrace_flags", | ||
description="Show ftrace flags associated with an ftrace_ops struct", | ||
optional=True, | ||
default=False, | ||
), | ||
] | ||
|
||
@staticmethod | ||
def extract_hash_table_filters( | ||
ftrace_ops: interfaces.objects.ObjectInterface, | ||
) -> Optional[Iterable[interfaces.objects.ObjectInterface]]: | ||
"""Wrap the process of walking to every ftrace_func_entry of an ftrace_ops. | ||
Those are stored in a hash table of filters that indicates the addresses hooked. | ||
|
||
Args: | ||
ftrace_ops: The ftrace_ops struct to walk through | ||
|
||
Returns: | ||
An iterable of ftrace_func_entry structs | ||
""" | ||
|
||
try: | ||
current_bucket_ptr = ftrace_ops.func_hash.filter_hash.buckets.first | ||
except exceptions.InvalidAddressException: | ||
vollog.log( | ||
constants.LOGLEVEL_VV, | ||
f"ftrace_func_entry list of ftrace_ops@{ftrace_ops.vol.offset:#x} is empty/invalid. Skipping it...", | ||
) | ||
return [] | ||
|
||
while current_bucket_ptr.is_readable(): | ||
yield current_bucket_ptr.dereference().cast("ftrace_func_entry") | ||
current_bucket_ptr = current_bucket_ptr.next | ||
|
||
return None | ||
ikelos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@classmethod | ||
def parse_ftrace_ops( | ||
cls, | ||
context: interfaces.context.ContextInterface, | ||
kernel_name: str, | ||
known_modules: Dict[str, List[extensions.module]], | ||
ftrace_ops: interfaces.objects.ObjectInterface, | ||
run_hidden_modules: bool = True, | ||
) -> Optional[Iterable[ParsedFtraceOps]]: | ||
"""Parse an ftrace_ops struct to highlight ftrace kernel hooking. | ||
Iterates over embedded ftrace_func_entry entries, which point to hooked memory areas. | ||
|
||
Args: | ||
known_modules: A dict of known modules, used to locate callbacks origin. Typically obtained through modxview.run_modules_scanners(). | ||
ftrace_ops: The ftrace_ops struct to parse | ||
run_hidden_modules: Whether to run the hidden_modules plugin or not. Note: it won't be run, even if specified, \ | ||
if the "hidden_modules" key is present in known_modules. | ||
|
||
Yields: | ||
An iterable of ParsedFtraceOps dataclasses, containing a selection of useful fields (callback, hook, module) related to an ftrace_ops struct | ||
""" | ||
kernel = context.modules[kernel_name] | ||
callback = ftrace_ops.func | ||
callback_symbol = module_address = module_name = None | ||
|
||
# Try to lookup within the known modules if the callback address fits | ||
module = modules_utilities.Modules.module_lookup_by_address( | ||
context, | ||
kernel.layer_name, | ||
modxview.Modxview.flatten_run_modules_results(known_modules), | ||
callback, | ||
) | ||
# Run hidden_modules plugin if a callback origin couldn't be determined (only done once, results are re-used afterwards) | ||
if ( | ||
module is None | ||
and run_hidden_modules | ||
and "hidden_modules" not in known_modules | ||
): | ||
vollog.info( | ||
"A callback module origin could not be determined. hidden_modules plugin will be run to detect additional modules.", | ||
) | ||
known_modules_addresses = set( | ||
context.layers[kernel.layer_name].canonicalize(module.vol.offset) | ||
ikelos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for module in modxview.Modxview.flatten_run_modules_results( | ||
known_modules | ||
) | ||
) | ||
modules_memory_boundaries = ( | ||
hidden_modules.Hidden_modules.get_modules_memory_boundaries( | ||
context, kernel_name | ||
) | ||
) | ||
known_modules["hidden_modules"] = list( | ||
hidden_modules.Hidden_modules.get_hidden_modules( | ||
context, | ||
kernel_name, | ||
known_modules_addresses, | ||
modules_memory_boundaries, | ||
) | ||
) | ||
# Lookup the updated list to see if hidden_modules was able | ||
# to find the missing module | ||
module = modules_utilities.Modules.module_lookup_by_address( | ||
context, | ||
kernel.layer_name, | ||
modxview.Modxview.flatten_run_modules_results(known_modules), | ||
callback, | ||
) | ||
|
||
# Fetch more information about the module | ||
if module is not None: | ||
module_address = module.vol.offset | ||
module_name = module.get_name() | ||
callback_symbol = module.get_symbol_by_address(callback) | ||
else: | ||
vollog.warning( | ||
f"Could not determine ftrace_ops@{ftrace_ops.vol.offset:#x} callback {callback:#x} module origin.", | ||
) | ||
|
||
# Iterate over ftrace_func_entry list | ||
for ftrace_func_entry in cls.extract_hash_table_filters(ftrace_ops): | ||
hook_address = ftrace_func_entry.ip.cast("pointer") | ||
|
||
# Determine the symbols associated with a hook | ||
hooked_symbols = kernel.get_symbols_by_absolute_location(hook_address) | ||
hooked_symbols = ",".join( | ||
[s.split(constants.BANG)[-1] for s in hooked_symbols] | ||
ikelos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
yield ParsedFtraceOps( | ||
ftrace_ops.vol.offset, | ||
callback_symbol, | ||
callback, | ||
hooked_symbols, | ||
module_name, | ||
module_address, | ||
# FtraceOpsFlags(ftrace_ops.flags).name is valid in > Python3.10, but | ||
# returns None <= Python 3.10. We need to manipulate it like so to ensure compatibility: | ||
# FtraceOpsFlags.FTRACE_OPS_FL_IPMODIFY|FTRACE_OPS_FL_ALLOC_TRAMP | ||
# -> FTRACE_OPS_FL_IPMODIFY,FTRACE_OPS_FL_ALLOC_TRAMP | ||
str(FtraceOpsFlags(ftrace_ops.flags)).split(".")[-1].replace("|", ","), | ||
) | ||
|
||
return None | ||
|
||
@staticmethod | ||
def iterate_ftrace_ops_list( | ||
context: interfaces.context.ContextInterface, kernel_name: str | ||
) -> Optional[Iterable[interfaces.objects.ObjectInterface]]: | ||
"""Iterate over (ftrace_ops *)ftrace_ops_list. | ||
|
||
Returns: | ||
An iterable of ftrace_ops structs | ||
""" | ||
kernel = context.modules[kernel_name] | ||
current_frace_ops_ptr = kernel.object_from_symbol("ftrace_ops_list") | ||
ftrace_list_end = kernel.object_from_symbol("ftrace_list_end") | ||
|
||
while current_frace_ops_ptr.is_readable(): | ||
# ftrace_list_end is not considered a valid struct | ||
# see kernel function test_rec_ops_needs_regs | ||
if current_frace_ops_ptr != ftrace_list_end.vol.offset: | ||
yield current_frace_ops_ptr.dereference() | ||
current_frace_ops_ptr = current_frace_ops_ptr.next | ||
else: | ||
break | ||
|
||
def _generator(self): | ||
kernel_name = self.config["kernel"] | ||
kernel = self.context.modules[kernel_name] | ||
|
||
if not kernel.has_symbol("ftrace_ops_list"): | ||
raise exceptions.SymbolError( | ||
"ftrace_ops_list", | ||
kernel.symbol_table_name, | ||
'The provided symbol table does not include the "ftrace_ops_list" symbol. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupted.', | ||
) | ||
|
||
# Do not run hidden_modules by default, but only on failure to find a module | ||
known_modules = modxview.Modxview.run_modules_scanners( | ||
self.context, kernel_name, run_hidden_modules=False | ||
) | ||
for ftrace_ops in self.iterate_ftrace_ops_list(self.context, kernel_name): | ||
for ftrace_ops_parsed in self.parse_ftrace_ops( | ||
self.context, | ||
kernel_name, | ||
known_modules, | ||
ftrace_ops, | ||
): | ||
formatted_results = ( | ||
format_hints.Hex(ftrace_ops_parsed.ftrace_ops_offset), | ||
ftrace_ops_parsed.callback_symbol or NotAvailableValue(), | ||
format_hints.Hex(ftrace_ops_parsed.callback_address), | ||
ftrace_ops_parsed.hooked_symbols or NotAvailableValue(), | ||
ftrace_ops_parsed.module_name or NotAvailableValue(), | ||
( | ||
format_hints.Hex(ftrace_ops_parsed.module_address) | ||
if ftrace_ops_parsed.module_address is not None | ||
else NotAvailableValue() | ||
), | ||
) | ||
if self.config["show_ftrace_flags"]: | ||
formatted_results += (ftrace_ops_parsed.flags,) | ||
yield (0, formatted_results) | ||
|
||
def run(self): | ||
columns = [ | ||
("ftrace_ops address", format_hints.Hex), | ||
("Callback", str), | ||
("Callback address", format_hints.Hex), | ||
("Hooked symbols", str), | ||
("Module", str), | ||
("Module address", format_hints.Hex), | ||
] | ||
|
||
if self.config.get("show_ftrace_flags"): | ||
columns.append(("Flags", str)) | ||
|
||
return TreeGrid( | ||
columns, | ||
self._generator(), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from volatility3 import framework | ||
from volatility3.framework import interfaces | ||
from volatility3.framework.symbols.linux import extensions, LinuxUtilities | ||
from typing import Iterable, Optional | ||
|
||
|
||
class Modules(interfaces.configuration.VersionableInterface): | ||
ikelos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Kernel modules related utilities.""" | ||
|
||
_version = (1, 0, 0) | ||
_required_framework_version = (2, 0, 0) | ||
|
||
framework.require_interface_version(*_required_framework_version) | ||
|
||
@staticmethod | ||
def module_lookup_by_address( | ||
context: interfaces.context.ContextInterface, | ||
layer_name: str, | ||
modules: Iterable[extensions.module], | ||
target_address: int, | ||
) -> Optional[extensions.module]: | ||
""" | ||
Determine if a target address lies in a module memory space. | ||
Returns the module where the provided address lies. | ||
|
||
Args: | ||
context: The context on which to operate | ||
layer_name: The name of the layer on which to operate | ||
modules: An iterable containing the modules to match the address against | ||
target_address: The address to check for a match | ||
""" | ||
|
||
for module in modules: | ||
_, start, end = LinuxUtilities.mask_mods_list( | ||
context, layer_name, [module] | ||
)[0] | ||
if start <= target_address <= end: | ||
ikelos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return module | ||
|
||
return None |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this not be better as a volatility enum, then the values could be stored in JSON as data rather than in code? It would mean adding an Ftrace symbol table, but then it should just be possible to cast ftrace_ops.flags as the enum? That might also then get around the python <= 3.10 issue you were seeing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not against the idea, both should work well. However
IntEnum
provides really useful APIs out of the box, and I don't have to do a manual list comprehension and mask the value. The little string manipulation I did was to prettify the output because we already use "|" as a column separator.I think JSON is more interesting when we have a lot of constants or nested structs to declare.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I'd like to improve our enum support to the point it becomes as easy to use as IntEnum? If you could tell me what APIs it's missing, we could add them and give people more reason to use it...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of an
IntEnum
does not require to specifically make a JSON dict with hardcoded powers of 2, and I think the readability offered byauto()
is quite nice.Nevertheless, I still looked on how we could add an easy int masking API (or leverage the one in https://github.com/volatilityfoundation/volatility3/blob/develop/volatility3/framework/symbols/wrappers.py) to enums.
I noticed that objects returned by
my_symbol_space.get_enumeration("XX")
aren't of typeEnumeration
, butvolatility3.framework.objects.templates.ObjectTemplate
I think I was able to understand the calling tree:
get_enumeration
instantiates anObjectTemplate
:volatility3/volatility3/framework/symbols/intermed.py
Lines 543 to 548 in ad90804
No inherited properties from
Enumeration
, but itsVolTemplateProxy
:volatility3/volatility3/framework/objects/templates.py
Lines 25 to 45 in ad90804
volatility3/volatility3/framework/objects/__init__.py
Lines 640 to 642 in ad90804
No
description
,is_valid_choice
etc. available, which also makes it difficult to add new methods. As opposed to a symbol for example, which value can change depending on the memory sample, isn't the enum a simple static definition from the ISF ? Why is the constructed enum limited ?I may have missed something obvious, but right now I am not sure if this is intended or if there is another way ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So
get_enumeration
would be the equivalent ofget_type
. If you wanted to apply it to some data, you'd usecontext.Object
to create, just as you would a type. Then you'll get a bunch of functions that should let you interrogate it. If there's mirror methods you think we can expose on the template because they don't require the data, I'm happy to add those in too. The point is, the enumeration type won't improve unless we have cases where we try to use it, so even if we do end up going with a normal IntEnum, I'd like to improve the enumeration API so that it's similarly easy to use to do the things we want to be able to do with it.Auto()
actually scares me, because an accidental addition (or a change of the upstream code) and all the autos fall out of place. That's one of the reasons I'd prefer having the actual data encoded into it, because then you can see exactly what you're getting, and other people can use the data themselves. The fact that it numbers them for you helps for precisely one time, so not as important as the thousands of time the enum is going to be used by everybody else...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks as though I have mirrored lookup, which I think allows you to pass in a value and get the identifier for it back...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enumeration
objects are designed to operate in acontext
, technically we could doftrace_ops.flags.cast("ftrace_symbol_table!ftrace_flags")
. Still, for this use case I'm not sure it is worth the overhead of a custom ISF loading.I think we'll have to do live enumeration parsing for later
tainting
plugins, I'll work on a way to parse the flags more intelligibly, if that is ok for you ? It should be easier to design APIs with a concrete use case.Happy to change
auto()
values to explicit1 << n
values, thanks for raising this concern !There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, fair enough. I am interested in getting our enum implementation up to the same ease-of-use as the built-in ones (or better!), but it might be a bit more overhead for this example now.
I would prefer the
auto()
values be changed to explicit values though, thanks.