From 53b24d33e0d3c63fec59d23bf553f35bfff92580 Mon Sep 17 00:00:00 2001 From: Eve Date: Tue, 13 Dec 2022 09:59:09 +0000 Subject: [PATCH 1/7] First attempt at adding a --dump option to linux.proc, aim to be similar to windows.vadinfo --dump --- volatility3/framework/plugins/linux/proc.py | 143 +++++++++++++++++++- 1 file changed, 141 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index 9d8af482e..6d182ff9e 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -4,18 +4,23 @@ """A module containing a collection of plugins that produce data typically found in Linux's /proc file system.""" -from volatility3.framework import renderers +import logging +from typing import Callable, List, Generator, Iterable, Type, Optional + +from volatility3.framework import renderers, interfaces, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) class Maps(plugins.PluginInterface): """Lists all memory maps for all processes.""" _required_framework_version = (2, 0, 0) + MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb @classmethod def get_requirements(cls): @@ -35,16 +40,138 @@ class Maps(plugins.PluginInterface): element_type=int, optional=True, ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed memory segments", + default=False, + optional=True, + ), + requirements.ListRequirement( + name="address", + description="Process virtual memory address to include " + "(all other address ranges are excluded). This must be " + "a base address, not an address within the desired range.", + element_type=int, + optional=True, + ), + requirements.IntRequirement( + name="maxsize", + description="Maximum size for dumped VMA sections " + "(all the bigger sections will be ignored)", + default=cls.MAXSIZE_DEFAULT, + optional=True, + ), ] + @classmethod + def list_vmas( + cls, + task: interfaces.objects.ObjectInterface, + filter_func: Callable[ + [interfaces.objects.ObjectInterface], bool + ] = lambda _: False, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """Lists the Virtual Memory Areas of a specific process. + + Args: + task: task object from which to list the vma + filter_func: Function to take a vma and return True if it should be filtered out + + Returns: + A list of vmas based on the task and filtered based on the filter function + """ + if task.mm: + for vma in task.mm.get_mmap_iter(): + if not filter_func(vma): + yield vma + + @classmethod + def vma_dump( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + vma: interfaces.objects.ObjectInterface, + open_method: Type[interfaces.plugins.FileHandlerInterface], + maxsize: int = MAXSIZE_DEFAULT, + ) -> Optional[interfaces.plugins.FileHandlerInterface]: + """Extracts the complete data for VMA as a FileInterface. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + task: an task_struct instance + vma: The suspected VMA to extract (ObjectInterface) + open_method: class to provide context manager for opening the file + maxsize: Max size of VMA section (default MAXSIZE_DEFAULT) + + Returns: + An open FileInterface object containing the complete data for the task or None in the case of failure + """ + try: + vm_start = vma.vm_start + vm_end = vma.vm_end + except AttributeError: + vollog.debug("Unable to find the vm_start and vm_end") + return None + + vm_size = vm_end - vm_start + if 0 < maxsize < vm_size: + vollog.debug( + f"Skip virtual memory dump {vm_start:#x}-{vm_end:#x} due to maxsize limit" + ) + return None + + pid = "Unknown" + try: + pid = task.tgid + proc_layer_name = task.add_process_layer() + except exceptions.InvalidAddressException as excp: + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + pid, excp.invalid_address, excp.layer_name + ) + ) + return None + + proc_layer = context.layers[proc_layer_name] + file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp" + try: + file_handle = open_method(file_name) + chunk_size = 1024 * 1024 * 10 + offset = vm_start + while offset < vm_start + vm_size: + to_read = min(chunk_size, vm_start + vm_size - offset) + data = proc_layer.read(offset, to_read, pad=True) + if not data: + break + file_handle.write(data) + offset += to_read + + except Exception as excp: + vollog.debug(f"Unable to dump virtual memory {file_name}: {excp}") + return None + + return file_handle + def _generator(self, tasks): + # build filter for addresses if required + address_list = self.config.get("address", []) + if address_list == []: + # do not filter as no address_list was supplied + filter_func = lambda _: False + else: + # filter for any vm_start that matches the supplied address config + def filter_function(x: interfaces.objects.ObjectInterface) -> bool: + return x.vm_start not in address_list + + filter_func = filter_function + for task in tasks: if not task.mm: continue name = utility.array_to_string(task.comm) - for vma in task.mm.get_mmap_iter(): + for vma in self.list_vmas(task, filter_func=filter_func): flags = vma.get_protection() page_offset = vma.get_page_offset() major = 0 @@ -61,6 +188,16 @@ class Maps(plugins.PluginInterface): path = vma.get_name(self.context, task) + file_output = "Disabled" + if self.config["dump"]: + file_handle = self.vma_dump( + self.context, task, vma, self.open, self.config["maxsize"] + ) + file_output = "Error outputting file" + if file_handle: + file_handle.close() + file_output = file_handle.preferred_filename + yield ( 0, ( @@ -74,6 +211,7 @@ class Maps(plugins.PluginInterface): minor, inode, path, + file_output, ), ) @@ -92,6 +230,7 @@ class Maps(plugins.PluginInterface): ("Minor", int), ("Inode", int), ("File Path", str), + ("File output", str), ], self._generator( pslist.PsList.list_tasks( From 4c0a0b923bc1b7c13e753e90f84db44d95b95368 Mon Sep 17 00:00:00 2001 From: Eve Date: Wed, 4 Jan 2023 16:05:00 +0000 Subject: [PATCH 2/7] Update linux.proc --dump changes based on comments from ikelos --- volatility3/framework/plugins/linux/proc.py | 64 ++++++++++++--------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index 6d182ff9e..d8a17ae38 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -5,7 +5,7 @@ found in Linux's /proc file system.""" import logging -from typing import Callable, List, Generator, Iterable, Type, Optional +from typing import Callable, Generator, Type, Optional from volatility3.framework import renderers, interfaces, exceptions from volatility3.framework.configuration import requirements @@ -16,6 +16,7 @@ from volatility3.plugins.linux import pslist vollog = logging.getLogger(__name__) + class Maps(plugins.PluginInterface): """Lists all memory maps for all processes.""" @@ -48,9 +49,9 @@ class Maps(plugins.PluginInterface): ), requirements.ListRequirement( name="address", - description="Process virtual memory address to include " - "(all other address ranges are excluded). This must be " - "a base address, not an address within the desired range.", + description="Process virtual memory addresses to include " + "(all other VMA sections are excluded). This can be any " + "virtual address within the VMA section.", element_type=int, optional=True, ), @@ -69,21 +70,25 @@ class Maps(plugins.PluginInterface): task: interfaces.objects.ObjectInterface, filter_func: Callable[ [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + ] = lambda _: True, ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Lists the Virtual Memory Areas of a specific process. Args: task: task object from which to list the vma - filter_func: Function to take a vma and return True if it should be filtered out + filter_func: Function to take a vma and return False if it should be filtered out Returns: - A list of vmas based on the task and filtered based on the filter function + Yields vmas based on the task and filtered based on the filter function """ if task.mm: for vma in task.mm.get_mmap_iter(): - if not filter_func(vma): + if filter_func(vma): yield vma + else: + vollog.debug( + f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.pid} due to filter_func" + ) @classmethod def vma_dump( @@ -106,23 +111,15 @@ class Maps(plugins.PluginInterface): Returns: An open FileInterface object containing the complete data for the task or None in the case of failure """ + pid = task.pid try: vm_start = vma.vm_start vm_end = vma.vm_end except AttributeError: - vollog.debug("Unable to find the vm_start and vm_end") - return None - - vm_size = vm_end - vm_start - if 0 < maxsize < vm_size: - vollog.debug( - f"Skip virtual memory dump {vm_start:#x}-{vm_end:#x} due to maxsize limit" - ) + vollog.debug(f"Unable to find the vm_start and vm_end for pid {pid}") return None - pid = "Unknown" try: - pid = task.tgid proc_layer_name = task.add_process_layer() except exceptions.InvalidAddressException as excp: vollog.debug( @@ -132,6 +129,13 @@ class Maps(plugins.PluginInterface): ) return None + vm_size = vm_end - vm_start + if 0 < maxsize < vm_size: + vollog.warning( + 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}" + ) + return None + proc_layer = context.layers[proc_layer_name] file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp" try: @@ -141,8 +145,6 @@ class Maps(plugins.PluginInterface): while offset < vm_start + vm_size: to_read = min(chunk_size, vm_start + vm_size - offset) data = proc_layer.read(offset, to_read, pad=True) - if not data: - break file_handle.write(data) offset += to_read @@ -154,16 +156,24 @@ class Maps(plugins.PluginInterface): def _generator(self, tasks): # build filter for addresses if required - address_list = self.config.get("address", []) - if address_list == []: + address_list = self.config.get("address", None) + if not address_list: # do not filter as no address_list was supplied - filter_func = lambda _: False + vma_filter_func = lambda _: True else: # filter for any vm_start that matches the supplied address config - def filter_function(x: interfaces.objects.ObjectInterface) -> bool: - return x.vm_start not in address_list + def vma_filter_function(x: interfaces.objects.ObjectInterface) -> bool: + addrs_in_vma = [ + addr for addr in address_list if x.vm_start <= addr <= x.vm_end + ] - filter_func = filter_function + # if any of the user supplied addresses would fall within this vma return true + if addrs_in_vma: + return True + else: + return False + + vma_filter_func = vma_filter_function for task in tasks: if not task.mm: @@ -171,7 +181,7 @@ class Maps(plugins.PluginInterface): name = utility.array_to_string(task.comm) - for vma in self.list_vmas(task, filter_func=filter_func): + for vma in self.list_vmas(task, filter_func=vma_filter_func): flags = vma.get_protection() page_offset = vma.get_page_offset() major = 0 From 560569e03e5e8f7f6f29d6695b57745b11e2afee Mon Sep 17 00:00:00 2001 From: Eve Date: Fri, 6 Jan 2023 22:12:20 +0000 Subject: [PATCH 3/7] update linux.proc --dump so that vma object is not passed to dump func --- volatility3/framework/plugins/linux/proc.py | 41 ++++++++++++++------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index d8a17ae38..99c8761c1 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -95,7 +95,8 @@ class Maps(plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, task: interfaces.objects.ObjectInterface, - vma: interfaces.objects.ObjectInterface, + vm_start: int, + vm_end: int, open_method: Type[interfaces.plugins.FileHandlerInterface], maxsize: int = MAXSIZE_DEFAULT, ) -> Optional[interfaces.plugins.FileHandlerInterface]: @@ -105,6 +106,8 @@ class Maps(plugins.PluginInterface): context: The context to retrieve required elements (layers, symbol tables) from task: an task_struct instance vma: The suspected VMA to extract (ObjectInterface) + vm_start: The start virtual address from the vma to dump + vm_end: The end virtual address from the vma to dump open_method: class to provide context manager for opening the file maxsize: Max size of VMA section (default MAXSIZE_DEFAULT) @@ -112,12 +115,6 @@ class Maps(plugins.PluginInterface): An open FileInterface object containing the complete data for the task or None in the case of failure """ pid = task.pid - try: - vm_start = vma.vm_start - vm_end = vma.vm_end - except AttributeError: - vollog.debug(f"Unable to find the vm_start and vm_end for pid {pid}") - return None try: proc_layer_name = task.add_process_layer() @@ -200,13 +197,31 @@ class Maps(plugins.PluginInterface): file_output = "Disabled" if self.config["dump"]: - file_handle = self.vma_dump( - self.context, task, vma, self.open, self.config["maxsize"] - ) file_output = "Error outputting file" - if file_handle: - file_handle.close() - file_output = file_handle.preferred_filename + try: + vm_start = vma.vm_start + vm_end = vma.vm_end + except AttributeError: + vollog.debug( + f"Unable to find the vm_start and vm_end for vma at {vma.vol.offset:#x} for pid {pid}" + ) + vm_start = None + vm_end = None + + if vm_start and vm_end: + # only attempt to dump the memory if we have vm_start and vm_end + file_handle = self.vma_dump( + self.context, + task, + vm_start, + vm_end, + self.open, + self.config["maxsize"], + ) + + if file_handle: + file_handle.close() + file_output = file_handle.preferred_filename yield ( 0, From 18a9f898325bf84ad48500a043a4c3b49d4afcdd Mon Sep 17 00:00:00 2001 From: Eve Date: Wed, 29 Mar 2023 10:00:55 +0100 Subject: [PATCH 4/7] update logic for checking if a vma should be saved to disk in linux.proc plugin --- volatility3/framework/plugins/linux/proc.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index 99c8761c1..e885978b1 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -105,7 +105,6 @@ class Maps(plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from task: an task_struct instance - vma: The suspected VMA to extract (ObjectInterface) vm_start: The start virtual address from the vma to dump vm_end: The end virtual address from the vma to dump open_method: class to provide context manager for opening the file @@ -127,7 +126,16 @@ class Maps(plugins.PluginInterface): return None vm_size = vm_end - vm_start - if 0 < maxsize < vm_size: + + # check if vm_size is negative, this should never happen. + if vm_size < 0: + vollog.warning( + f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is negative." + ) + return None + + # check if vm_size is larger than the maxsize limit, and therefore is not saved out. + if maxsize <= vm_size: vollog.warning( 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}" ) From 61a2f78baaaa6bc7a957ab5c811494e2923ebf0d Mon Sep 17 00:00:00 2001 From: Eve Date: Wed, 29 Mar 2023 10:05:37 +0100 Subject: [PATCH 5/7] fix black linting in linux.proc plugin --- volatility3/framework/plugins/linux/proc.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index e885978b1..2d7348fff 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -124,7 +124,6 @@ class Maps(plugins.PluginInterface): ) ) return None - vm_size = vm_end - vm_start # check if vm_size is negative, this should never happen. @@ -133,14 +132,12 @@ class Maps(plugins.PluginInterface): f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is negative." ) return None - # check if vm_size is larger than the maxsize limit, and therefore is not saved out. if maxsize <= vm_size: vollog.warning( 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}" ) return None - proc_layer = context.layers[proc_layer_name] file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp" try: @@ -152,11 +149,9 @@ class Maps(plugins.PluginInterface): data = proc_layer.read(offset, to_read, pad=True) file_handle.write(data) offset += to_read - except Exception as excp: vollog.debug(f"Unable to dump virtual memory {file_name}: {excp}") return None - return file_handle def _generator(self, tasks): @@ -179,11 +174,9 @@ class Maps(plugins.PluginInterface): return False vma_filter_func = vma_filter_function - for task in tasks: if not task.mm: continue - name = utility.array_to_string(task.comm) for vma in self.list_vmas(task, filter_func=vma_filter_func): @@ -200,7 +193,6 @@ class Maps(plugins.PluginInterface): major = inode_object.i_sb.major minor = inode_object.i_sb.minor inode = inode_object.i_ino - path = vma.get_name(self.context, task) file_output = "Disabled" @@ -215,7 +207,6 @@ class Maps(plugins.PluginInterface): ) vm_start = None vm_end = None - if vm_start and vm_end: # only attempt to dump the memory if we have vm_start and vm_end file_handle = self.vma_dump( @@ -230,7 +221,6 @@ class Maps(plugins.PluginInterface): if file_handle: file_handle.close() file_output = file_handle.preferred_filename - yield ( 0, ( From 90b9157dcf2ababcd1c52128d72aa9732319701a Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 30 Mar 2023 08:54:56 +0100 Subject: [PATCH 6/7] Linux.proc: Fix broken variable in debug msg. --- volatility3/framework/plugins/linux/proc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index 2d7348fff..c1a834bbe 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -21,6 +21,7 @@ class Maps(plugins.PluginInterface): """Lists all memory maps for all processes.""" _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb @classmethod @@ -203,7 +204,7 @@ class Maps(plugins.PluginInterface): vm_end = vma.vm_end except AttributeError: vollog.debug( - f"Unable to find the vm_start and vm_end for vma at {vma.vol.offset:#x} for pid {pid}" + f"Unable to find the vm_start and vm_end for vma at {vma.vol.offset:#x} for pid {task.pid}" ) vm_start = None vm_end = None From 8d47f89eddbdf958406290f2da5affeb934dfc7a Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 30 Mar 2023 08:57:43 +0100 Subject: [PATCH 7/7] Linux.proc: Add debug msg when task has no mm member. --- volatility3/framework/plugins/linux/proc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index c1a834bbe..3ce9216fa 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -90,6 +90,10 @@ class Maps(plugins.PluginInterface): vollog.debug( f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.pid} due to filter_func" ) + else: + vollog.debug( + f"Excluded pid {task.pid} as there is no mm member. It is likely a kernel thread." + ) @classmethod def vma_dump(