From 735f5933ba72a261d2687ca2ba0fdeb0cd72a15e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 15:05:28 +0200 Subject: [PATCH 01/29] make module_sect_attr and bin_attribute optional --- volatility3/framework/symbols/linux/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 19fb8f1d45..7560da3eca 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -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) @@ -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) From ac4326f4780dce79d31bb6c92f1c00a3961c1f7f Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 15:06:00 +0200 Subject: [PATCH 02/29] determine sect_attrs.attrs subtype dynamically --- .../symbols/linux/extensions/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0136d749a7..f7c60d6178 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -211,9 +211,7 @@ def get_sections(self) -> Iterable[interfaces.objects.ObjectInterface]: 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, ) @@ -3189,3 +3187,17 @@ def get_name(self) -> Optional[str]: ) return None + +class bin_attribute(objects.StructType): + def get_name(self) -> Optional[str]: + """ + Performs extraction of the bin_attribute name + """ + if hasattr(self, "attr"): + try: + return utility.pointer_to_string(self.attr.name, count=32) + except exceptions.InvalidAddressException: + vollog.debug(f"Invalid attr name for bin_attribute at {self.vol.offset:#x}") + return None + + return None \ No newline at end of file From 4f296f7f2d8b2d146b59c37ee954489bd9d7a321 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 15:08:27 +0200 Subject: [PATCH 03/29] black formatting --- volatility3/framework/symbols/linux/extensions/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index f7c60d6178..0fed4d71f4 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3188,6 +3188,7 @@ def get_name(self) -> Optional[str]: return None + class bin_attribute(objects.StructType): def get_name(self) -> Optional[str]: """ @@ -3197,7 +3198,9 @@ def get_name(self) -> Optional[str]: try: return utility.pointer_to_string(self.attr.name, count=32) except exceptions.InvalidAddressException: - vollog.debug(f"Invalid attr name for bin_attribute at {self.vol.offset:#x}") + vollog.debug( + f"Invalid attr name for bin_attribute at {self.vol.offset:#x}" + ) return None - return None \ No newline at end of file + return None From 75e0d041a2ac1df274ffa845961477934d1b5652 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 17:11:44 +0200 Subject: [PATCH 04/29] sections manual enumeration adjustment --- .../symbols/linux/extensions/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0fed4d71f4..2830a8e58b 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -179,16 +179,27 @@ 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""" + """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 = grp.bin_attrs + else: + arr_offset = grp.attrs + 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, + offset=arr_offset, subtype=self._context.symbol_space.get_type( symbol_table_name + constants.BANG + "pointer" ), - count=25, + count=50, ) idx = 0 @@ -198,6 +209,7 @@ def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int: @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 From 825cb321a58ae211a60a1419fa1ea98d7122ceb5 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 17:11:53 +0200 Subject: [PATCH 05/29] sections manual enumeration adjustment --- .../framework/symbols/linux/utilities/module_extract.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index e55f776682..c2732f2163 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -38,7 +38,7 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 0) + _version = (1, 0, 1) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) @@ -60,9 +60,14 @@ def _get_module_section_count( count = 0 try: + if grp.has_member("bin_attrs"): + arr_offset = grp.bin_attrs + else: + arr_offset = grp.attrs + array = kernel.object( object_type="array", - offset=grp.attrs, + offset=arr_offset, sub_type=kernel.get_type("pointer"), count=50, absolute=True, From 9478d36d7cc13123cfe20e5a369cfc58a719bade Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:31:25 +0200 Subject: [PATCH 06/29] get_sections() sanity check --- volatility3/framework/symbols/linux/extensions/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 2830a8e58b..d41386e7e0 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -217,6 +217,11 @@ def number_of_sections(self) -> int: 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( From 70dfa09b26c66bcb8996e6399522e1053dd121b1 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:31:34 +0200 Subject: [PATCH 07/29] add bin_attribute address virtual member --- volatility3/framework/symbols/linux/extensions/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index d41386e7e0..b02dc3dd29 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3221,3 +3221,10 @@ def get_name(self) -> Optional[str]: return None return None + + @property + def address(self) -> int: + """Equivalent to module_sect_attr.address: + - https://github.com/torvalds/linux/commit/4b2c11e4aaf7e3d7fd9ce8e5995a32ff5e27d74f + """ + return self.private From d8518b387896a18d4ea04a75791f12fe8d28f062 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:32:22 +0200 Subject: [PATCH 08/29] 1774: consolidate module helpers --- .../symbols/linux/utilities/module_extract.py | 105 ++---------------- 1 file changed, 7 insertions(+), 98 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index c2732f2163..64740b6823 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -38,56 +38,11 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 1) + _version = (1, 0, 2) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) - @classmethod - def _get_module_section_count( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - module: extensions.module, - grp: interfaces.objects.ObjectInterface, - ) -> int: - """ - Used to manually determine the section count for kernels that do not track - this count directly within the attribute structures - """ - kernel = context.modules[vmlinux_name] - - count = 0 - - try: - if grp.has_member("bin_attrs"): - arr_offset = grp.bin_attrs - else: - arr_offset = grp.attrs - - array = kernel.object( - object_type="array", - offset=arr_offset, - sub_type=kernel.get_type("pointer"), - count=50, - absolute=True, - ) - - # Walk up to 50 sections counting until we reach the end or a page fault - for sect in array: - if sect.vol.offset == 0: - break - - count += 1 - - except exceptions.InvalidAddressException: - # Use whatever count we reached before the error - vollog.debug( - f"Exception hit counting sections for module at {module.vol.offset:#x}" - ) - - return count - @classmethod def _find_section( cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int @@ -266,54 +221,6 @@ def _fix_sym_table( return sym_table_data - @classmethod - def _enumerate_original_sections( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - module: extensions.module, - ) -> Optional[Dict[int, str]]: - """ - Enumerates the module's sections as maintained by the kernel after load time - 'Early' sections like .init.text and .init.data are discarded after module - initialization, so they are not expected to be in memory during extraction - """ - if hasattr(module.sect_attrs, "nsections"): - num_sections = module.sect_attrs.nsections - else: - num_sections = cls._get_module_section_count( - context, vmlinux_name, module.sect_attrs.grp - ) - - if num_sections > 1024 or num_sections == 0: - vollog.debug( - f"Invalid number of sections ({num_sections}) for module at offset {module.vol.offset:#x}" - ) - return None - - vmlinux = context.modules[vmlinux_name] - - # This is declared as a zero sized array, so we create ourselves - attribute_type = module.sect_attrs.attrs.vol.subtype - - sect_array = vmlinux.object( - object_type="array", - subtype=attribute_type, - offset=module.sect_attrs.attrs.vol.offset, - count=num_sections, - absolute=True, - ) - - sections: Dict[int, str] = {} - - # for each section, gather its name and address - for index, section in enumerate(sect_array): - name = section.get_name() - - sections[section.address] = name - - return sections - @classmethod def _parse_sections( cls, @@ -330,10 +237,12 @@ def _parse_sections( The data of .strtab is read directly off the module structure and not its section as the section from the original module has no meaning after loading as the kernel does not reference it. """ - original_sections = cls._enumerate_original_sections( - context, vmlinux_name, module - ) - if original_sections is None: + original_sections = {} + for index, section in enumerate(module.get_sections()): + name = section.get_name() + original_sections[section.address] = name + + if not original_sections: return None kernel = context.modules[vmlinux_name] From c223a12be9ceffd676154713001dda8ce5c5a078 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:33:00 +0200 Subject: [PATCH 09/29] handle None in _parse_sections caller --- .../framework/symbols/linux/utilities/module_extract.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 64740b6823..500e819487 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -616,9 +616,10 @@ def extract_module( return None # Gather sections - updated_sections, strtab_index, symtab_index = cls._parse_sections( - context, vmlinux_name, module - ) + parse_sections_result = cls._parse_sections(context, vmlinux_name, module) + if parse_sections_result is None: + return None + updated_sections, strtab_index, symtab_index = parse_sections_result kernel = context.modules[vmlinux_name] From 522c2d435c7a38a5d37b4afd8180a46f96eef779 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:36:20 +0200 Subject: [PATCH 10/29] rollback to already patched version --- volatility3/framework/symbols/linux/utilities/module_extract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 500e819487..e4f5705cfc 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -38,7 +38,7 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 2) + _version = (1, 0, 1) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) From 910488235478410cd9d8920546f9b91dfc4b2c99 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 5 May 2025 12:02:41 +0200 Subject: [PATCH 11/29] add ATTRIBUTE_NAME_MAX_SIZE constant --- volatility3/framework/constants/linux/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 3f8c52b43b..72440e23ef 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -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: From a5d1641db338c1aeae95ffe618908cf5d06e1fe8 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 5 May 2025 12:03:03 +0200 Subject: [PATCH 12/29] use ATTRIBUTE_NAME_MAX_SIZE constant --- .../symbols/linux/extensions/__init__.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index b02dc3dd29..037ca1f6b1 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3172,7 +3172,9 @@ 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}") @@ -3180,14 +3182,18 @@ def get_name(self) -> Optional[str]: 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( @@ -3197,7 +3203,9 @@ 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}" @@ -3213,7 +3221,9 @@ def get_name(self) -> Optional[str]: """ if hasattr(self, "attr"): try: - return utility.pointer_to_string(self.attr.name, count=32) + 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}" From 2cda8e3cb39551e7b2c94297a75d88499423ca0e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 6 May 2025 01:03:35 +0200 Subject: [PATCH 13/29] make binary attributes iteration NULL terminated --- .../symbols/linux/extensions/__init__.py | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 037ca1f6b1..22eb464609 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -191,20 +191,27 @@ def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int: else: arr_offset = grp.attrs - 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=arr_offset, - subtype=self._context.symbol_space.get_type( - symbol_table_name + constants.BANG + "pointer" - ), - count=50, - ) + if not arr_offset.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 + entry = arr_offset.dereference() + symbol_table_name = self.get_symbol_table_name() idx = 0 - while arr[idx] and arr[idx].is_readable(): - idx = idx + 1 + while entry.is_readable(): + idx += 1 + entry = self._context.object( + symbol_table_name + constants.BANG + "pointer", + layer_name=self.vol.layer_name, + offset=entry.vol.offset + + self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + "pointer" + ).size, + ) + return idx @functools.cached_property From 1c11791b7fbb2b1dd8c2589f986f097b1a607da8 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 7 May 2025 16:10:16 +0200 Subject: [PATCH 14/29] add dynamically_sized_array_of_pointers() helper --- volatility3/framework/objects/utility.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index 59ac0ee55f..9e9fef33a0 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -250,3 +250,52 @@ 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, + layer_name: str, + symbol_table_name: str, + array_offset: int, + stop_value: int = 0, + iterator_guard_value: int = None, + stop_on_invalid_pointers: bool = True, +) -> interfaces.objects.ObjectInterface: + """Iterates over a dynamically sized array of pointers (e.g. NULL-terminated). + + Args: + context: The context on which to operate. + layer_name: The layer on which the array should be constructed. + symbol_table_name: The symbol table to use to construct object types. + array_offset: The array offset within the layer, from which to start iterating. + stop_value: Stop value used to determine when to terminate iteration once it is encountered. Defaults to 0 (NULL-terminated arrays). + iterator_guard_value: Stop iterating when the iterator index is greater than this value. This is an extra-safety against smearing. + 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. + """ + pointer_type = context.symbol_space.get_type( + symbol_table_name + constants.BANG + "pointer" + ) + entry = context.object( + pointer_type, + layer_name=layer_name, + offset=array_offset, + ) + i = 0 + array = [] + # entry and entry.vol.offset aren't the same thing, as + # - entry is naturally represented by the address that the pointer refers to; + # - entry.vol.offset is the offset at which the pointer lives. + while entry != stop_value: + if (not entry.is_readable() and stop_on_invalid_pointers) or ( + iterator_guard_value is not None and i >= iterator_guard_value + ): + break + array.append(entry) + entry = context.object( + pointer_type, + layer_name=layer_name, + offset=entry.vol.offset + pointer_type.size, + ) + i += 1 + return array From 15a8af5c93bbef025fc0c0d3fcb69abc83854f9e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 7 May 2025 16:10:37 +0200 Subject: [PATCH 15/29] use dynamically_sized_array_of_pointers in _get_sect_count --- .../symbols/linux/extensions/__init__.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 22eb464609..22d06baea3 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -198,21 +198,17 @@ def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int: ) return 0 - entry = arr_offset.dereference() - symbol_table_name = self.get_symbol_table_name() - idx = 0 - while entry.is_readable(): - idx += 1 - entry = self._context.object( - symbol_table_name + constants.BANG + "pointer", - layer_name=self.vol.layer_name, - offset=entry.vol.offset - + self._context.symbol_space.get_type( - symbol_table_name + constants.BANG + "pointer" - ).size, - ) - - return idx + # We chose 1000 as an arbitrary guard value against + # extreme cases of smearing. + # See PR #1773 for more information. + bin_attrs_list = utility.dynamically_sized_array_of_pointers( + context=self._context, + layer_name=self.vol.layer_name, + symbol_table_name=self.get_symbol_table_name(), + array_offset=arr_offset.dereference().vol.offset, + iterator_guard_value=1000, + ) + return len(bin_attrs_list) @functools.cached_property def number_of_sections(self) -> int: From b20da9c7f553d9e2b9580727d37113a57dcd5d09 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 7 May 2025 16:26:47 +0200 Subject: [PATCH 16/29] correct type hinting --- volatility3/framework/objects/utility.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index 9e9fef33a0..afea99b0c9 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -4,7 +4,7 @@ import re -from typing import Optional, Union +from typing import List, Optional, Union from volatility3.framework import interfaces, objects, constants, exceptions @@ -260,7 +260,7 @@ def dynamically_sized_array_of_pointers( stop_value: int = 0, iterator_guard_value: int = None, stop_on_invalid_pointers: bool = True, -) -> interfaces.objects.ObjectInterface: +) -> List[interfaces.objects.ObjectInterface]: """Iterates over a dynamically sized array of pointers (e.g. NULL-terminated). Args: @@ -272,6 +272,9 @@ def dynamically_sized_array_of_pointers( iterator_guard_value: Stop iterating when the iterator index is greater than this value. This is an extra-safety against smearing. 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 """ pointer_type = context.symbol_space.get_type( symbol_table_name + constants.BANG + "pointer" From 44969090e71f9d63a8d2a23ea94f5a61c8459923 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:41:39 +0200 Subject: [PATCH 17/29] lru_cache get_modules_memory_boundaries() --- volatility3/framework/symbols/linux/utilities/modules.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 2b16ec6e08..bb360e0ba5 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1,6 +1,7 @@ import logging import warnings from abc import ABCMeta, abstractmethod +import functools from typing import ( Callable, Dict, @@ -71,7 +72,7 @@ def gather_modules( class Modules(interfaces.configuration.VersionableInterface): """Kernel modules related utilities.""" - _version = (3, 0, 1) + _version = (3, 0, 2) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -311,6 +312,7 @@ def run_modules_scanners( return run_results @staticmethod + @functools.lru_cache def get_modules_memory_boundaries( context: interfaces.context.ContextInterface, vmlinux_module_name: str, From 6e67674c20c7e1e3084a0106eca842efe6e1101d Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:46:25 +0200 Subject: [PATCH 18/29] add section address sanity check --- .../symbols/linux/utilities/module_extract.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index e4f5705cfc..1ff0f35fc6 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -10,12 +10,14 @@ Dict, ) +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules from volatility3 import framework from volatility3.framework import ( interfaces, exceptions, symbols, ) +from volatility3.framework.configuration import requirements from volatility3.framework.constants import linux as linux_constants from volatility3.framework.symbols.linux import extensions @@ -38,11 +40,21 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 1) + _version = (1, 0, 2) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 2), + ), + ] + @classmethod def _find_section( cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int @@ -237,17 +249,32 @@ def _parse_sections( The data of .strtab is read directly off the module structure and not its section as the section from the original module has no meaning after loading as the kernel does not reference it. """ + kernel = context.modules[vmlinux_name] + kernel_layer = context.layers[kernel.layer_name] + modules_addr_min, modules_addr_max = ( + linux_utilities_modules.Modules.get_modules_memory_boundaries( + context, vmlinux_name + ) + ) + modules_addr_min &= kernel_layer.address_mask + modules_addr_max &= kernel_layer.address_mask + original_sections = {} for index, section in enumerate(module.get_sections()): + # Extra sanity check, to prevent OOM on heavily smeared samples at line + # "size = next_address - address" + if ( + not modules_addr_min + <= kernel_layer.address_mask & section.address + < modules_addr_max + ): + continue name = section.get_name() original_sections[section.address] = name if not original_sections: return None - kernel = context.modules[vmlinux_name] - kernel_layer = context.layers[kernel.layer_name] - if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): sym_type = "Elf64_Sym" elf_hdr_type = "Elf64_Ehdr" From a5cc616d53f2db0d20e1dfff81e9d26ed2672b78 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:49:56 +0200 Subject: [PATCH 19/29] leverage the existing Array facility --- volatility3/framework/objects/utility.py | 65 +++++++++++------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index afea99b0c9..be57f9ad71 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -3,11 +3,13 @@ # import re - -from typing import List, Optional, Union +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""" @@ -254,51 +256,46 @@ def array_of_pointers( def dynamically_sized_array_of_pointers( context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table_name: str, - array_offset: int, + array: interfaces.objects.ObjectInterface, + iterator_guard_value: int, + subtype: Union[str, interfaces.objects.Template], stop_value: int = 0, - iterator_guard_value: int = None, stop_on_invalid_pointers: bool = True, -) -> List[interfaces.objects.ObjectInterface]: +) -> 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. - layer_name: The layer on which the array should be constructed. - symbol_table_name: The symbol table to use to construct object types. - array_offset: The array offset within the layer, from which to start iterating. - stop_value: Stop value used to determine when to terminate iteration once it is encountered. Defaults to 0 (NULL-terminated arrays). + 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 """ - pointer_type = context.symbol_space.get_type( - symbol_table_name + constants.BANG + "pointer" - ) - entry = context.object( - pointer_type, - layer_name=layer_name, - offset=array_offset, - ) - i = 0 - array = [] - # entry and entry.vol.offset aren't the same thing, as - # - entry is naturally represented by the address that the pointer refers to; - # - entry.vol.offset is the offset at which the pointer lives. - while entry != stop_value: - if (not entry.is_readable() and stop_on_invalid_pointers) or ( - iterator_guard_value is not None and i >= iterator_guard_value + new_count = 0 + for entry in array_of_pointers( + 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 - array.append(entry) - entry = context.object( - pointer_type, - layer_name=layer_name, - offset=entry.vol.offset + pointer_type.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.""", ) - i += 1 - return array + + # Leverage the "Array" object instead of returning a Python list + return array_of_pointers( + array=array, count=new_count, subtype=subtype, context=context + ) From c8d90cffe43a7440b60322a2b19a16cdd7c5f22c Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:52:18 +0200 Subject: [PATCH 20/29] adjust to the new NULL-terminated processing --- .../symbols/linux/extensions/__init__.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 22d06baea3..953e3cb892 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -187,26 +187,31 @@ def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int: """ if grp.has_member("bin_attrs"): - arr_offset = grp.bin_attrs + arr_offset_ptr = grp.bin_attrs + arr_subtype = "bin_attribute" else: - arr_offset = grp.attrs + arr_offset_ptr = grp.attrs + arr_subtype = "attribute" - if not arr_offset.is_readable(): + 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 - # We chose 1000 as an arbitrary guard value against - # extreme cases of smearing. + # 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, - layer_name=self.vol.layer_name, - symbol_table_name=self.get_symbol_table_name(), - array_offset=arr_offset.dereference().vol.offset, - iterator_guard_value=1000, + array=arr_offset_ptr.dereference(), + iterator_guard_value=100, + subtype=self.get_symbol_table_name() + constants.BANG + arr_subtype, ) return len(bin_attrs_list) From 5d50ad2b2eab7fa54d3be917063ec9e649f25536 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:54:45 +0200 Subject: [PATCH 21/29] slight readability adjustment --- .../framework/symbols/linux/utilities/module_extract.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 1ff0f35fc6..161f2a1671 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -263,9 +263,9 @@ def _parse_sections( for index, section in enumerate(module.get_sections()): # Extra sanity check, to prevent OOM on heavily smeared samples at line # "size = next_address - address" - if ( - not modules_addr_min - <= kernel_layer.address_mask & section.address + if not ( + modules_addr_min + <= section.address & kernel_layer.address_mask < modules_addr_max ): continue From c89c7c3a06349f8e1fb518d9520e3784a7d4096d Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 14:03:28 +0200 Subject: [PATCH 22/29] rollback to 635237b to prevent circular import --- .../symbols/linux/utilities/module_extract.py | 35 +++---------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 161f2a1671..e4f5705cfc 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -10,14 +10,12 @@ Dict, ) -import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules from volatility3 import framework from volatility3.framework import ( interfaces, exceptions, symbols, ) -from volatility3.framework.configuration import requirements from volatility3.framework.constants import linux as linux_constants from volatility3.framework.symbols.linux import extensions @@ -40,21 +38,11 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 2) + _version = (1, 0, 1) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.VersionRequirement( - name="linux_utilities_modules", - component=linux_utilities_modules.Modules, - version=(3, 0, 2), - ), - ] - @classmethod def _find_section( cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int @@ -249,32 +237,17 @@ def _parse_sections( The data of .strtab is read directly off the module structure and not its section as the section from the original module has no meaning after loading as the kernel does not reference it. """ - kernel = context.modules[vmlinux_name] - kernel_layer = context.layers[kernel.layer_name] - modules_addr_min, modules_addr_max = ( - linux_utilities_modules.Modules.get_modules_memory_boundaries( - context, vmlinux_name - ) - ) - modules_addr_min &= kernel_layer.address_mask - modules_addr_max &= kernel_layer.address_mask - original_sections = {} for index, section in enumerate(module.get_sections()): - # Extra sanity check, to prevent OOM on heavily smeared samples at line - # "size = next_address - address" - if not ( - modules_addr_min - <= section.address & kernel_layer.address_mask - < modules_addr_max - ): - continue name = section.get_name() original_sections[section.address] = name if not original_sections: return None + kernel = context.modules[vmlinux_name] + kernel_layer = context.layers[kernel.layer_name] + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): sym_type = "Elf64_Sym" elf_hdr_type = "Elf64_Ehdr" From b8e12bec297a4f61b17ff10b0c27224ad45ec37d Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 20 Nov 2025 15:18:53 +0100 Subject: [PATCH 23/29] move ModuleExtract class in modules.py --- .../framework/plugins/linux/module_extract.py | 12 +- .../symbols/linux/utilities/module_extract.py | 8 +- .../symbols/linux/utilities/modules.py | 732 +++++++++++++++++- 3 files changed, 742 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/linux/module_extract.py b/volatility3/framework/plugins/linux/module_extract.py index a8864281af..3b6b6f0e5f 100644 --- a/volatility3/framework/plugins/linux/module_extract.py +++ b/volatility3/framework/plugins/linux/module_extract.py @@ -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 @@ -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) @@ -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, ), ] @@ -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: diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index e4f5705cfc..3cc15085af 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -15,6 +15,7 @@ interfaces, exceptions, symbols, + deprecation ) from volatility3.framework.constants import linux as linux_constants from volatility3.framework.symbols.linux import extensions @@ -34,7 +35,12 @@ # ModuleExtract.extract_module is the entry point and only visible method for plugins - +# See PR #1773 +@deprecation.renamed_class( + deprecated_class_name="ModuleExtract", + removal_date="2026-06-01", + message="volatility3.framework.symbols.linux.utilities.module_extract.ModuleExtract is to be deprecated. Use volatility3.framework.symbols.linux.utilities.modules.ModuleExtract instead.", +) class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index bb360e0ba5..42383ecae4 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1,7 +1,8 @@ import logging import warnings -from abc import ABCMeta, abstractmethod import functools +import struct +from abc import ABCMeta, abstractmethod from typing import ( Callable, Dict, @@ -16,7 +17,6 @@ Union, ) -import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract from volatility3 import framework from volatility3.framework import ( constants, @@ -25,12 +25,14 @@ interfaces, objects, renderers, + symbols ) from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.linux import extensions from volatility3.framework.symbols.linux.utilities import tainting +from volatility3.framework.constants import linux as linux_constants vollog = logging.getLogger(__name__) @@ -782,6 +784,730 @@ def get_load_parameters( yield name, value +# This module is responsible for producing an ELF file of a kernel module (LKM) loaded in memory +# This extraction task is quite complicated as the Linux kernel discards the ELF header at load time +# Due to this, to support static analysis, we must create an ELF header and proper file based on the sections +# There are also several other significant complications that we must deal with when trying to extract an LKM +# that can be analyzed with static analysis tools +# First, the .strtab points somewhere random and is kept off the module structure, not with the other sections +# Second, all of the symbols (.symtab) have mangled members that we must patch for anything to make sense +# Third, the section name string table (.shstrtab) is not an allocated section, meaning its not in memory +# Not having the .shstrtab makes analysis impossible-to-difficult for static analysis tools. To work around this, +# we create the .shstrtab based on the sections in memory and then glue it in as the final section + +# ModuleExtract.extract_module is the entry point and only visible method for plugins +class ModuleExtract(interfaces.configuration.VersionableInterface): + """Extracts Linux kernel module structures into an analyzable ELF file""" + + _version = (1, 0, 2) + _required_framework_version = (2, 25, 0) + + framework.require_interface_version(*_required_framework_version) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="linux_utilities_modules_modules", + component=Modules, + version=(3, 0, 2), + ), + ] + + @classmethod + def _find_section( + cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int + ) -> Optional[Tuple[str, int, int, int]]: + """ + Finds the section containing `sym_address` + """ + for name, index, address, size in section_lookups: + if address <= sym_address < address + size: + return name, index, address, size + + return None + + @classmethod + def _get_st_info_for_sym( + cls, sym: interfaces.objects.ObjectInterface, sym_address: int, sect_name: str + ) -> bytes: + """ + This is a helper function called from `_fix_sym_table` + + Calculates the `st_info` value for the given symbol + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html + """ + if sym.st_name > 0: + # Global symbol + bind = linux_constants.STB_GLOBAL + + if sym_address == 0: + sect_type = linux_constants.STT_NOTYPE + elif sect_name: + # rela = relocations + if sect_name.find(".text") != -1 and sect_name.find(".rela") == -1: + sect_type = linux_constants.STT_FUNC + else: + sect_type = linux_constants.STT_OBJECT + + else: + # outside the module being extracted + sect_type = linux_constants.STT_NOTYPE + + else: + # Local symbol + bind = linux_constants.STB_LOCAL + sect_type = linux_constants.STT_SECTION + + # Build the st_info as ELF32_ST_INFO/ELF64_ST_INFO + bind_bits = (bind << 4) & 0xF0 + type_bits = sect_type & 0xF + + st_info_int = (bind_bits | type_bits) & 0xFF + + return struct.pack("B", st_info_int) + + @classmethod + def _get_fixed_sym_fields( + cls, + st_fmt: str, + sym: interfaces.objects.ObjectInterface, + sections: List[Tuple[str, int, int, int]], + ) -> Tuple[str, int, int, int]: + """ + This is a helper function called from `_fix_sym_table` + + The st_value, st_info, and st_shndx fields of each symbol are changed/mangled while loading + Static analysis tools do not understand these transformed values as they only make sense to the kernel loader + We must de-mangle these to have analysis tools understand symbols (a key aspect) + """ + # Start by trying to map a symbol to its section + sym_address = sym.st_value + sect_info = cls._find_section(sections, sym_address) + + if not sect_info: + # Symbol does not point into the module being extracted + sect_name, sect_index, sect_address = None, None, None + st_value_int = sym_address + else: + # relative address inside the section + sect_name, sect_index, sect_address, _ = sect_info + st_value_int = sym_address - sect_address + + # Get the fixed st_value, st_info, and st_shndx that are broken in the mapped file + + # formatted to be written into the extracted file + st_value = struct.pack(st_fmt, st_value_int) + + # returns formatted to be written into the extracted file + st_info = cls._get_st_info_for_sym(sym, sym_address, sect_name) + + # format to reference its section, if any + if sect_name: + st_shndx = struct.pack(" Optional[bytes]: + """ + This function implements the most painful part of the reconstruction + + The symbols in .symtab are broken/mangled during loading. + We need to normalize these for static analysis tools to understand the references. + Without proper symbols, analysis is pretty pointless and gets nowhere. + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html + """ + kernel = context.modules[vmlinux_name] + + # Gather the section information into a list + section_lookups: List[Tuple[str, int, int, int]] = [] + for index, (address, name) in enumerate(original_sections.items()): + # We are fixing symtab references... + if name == ".symtab": + continue + + size = section_sizes[address] + + # Add 1 to account for leading NULL section + section_lookups.append((name, index + 1, address, size)) + + # Build the array of symbols as they are in memory + sym_type = kernel.get_type(sym_type_name) + + symbols = kernel.object( + object_type="array", + subtype=sym_type, + offset=module.section_symtab, + count=module.num_symtab, + absolute=True, + ) + + # used to hold the new (fixed) symbol table + sym_table_data = b"" + + # build a correct/normalized Elf32_Sym or Elf64_Sym for each symbol + for sym in symbols: + # get the mangled fields' correct values + sect_name, st_value, st_info, st_shndx = cls._get_fixed_sym_fields( + st_fmt, sym, section_lookups + ) + + # these aren't mangled during loading + st_name = struct.pack(" Optional[Tuple[List, int, int]]: + """ + This function first parses the sections as maintained by the kernel + It then orders the sections by load address, and then gathers the data of each section + We also track the file_offset to correctly have alignment in the output file + + .symtab requires special handling as its so broken in memory as described in `_fix_sym_table` + The data of .strtab is read directly off the module structure and not its section + as the section from the original module has no meaning after loading as the kernel does not reference it. + """ + kernel = context.modules[vmlinux_name] + kernel_layer = context.layers[kernel.layer_name] + modules_addr_min, modules_addr_max = ( + Modules.get_modules_memory_boundaries( + context, vmlinux_name + ) + ) + modules_addr_min &= kernel_layer.address_mask + modules_addr_max &= kernel_layer.address_mask + original_sections = {} + for index, section in enumerate(module.get_sections()): + # Extra sanity check, to prevent OOM on heavily smeared samples at line + # "size = next_address - address" + if not ( + modules_addr_min + <= section.address & kernel_layer.address_mask + < modules_addr_max + ): + continue + + name = section.get_name() + original_sections[section.address] = name + + if not original_sections: + return None + + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + sym_type = "Elf64_Sym" + elf_hdr_type = "Elf64_Ehdr" + st_fmt = " Optional[bytes]: + """ + Creates a `bits` bit ELF header for the file based on recovered values + Called last as it needs information computed from the sections + + Spec: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html + """ + if bits == 32: + fmt = " Optional[int]: + """ + This function makes a best effort to map common section names + to their attributes + """ + known_sections = { + ".note.gnu.build-id": linux_constants.SHT_NOTE, + ".text": linux_constants.SHT_PROGBITS, + ".init.text": linux_constants.SHT_PROGBITS, + ".exit.text": linux_constants.SHT_PROGBITS, + ".static_call.text": linux_constants.SHT_PROGBITS, + ".rodata": linux_constants.SHT_PROGBITS, + ".modinfo": linux_constants.SHT_PROGBITS, + "__param": linux_constants.SHT_PROGBITS, + ".data": linux_constants.SHT_PROGBITS, + ".gnu.linkonce.this_module": linux_constants.SHT_PROGBITS, + ".comment": linux_constants.SHT_PROGBITS, + ".shstrtab": linux_constants.SHT_STRTAB, + ".symtab": linux_constants.SHT_SYMTAB, + ".strtab": linux_constants.SHT_STRTAB, + } + + sect_type_val = linux_constants.SHT_PROGBITS + + if section_name.find(".rela.") != -1: + sect_type_val = linux_constants.SHT_RELA + + elif section_name in known_sections: + sect_type_val = known_sections[section_name] + + return sect_type_val + + # all sections from memory are allocated (SHF_ALLOC) + # special check certain other sections to try and ensure extra flags are added where needed + @classmethod + def _calc_sect_flags(cls, name: str) -> int: + """ + Make a best effort to map common section names to their permissions + If we miss a section here, users of common static analysis tools can mark the + sections are writable or executable manually, but that becomes very cumbersome + and breaks initial analysis by the tool + """ + # All sections in memory are allocated (`A` in readelf -S) + flags = linux_constants.SHF_ALLOC + + if name in [".text", ".init.text", ".exit.text", ".static_call.text"]: + flags = flags | linux_constants.SHF_EXECINSTR + + elif name in [ + ".data", + ".init.data", + ".exit.data", + ".bss", + "__tracepoints", + ".data.once", + "_ftrace_events", + ".gnu.linkonce.this_module", + ]: + flags = flags | linux_constants.SHF_WRITE + + return flags + + @classmethod + def _calc_link( + cls, name: str, strtab_index: int, symtab_index: int, sect_type: int + ) -> int: + """ + Calculates the link value for a section + + The most important ones are symtab indexes for relocations + and to point the symbol table to the string tab + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return symtab_index + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + return strtab_index + + return 0 + + @classmethod + def _calc_entsize(cls, name: str, sect_type: int, bits: int) -> int: + """ + Calculates the entsize for relocation sections and the symbol table section + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return 24 + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + if bits == 32: + return 16 + else: + return 24 + + return 0 + + @classmethod + def _make_section_header( + cls, + bits: int, + name_index: int, + name: str, + address: int, + size: int, + file_offset: int, + strtab_index: int, + symtab_index: int, + ) -> Optional[bytes]: + """ + Creates a section header (Elf32_Shdr or Elf64_Shdr) for the given section + """ + if bits == 32: + fmt = " Optional[bytes]: + # Bail early if bad address sent in + try: + hasattr(module.sect_attrs, "nsections") + except exceptions.InvalidAddressException: + vollog.debug(f"module at offset {module.vol.offset:#x} is paged out.") + return None + + # Gather sections + parse_sections_result = cls._parse_sections(context, vmlinux_name, module) + if parse_sections_result is None: + return None + updated_sections, strtab_index, symtab_index = parse_sections_result + + kernel = context.modules[vmlinux_name] + + # Figure out header sizes + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + header_type = "Elf64_Ehdr" + section_type = "Elf64_Shdr" + bits = 64 + else: + header_type = "Elf32_Ehdr" + section_type = "Elf32_Shdr" + bits = 32 + + header_type_size = kernel.get_type(header_type).size + section_type_size = kernel.get_type(section_type).size + + # Per Linux-spec, all LKMs must start with a null section header + # This buffer is used to hold the headers as they are built + sections_headers = b"\x00" * section_type_size + + # Holder of the data of the sections + sections_data = b"" + + # the .shstrtab section is "\x00" + section name for each section + # followed by a terminating null. + # It starts with the null string (\x00) + shstrtab_data = b"\x00" + + # Track where we end the sections and data to glue `.shstrtab` after + last_file_offset = None + last_sect_size = None + + # Start at 1 in the string table + name_index = 1 + + # Create the actual section headers + for index, (name, address, file_offset, section_data) in enumerate( + updated_sections + ): + # Make the section header + header_bytes = cls._make_section_header( + bits, + name_index, + name, + address, + len(section_data), + file_offset, + strtab_index, + symtab_index, + ) + if not header_bytes: + vollog.debug(f"make_section_header failed for section {name}") + return None + + # ndex into the string table + name_index += len(name) + 1 + + # concatenate the header and section bytes + sections_headers += header_bytes + sections_data += section_data + + # track where we are so .shstrtab goes into correct offset + last_file_offset = file_offset + last_sect_size = len(section_data) + + # append each section name to what will become .shstrtab + shstrtab_data += bytes(name, encoding="utf8") + b"\x00" + + # stick our own section reference string at end + # name_index points to the end of the last section string after the loop ends + shstrtab_data += b".shstrtab\x00" + + # create our .shstrtab section so sections have names + sections_headers += cls._make_section_header( + bits, + name_index, + ".shstrtab", + 0, + len(shstrtab_data), + last_file_offset + last_sect_size, + strtab_index, + symtab_index, + ) + + sections_data += shstrtab_data + + num_sections = len(updated_sections) + 1 + + header = cls._make_elf_header( + bits, + header_type_size + len(sections_data), + num_sections, + ) + + if not header: + vollog.error( + f"Hit error creating Elf header for module at {module.vol.offset:#x}" + ) + return None + + # Return our beautiful, hand-crafted, farm raised ELF file + return header + sections_data + sections_headers class ModuleGathererLsmod(ModuleGathererInterface): """ @@ -978,7 +1704,7 @@ def generate_results( file_name = renderers.NotApplicableValue() if dump and open_implementation: - elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( + elf_data = ModuleExtract.extract_module( context, kernel_module_name, module ) if not elf_data: From e9ce095b2924eb7d143aa7da24b79ed69850097b Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 20 Nov 2025 15:57:56 +0100 Subject: [PATCH 24/29] black formatting --- .../symbols/linux/utilities/module_extract.py | 8 ++------ .../framework/symbols/linux/utilities/modules.py | 11 ++++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 3cc15085af..5ec2543683 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -11,12 +11,7 @@ ) from volatility3 import framework -from volatility3.framework import ( - interfaces, - exceptions, - symbols, - deprecation -) +from volatility3.framework import interfaces, exceptions, symbols, deprecation from volatility3.framework.constants import linux as linux_constants from volatility3.framework.symbols.linux import extensions @@ -35,6 +30,7 @@ # ModuleExtract.extract_module is the entry point and only visible method for plugins + # See PR #1773 @deprecation.renamed_class( deprecated_class_name="ModuleExtract", diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 42383ecae4..adacc54e35 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -25,7 +25,7 @@ interfaces, objects, renderers, - symbols + symbols, ) from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility @@ -784,6 +784,7 @@ def get_load_parameters( yield name, value + # This module is responsible for producing an ELF file of a kernel module (LKM) loaded in memory # This extraction task is quite complicated as the Linux kernel discards the ELF header at load time # Due to this, to support static analysis, we must create an ELF header and proper file based on the sections @@ -795,6 +796,7 @@ def get_load_parameters( # Not having the .shstrtab makes analysis impossible-to-difficult for static analysis tools. To work around this, # we create the .shstrtab based on the sections in memory and then glue it in as the final section + # ModuleExtract.extract_module is the entry point and only visible method for plugins class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" @@ -1010,10 +1012,8 @@ def _parse_sections( """ kernel = context.modules[vmlinux_name] kernel_layer = context.layers[kernel.layer_name] - modules_addr_min, modules_addr_max = ( - Modules.get_modules_memory_boundaries( - context, vmlinux_name - ) + modules_addr_min, modules_addr_max = Modules.get_modules_memory_boundaries( + context, vmlinux_name ) modules_addr_min &= kernel_layer.address_mask modules_addr_max &= kernel_layer.address_mask @@ -1509,6 +1509,7 @@ def extract_module( # Return our beautiful, hand-crafted, farm raised ELF file return header + sections_data + sections_headers + class ModuleGathererLsmod(ModuleGathererInterface): """ Gathers modules from the main kernel list From 32f37ee6323ea2f7ee206aafe7d7a0faf60ac36f Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 24 Nov 2025 17:34:31 +0100 Subject: [PATCH 25/29] remove hasattr check on bin_attribute --- .../symbols/linux/extensions/__init__.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 953e3cb892..68d1aae52b 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3227,18 +3227,13 @@ def get_name(self) -> Optional[str]: """ Performs extraction of the bin_attribute name """ - if hasattr(self, "attr"): - 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 - - return None + 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: From 8b132eafd15f885806028e522887e520d22c60bb Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 24 Nov 2025 17:57:37 +0100 Subject: [PATCH 26/29] extend _fix_sym_table's docstring --- volatility3/framework/symbols/linux/utilities/modules.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index adacc54e35..598e79b180 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -925,6 +925,15 @@ def _fix_sym_table( module: extensions.module, ) -> Optional[bytes]: """ + Args: + context: The context on which to operate. + vmlinux_name: The name of the kernel module. + original_sections: Dict of module section addresses and names. + section_sizes: Dict of module section addresses and sizes. + sym_type_name: ELF symbol type name (should be one of "Elf64_Sym" or "Elf32_Sym"). + st_fmt: "struct"-like unpack format string (should be one of " Date: Wed, 3 Dec 2025 17:34:25 +0100 Subject: [PATCH 27/29] move iterator_guard_value down the parameters list --- volatility3/framework/objects/utility.py | 2 +- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index be57f9ad71..05ad76d11c 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -257,8 +257,8 @@ def array_of_pointers( def dynamically_sized_array_of_pointers( context: interfaces.context.ContextInterface, array: interfaces.objects.ObjectInterface, - iterator_guard_value: int, subtype: Union[str, interfaces.objects.Template], + iterator_guard_value: int, stop_value: int = 0, stop_on_invalid_pointers: bool = True, ) -> interfaces.objects.ObjectInterface: diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 68d1aae52b..71ce82d82a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -210,8 +210,8 @@ def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int: 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, + iterator_guard_value=100, ) return len(bin_attrs_list) From d7103a22141c2f8d9e40c524cb56b5fefe032868 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 3 Dec 2025 17:41:58 +0100 Subject: [PATCH 28/29] ensure that _parse_sections returns a constant number of None on failure --- volatility3/framework/symbols/linux/utilities/modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 598e79b180..63b25380ae 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1134,7 +1134,7 @@ def _parse_sections( vollog.debug( f"Could not construct a symbol table for module at {module.vol.offset}. Cannot recover." ) - return None, None, None + return None symtab_index = len(updated_sections) @@ -1145,7 +1145,7 @@ def _parse_sections( vollog.debug( f"Did not find a .symtab section for module at {module.vol.offset:#x}. Cannot recover." ) - return None, None, None + return None return updated_sections, strtab_index, symtab_index From 0138aec419e5febdc13781b2ea6832bd151c9e1e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 3 Dec 2025 17:43:39 +0100 Subject: [PATCH 29/29] manually iterate over the pointers to detect OOB locally --- volatility3/framework/objects/utility.py | 27 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index 05ad76d11c..799639a671 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -279,14 +279,27 @@ def dynamically_sized_array_of_pointers( An array of pointer objects """ new_count = 0 - for entry in array_of_pointers( - 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 - ): + 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(