Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
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
51 changes: 50 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,50 @@ 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,
iterator_guard_value: int,
subtype: Union[str, interfaces.objects.Template],
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
for entry in array_of_pointers(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know whether this is handled in array_of_pointers, but my worry is reading off the edge of mapped memory, because we're reading all values up to iterator_guard_value. My concern is that somehow that trips an exception which we don't seem to catch in here?

I guess the iteration should stop when one of the return values isn't readable, but what about applying this to 100 bytes of mapped memory and asking for 100 4-byte pointers? How would this handle simply not being able to read enough memory? Presumably it'd throw an exception, and that may be what we want, because it should only throw an exception when one of the values it expected to be able to read, could be read, but I just wanted to check...

Copy link
Contributor Author

@Abyss-W4tcher Abyss-W4tcher Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is array_of_pointers's responsibility, and it seems to raise an exception if it goes OOB:

array = context.modules["kernel"].object("array", offset=(2**64)-1)
for entry in array_of_pointers(
    array=array, count=iterator_guard_value, subtype=subtype, context=context
):
   # ...
Volatility was unable to read a requested page:
Page error 0x329ff000 in layer layer_name (Page Fault at entry 0x0 in table page directory pointer)

We could add a try/except block right here:

object_info = interfaces.objects.ObjectInformation(
layer_name=self.vol.layer_name,
offset=mask & (self.vol.offset + (self.vol.subtype.size * index)),
parent=self,
native_layer_name=self.vol.native_layer_name or self.vol.layer_name,
size=self.vol.subtype.size,
)
result += [self.vol.subtype(context=self._context, object_info=object_info)]

+ a error_on_oob-ish parameter to the array object builder (default to True?):

object("array", offset=(2**64)-1), error_on_oob=False

which would return all the entries constructed up to the breaking point, but without raising an exception. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think making a "give me your best attempt" flag can be dangerous because people will use it all the time without considering the consequences, so I'd rather not provide it as a matter of course, but have each caller handle and explain why they can make use of partial results. Does that seem reasonable or am I just making everyone write checking code that could be done once because I don't trust people to code sensibly? What do you think would be best? 5:S

array=array, count=iterator_guard_value, subtype=subtype, context=context
):
# "entry" is naturally represented by the address that the pointer refers to
if (entry == stop_value) or (
not entry.is_readable() and stop_on_invalid_pointers
):
break
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(),
iterator_guard_value=100,
subtype=self.get_symbol_table_name() + constants.BANG + arr_subtype,
)
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