44"""A module containing a collection of plugins that produce data typically
55found 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
811from volatility3 .framework .configuration import requirements
912from volatility3 .framework .interfaces import plugins
1013from volatility3 .framework .objects import utility
1114from volatility3 .framework .renderers import format_hints
1215from volatility3 .plugins .linux import pslist
1316
17+ vollog = logging .getLogger (__name__ )
18+
1419
1520class 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