Skip to content

Commit 5aa0a64

Browse files
authored
Merge pull request #884 from eve-mem/linux_proc_dump
First attempt at adding a --dump option to linux.proc
2 parents e17cb15 + fa5af08 commit 5aa0a64

File tree

1 file changed

+171
-4
lines changed
  • volatility3/framework/plugins/linux

1 file changed

+171
-4
lines changed

volatility3/framework/plugins/linux/proc.py

Lines changed: 171 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,25 @@
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, Generator, 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__)
18+
1419

1520
class Maps(plugins.PluginInterface):
1621
"""Lists all memory maps for all processes."""
1722

1823
_required_framework_version = (2, 0, 0)
24+
_version = (1, 0, 0)
25+
MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb
1926

2027
@classmethod
2128
def get_requirements(cls):
@@ -35,16 +42,149 @@ def get_requirements(cls):
3542
element_type=int,
3643
optional=True,
3744
),
45+
requirements.BooleanRequirement(
46+
name="dump",
47+
description="Extract listed memory segments",
48+
default=False,
49+
optional=True,
50+
),
51+
requirements.ListRequirement(
52+
name="address",
53+
description="Process virtual memory addresses to include "
54+
"(all other VMA sections are excluded). This can be any "
55+
"virtual address within the VMA section.",
56+
element_type=int,
57+
optional=True,
58+
),
59+
requirements.IntRequirement(
60+
name="maxsize",
61+
description="Maximum size for dumped VMA sections "
62+
"(all the bigger sections will be ignored)",
63+
default=cls.MAXSIZE_DEFAULT,
64+
optional=True,
65+
),
3866
]
3967

68+
@classmethod
69+
def list_vmas(
70+
cls,
71+
task: interfaces.objects.ObjectInterface,
72+
filter_func: Callable[
73+
[interfaces.objects.ObjectInterface], bool
74+
] = lambda _: True,
75+
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
76+
"""Lists the Virtual Memory Areas of a specific process.
77+
78+
Args:
79+
task: task object from which to list the vma
80+
filter_func: Function to take a vma and return False if it should be filtered out
81+
82+
Returns:
83+
Yields vmas based on the task and filtered based on the filter function
84+
"""
85+
if task.mm:
86+
for vma in task.mm.get_vma_iter():
87+
if filter_func(vma):
88+
yield vma
89+
else:
90+
vollog.debug(
91+
f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.pid} due to filter_func"
92+
)
93+
else:
94+
vollog.debug(
95+
f"Excluded pid {task.pid} as there is no mm member. It is likely a kernel thread."
96+
)
97+
98+
@classmethod
99+
def vma_dump(
100+
cls,
101+
context: interfaces.context.ContextInterface,
102+
task: interfaces.objects.ObjectInterface,
103+
vm_start: int,
104+
vm_end: int,
105+
open_method: Type[interfaces.plugins.FileHandlerInterface],
106+
maxsize: int = MAXSIZE_DEFAULT,
107+
) -> Optional[interfaces.plugins.FileHandlerInterface]:
108+
"""Extracts the complete data for VMA as a FileInterface.
109+
110+
Args:
111+
context: The context to retrieve required elements (layers, symbol tables) from
112+
task: an task_struct instance
113+
vm_start: The start virtual address from the vma to dump
114+
vm_end: The end virtual address from the vma to dump
115+
open_method: class to provide context manager for opening the file
116+
maxsize: Max size of VMA section (default MAXSIZE_DEFAULT)
117+
118+
Returns:
119+
An open FileInterface object containing the complete data for the task or None in the case of failure
120+
"""
121+
pid = task.pid
122+
123+
try:
124+
proc_layer_name = task.add_process_layer()
125+
except exceptions.InvalidAddressException as excp:
126+
vollog.debug(
127+
"Process {}: invalid address {} in layer {}".format(
128+
pid, excp.invalid_address, excp.layer_name
129+
)
130+
)
131+
return None
132+
vm_size = vm_end - vm_start
133+
134+
# check if vm_size is negative, this should never happen.
135+
if vm_size < 0:
136+
vollog.warning(
137+
f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is negative."
138+
)
139+
return None
140+
# check if vm_size is larger than the maxsize limit, and therefore is not saved out.
141+
if maxsize <= vm_size:
142+
vollog.warning(
143+
f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is larger than maxsize limit of {maxsize}"
144+
)
145+
return None
146+
proc_layer = context.layers[proc_layer_name]
147+
file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp"
148+
try:
149+
file_handle = open_method(file_name)
150+
chunk_size = 1024 * 1024 * 10
151+
offset = vm_start
152+
while offset < vm_start + vm_size:
153+
to_read = min(chunk_size, vm_start + vm_size - offset)
154+
data = proc_layer.read(offset, to_read, pad=True)
155+
file_handle.write(data)
156+
offset += to_read
157+
except Exception as excp:
158+
vollog.debug(f"Unable to dump virtual memory {file_name}: {excp}")
159+
return None
160+
return file_handle
161+
40162
def _generator(self, tasks):
163+
# build filter for addresses if required
164+
address_list = self.config.get("address", None)
165+
if not address_list:
166+
# do not filter as no address_list was supplied
167+
vma_filter_func = lambda _: True
168+
else:
169+
# filter for any vm_start that matches the supplied address config
170+
def vma_filter_function(x: interfaces.objects.ObjectInterface) -> bool:
171+
addrs_in_vma = [
172+
addr for addr in address_list if x.vm_start <= addr <= x.vm_end
173+
]
174+
175+
# if any of the user supplied addresses would fall within this vma return true
176+
if addrs_in_vma:
177+
return True
178+
else:
179+
return False
180+
181+
vma_filter_func = vma_filter_function
41182
for task in tasks:
42183
if not task.mm:
43184
continue
44-
45185
name = utility.array_to_string(task.comm)
46186

47-
for vma in task.mm.get_vma_iter():
187+
for vma in self.list_vmas(task, filter_func=vma_filter_func):
48188
flags = vma.get_protection()
49189
page_offset = vma.get_page_offset()
50190
major = 0
@@ -58,9 +198,34 @@ def _generator(self, tasks):
58198
major = inode_object.i_sb.major
59199
minor = inode_object.i_sb.minor
60200
inode = inode_object.i_ino
61-
62201
path = vma.get_name(self.context, task)
63202

203+
file_output = "Disabled"
204+
if self.config["dump"]:
205+
file_output = "Error outputting file"
206+
try:
207+
vm_start = vma.vm_start
208+
vm_end = vma.vm_end
209+
except AttributeError:
210+
vollog.debug(
211+
f"Unable to find the vm_start and vm_end for vma at {vma.vol.offset:#x} for pid {task.pid}"
212+
)
213+
vm_start = None
214+
vm_end = None
215+
if vm_start and vm_end:
216+
# only attempt to dump the memory if we have vm_start and vm_end
217+
file_handle = self.vma_dump(
218+
self.context,
219+
task,
220+
vm_start,
221+
vm_end,
222+
self.open,
223+
self.config["maxsize"],
224+
)
225+
226+
if file_handle:
227+
file_handle.close()
228+
file_output = file_handle.preferred_filename
64229
yield (
65230
0,
66231
(
@@ -74,6 +239,7 @@ def _generator(self, tasks):
74239
minor,
75240
inode,
76241
path,
242+
file_output,
77243
),
78244
)
79245

@@ -92,6 +258,7 @@ def run(self):
92258
("Minor", int),
93259
("Inode", int),
94260
("File Path", str),
261+
("File output", str),
95262
],
96263
self._generator(
97264
pslist.PsList.list_tasks(

0 commit comments

Comments
 (0)