Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
735f593
make module_sect_attr and bin_attribute optional
Abyss-W4tcher Apr 15, 2025
ac4326f
determine sect_attrs.attrs subtype dynamically
Abyss-W4tcher Apr 15, 2025
4f296f7
black formatting
Abyss-W4tcher Apr 15, 2025
75e0d04
sections manual enumeration adjustment
Abyss-W4tcher Apr 15, 2025
825cb32
sections manual enumeration adjustment
Abyss-W4tcher Apr 15, 2025
9478d36
get_sections() sanity check
Abyss-W4tcher May 2, 2025
70dfa09
add bin_attribute address virtual member
Abyss-W4tcher May 2, 2025
d8518b3
1774: consolidate module helpers
Abyss-W4tcher May 2, 2025
c223a12
handle None in _parse_sections caller
Abyss-W4tcher May 2, 2025
522c2d4
rollback to already patched version
Abyss-W4tcher May 2, 2025
9104882
add ATTRIBUTE_NAME_MAX_SIZE constant
Abyss-W4tcher May 5, 2025
a5d1641
use ATTRIBUTE_NAME_MAX_SIZE constant
Abyss-W4tcher May 5, 2025
2cda8e3
make binary attributes iteration NULL terminated
Abyss-W4tcher May 5, 2025
1c11791
add dynamically_sized_array_of_pointers() helper
Abyss-W4tcher May 7, 2025
15a8af5
use dynamically_sized_array_of_pointers in _get_sect_count
Abyss-W4tcher May 7, 2025
b20da9c
correct type hinting
Abyss-W4tcher May 7, 2025
4496909
lru_cache get_modules_memory_boundaries()
Abyss-W4tcher May 8, 2025
6e67674
add section address sanity check
Abyss-W4tcher May 8, 2025
a5cc616
leverage the existing Array facility
Abyss-W4tcher May 8, 2025
c8d90cf
adjust to the new NULL-terminated processing
Abyss-W4tcher May 8, 2025
5d50ad2
slight readability adjustment
Abyss-W4tcher May 8, 2025
c89c7c3
rollback to 635237b to prevent circular import
Abyss-W4tcher May 8, 2025
b8e12be
move ModuleExtract class in modules.py
Abyss-W4tcher Nov 20, 2025
e9ce095
black formatting
Abyss-W4tcher Nov 20, 2025
32f37ee
remove hasattr check on bin_attribute
Abyss-W4tcher Nov 24, 2025
8b132ea
extend _fix_sym_table's docstring
Abyss-W4tcher Nov 24, 2025
89c5fc3
move iterator_guard_value down the parameters list
Abyss-W4tcher Dec 3, 2025
d7103a2
ensure that _parse_sections returns a constant number of None on failure
Abyss-W4tcher Dec 3, 2025
0138aec
manually iterate over the pointers to detect OOB locally
Abyss-W4tcher Dec 3, 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
11 changes: 11 additions & 0 deletions volatility3/framework/constants/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,17 @@ def flags(self) -> str:
VMCOREINFO_MAGIC_ALIGNED = VMCOREINFO_MAGIC + b"\x00"
OSRELEASE_TAG = b"OSRELEASE="

ATTRIBUTE_NAME_MAX_SIZE = 255
"""
In 5.9-rc1+, the Linux kernel limits the READ size of a section bin_attribute name to MODULE_SECT_READ_SIZE:

- https://elixir.bootlin.com/linux/v6.15-rc4/source/kernel/module/sysfs.c#L106
- https://github.com/torvalds/linux/commit/11990a5bd7e558e9203c1070fc52fb6f0488e75b

However, the raw section name loaded from the .ko ELF can in theory be thousands of characters,
and unless we do a NULL terminated search we can't set a perfect value.
"""


@dataclass
class TaintFlag:
Expand Down
64 changes: 63 additions & 1 deletion volatility3/framework/objects/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
#

import re

import logging
from typing import Optional, Union

from volatility3.framework import interfaces, objects, constants, exceptions

vollog = logging.getLogger(__name__)


def rol(value: int, count: int, max_bits: int = 64) -> int:
"""A rotate-left instruction in Python"""
Expand Down Expand Up @@ -250,3 +252,63 @@ def array_of_pointers(
).clone()
subtype_pointer.update_vol(subtype=subtype)
return array.cast("array", count=count, subtype=subtype_pointer)


def dynamically_sized_array_of_pointers(
context: interfaces.context.ContextInterface,
array: interfaces.objects.ObjectInterface,
subtype: Union[str, interfaces.objects.Template],
iterator_guard_value: int,
stop_value: int = 0,
stop_on_invalid_pointers: bool = True,
) -> interfaces.objects.ObjectInterface:
"""Iterates over a dynamically sized array of pointers (e.g. NULL-terminated).
Array iteration should always be performed with an arbitrary guard value as maximum size,
to prevent running forever in case something unexpected happens.

Args:
context: The context on which to operate.
array: The object to cast to an array.
iterator_guard_value: Stop iterating when the iterator index is greater than this value. This is an extra-safety against smearing.
subtype: The subtype of the array's pointers.
stop_value: Stop value used to determine when to terminate iteration once it is encountered. Defaults to 0 (NULL-terminated arrays).
stop_on_invalid_pointers: Determines whether to stop iterating or not when an invalid pointer is encountered. This can be useful for arrays
that are known to have smeared entries before the end.

Returns:
An array of pointer objects
"""
new_count = 0
sym_table_name = array.get_symbol_table_name()
sym_table = context.symbol_space[sym_table_name]
ptr_size = sym_table.get_type("pointer").size
layer_name = array.vol.layer_name

offset = array.vol.offset
entry = None
while entry != stop_value and new_count < iterator_guard_value:
try:
entry = context.object(
sym_table_name + constants.BANG + "pointer",
offset=offset,
layer_name=layer_name,
)
except exceptions.InvalidAddressException:
break

if not entry.is_readable() and stop_on_invalid_pointers:
break

offset += ptr_size
new_count += 1
else:
vollog.log(
constants.LOGLEVEL_V,
f"""Iterator guard value {iterator_guard_value} reached while iterating over array at offset {array.vol.offset:#x}.\
This means that there is a bug (e.g. smearing) with this array, or that it may contain valid entries past the iterator guard value.""",
)

# Leverage the "Array" object instead of returning a Python list
return array_of_pointers(
array=array, count=new_count, subtype=subtype, context=context
)
12 changes: 6 additions & 6 deletions volatility3/framework/plugins/linux/module_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import logging
from typing import List

import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3 import framework
import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract
from volatility3.framework import interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
Expand All @@ -17,7 +17,7 @@
class ModuleExtract(interfaces.plugins.PluginInterface):
"""Recreates an ELF file from a specific address in the kernel"""

_version = (1, 0, 0)
_version = (1, 0, 1)
_required_framework_version = (2, 25, 0)

framework.require_interface_version(*_required_framework_version)
Expand All @@ -37,9 +37,9 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
optional=False,
),
requirements.VersionRequirement(
name="linux_utilities_module_extract",
version=(1, 0, 0),
component=linux_utilities_module_extract.ModuleExtract,
name="linux_utilities_modules_module_extract",
version=(1, 0, 2),
component=linux_utilities_modules.ModuleExtract,
),
]

Expand All @@ -58,7 +58,7 @@ def _generator(self):

module = kernel.object(object_type="module", offset=base_address, absolute=True)

elf_data = linux_utilities_module_extract.ModuleExtract.extract_module(
elf_data = linux_utilities_modules.ModuleExtract.extract_module(
self.context, self.config["kernel"], module
)
if not elf_data:
Expand Down
3 changes: 2 additions & 1 deletion volatility3/framework/symbols/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ def __init__(self, *args, **kwargs) -> None:
self.set_type_class("idr", extensions.IDR)
self.set_type_class("address_space", extensions.address_space)
self.set_type_class("page", extensions.page)
self.set_type_class("module_sect_attr", extensions.module_sect_attr)

# Might not exist in the current symbols
self.optional_set_type_class("module", extensions.module)
Expand All @@ -61,6 +60,8 @@ def __init__(self, *args, **kwargs) -> None:
self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct)
self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t)
self.optional_set_type_class("scatterlist", extensions.scatterlist)
self.optional_set_type_class("module_sect_attr", extensions.module_sect_attr)
self.optional_set_type_class("bin_attribute", extensions.bin_attribute)

# kernels >= 4.18
self.optional_set_type_class("timespec64", extensions.timespec64)
Expand Down
96 changes: 74 additions & 22 deletions volatility3/framework/symbols/linux/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,41 +179,64 @@ def get_name(self) -> Optional[str]:
return None

def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int:
"""Try to determine the number of valid sections"""
symbol_table_name = self.get_symbol_table_name()
arr = self._context.object(
symbol_table_name + constants.BANG + "array",
layer_name=self.vol.layer_name,
offset=grp.attrs,
subtype=self._context.symbol_space.get_type(
symbol_table_name + constants.BANG + "pointer"
),
count=25,
)
"""Try to determine the number of valid sections. Support for kernels > 6.14-rc1.

Resources:
- https://github.com/torvalds/linux/commit/d8959b947a8dfab1047c6fd5e982808f65717bfe
- https://github.com/torvalds/linux/commit/e0349c46cb4fbbb507fa34476bd70f9c82bad359
"""

if grp.has_member("bin_attrs"):
arr_offset_ptr = grp.bin_attrs
arr_subtype = "bin_attribute"
else:
arr_offset_ptr = grp.attrs
arr_subtype = "attribute"

if not arr_offset_ptr.is_readable():
vollog.log(
constants.LOGLEVEL_V,
f"Cannot dereference the pointer to the NULL-terminated list of binary attributes for module at offset {self.vol.offset:#x}",
)
return 0

idx = 0
while arr[idx] and arr[idx].is_readable():
idx = idx + 1
return idx
# We chose 100 as an arbitrary guard value to prevent
# looping forever in extreme cases, and because 100 is not expected
# to be a valid number of sections. If that still happens,
# Vol3 module processing will indicate that it is missing information
# with the following message:
# "Unable to reconstruct the ELF for module struct at"
# See PR #1773 for more information.
bin_attrs_list = utility.dynamically_sized_array_of_pointers(
context=self._context,
array=arr_offset_ptr.dereference(),
subtype=self.get_symbol_table_name() + constants.BANG + arr_subtype,
iterator_guard_value=100,
)
return len(bin_attrs_list)

@functools.cached_property
def number_of_sections(self) -> int:
# Dropped in 6.14-rc1: d8959b947a8dfab1047c6fd5e982808f65717bfe
if self.sect_attrs.has_member("nsections"):
return self.sect_attrs.nsections

return self._get_sect_count(self.sect_attrs.grp)

def get_sections(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Get a list of section attributes for the given module."""
if self.number_of_sections == 0:
vollog.debug(
f"Invalid number of sections ({self.number_of_sections}) for module at offset {self.vol.offset:#x}"
)
return []

symbol_table_name = self.get_symbol_table_name()
arr = self._context.object(
symbol_table_name + constants.BANG + "array",
layer_name=self.vol.layer_name,
offset=self.sect_attrs.attrs.vol.offset,
subtype=self._context.symbol_space.get_type(
symbol_table_name + constants.BANG + "module_sect_attr"
),
subtype=self.sect_attrs.attrs.vol.subtype,
count=self.number_of_sections,
)

Expand Down Expand Up @@ -3157,22 +3180,28 @@ def get_name(self) -> Optional[str]:
"""
if hasattr(self, "battr"):
try:
return utility.pointer_to_string(self.battr.attr.name, count=32)
return utility.pointer_to_string(
self.battr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
# if battr is present then its name attribute needs to be valid
vollog.debug(f"Invalid battr name for section at {self.vol.offset:#x}")
return None

elif self.name.vol.type_name == "array":
try:
return utility.array_to_string(self.name, count=32)
return utility.array_to_string(
self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
# specifically do not return here to give `mattr` a chance
vollog.debug(f"Invalid direct name for section at {self.vol.offset:#x}")

elif self.name.vol.type_name == "pointer":
try:
return utility.pointer_to_string(self.name, count=32)
return utility.pointer_to_string(
self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
# specifically do not return here to give `mattr` a chance
vollog.debug(
Expand All @@ -3182,10 +3211,33 @@ def get_name(self) -> Optional[str]:
# if everything else failed...
if hasattr(self, "mattr"):
try:
return utility.pointer_to_string(self.mattr.attr.name, count=32)
return utility.pointer_to_string(
self.mattr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
vollog.debug(
f"Unresolvable name for for section at {self.vol.offset:#x}"
)

return None


class bin_attribute(objects.StructType):
def get_name(self) -> Optional[str]:
"""
Performs extraction of the bin_attribute name
"""
try:
return utility.pointer_to_string(
self.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
vollog.debug(f"Invalid attr name for bin_attribute at {self.vol.offset:#x}")
return None

@property
def address(self) -> int:
"""Equivalent to module_sect_attr.address:
- https://github.com/torvalds/linux/commit/4b2c11e4aaf7e3d7fd9ce8e5995a32ff5e27d74f
"""
return self.private
Loading
Loading