Skip to content

Commit 56d28e7

Browse files
committed
Plugins: Windows.string speed enhancements by eve
1 parent 1d4a27e commit 56d28e7

File tree

1 file changed

+164
-85
lines changed

1 file changed

+164
-85
lines changed

volatility3/framework/plugins/windows/strings.py

Lines changed: 164 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
33
#
44

5+
from dataclasses import dataclass
56
import logging
67
import re
78
from typing import Dict, Generator, List, Set, Tuple, Optional
@@ -15,10 +16,62 @@
1516
vollog = logging.getLogger(__name__)
1617

1718

19+
@dataclass
20+
class MappingNode:
21+
def __init__(
22+
self,
23+
physical_addr_start,
24+
physical_addr_end,
25+
virtual_addr_start,
26+
virtual_addr_end,
27+
process_id,
28+
region,
29+
) -> None:
30+
self.physical_addr_start = physical_addr_start
31+
self.physical_addr_end = physical_addr_end
32+
self.virtual_addr_start = virtual_addr_start
33+
self.virtual_addr_end = virtual_addr_end
34+
self.process_id = process_id
35+
self.region = region
36+
37+
38+
class MappingTree:
39+
def __init__(self, root=None) -> None:
40+
self.root = root
41+
self.left = None
42+
self.right = None
43+
44+
def add(self, node):
45+
if isinstance(node, MappingNode):
46+
if self.root == None:
47+
self.root = node
48+
elif node.physical_addr_start < self.root.physical_addr_start:
49+
if self.left == None:
50+
self.left = MappingTree(node)
51+
else:
52+
self.left.add(node)
53+
else:
54+
if self.right == None:
55+
self.right = MappingTree(node)
56+
else:
57+
self.right.add(node)
58+
else:
59+
raise TypeError()
60+
61+
def at(self, point):
62+
if self.root:
63+
if self.root.physical_addr_start <= point <= self.root.physical_addr_end:
64+
yield self.root
65+
if point < self.root.physical_addr_start and self.left:
66+
yield from self.left.at(point)
67+
elif self.right:
68+
yield from self.right.at(point)
69+
70+
1871
class Strings(interfaces.plugins.PluginInterface):
1972
"""Reads output from the strings command and indicates which process(es) each string belongs to."""
2073

21-
_version = (1, 2, 0)
74+
_version = (2, 0, 0)
2275
_required_framework_version = (2, 0, 0)
2376
strings_pattern = re.compile(rb"^(?:\W*)([0-9]+)(?:\W*)(\w[\w\W]+)\n?")
2477

@@ -43,11 +96,16 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
4396
name="strings_file", description="Strings file"
4497
),
4598
]
46-
# TODO: Make URLRequirement that can accept a file address which the framework can open
4799

48100
def run(self):
49101
return renderers.TreeGrid(
50-
[("String", str), ("Physical Address", format_hints.Hex), ("Result", str)],
102+
[
103+
("String", str),
104+
("Region", str),
105+
("PID", int),
106+
("Physical Address", format_hints.Hex),
107+
("Virtual Address", format_hints.Hex),
108+
],
51109
self._generator(),
52110
)
53111

@@ -70,7 +128,7 @@ def _generator(self) -> Generator[Tuple, None, None]:
70128
line = strings_fp.readline()
71129
kernel = self.context.modules[self.config["kernel"]]
72130

73-
revmap = self.generate_mapping(
131+
revmap_tree = self.generate_mapping(
74132
self.context,
75133
kernel.layer_name,
76134
kernel.symbol_table_name,
@@ -81,26 +139,39 @@ def _generator(self) -> Generator[Tuple, None, None]:
81139
last_prog: float = 0
82140
line_count: float = 0
83141
num_strings = len(string_list)
84-
for offset, string in string_list:
142+
143+
for phys_offset, string in string_list:
85144
line_count += 1
86-
try:
87-
revmap_list = [
88-
name + ":" + hex(offset) for (name, offset) in revmap[offset >> 12]
89-
]
90-
except (IndexError, KeyError):
91-
revmap_list = ["FREE MEMORY"]
92-
yield (
93-
0,
94-
(
95-
str(string, "latin-1"),
96-
format_hints.Hex(offset),
97-
", ".join(revmap_list),
98-
),
99-
)
100-
prog = line_count / num_strings * 100
101-
if round(prog, 1) > last_prog:
102-
last_prog = round(prog, 1)
103-
self._progress_callback(prog, "Matching strings in memory")
145+
146+
matched_region = False
147+
for node in revmap_tree.at(phys_offset):
148+
matched_region = True
149+
150+
region_offset = phys_offset - node.physical_addr_start
151+
offset = node.virtual_addr_start + region_offset
152+
yield (
153+
0,
154+
(
155+
str(string.strip(), "latin-1"),
156+
node.region,
157+
node.process_id,
158+
format_hints.Hex(phys_offset),
159+
format_hints.Hex(offset),
160+
),
161+
)
162+
163+
if not matched_region:
164+
# no maps found for this offset
165+
yield (
166+
0,
167+
(
168+
str(string.strip(), "latin-1"),
169+
"Unallocated",
170+
-1,
171+
format_hints.Hex(phys_offset),
172+
format_hints.Hex(0x00),
173+
),
174+
)
104175

105176
def _parse_line(self, line: bytes) -> Tuple[int, bytes]:
106177
"""Parses a single line from a strings file.
@@ -118,83 +189,91 @@ def _parse_line(self, line: bytes) -> Tuple[int, bytes]:
118189
offset, string = match.group(1, 2)
119190
return int(offset), string
120191

121-
@classmethod
122192
def generate_mapping(
123193
cls,
124194
context: interfaces.context.ContextInterface,
125195
layer_name: str,
126196
symbol_table: str,
127197
progress_callback: constants.ProgressCallback = None,
128198
pid_list: Optional[List[int]] = None,
129-
) -> Dict[int, Set[Tuple[str, int]]]:
130-
"""Creates a reverse mapping between virtual addresses and physical
131-
addresses.
132-
133-
Args:
134-
context: the context for the method to run against
135-
layer_name: the layer to map against the string lines
136-
symbol_table: the name of the symbol table for the provided layer
137-
progress_callback: an optional callable to display progress
138-
pid_list: a lit of process IDs to consider when generating the reverse map
139-
140-
Returns:
141-
A mapping of virtual offsets to strings and physical offsets
142-
"""
199+
):
143200
filter = pslist.PsList.create_pid_filter(pid_list)
201+
revmap_tree = MappingTree()
144202

203+
# start with kernel mappings
145204
layer = context.layers[layer_name]
146-
reverse_map: Dict[int, Set[Tuple[str, int]]] = dict()
205+
min_kernel_addr = 2 ** (layer._maxvirtaddr - 1)
147206
if isinstance(layer, intel.Intel):
148207
# We don't care about errors, we just wanted chunks that map correctly
149-
for mapval in layer.mapping(0x0, layer.maximum_address, ignore_errors=True):
150-
offset, _, mapped_offset, mapped_size, maplayer = mapval
151-
for val in range(mapped_offset, mapped_offset + mapped_size, 0x1000):
152-
cur_set = reverse_map.get(val >> 12, set())
153-
cur_set.add(("kernel", offset))
154-
reverse_map[val >> 12] = cur_set
208+
for mapval in layer.mapping(
209+
min_kernel_addr, layer.maximum_address, ignore_errors=True
210+
):
211+
(
212+
virt_offset,
213+
virt_size,
214+
phy_offset,
215+
phy_mapping_size,
216+
_phy_layer_name,
217+
) = mapval
218+
219+
node = MappingNode(
220+
phy_offset,
221+
phy_offset + phy_mapping_size,
222+
virt_offset,
223+
virt_offset + virt_size,
224+
-1,
225+
"Kernel",
226+
)
227+
revmap_tree.add(node)
228+
155229
if progress_callback:
156230
progress_callback(
157-
(offset * 100) / layer.maximum_address,
158-
"Creating reverse kernel map",
231+
(virt_offset * 100) / layer.maximum_address,
232+
f"Creating custom tree mapping for kernel",
159233
)
160234

161-
# TODO: Include kernel modules
235+
# now process normal processes, ignoring kernel addrs
236+
for process in pslist.PsList.list_processes(context, layer_name, symbol_table):
237+
if not filter(process):
238+
proc_id = "Unknown"
239+
try:
240+
proc_id = process.UniqueProcessId
241+
proc_layer_name = process.add_process_layer()
242+
except exceptions.InvalidAddressException as excp:
243+
vollog.debug(
244+
"Process {}: invalid address {} in layer {}".format(
245+
proc_id, excp.invalid_address, excp.layer_name
246+
)
247+
)
248+
continue
162249

163-
for process in pslist.PsList.list_processes(
164-
context, layer_name, symbol_table
165-
):
166-
if not filter(process):
167-
proc_id = "Unknown"
168-
try:
169-
proc_id = process.UniqueProcessId
170-
proc_layer_name = process.add_process_layer()
171-
except exceptions.InvalidAddressException as excp:
172-
vollog.debug(
173-
"Process {}: invalid address {} in layer {}".format(
174-
proc_id, excp.invalid_address, excp.layer_name
175-
)
250+
proc_layer = context.layers[proc_layer_name]
251+
max_proc_addr = (2 ** (proc_layer._maxvirtaddr - 1)) - 1
252+
if isinstance(proc_layer, linear.LinearlyMappedLayer):
253+
for mapval in proc_layer.mapping(
254+
0, max_proc_addr, ignore_errors=True
255+
):
256+
(
257+
virt_offset,
258+
virt_size,
259+
phy_offset,
260+
phy_mapping_size,
261+
_phy_layer_name,
262+
) = mapval
263+
264+
node = MappingNode(
265+
phy_offset,
266+
phy_offset + phy_mapping_size,
267+
virt_offset,
268+
virt_offset + virt_size,
269+
proc_id,
270+
"Process",
176271
)
177-
continue
178-
179-
proc_layer = context.layers[proc_layer_name]
180-
if isinstance(proc_layer, linear.LinearlyMappedLayer):
181-
for mapval in proc_layer.mapping(
182-
0x0, proc_layer.maximum_address, ignore_errors=True
183-
):
184-
mapped_offset, _, offset, mapped_size, maplayer = mapval
185-
for val in range(
186-
mapped_offset, mapped_offset + mapped_size, 0x1000
187-
):
188-
cur_set = reverse_map.get(mapped_offset >> 12, set())
189-
cur_set.add(
190-
(f"Process {process.UniqueProcessId}", offset)
191-
)
192-
reverse_map[mapped_offset >> 12] = cur_set
193-
# FIXME: make the progress for all processes, rather than per-process
194-
if progress_callback:
195-
progress_callback(
196-
(offset * 100) / layer.maximum_address,
197-
f"Creating mapping for task {process.UniqueProcessId}",
198-
)
199-
200-
return reverse_map
272+
revmap_tree.add(node)
273+
274+
if progress_callback:
275+
progress_callback(
276+
(virt_offset * 100) / max_proc_addr,
277+
f"Creating custom tree mapping for task {proc_id}",
278+
)
279+
return revmap_tree

0 commit comments

Comments
 (0)