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: linux.tracing.ftrace #1564

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
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 Jan 21, 2025
f007a28
initial linux.tracing.ftrace.Check_ftrace
Abyss-W4tcher Jan 21, 2025
414cab1
2.18.0 -> 2.19.0 bump
Abyss-W4tcher Jan 21, 2025
d5a2134
modules utilities __init__.py
Abyss-W4tcher Jan 21, 2025
37b792b
ruff fix
Abyss-W4tcher Jan 21, 2025
614ac50
explicit returns and extra Optional type hinting
Abyss-W4tcher Jan 21, 2025
d297693
correct required framework version
Abyss-W4tcher Jan 21, 2025
3ba60d5
remove self-contained hidden_modules check, switch to dataclass
Abyss-W4tcher Jan 21, 2025
e7b51bd
prefer staticmethod when cls is not needed
Abyss-W4tcher Jan 22, 2025
3b05ed7
Merge remote-tracking branch 'origin/develop' into linux_tracing_ftrace
Abyss-W4tcher Jan 26, 2025
f75a4be
rename modules_utilities to linux_utilities_modules
Abyss-W4tcher Jan 26, 2025
48eca36
1.0.0 -> 1.1.0 Modules bump
Abyss-W4tcher Jan 26, 2025
c57f607
require Modules >= 1.1.0
Abyss-W4tcher Jan 26, 2025
f2ac621
use classmethod instead of staticmethod
Abyss-W4tcher Jan 26, 2025
22a2fe1
clarify generator variable
Abyss-W4tcher Jan 27, 2025
03cf84f
assign kernel layer to variable early
Abyss-W4tcher Jan 27, 2025
9d11c1f
prevent modules memory space overlap scenario
Abyss-W4tcher Jan 27, 2025
d777ab3
Merge branch 'volatilityfoundation:develop' into linux_tracing_ftrace
Abyss-W4tcher Jan 27, 2025
c000e81
tune with the new additional_description mechanism
Abyss-W4tcher Jan 27, 2025
b055848
explicit powers of two instead of auto()
Abyss-W4tcher Jan 29, 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
2 changes: 1 addition & 1 deletion volatility3/framework/constants/_version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 18 # Number of changes that only add to the interface
VERSION_MINOR = 19 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""

Expand Down
Empty file.
310 changes: 310 additions & 0 deletions volatility3/framework/plugins/linux/tracing/ftrace.py
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()
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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...

Copy link
Contributor Author

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 by auto() 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 type Enumeration, but volatility3.framework.objects.templates.ObjectTemplate

I think I was able to understand the calling tree:

  • get_enumeration instantiates an ObjectTemplate:

    return objects.templates.ObjectTemplate(
    type_name=self.name + constants.BANG + enum_name,
    object_class=objects.Enumeration,
    base_type=base_type,
    choices=curdict["constants"],
    )

  • No inherited properties from Enumeration, but its VolTemplateProxy:

def __init__(
self,
object_class: Type[interfaces.objects.ObjectInterface],
type_name: str,
**arguments,
) -> None:
arguments["object_class"] = object_class
super().__init__(type_name=type_name, **arguments)
proxy_cls = self.vol.object_class.VolTemplateProxy
for method_name in proxy_cls._methods:
setattr(
self,
method_name,
functools.partial(getattr(proxy_cls, method_name), self),
)
@property
def size(self) -> int:
"""Returns the children of the templated object (see :class:`~volatilit
y.framework.interfaces.objects.ObjectInterface.VolTemplateProxy`)"""

class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
_methods = ["lookup"]

>>> dir(kernel.get_enumeration("XX"))
['__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_vol', 'child_template', 'children', 'clone', 'has_member', 'lookup', 'relative_child_offset', 'replace_child', 'size', 'update_vol', 'vol']

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 ?

Copy link
Member

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 of get_type. If you wanted to apply it to some data, you'd use context.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...

Copy link
Member

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...

Copy link
Contributor Author

@Abyss-W4tcher Abyss-W4tcher Jan 29, 2025

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 a context, technically we could do ftrace_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 explicit 1 << n values, thanks for raising this concern !

Copy link
Member

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.

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(),
)
40 changes: 40 additions & 0 deletions volatility3/framework/symbols/linux/utilities/modules.py
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
Loading