Skip to content

Commit 2efb4e7

Browse files
committed
Merge branch 'develop' into linux_boottime_support
1 parent a05397e commit 2efb4e7

File tree

4 files changed

+383
-102
lines changed

4 files changed

+383
-102
lines changed

volatility3/framework/constants/linux/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,11 @@ def flags(self) -> str:
344344

345345
# Boot time
346346
NSEC_PER_SEC = 1e9
347+
348+
349+
# Valid sizes for modules. Note that the Linux kernel does not define these values; they
350+
# are based on empirical observations of typical memory allocations for kernel modules.
351+
# We use this to verify that the found module falls within reasonable limits.
352+
MODULE_MAXIMUM_CORE_SIZE = 20000000
353+
MODULE_MAXIMUM_CORE_TEXT_SIZE = 20000000
354+
MODULE_MINIMUM_SIZE = 4096
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
2+
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
3+
#
4+
import logging
5+
from typing import List, Set, Tuple, Iterable
6+
from volatility3.framework import renderers, interfaces, exceptions, objects
7+
from volatility3.framework.constants import architectures
8+
from volatility3.framework.renderers import format_hints
9+
from volatility3.framework.configuration import requirements
10+
from volatility3.plugins.linux import lsmod
11+
12+
vollog = logging.getLogger(__name__)
13+
14+
15+
class Hidden_modules(interfaces.plugins.PluginInterface):
16+
"""Carves memory to find hidden kernel modules"""
17+
18+
_required_framework_version = (2, 10, 0)
19+
20+
_version = (1, 0, 0)
21+
22+
@classmethod
23+
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
24+
return [
25+
requirements.ModuleRequirement(
26+
name="kernel",
27+
description="Linux kernel",
28+
architectures=architectures.LINUX_ARCHS,
29+
),
30+
requirements.PluginRequirement(
31+
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
32+
),
33+
]
34+
35+
@staticmethod
36+
def get_modules_memory_boundaries(
37+
context: interfaces.context.ContextInterface,
38+
vmlinux_module_name: str,
39+
) -> Tuple[int]:
40+
"""Determine the boundaries of the module allocation area
41+
42+
Args:
43+
context: The context to retrieve required elements (layers, symbol tables) from
44+
vmlinux_module_name: The name of the kernel module on which to operate
45+
46+
Returns:
47+
A tuple containing the minimum and maximum addresses for the module allocation area.
48+
"""
49+
vmlinux = context.modules[vmlinux_module_name]
50+
if vmlinux.has_symbol("mod_tree"):
51+
# Kernel >= 5.19 58d208de3e8d87dbe196caf0b57cc58c7a3836ca
52+
mod_tree = vmlinux.object_from_symbol("mod_tree")
53+
modules_addr_min = mod_tree.addr_min
54+
modules_addr_max = mod_tree.addr_max
55+
elif vmlinux.has_symbol("module_addr_min"):
56+
# 2.6.27 <= kernel < 5.19 3a642e99babe0617febb6f402e1e063479f489db
57+
modules_addr_min = vmlinux.object_from_symbol("module_addr_min")
58+
modules_addr_max = vmlinux.object_from_symbol("module_addr_max")
59+
60+
if isinstance(modules_addr_min, objects.Void):
61+
raise exceptions.VolatilityException(
62+
"Your ISF symbols lack type information. You may need to update the"
63+
"ISF using the latest version of dwarf2json"
64+
)
65+
else:
66+
raise exceptions.VolatilityException(
67+
"Cannot find the module memory allocation area. Unsupported kernel"
68+
)
69+
70+
return modules_addr_min, modules_addr_max
71+
72+
@classmethod
73+
def _get_module_address_alignment(
74+
cls,
75+
context: interfaces.context.ContextInterface,
76+
vmlinux_module_name: str,
77+
) -> int:
78+
"""Obtain the module memory address alignment.
79+
80+
struct module is aligned to the L1 cache line, which is typically 64 bytes for most
81+
common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this
82+
will still work.
83+
84+
Args:
85+
context: The context to retrieve required elements (layers, symbol tables) from
86+
vmlinux_module_name: The name of the kernel module on which to operate
87+
88+
Returns:
89+
The struct module alignment
90+
"""
91+
# FIXME: When dwarf2json/ISF supports type alignments. Read it directly from the type metadata
92+
# Additionally, while 'context' and 'vmlinux_module_name' are currently unused, they will be
93+
# essential for retrieving type metadata in the future.
94+
return 64
95+
96+
@staticmethod
97+
def _validate_alignment_patterns(
98+
addresses: Iterable[int],
99+
address_alignment: int,
100+
) -> bool:
101+
"""Check if the memory addresses meet our alignments patterns
102+
103+
Args:
104+
addresses: Iterable with the address values
105+
address_alignment: Number of bytes for alignment validation
106+
107+
Returns:
108+
True if all the addresses meet the alignment
109+
"""
110+
return all(addr % address_alignment == 0 for addr in addresses)
111+
112+
@classmethod
113+
def get_hidden_modules(
114+
cls,
115+
context: interfaces.context.ContextInterface,
116+
vmlinux_module_name: str,
117+
known_module_addresses: Set[int],
118+
modules_memory_boundaries: Tuple,
119+
) -> Iterable[interfaces.objects.ObjectInterface]:
120+
"""Enumerate hidden modules by taking advantage of memory address alignment patterns
121+
122+
This technique is much faster and uses less memory than the traditional scan method
123+
in Volatility2, but it doesn't work with older kernels.
124+
125+
From kernels 4.2 struct module allocation are aligned to the L1 cache line size.
126+
In i386/amd64/arm64 this is typically 64 bytes. However, this can be changed in
127+
the Linux kernel configuration via CONFIG_X86_L1_CACHE_SHIFT. The alignment can
128+
also be obtained from the DWARF info i.e. DW_AT_alignment<64>, but dwarf2json
129+
doesn't support this feature yet.
130+
In kernels < 4.2, alignment attributes are absent in the struct module, meaning
131+
alignment cannot be guaranteed. Therefore, for older kernels, it's better to use
132+
the traditional scan technique.
133+
134+
Args:
135+
context: The context to retrieve required elements (layers, symbol tables) from
136+
vmlinux_module_name: The name of the kernel module on which to operate
137+
known_module_addresses: Set with known module addresses
138+
modules_memory_boundaries: Minimum and maximum address boundaries for module allocation.
139+
Yields:
140+
module objects
141+
"""
142+
vmlinux = context.modules[vmlinux_module_name]
143+
vmlinux_layer = context.layers[vmlinux.layer_name]
144+
145+
module_addr_min, module_addr_max = modules_memory_boundaries
146+
module_address_alignment = cls._get_module_address_alignment(
147+
context, vmlinux_module_name
148+
)
149+
if not cls._validate_alignment_patterns(
150+
known_module_addresses, module_address_alignment
151+
):
152+
vollog.warning(
153+
f"Module addresses aren't aligned to {module_address_alignment} bytes. "
154+
"Switching to 1 byte aligment scan method."
155+
)
156+
module_address_alignment = 1
157+
158+
mkobj_offset = vmlinux.get_type("module").relative_child_offset("mkobj")
159+
mod_offset = vmlinux.get_type("module_kobject").relative_child_offset("mod")
160+
offset_to_mkobj_mod = mkobj_offset + mod_offset
161+
mod_member_template = vmlinux.get_type("module_kobject").child_template("mod")
162+
mod_size = mod_member_template.size
163+
mod_member_data_format = mod_member_template.data_format
164+
165+
for module_addr in range(
166+
module_addr_min, module_addr_max, module_address_alignment
167+
):
168+
if module_addr in known_module_addresses:
169+
continue
170+
171+
try:
172+
# This is just a pre-filter. Module readability and consistency are verified in module.is_valid()
173+
self_referential_bytes = vmlinux_layer.read(
174+
module_addr + offset_to_mkobj_mod, mod_size
175+
)
176+
self_referential = objects.convert_data_to_value(
177+
self_referential_bytes, int, mod_member_data_format
178+
)
179+
if self_referential != module_addr:
180+
continue
181+
except (
182+
exceptions.PagedInvalidAddressException,
183+
exceptions.InvalidAddressException,
184+
):
185+
continue
186+
187+
module = vmlinux.object("module", offset=module_addr, absolute=True)
188+
if module and module.is_valid():
189+
yield module
190+
191+
@classmethod
192+
def get_lsmod_module_addresses(
193+
cls,
194+
context: interfaces.context.ContextInterface,
195+
vmlinux_module_name: str,
196+
) -> Set[int]:
197+
"""Obtain a set the known module addresses from linux.lsmod plugin
198+
199+
Args:
200+
context: The context to retrieve required elements (layers, symbol tables) from
201+
vmlinux_module_name: The name of the kernel module on which to operate
202+
203+
Returns:
204+
A set containing known kernel module addresses
205+
"""
206+
vmlinux = context.modules[vmlinux_module_name]
207+
vmlinux_layer = context.layers[vmlinux.layer_name]
208+
209+
known_module_addresses = {
210+
vmlinux_layer.canonicalize(module.vol.offset)
211+
for module in lsmod.Lsmod.list_modules(context, vmlinux_module_name)
212+
}
213+
return known_module_addresses
214+
215+
def _generator(self):
216+
vmlinux_module_name = self.config["kernel"]
217+
known_module_addresses = self.get_lsmod_module_addresses(
218+
self.context, vmlinux_module_name
219+
)
220+
modules_memory_boundaries = self.get_modules_memory_boundaries(
221+
self.context, vmlinux_module_name
222+
)
223+
for module in self.get_hidden_modules(
224+
self.context,
225+
vmlinux_module_name,
226+
known_module_addresses,
227+
modules_memory_boundaries,
228+
):
229+
module_addr = module.vol.offset
230+
module_name = module.get_name() or renderers.NotAvailableValue()
231+
fields = (format_hints.Hex(module_addr), module_name)
232+
yield (0, fields)
233+
234+
def run(self):
235+
if self.context.symbol_space.verify_table_versions(
236+
"dwarf2json", lambda version, _: (not version) or version < (0, 8, 0)
237+
):
238+
raise exceptions.SymbolSpaceError(
239+
"Invalid symbol table, please ensure the ISF table produced by dwarf2json was created with version 0.8.0 or later"
240+
)
241+
242+
headers = [
243+
("Address", format_hints.Hex),
244+
("Name", str),
245+
]
246+
return renderers.TreeGrid(headers, self._generator())

volatility3/framework/symbols/linux/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ def _slot_to_nodep(self, slot) -> int:
615615

616616
return nodep
617617

618-
def _iter_node(self, nodep, height) -> int:
618+
def _iter_node(self, nodep, height) -> Iterator[int]:
619619
node = self.nodep_to_node(nodep)
620620
node_slots = node.slots
621621
for off in range(self.CHUNK_SIZE):
@@ -632,7 +632,7 @@ def _iter_node(self, nodep, height) -> int:
632632
for child_node in self._iter_node(nodep, height - 1):
633633
yield child_node
634634

635-
def get_entries(self, root: interfaces.objects.ObjectInterface) -> int:
635+
def get_entries(self, root: interfaces.objects.ObjectInterface) -> Iterator[int]:
636636
"""Walks the tree data structure
637637
638638
Args:
@@ -818,7 +818,7 @@ def __init__(
818818
self._page_cache = page_cache
819819
self._idstorage = IDStorage.choose_id_storage(context, kernel_module_name)
820820

821-
def get_cached_pages(self) -> interfaces.objects.ObjectInterface:
821+
def get_cached_pages(self) -> Iterator[interfaces.objects.ObjectInterface]:
822822
"""Returns all page cache contents
823823
824824
Yields:

0 commit comments

Comments
 (0)