Plugins: Windows.string speed enhancements by eve

This commit is contained in:
Mike Auty
2025-10-11 22:35:54 +01:00
parent 7536e9dd14
commit 1f9d58dc61
+172 -74
View File
@@ -2,6 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from dataclasses import dataclass
import logging
import re
from typing import Dict, Generator, List, Set, Tuple, Optional
@@ -15,13 +16,66 @@ from volatility3.plugins.windows import pslist
vollog = logging.getLogger(__name__)
@dataclass
class MappingNode:
def __init__(
self,
physical_addr_start,
physical_addr_end,
virtual_addr_start,
virtual_addr_end,
process_id,
region,
) -> None:
self.physical_addr_start = physical_addr_start
self.physical_addr_end = physical_addr_end
self.virtual_addr_start = virtual_addr_start
self.virtual_addr_end = virtual_addr_end
self.process_id = process_id
self.region = region
class MappingTree:
def __init__(self, root=None) -> None:
self.root = root
self.left = None
self.right = None
def add(self, node):
if isinstance(node, MappingNode):
if self.root == None:
self.root = node
elif node.physical_addr_start < self.root.physical_addr_start:
if self.left == None:
self.left = MappingTree(node)
else:
self.left.add(node)
else:
if self.right == None:
self.right = MappingTree(node)
else:
self.right.add(node)
else:
raise TypeError()
def at(self, point):
if self.root:
if self.root.physical_addr_start <= point <= self.root.physical_addr_end:
yield self.root
if point < self.root.physical_addr_start and self.left:
yield from self.left.at(point)
elif self.right:
yield from self.right.at(point)
class Strings(interfaces.plugins.PluginInterface):
"""Reads output from the strings command and indicates which process(es) each string belongs to."""
_required_framework_version = (2, 0, 0)
# 2.0.0 - change signature of `generate_mapping`
_version = (2, 0, 0)
# 3.0.0 - Interval mapping
_version = (3, 0, 0)
strings_pattern = re.compile(rb"^(?:\W*)([0-9]+)(?:\W*)(\w[\w\W]+)\n?")
@@ -46,11 +100,16 @@ class Strings(interfaces.plugins.PluginInterface):
name="strings_file", description="Strings file"
),
]
# TODO: Make URLRequirement that can accept a file address which the framework can open
def run(self):
return renderers.TreeGrid(
[("String", str), ("Physical Address", format_hints.Hex), ("Result", str)],
[
("String", str),
("Region", str),
("PID", int),
("Physical Address", format_hints.Hex),
("Virtual Address", format_hints.Hex),
],
self._generator(),
)
@@ -71,10 +130,12 @@ class Strings(interfaces.plugins.PluginInterface):
except ValueError:
vollog.error(f"Line in unrecognized format: line {count}")
line = strings_fp.readline()
kernel = self.context.modules[self.config["kernel"]]
revmap = self.generate_mapping(
context=self.context,
kernel_module_name=self.config["kernel"],
revmap_tree = self.generate_mapping(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
progress_callback=self._progress_callback,
pid_list=self.config["pid"],
)
@@ -82,26 +143,39 @@ class Strings(interfaces.plugins.PluginInterface):
last_prog: float = 0
line_count: float = 0
num_strings = len(string_list)
for offset, string in string_list:
for phys_offset, string in string_list:
line_count += 1
try:
revmap_list = [
name + ":" + hex(offset) for (name, offset) in revmap[offset >> 12]
]
except (IndexError, KeyError):
revmap_list = ["FREE MEMORY"]
yield (
0,
(
str(string, "latin-1"),
format_hints.Hex(offset),
", ".join(revmap_list),
),
)
prog = line_count / num_strings * 100
if round(prog, 1) > last_prog:
last_prog = round(prog, 1)
self._progress_callback(prog, "Matching strings in memory")
matched_region = False
for node in revmap_tree.at(phys_offset):
matched_region = True
region_offset = phys_offset - node.physical_addr_start
offset = node.virtual_addr_start + region_offset
yield (
0,
(
str(string.strip(), "latin-1"),
node.region,
node.process_id,
format_hints.Hex(phys_offset),
format_hints.Hex(offset),
),
)
if not matched_region:
# no maps found for this offset
yield (
0,
(
str(string.strip(), "latin-1"),
"Unallocated",
-1,
format_hints.Hex(phys_offset),
format_hints.Hex(0x00),
),
)
def _parse_line(self, line: bytes) -> Tuple[int, bytes]:
"""Parses a single line from a strings file.
@@ -123,7 +197,8 @@ class Strings(interfaces.plugins.PluginInterface):
def generate_mapping(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
layer_name: str,
symbol_table: str,
progress_callback: constants.ProgressCallback = None,
pid_list: Optional[List[int]] = None,
) -> Dict[int, Set[Tuple[str, int]]]:
@@ -132,7 +207,8 @@ class Strings(interfaces.plugins.PluginInterface):
Args:
context: the context for the method to run against
kernel_module_name: the name of the module forthe kernel
layer_name: the name of the windows intel layer to be scanned
symbol_table: the name of the kernel symbol table
progress_callback: an optional callable to display progress
pid_list: a lit of process IDs to consider when generating the reverse map
@@ -140,60 +216,82 @@ class Strings(interfaces.plugins.PluginInterface):
A mapping of virtual offsets to strings and physical offsets
"""
filter = pslist.PsList.create_pid_filter(pid_list)
revmap_tree = MappingTree()
kernel = context.modules[kernel_module_name]
layer = context.layers[kernel.layer_name]
reverse_map: Dict[int, Set[Tuple[str, int]]] = dict()
# start with kernel mappings
layer = context.layers[layer_name]
min_kernel_addr = 2 ** (layer._maxvirtaddr - 1)
if isinstance(layer, intel.Intel):
# We don't care about errors, we just wanted chunks that map correctly
for mapval in layer.mapping(0x0, layer.maximum_address, ignore_errors=True):
offset, _, mapped_offset, mapped_size, maplayer = mapval
for val in range(mapped_offset, mapped_offset + mapped_size, 0x1000):
cur_set = reverse_map.get(val >> 12, set())
cur_set.add(("kernel", offset))
reverse_map[val >> 12] = cur_set
for mapval in layer.mapping(
min_kernel_addr, layer.maximum_address, ignore_errors=True
):
(
virt_offset,
virt_size,
phy_offset,
phy_mapping_size,
_phy_layer_name,
) = mapval
node = MappingNode(
phy_offset,
phy_offset + phy_mapping_size,
virt_offset,
virt_offset + virt_size,
-1,
"Kernel",
)
revmap_tree.add(node)
if progress_callback:
progress_callback(
(offset * 100) / layer.maximum_address,
"Creating reverse kernel map",
(virt_offset * 100) / layer.maximum_address,
f"Creating custom tree mapping for kernel",
)
# TODO: Include kernel modules
for process in pslist.PsList.list_processes(
context=context, kernel_module_name=kernel_module_name
):
if not filter(process):
proc_id = "Unknown"
try:
proc_id = process.UniqueProcessId
proc_layer_name = process.add_process_layer()
except exceptions.InvalidAddressException as excp:
vollog.debug(
f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}"
# now process normal processes, ignoring kernel addrs
for process in pslist.PsList.list_processes(context, layer_name, symbol_table):
if not filter(process):
proc_id = "Unknown"
try:
proc_id = process.UniqueProcessId
proc_layer_name = process.add_process_layer()
except exceptions.InvalidAddressException as excp:
vollog.debug(
"Process {}: invalid address {} in layer {}".format(
proc_id, excp.invalid_address, excp.layer_name
)
continue
)
continue
proc_layer = context.layers[proc_layer_name]
if isinstance(proc_layer, linear.LinearlyMappedLayer):
for mapval in proc_layer.mapping(
0x0, proc_layer.maximum_address, ignore_errors=True
):
mapped_offset, _, offset, mapped_size, _maplayer = mapval
for val in range(
mapped_offset, mapped_offset + mapped_size, 0x1000
):
cur_set = reverse_map.get(mapped_offset >> 12, set())
cur_set.add(
(f"Process {process.UniqueProcessId}", offset)
)
reverse_map[mapped_offset >> 12] = cur_set
# FIXME: make the progress for all processes, rather than per-process
if progress_callback:
progress_callback(
(offset * 100) / layer.maximum_address,
f"Creating mapping for task {process.UniqueProcessId}",
)
proc_layer = context.layers[proc_layer_name]
max_proc_addr = (2 ** (proc_layer._maxvirtaddr - 1)) - 1
if isinstance(proc_layer, linear.LinearlyMappedLayer):
for mapval in proc_layer.mapping(
0, max_proc_addr, ignore_errors=True
):
(
virt_offset,
virt_size,
phy_offset,
phy_mapping_size,
_phy_layer_name,
) = mapval
return reverse_map
node = MappingNode(
phy_offset,
phy_offset + phy_mapping_size,
virt_offset,
virt_offset + virt_size,
proc_id,
"Process",
)
revmap_tree.add(node)
if progress_callback:
progress_callback(
(virt_offset * 100) / max_proc_addr,
f"Creating custom tree mapping for task {proc_id}",
)
return revmap_tree