Merge pull request #1773 from Abyss-W4tcher/issue_1761_module_sect_attr_fix

Issue #1761: handle new bin_attribute format for module sections
This commit is contained in:
ikelos
2025-12-29 10:01:33 +00:00
committed by GitHub
7 changed files with 915 additions and 134 deletions
@@ -432,6 +432,17 @@ VMCOREINFO_MAGIC = b"VMCOREINFO\x00"
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:
+63 -1
View File
@@ -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"""
@@ -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
)
@@ -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 @@ vollog = logging.getLogger(__name__)
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 @@ class ModuleExtract(interfaces.plugins.PluginInterface):
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 @@ class ModuleExtract(interfaces.plugins.PluginInterface):
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:
@@ -52,7 +52,6 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
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 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
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)
@@ -179,25 +179,45 @@ class module(generic.GenericIntelProcess):
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.
idx = 0
while arr[idx] and arr[idx].is_readable():
idx = idx + 1
return idx
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
# 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
@@ -205,15 +225,18 @@ class module(generic.GenericIntelProcess):
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,
)
@@ -3158,7 +3181,9 @@ class module_sect_attr(objects.StructType):
"""
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}")
@@ -3166,14 +3191,18 @@ class module_sect_attr(objects.StructType):
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(
@@ -3183,10 +3212,33 @@ class module_sect_attr(objects.StructType):
# 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
@@ -11,11 +11,7 @@ from typing import (
)
from volatility3 import framework
from volatility3.framework import (
interfaces,
exceptions,
symbols,
)
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,54 +31,20 @@ vollog = logging.getLogger(__name__)
# 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"""
_version = (1, 0, 0)
_version = (1, 0, 1)
_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:
array = kernel.object(
object_type="array",
offset=grp.attrs,
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
@@ -261,54 +223,6 @@ class ModuleExtract(interfaces.configuration.VersionableInterface):
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,
@@ -325,10 +239,12 @@ class ModuleExtract(interfaces.configuration.VersionableInterface):
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]
@@ -702,9 +618,10 @@ class ModuleExtract(interfaces.configuration.VersionableInterface):
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]
@@ -1,5 +1,7 @@
import logging
import warnings
import functools
import struct
from abc import ABCMeta, abstractmethod
from typing import (
Callable,
@@ -15,7 +17,6 @@ from typing import (
Union,
)
import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract
from volatility3 import framework
from volatility3.framework import (
constants,
@@ -24,12 +25,14 @@ from volatility3.framework import (
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__)
@@ -71,7 +74,7 @@ class ModuleGathererInterface(
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 +314,7 @@ class Modules(interfaces.configuration.VersionableInterface):
return run_results
@staticmethod
@functools.lru_cache
def get_modules_memory_boundaries(
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
@@ -781,6 +785,740 @@ class Modules(interfaces.configuration.VersionableInterface):
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("<H", sect_index)
else:
st_shndx = struct.pack("<H", sym.st_shndx)
return sect_name, st_value, st_info, st_shndx
@classmethod
def _fix_sym_table(
cls,
context: interfaces.context.ContextInterface,
vmlinux_name: str,
original_sections: Dict[int, str],
section_sizes: Dict[int, int],
sym_type_name: str,
st_fmt: str,
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 "<Q" or "<I").
module: The Linux "module" object we're currently parsing.
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("<I", sym.st_name)
st_other = struct.pack("B", sym.st_other)
st_size = struct.pack(st_fmt, sym.st_size)
# The order as in the ELF specification. The order is not the same between 32 and 64 bit symbols
if st_fmt == "<I":
sym_data = st_name + st_value + st_size + st_info + st_other + st_shndx
else:
sym_data = st_name + st_info + st_other + st_shndx + st_value + st_size
# This should never happen regardless of smear or other issues in the data. We build the structure to spec.
if len(sym_data) != sym_type.size:
vollog.error(
f"Size of sym_data is {len(sym_data)} expected {sym_type.size} for symbol at value {sym.st_value} in section {sect_name}"
)
return None
# add the symbol's data to the overall symbol table
sym_table_data += sym_data
if len(sym_table_data) == 0:
sym_table_data = None
return sym_table_data
@classmethod
def _parse_sections(
cls,
context: interfaces.context.ContextInterface,
vmlinux_name: str,
module: extensions.module,
) -> 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 = "<Q"
else:
sym_type = "Elf32_Sym"
elf_hdr_type = "Elf32_Ehdr"
st_fmt = "<I"
# At this point, we have the sections starting addresses and names,
# but the kernel does not track the size
# To recover the size, we sort by address and then use the next section as the boundary to calculate size
# .symtab (the symbol table) and .strtab (the strings table) require special handling.
# All others can be read with padding
# get the addresses in sorted order, can index into `original_sections` for names
sorted_addresses = sorted(original_sections.keys())
# We need to track where .symtab is for symbol name offsets
symtab_address = None
strtab_index = None
# Section data starts after the file header
file_offset = kernel.get_type(elf_hdr_type).vol.size
# The ordered set of sections along with their fixed data
updated_sections: List[Tuple[str, int, int, bytes]] = []
# A mapping of section start addresses to sizes
# original_sections does not have this information for reasons explained above
section_sizes: Dict[int, int] = {}
for index, address in enumerate(sorted_addresses):
sect_name = original_sections[address]
# Read out the string table. The full size is not kept, so we give each symbol's string up to 256 bytes
if sect_name == ".strtab":
# Read out symbol strings, giving up to 256 bytes per symbol
data = kernel_layer.read(
module.section_strtab, module.num_symtab * 256, pad=True
)
# The string table should end with two NULLs, but the kernel does not enforce this
end_index = data.find(b"\x00\x00")
if end_index != -1:
data = data[: end_index + 1]
strtab_index = index
# The symbol table in memory is completely transformed and broken from how it appears on disk
# We need to process it last to fix the symbol table entries back to their correct values
elif sect_name == ".symtab":
symtab_address = address
continue
else:
# Compute based on the boundary of the next address-sorted section
try:
# Get the next section in order
next_address = sorted_addresses[index + 1]
size = next_address - address
except IndexError:
## We are at the last section so we need to pick a size
size = 0x10000
vollog.debug(f"Defaulting section {sect_name} to size {size:#x}")
# Read the section normally..
data = kernel_layer.read(address, size, pad=True)
# store the section information in order
updated_sections.append((sect_name, address, file_offset, data))
# Track sizes of each section
section_sizes[address] = len(data)
file_offset += len(data)
if symtab_address:
# Perform the painful demangling of symbol table structures
data = cls._fix_sym_table(
context,
vmlinux_name,
original_sections,
section_sizes,
sym_type,
st_fmt,
module,
)
if not data:
vollog.debug(
f"Could not construct a symbol table for module at {module.vol.offset}. Cannot recover."
)
return None
symtab_index = len(updated_sections)
# Manually add symtab with the correct data
updated_sections.append((".symtab", symtab_address, file_offset, data))
else:
vollog.debug(
f"Did not find a .symtab section for module at {module.vol.offset:#x}. Cannot recover."
)
return None
return updated_sections, strtab_index, symtab_index
@classmethod
def _make_elf_header(
cls, bits: int, sect_hdr_offset: int, num_sections: int
) -> 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 = "<I"
e_ident = (
b"\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"
)
e_machine_int = 3 # EM_X86_86
e_ehsize_int = 52
e_shentsize_int = 40
header_size = 52
else:
fmt = "<Q"
e_ident = (
b"\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"
)
e_machine_int = 0x3E # EM_X86_64
e_ehsize_int = 64
e_shentsize_int = 64
header_size = 64
e_type = struct.pack("<H", 1) # relocatable
e_machine = struct.pack("<H", e_machine_int)
e_version = struct.pack("<I", 1)
e_entry = b"\x00" * int(
bits / 8
) # The .init sections are freed after module load
e_phoff = b"\x00" * int(bits / 8) # No program headers
e_shoff = struct.pack(fmt, sect_hdr_offset)
e_flags = b"\x00\x00\x00\x00"
e_ehsize = struct.pack("<H", e_ehsize_int)
e_phentsize = b"\x00\x00"
e_phnum = b"\x00\x00"
e_shentsize = struct.pack("<H", e_shentsize_int)
e_shnum = struct.pack("<H", num_sections + 1)
e_shstrndx = struct.pack("<H", num_sections)
header = (
e_ident
+ e_type
+ e_machine
+ e_version
+ e_entry
+ e_phoff
+ e_shoff
+ e_flags
+ e_ehsize
+ e_phentsize
+ e_phnum
+ e_shentsize
+ e_shnum
+ e_shstrndx
)
# should never happen as we make the header ourselves
if len(header) != header_size:
vollog.error(
f"Making Elf header for arch {bits} created a header of {len(header)} bytes. Cannot proceed"
)
return None
return header
@classmethod
def _calc_sect_type(cls, section_name: str) -> 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 = "<I"
sect_size = 40
else:
fmt = "<Q"
sect_size = 64
sect_header_type_int = cls._calc_sect_type(name)
flags = cls._calc_sect_flags(name)
link = cls._calc_link(name, strtab_index, symtab_index, sect_header_type_int)
entsize = cls._calc_entsize(name, sect_header_type_int, bits)
try:
sh_name = struct.pack("<I", name_index)
sh_type = struct.pack("<I", sect_header_type_int)
sh_flags = struct.pack(fmt, flags)
sh_addr = struct.pack(fmt, address)
sh_offset = struct.pack(fmt, file_offset)
sh_size = struct.pack(fmt, size)
sh_link = struct.pack("<I", link)
sh_info = b"\x00" * 4
sh_addralign = struct.pack(fmt, 1)
sh_entsize = struct.pack(fmt, entsize)
# catch overflows of offset/address/size
except struct.error:
vollog.debug(
f"Unable to build section header for section {name} at address {address:#x}"
)
return None
data = (
sh_name
+ sh_type
+ sh_flags
+ sh_addr
+ sh_offset
+ sh_size
+ sh_link
+ sh_info
+ sh_addralign
+ sh_entsize
)
# This should never happen regardless of smear or other issues in the data. We build the structure to spec.
if len(data) != sect_size:
vollog.error(
f"Size of section data is {len(data)} expected {sect_size} for section {name} at address {address:#x}"
)
return None
return data
@classmethod
def extract_module(
cls,
context: interfaces.context.ContextInterface,
vmlinux_name: str,
module: extensions.module,
) -> 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):
"""
Gathers modules from the main kernel list
@@ -976,7 +1714,7 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface):
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: