Skip to content

Commit 53b24d3

Browse files
committed
First attempt at adding a --dump option to linux.proc, aim to be similar to windows.vadinfo --dump
1 parent 057d314 commit 53b24d3

File tree

1 file changed

+141
-2
lines changed
  • volatility3/framework/plugins/linux

1 file changed

+141
-2
lines changed

volatility3/framework/plugins/linux/proc.py

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44
"""A module containing a collection of plugins that produce data typically
55
found in Linux's /proc file system."""
66

7-
from volatility3.framework import renderers
7+
import logging
8+
from typing import Callable, List, Generator, Iterable, Type, Optional
9+
10+
from volatility3.framework import renderers, interfaces, exceptions
811
from volatility3.framework.configuration import requirements
912
from volatility3.framework.interfaces import plugins
1013
from volatility3.framework.objects import utility
1114
from volatility3.framework.renderers import format_hints
1215
from volatility3.plugins.linux import pslist
1316

17+
vollog = logging.getLogger(__name__)
1418

1519
class Maps(plugins.PluginInterface):
1620
"""Lists all memory maps for all processes."""
1721

1822
_required_framework_version = (2, 0, 0)
23+
MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb
1924

2025
@classmethod
2126
def get_requirements(cls):
@@ -35,16 +40,138 @@ def get_requirements(cls):
3540
element_type=int,
3641
optional=True,
3742
),
43+
requirements.BooleanRequirement(
44+
name="dump",
45+
description="Extract listed memory segments",
46+
default=False,
47+
optional=True,
48+
),
49+
requirements.ListRequirement(
50+
name="address",
51+
description="Process virtual memory address to include "
52+
"(all other address ranges are excluded). This must be "
53+
"a base address, not an address within the desired range.",
54+
element_type=int,
55+
optional=True,
56+
),
57+
requirements.IntRequirement(
58+
name="maxsize",
59+
description="Maximum size for dumped VMA sections "
60+
"(all the bigger sections will be ignored)",
61+
default=cls.MAXSIZE_DEFAULT,
62+
optional=True,
63+
),
3864
]
3965

66+
@classmethod
67+
def list_vmas(
68+
cls,
69+
task: interfaces.objects.ObjectInterface,
70+
filter_func: Callable[
71+
[interfaces.objects.ObjectInterface], bool
72+
] = lambda _: False,
73+
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
74+
"""Lists the Virtual Memory Areas of a specific process.
75+
76+
Args:
77+
task: task object from which to list the vma
78+
filter_func: Function to take a vma and return True if it should be filtered out
79+
80+
Returns:
81+
A list of vmas based on the task and filtered based on the filter function
82+
"""
83+
if task.mm:
84+
for vma in task.mm.get_mmap_iter():
85+
if not filter_func(vma):
86+
yield vma
87+
88+
@classmethod
89+
def vma_dump(
90+
cls,
91+
context: interfaces.context.ContextInterface,
92+
task: interfaces.objects.ObjectInterface,
93+
vma: interfaces.objects.ObjectInterface,
94+
open_method: Type[interfaces.plugins.FileHandlerInterface],
95+
maxsize: int = MAXSIZE_DEFAULT,
96+
) -> Optional[interfaces.plugins.FileHandlerInterface]:
97+
"""Extracts the complete data for VMA as a FileInterface.
98+
99+
Args:
100+
context: The context to retrieve required elements (layers, symbol tables) from
101+
task: an task_struct instance
102+
vma: The suspected VMA to extract (ObjectInterface)
103+
open_method: class to provide context manager for opening the file
104+
maxsize: Max size of VMA section (default MAXSIZE_DEFAULT)
105+
106+
Returns:
107+
An open FileInterface object containing the complete data for the task or None in the case of failure
108+
"""
109+
try:
110+
vm_start = vma.vm_start
111+
vm_end = vma.vm_end
112+
except AttributeError:
113+
vollog.debug("Unable to find the vm_start and vm_end")
114+
return None
115+
116+
vm_size = vm_end - vm_start
117+
if 0 < maxsize < vm_size:
118+
vollog.debug(
119+
f"Skip virtual memory dump {vm_start:#x}-{vm_end:#x} due to maxsize limit"
120+
)
121+
return None
122+
123+
pid = "Unknown"
124+
try:
125+
pid = task.tgid
126+
proc_layer_name = task.add_process_layer()
127+
except exceptions.InvalidAddressException as excp:
128+
vollog.debug(
129+
"Process {}: invalid address {} in layer {}".format(
130+
pid, excp.invalid_address, excp.layer_name
131+
)
132+
)
133+
return None
134+
135+
proc_layer = context.layers[proc_layer_name]
136+
file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp"
137+
try:
138+
file_handle = open_method(file_name)
139+
chunk_size = 1024 * 1024 * 10
140+
offset = vm_start
141+
while offset < vm_start + vm_size:
142+
to_read = min(chunk_size, vm_start + vm_size - offset)
143+
data = proc_layer.read(offset, to_read, pad=True)
144+
if not data:
145+
break
146+
file_handle.write(data)
147+
offset += to_read
148+
149+
except Exception as excp:
150+
vollog.debug(f"Unable to dump virtual memory {file_name}: {excp}")
151+
return None
152+
153+
return file_handle
154+
40155
def _generator(self, tasks):
156+
# build filter for addresses if required
157+
address_list = self.config.get("address", [])
158+
if address_list == []:
159+
# do not filter as no address_list was supplied
160+
filter_func = lambda _: False
161+
else:
162+
# filter for any vm_start that matches the supplied address config
163+
def filter_function(x: interfaces.objects.ObjectInterface) -> bool:
164+
return x.vm_start not in address_list
165+
166+
filter_func = filter_function
167+
41168
for task in tasks:
42169
if not task.mm:
43170
continue
44171

45172
name = utility.array_to_string(task.comm)
46173

47-
for vma in task.mm.get_mmap_iter():
174+
for vma in self.list_vmas(task, filter_func=filter_func):
48175
flags = vma.get_protection()
49176
page_offset = vma.get_page_offset()
50177
major = 0
@@ -61,6 +188,16 @@ def _generator(self, tasks):
61188

62189
path = vma.get_name(self.context, task)
63190

191+
file_output = "Disabled"
192+
if self.config["dump"]:
193+
file_handle = self.vma_dump(
194+
self.context, task, vma, self.open, self.config["maxsize"]
195+
)
196+
file_output = "Error outputting file"
197+
if file_handle:
198+
file_handle.close()
199+
file_output = file_handle.preferred_filename
200+
64201
yield (
65202
0,
66203
(
@@ -74,6 +211,7 @@ def _generator(self, tasks):
74211
minor,
75212
inode,
76213
path,
214+
file_output,
77215
),
78216
)
79217

@@ -92,6 +230,7 @@ def run(self):
92230
("Minor", int),
93231
("Inode", int),
94232
("File Path", str),
233+
("File output", str),
95234
],
96235
self._generator(
97236
pslist.PsList.list_tasks(

0 commit comments

Comments
 (0)