From af2d6206763a661ced9687f1c7b31b7888d3d0ce Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Tue, 23 Jul 2024 22:02:34 +0200 Subject: [PATCH 01/85] Improving lsof --- volatility3/framework/plugins/linux/lsof.py | 53 +++++++++++++++++-- .../framework/symbols/linux/__init__.py | 21 +++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index d970ad8a9..f9aeafe14 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -3,7 +3,7 @@ # """A module containing a collection of plugins that produce data typically found in Linux's /proc file system.""" -import logging +import logging, datetime from typing import List, Callable from volatility3.framework import renderers, interfaces, constants @@ -76,14 +76,59 @@ class Lsof(plugins.PluginInterface): ) for pid, task_comm, _task, fd_fields in fds_generator: - fd_num, _filp, full_path = fd_fields + ( + fd_num, + _filp, + full_path, + inode_num, + imode, + ctime, + mtime, + atime, + file_size, + ) = fd_fields - fields = (pid, task_comm, fd_num, full_path) + fields = ( + pid, + task_comm, + fd_num, + full_path, + inode_num, + imode, + ctime, + mtime, + atime, + file_size, + ) yield (0, fields) def run(self): pids = self.config.get("pid", None) symbol_table = self.config["kernel"] - tree_grid_args = [("PID", int), ("Process", str), ("FD", int), ("Path", str)] + tree_grid_args = [ + ("PID", int), + ("Process", str), + ("FD", int), + ("Path", str), + ("Inode", int), + ("Mode", str), + ("LastChange", datetime.datetime), + ("LastModify", datetime.datetime), + ("LastAccessed", datetime.datetime), + ("Size", int), + ] return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table)) + + def generate_timeline(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + for row in self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ): + _depth, row_data = row + description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[4]}"' + yield description, timeliner.TimeLinerType.CHANGED, row_data[5] + yield description, timeliner.TimeLinerType.MODIFIED, row_data[6] + yield description, timeliner.TimeLinerType.ACCESSED, row_data[7] diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index c4e2587f4..b9321f369 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,6 +1,7 @@ # This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import stat, datetime from typing import Iterator, List, Tuple, Optional, Union from volatility3 import framework @@ -265,8 +266,26 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): for fd_num, filp in enumerate(fds): if filp != 0: full_path = LinuxUtilities.path_for_file(context, task, filp) + dentry = filp.get_dentry() + if dentry != 0: + inode_object = dentry.d_inode + inode_num = inode_object.i_ino + file_size = inode_object.i_size # file size in bytes + imode = stat.filemode( + inode_object.i_mode + ) # file type & Permissions - yield fd_num, filp, full_path + # Timestamps + ctime = datetime.datetime.fromtimestamp( + inode_object.i_ctime.tv_sec + ) # last change time + mtime = datetime.datetime.fromtimestamp( + inode_object.i_mtime.tv_sec + ) # last modify time + atime = datetime.datetime.fromtimestamp( + inode_object.i_atime.tv_sec + ) # last access time + yield fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size @classmethod def mask_mods_list( From 6f3f645dbcfb94b797f2e244e56e43dd8e4cba0a Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 25 Jul 2024 17:59:10 +0100 Subject: [PATCH 02/85] Windows: update handles plugin sar warnings to use DEFAULT_SAR_VALUE var --- volatility3/framework/plugins/windows/handles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/handles.py b/volatility3/framework/plugins/windows/handles.py index abe154fa1..6010b4c72 100644 --- a/volatility3/framework/plugins/windows/handles.py +++ b/volatility3/framework/plugins/windows/handles.py @@ -177,7 +177,7 @@ class Handles(interfaces.plugins.PluginInterface): ) except exceptions.InvalidAddressException: vollog.warning( - f"Failed to read {hex(num_bytes_to_read)} bytes at symbol {hex(func_addr_to_read)}. Unable to decode SAR value. Failing back to a common value of 0x10" + f"Failed to read {hex(num_bytes_to_read)} bytes at symbol {hex(func_addr_to_read)}. Unable to decode SAR value. Failing back to a common value of {hex(DEFAULT_SAR_VALUE)}}" ) self._sar_value = DEFAULT_SAR_VALUE return self._sar_value @@ -201,7 +201,7 @@ class Handles(interfaces.plugins.PluginInterface): if self._sar_value is None: vollog.warning( - f"Failed to to locate SAR value having parsed {instruction_count} instructions, failing back to a common value of 0x10" + f"Failed to to locate SAR value having parsed {instruction_count} instructions, failing back to a common value of {hex(DEFAULT_SAR_VALUE)}" ) self._sar_value = DEFAULT_SAR_VALUE From 799afe6e51558dc4fe3828276f28b888e866e708 Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 25 Jul 2024 18:02:30 +0100 Subject: [PATCH 03/85] Windows: fix type in handles plugin --- volatility3/framework/plugins/windows/handles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/handles.py b/volatility3/framework/plugins/windows/handles.py index 6010b4c72..3e5a2fd82 100644 --- a/volatility3/framework/plugins/windows/handles.py +++ b/volatility3/framework/plugins/windows/handles.py @@ -177,7 +177,7 @@ class Handles(interfaces.plugins.PluginInterface): ) except exceptions.InvalidAddressException: vollog.warning( - f"Failed to read {hex(num_bytes_to_read)} bytes at symbol {hex(func_addr_to_read)}. Unable to decode SAR value. Failing back to a common value of {hex(DEFAULT_SAR_VALUE)}}" + f"Failed to read {hex(num_bytes_to_read)} bytes at symbol {hex(func_addr_to_read)}. Unable to decode SAR value. Failing back to a common value of {hex(DEFAULT_SAR_VALUE)}" ) self._sar_value = DEFAULT_SAR_VALUE return self._sar_value From 650dd06245918f1b14d8477eff85a743c9e42c4f Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Sat, 27 Jul 2024 16:03:59 +0200 Subject: [PATCH 04/85] Modifications following the review --- volatility3/framework/plugins/linux/lsof.py | 74 +++++++++++-------- .../framework/symbols/linux/__init__.py | 51 +++++++------ 2 files changed, 70 insertions(+), 55 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index f9aeafe14..3bbc855f9 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -1,4 +1,4 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """A module containing a collection of plugins that produce data typically @@ -12,16 +12,17 @@ from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility from volatility3.framework.symbols import linux from volatility3.plugins.linux import pslist +from volatility3.plugins import timeliner vollog = logging.getLogger(__name__) -class Lsof(plugins.PluginInterface): +class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists all memory maps for all processes.""" _required_framework_version = (2, 0, 0) - _version = (1, 1, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -46,7 +47,7 @@ class Lsof(plugins.PluginInterface): ] @classmethod - def list_fds( + def list_fds_and_inodes( cls, context: interfaces.context.ContextInterface, symbol_table: str, @@ -67,27 +68,38 @@ class Lsof(plugins.PluginInterface): ) for fd_fields in fd_generator: - yield pid, task_comm, task, fd_fields + fd_num, filp, full_path = fd_fields + inode_metadata = linux.LinuxUtilities.get_inode_metadata(context, filp) + try: + inode_num, file_size, imode, ctime, mtime, atime = next( + inode_metadata + ) + except Exception as e: + vollog.warning( + f"Can't get inode metadata for file descriptor {fd_num}: {e}" + ) + continue + yield pid, task_comm, task, fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size def _generator(self, pids, symbol_table): filter_func = pslist.PsList.create_pid_filter(pids) - fds_generator = self.list_fds( + fds_generator = self.list_fds_and_inodes( self.context, symbol_table, filter_func=filter_func ) - - for pid, task_comm, _task, fd_fields in fds_generator: - ( - fd_num, - _filp, - full_path, - inode_num, - imode, - ctime, - mtime, - atime, - file_size, - ) = fd_fields - + for ( + pid, + task_comm, + task, + fd_num, + filp, + full_path, + inode_num, + imode, + ctime, + mtime, + atime, + file_size, + ) in fds_generator: fields = ( pid, task_comm, @@ -113,22 +125,20 @@ class Lsof(plugins.PluginInterface): ("Path", str), ("Inode", int), ("Mode", str), - ("LastChange", datetime.datetime), - ("LastModify", datetime.datetime), - ("LastAccessed", datetime.datetime), + ("Changed", datetime.datetime), + ("Modified", datetime.datetime), + ("Accessed", datetime.datetime), ("Size", int), ] return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table)) def generate_timeline(self): + pids = self.config.get("pid", None) + symbol_table = self.config["kernel"] filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - for row in self._generator( - pslist.PsList.list_tasks( - self.context, self.config["kernel"], filter_func=filter_func - ) - ): + for row in self._generator(pids, symbol_table): _depth, row_data = row - description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[4]}"' - yield description, timeliner.TimeLinerType.CHANGED, row_data[5] - yield description, timeliner.TimeLinerType.MODIFIED, row_data[6] - yield description, timeliner.TimeLinerType.ACCESSED, row_data[7] + description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"' + yield description, timeliner.TimeLinerType.CHANGED, row_data[6] + yield description, timeliner.TimeLinerType.MODIFIED, row_data[7] + yield description, timeliner.TimeLinerType.ACCESSED, row_data[8] diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index b9321f369..2b97bc5f4 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,9 +1,8 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import stat, datetime from typing import Iterator, List, Tuple, Optional, Union - +import logging, datetime, stat from volatility3 import framework from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.objects import utility @@ -62,7 +61,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 1, 0) + _version = (2, 2, 0) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -266,26 +265,32 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): for fd_num, filp in enumerate(fds): if filp != 0: full_path = LinuxUtilities.path_for_file(context, task, filp) - dentry = filp.get_dentry() - if dentry != 0: - inode_object = dentry.d_inode - inode_num = inode_object.i_ino - file_size = inode_object.i_size # file size in bytes - imode = stat.filemode( - inode_object.i_mode - ) # file type & Permissions - # Timestamps - ctime = datetime.datetime.fromtimestamp( - inode_object.i_ctime.tv_sec - ) # last change time - mtime = datetime.datetime.fromtimestamp( - inode_object.i_mtime.tv_sec - ) # last modify time - atime = datetime.datetime.fromtimestamp( - inode_object.i_atime.tv_sec - ) # last access time - yield fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size + yield fd_num, filp, full_path + + @classmethod + def get_inode_metadata(cls, context: interfaces.context.ContextInterface, filp): + """ + A helper function that gets the inodes metadata from a file descriptor + """ + dentry = filp.get_dentry() + if dentry != 0: + inode_object = dentry.d_inode + inode_num = inode_object.i_ino + file_size = inode_object.i_size # file size in bytes + imode = stat.filemode(inode_object.i_mode) # file type & Permissions + + # Timestamps + ctime = datetime.datetime.fromtimestamp( + inode_object.i_ctime.tv_sec + ) # last change time + mtime = datetime.datetime.fromtimestamp( + inode_object.i_mtime.tv_sec + ) # last modify time + atime = datetime.datetime.fromtimestamp( + inode_object.i_atime.tv_sec + ) # last access time + yield inode_num, file_size, imode, ctime, mtime, atime @classmethod def mask_mods_list( From 7024588076adf95c2d6667c851cddf87e8c68555 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Sat, 27 Jul 2024 16:09:52 +0200 Subject: [PATCH 05/85] Code clean --- volatility3/framework/plugins/linux/lsof.py | 1 - volatility3/framework/symbols/linux/__init__.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 3bbc855f9..98a215ecf 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -135,7 +135,6 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): def generate_timeline(self): pids = self.config.get("pid", None) symbol_table = self.config["kernel"] - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) for row in self._generator(pids, symbol_table): _depth, row_data = row description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"' diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 2b97bc5f4..1b3f75e98 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # from typing import Iterator, List, Tuple, Optional, Union -import logging, datetime, stat +import datetime, stat from volatility3 import framework from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.objects import utility From 815252c9ba31913d22e8836183828059217c4e9d Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Sat, 27 Jul 2024 16:35:22 +0200 Subject: [PATCH 06/85] Adding watchdogs --- volatility3/framework/plugins/linux/lsof.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 98a215ecf..c1de48c1a 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -78,7 +78,13 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): vollog.warning( f"Can't get inode metadata for file descriptor {fd_num}: {e}" ) - continue + # Yield NotAvailableValue for each field in case of an exception + inode_num = renderers.NotAvailableValue() + file_size = renderers.NotAvailableValue() + imode = renderers.NotAvailableValue() + ctime = renderers.NotAvailableValue() + mtime = renderers.NotAvailableValue() + atime = renderers.NotAvailableValue() yield pid, task_comm, task, fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size def _generator(self, pids, symbol_table): From f44ceb321d0c3b10249c1c697b3fb0c40f38ba39 Mon Sep 17 00:00:00 2001 From: qpalzmz112 <68213464+qpalzmz112@users.noreply.github.com> Date: Sat, 27 Jul 2024 20:11:39 -0500 Subject: [PATCH 07/85] Added psxview --- .../framework/plugins/windows/psxview.py | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 volatility3/framework/plugins/windows/psxview.py diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py new file mode 100644 index 000000000..3bf6c27f5 --- /dev/null +++ b/volatility3/framework/plugins/windows/psxview.py @@ -0,0 +1,188 @@ +import datetime, logging + +from volatility3.framework import constants, exceptions +from volatility3.framework.interfaces import plugins +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints, TreeGrid +from volatility3.plugins.windows import handles, info, pslist, psscan, sessions, thrdscan + +vollog = logging.getLogger(__name__) + +class PsXView(plugins.PluginInterface): + """Lists all processes found via 6 of the methods described in \"The Art of Memory Forensics,\" which may help + identify processes that are trying to hide themselves. I recommend using -r pretty if you are looking at this + plugin's output in a terminal.""" + # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the funcitonality + # which the original plugin to do it. + + # I don't think it's worth including the sessions method either because both the original psxview plugin + # and Volatility3's sessions plugin begin with the list of processes found by PsList. + # The original psxview plugin's session code essentially just filters the pslist for processes + # whose session ID is not None. I've matched this in my code, but again, it doesn't seem worth including. + + # Lastly, I've omitted the pspcid method because I could not for the life of me get it to work. I saved the + # code I do have from it, and will happily share it if anyone else wants to add it. + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [requirements.ModuleRequirement(name="kernel", description="Windows kernel", architectures=["Intel32", "Intel64"]), + requirements.VersionRequirement(name="info", component=info.Info, version=(1, 0, 0)), + requirements.VersionRequirement(name="pslist", component=pslist.PsList, version=(2, 0, 0)), + requirements.VersionRequirement(name="psscan", component=psscan.PsScan, version=(1, 0, 0)), + requirements.VersionRequirement(name="thrdscan", component=thrdscan.ThrdScan, version=(1, 0, 0)), + requirements.VersionRequirement(name="handles", component=handles.Handles, version=(1, 0, 0)), + requirements.VersionRequirement(name="sessions", component=sessions.Sessions, version=(0, 0, 0)), + requirements.BooleanRequirement(name="identify-expected", description="In the plugin's output, replace false with \ + normal where false is the expected result for a Windows machine running normally. \ + Keep in mind that this plugin uses simple checks to identify \"normal\" behavior, \ + so you may want to double-check the legitimacy of these processes yourself.", optional=True), + requirements.BooleanRequirement(name="physical-offsets", description="List processes with phyiscall offsets instead of virtual offsets.", optional=True)] + + def proc_name_to_string(self, proc): + return proc.ImageFileName.cast("string", max_length=proc.ImageFileName.vol.count, errors="replace") + + def is_ascii(self, str): + return str.split('.')[0].isalnum() + + def filter_garbage_procs(self, proc_list): + return [p for p in proc_list if p.is_valid() and self.is_ascii(self.proc_name_to_string(p))] + + def translate_offset(self, offset): + if self.config["physical-offsets"]: + return offset + + kernel = self.context.modules[self.config["kernel"]] + layer_name = kernel.layer_name + + try: + offset = list(self.context.layers[layer_name].mapping(offset=offset, length=0))[0][2] + except: + # already have physical address + pass + + return offset + + def proc_list_to_dict(self, tasks): + return {self.translate_offset(proc.vol.offset):proc for proc in tasks} + + def check_pslist(self, tasks): + res = self.filter_garbage_procs(tasks) + return self.proc_list_to_dict(tasks) + + def check_psscan(self, layer_name, symbol_table): + res = psscan.PsScan.scan_processes(context=self.context, layer_name=layer_name, symbol_table=symbol_table) + res = self.filter_garbage_procs(res) + + return self.proc_list_to_dict(res) + + def check_thrdscan(self): + ret = [] + + for ethread in thrdscan.ThrdScan.scan_threads(self.context, module_name='kernel'): + process = None + try: + process = ethread.owning_process() + if not process.is_valid(): + continue + + ret.append(process) + except AttributeError: + vollog.log(constants.LOGLEVEL_VVV, "Unable to find the owning process of ethread") + + return self.proc_list_to_dict(ret) + + def check_csrss_handles(self, tasks, layer_name, symbol_table): + ret = [] + + for p in tasks: + name = self.proc_name_to_string(p) + if name == 'csrss.exe': + try: + if p.has_member("ObjectTable"): + handles_plugin = handles.Handles(context=self.context, config_path=self.config_path) + hndls = list(handles_plugin.handles(p.ObjectTable)) + for h in hndls: + if (h.get_object_type(handles_plugin.get_type_map(self.context, layer_name, symbol_table)) == "Process"): + ret.append(h.Body.cast("_EPROCESS")) + + except exceptions.InvalidAddressException: + vollog.log(constants.LOGLEVEL_VVV, "Cannot access eprocess object table") + + ret = self.filter_garbage_procs(ret) + return self.proc_list_to_dict(ret) + + def check_session(self, pslist_procs): + procs = [p for p in pslist_procs if p.get_session_id() != None] + + return self.proc_list_to_dict(procs) + + def _generator(self): + kernel = self.context.modules[self.config["kernel"]] + + layer_name = kernel.layer_name + symbol_table = kernel.symbol_table_name + + kdbg_list_processes = list(pslist.PsList.list_processes(context=self.context, layer_name=layer_name, symbol_table=symbol_table)) + + processes = {} + + processes['pslist'] = self.check_pslist(kdbg_list_processes) + processes['psscan'] = self.check_psscan(layer_name, symbol_table) + processes['thrdscan'] = self.check_thrdscan() + processes['csrss'] = self.check_csrss_handles(kdbg_list_processes, layer_name, symbol_table) + processes['sessions'] = self.check_session(kdbg_list_processes) + + seen_offsets = set() + for source in processes: + for offset in processes[source]: + if offset not in seen_offsets: + seen_offsets.add(offset) + proc = processes[source][offset] + + pid = proc.UniqueProcessId + name = self.proc_name_to_string(proc) + + exit_time = proc.get_exit_time() + if (type(exit_time) != datetime.datetime): + exit_time = "" + else: + exit_time = str(exit_time) + + in_sources = {src:str(offset in processes[src]) for src in processes} + + if self.config["identify-expected"]: + f = "False" + n = "Normal" + + if in_sources["pslist"] == f: + if exit_time != "": + in_sources["pslist"] = n + + if in_sources["thrdscan"] == f: + if exit_time != "": + in_sources["thrdscan"] = n + + if in_sources["csrss"] == f: + if name.lower() in ["system", "smss.exe", "csrss.exe"]: + in_sources["csrss"] = n + elif exit_time != "": + in_sources["csrss"] = n + + if in_sources["sessions"] == f: + if name.lower() in ["system", "smss.exe"]: + in_sources["sessions"] = n + + yield (0, (format_hints.Hex(offset), name, pid, in_sources["pslist"], + in_sources["psscan"], in_sources["thrdscan"], in_sources["csrss"], + in_sources["sessions"], exit_time)) + + + def run(self): + offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)" + offset_str = "Offset" + offset_type + + return TreeGrid([(offset_str, format_hints.Hex), ("Name", str), ("PID", int), ("pslist", str), ("psscan", str), + ("thrdscan", str), ("csrss", str), ("sessions", str), ("Exit Time", str) ], self._generator()) \ No newline at end of file From 824b0599f20d2a1f0e7f16ca08f9b498898e4c52 Mon Sep 17 00:00:00 2001 From: qpalzmz112 <68213464+qpalzmz112@users.noreply.github.com> Date: Sat, 27 Jul 2024 20:35:08 -0500 Subject: [PATCH 08/85] formatted --- .../framework/plugins/windows/psxview.py | 203 +++++++++++++----- 1 file changed, 147 insertions(+), 56 deletions(-) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 3bf6c27f5..1fde5b12d 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -4,19 +4,28 @@ from volatility3.framework import constants, exceptions from volatility3.framework.interfaces import plugins from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints, TreeGrid -from volatility3.plugins.windows import handles, info, pslist, psscan, sessions, thrdscan +from volatility3.plugins.windows import ( + handles, + info, + pslist, + psscan, + sessions, + thrdscan, +) vollog = logging.getLogger(__name__) + class PsXView(plugins.PluginInterface): - """Lists all processes found via 6 of the methods described in \"The Art of Memory Forensics,\" which may help + """Lists all processes found via 6 of the methods described in \"The Art of Memory Forensics,\" which may help identify processes that are trying to hide themselves. I recommend using -r pretty if you are looking at this plugin's output in a terminal.""" - # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the funcitonality - # which the original plugin to do it. - # I don't think it's worth including the sessions method either because both the original psxview plugin - # and Volatility3's sessions plugin begin with the list of processes found by PsList. + # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the funcitonality + # which the original plugin to do it. + + # I don't think it's worth including the sessions method either because both the original psxview plugin + # and Volatility3's sessions plugin begin with the list of processes found by PsList. # The original psxview plugin's session code essentially just filters the pslist for processes # whose session ID is not None. I've matched this in my code, but again, it doesn't seem worth including. @@ -28,52 +37,88 @@ class PsXView(plugins.PluginInterface): @classmethod def get_requirements(cls): - return [requirements.ModuleRequirement(name="kernel", description="Windows kernel", architectures=["Intel32", "Intel64"]), - requirements.VersionRequirement(name="info", component=info.Info, version=(1, 0, 0)), - requirements.VersionRequirement(name="pslist", component=pslist.PsList, version=(2, 0, 0)), - requirements.VersionRequirement(name="psscan", component=psscan.PsScan, version=(1, 0, 0)), - requirements.VersionRequirement(name="thrdscan", component=thrdscan.ThrdScan, version=(1, 0, 0)), - requirements.VersionRequirement(name="handles", component=handles.Handles, version=(1, 0, 0)), - requirements.VersionRequirement(name="sessions", component=sessions.Sessions, version=(0, 0, 0)), - requirements.BooleanRequirement(name="identify-expected", description="In the plugin's output, replace false with \ + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="info", component=info.Info, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="psscan", component=psscan.PsScan, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="thrdscan", component=thrdscan.ThrdScan, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="handles", component=handles.Handles, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="sessions", component=sessions.Sessions, version=(0, 0, 0) + ), + requirements.BooleanRequirement( + name="identify-expected", + description='In the plugin\'s output, replace false with \ normal where false is the expected result for a Windows machine running normally. \ - Keep in mind that this plugin uses simple checks to identify \"normal\" behavior, \ - so you may want to double-check the legitimacy of these processes yourself.", optional=True), - requirements.BooleanRequirement(name="physical-offsets", description="List processes with phyiscall offsets instead of virtual offsets.", optional=True)] - + Keep in mind that this plugin uses simple checks to identify "normal" behavior, \ + so you may want to double-check the legitimacy of these processes yourself.', + optional=True, + ), + requirements.BooleanRequirement( + name="physical-offsets", + description="List processes with phyiscall offsets instead of virtual offsets.", + optional=True, + ), + ] + def proc_name_to_string(self, proc): - return proc.ImageFileName.cast("string", max_length=proc.ImageFileName.vol.count, errors="replace") + return proc.ImageFileName.cast( + "string", max_length=proc.ImageFileName.vol.count, errors="replace" + ) def is_ascii(self, str): - return str.split('.')[0].isalnum() - + return str.split(".")[0].isalnum() + def filter_garbage_procs(self, proc_list): - return [p for p in proc_list if p.is_valid() and self.is_ascii(self.proc_name_to_string(p))] - + return [ + p + for p in proc_list + if p.is_valid() and self.is_ascii(self.proc_name_to_string(p)) + ] + def translate_offset(self, offset): if self.config["physical-offsets"]: return offset - + kernel = self.context.modules[self.config["kernel"]] layer_name = kernel.layer_name try: - offset = list(self.context.layers[layer_name].mapping(offset=offset, length=0))[0][2] + offset = list( + self.context.layers[layer_name].mapping(offset=offset, length=0) + )[0][2] except: # already have physical address pass return offset - + def proc_list_to_dict(self, tasks): - return {self.translate_offset(proc.vol.offset):proc for proc in tasks} - + return {self.translate_offset(proc.vol.offset): proc for proc in tasks} + def check_pslist(self, tasks): res = self.filter_garbage_procs(tasks) return self.proc_list_to_dict(tasks) - + def check_psscan(self, layer_name, symbol_table): - res = psscan.PsScan.scan_processes(context=self.context, layer_name=layer_name, symbol_table=symbol_table) + res = psscan.PsScan.scan_processes( + context=self.context, layer_name=layer_name, symbol_table=symbol_table + ) res = self.filter_garbage_procs(res) return self.proc_list_to_dict(res) @@ -81,7 +126,9 @@ class PsXView(plugins.PluginInterface): def check_thrdscan(self): ret = [] - for ethread in thrdscan.ThrdScan.scan_threads(self.context, module_name='kernel'): + for ethread in thrdscan.ThrdScan.scan_threads( + self.context, module_name="kernel" + ): process = None try: process = ethread.owning_process() @@ -90,50 +137,70 @@ class PsXView(plugins.PluginInterface): ret.append(process) except AttributeError: - vollog.log(constants.LOGLEVEL_VVV, "Unable to find the owning process of ethread") + vollog.log( + constants.LOGLEVEL_VVV, + "Unable to find the owning process of ethread", + ) return self.proc_list_to_dict(ret) - + def check_csrss_handles(self, tasks, layer_name, symbol_table): ret = [] for p in tasks: name = self.proc_name_to_string(p) - if name == 'csrss.exe': + if name == "csrss.exe": try: if p.has_member("ObjectTable"): - handles_plugin = handles.Handles(context=self.context, config_path=self.config_path) + handles_plugin = handles.Handles( + context=self.context, config_path=self.config_path + ) hndls = list(handles_plugin.handles(p.ObjectTable)) for h in hndls: - if (h.get_object_type(handles_plugin.get_type_map(self.context, layer_name, symbol_table)) == "Process"): + if ( + h.get_object_type( + handles_plugin.get_type_map( + self.context, layer_name, symbol_table + ) + ) + == "Process" + ): ret.append(h.Body.cast("_EPROCESS")) except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, "Cannot access eprocess object table") + vollog.log( + constants.LOGLEVEL_VVV, "Cannot access eprocess object table" + ) ret = self.filter_garbage_procs(ret) return self.proc_list_to_dict(ret) def check_session(self, pslist_procs): procs = [p for p in pslist_procs if p.get_session_id() != None] - + return self.proc_list_to_dict(procs) def _generator(self): kernel = self.context.modules[self.config["kernel"]] layer_name = kernel.layer_name - symbol_table = kernel.symbol_table_name + symbol_table = kernel.symbol_table_name + + kdbg_list_processes = list( + pslist.PsList.list_processes( + context=self.context, layer_name=layer_name, symbol_table=symbol_table + ) + ) - kdbg_list_processes = list(pslist.PsList.list_processes(context=self.context, layer_name=layer_name, symbol_table=symbol_table)) - processes = {} - processes['pslist'] = self.check_pslist(kdbg_list_processes) - processes['psscan'] = self.check_psscan(layer_name, symbol_table) - processes['thrdscan'] = self.check_thrdscan() - processes['csrss'] = self.check_csrss_handles(kdbg_list_processes, layer_name, symbol_table) - processes['sessions'] = self.check_session(kdbg_list_processes) + processes["pslist"] = self.check_pslist(kdbg_list_processes) + processes["psscan"] = self.check_psscan(layer_name, symbol_table) + processes["thrdscan"] = self.check_thrdscan() + processes["csrss"] = self.check_csrss_handles( + kdbg_list_processes, layer_name, symbol_table + ) + processes["sessions"] = self.check_session(kdbg_list_processes) seen_offsets = set() for source in processes: @@ -146,12 +213,14 @@ class PsXView(plugins.PluginInterface): name = self.proc_name_to_string(proc) exit_time = proc.get_exit_time() - if (type(exit_time) != datetime.datetime): + if type(exit_time) != datetime.datetime: exit_time = "" else: exit_time = str(exit_time) - in_sources = {src:str(offset in processes[src]) for src in processes} + in_sources = { + src: str(offset in processes[src]) for src in processes + } if self.config["identify-expected"]: f = "False" @@ -173,16 +242,38 @@ class PsXView(plugins.PluginInterface): if in_sources["sessions"] == f: if name.lower() in ["system", "smss.exe"]: - in_sources["sessions"] = n + in_sources["sessions"] = n + + yield ( + 0, + ( + format_hints.Hex(offset), + name, + pid, + in_sources["pslist"], + in_sources["psscan"], + in_sources["thrdscan"], + in_sources["csrss"], + in_sources["sessions"], + exit_time, + ), + ) - yield (0, (format_hints.Hex(offset), name, pid, in_sources["pslist"], - in_sources["psscan"], in_sources["thrdscan"], in_sources["csrss"], - in_sources["sessions"], exit_time)) - - def run(self): offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)" offset_str = "Offset" + offset_type - return TreeGrid([(offset_str, format_hints.Hex), ("Name", str), ("PID", int), ("pslist", str), ("psscan", str), - ("thrdscan", str), ("csrss", str), ("sessions", str), ("Exit Time", str) ], self._generator()) \ No newline at end of file + return TreeGrid( + [ + (offset_str, format_hints.Hex), + ("Name", str), + ("PID", int), + ("pslist", str), + ("psscan", str), + ("thrdscan", str), + ("csrss", str), + ("sessions", str), + ("Exit Time", str), + ], + self._generator(), + ) From 44f26c928eddaaf6743ac07b22331ad082e86bc1 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 28 Jul 2024 12:35:02 +0100 Subject: [PATCH 09/85] Add in shtab autocompletion --- volatility3/cli/__init__.py | 22 +++++++++++++++++++--- volatility3/cli/volargparse.py | 6 ++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 6b17edac0..883b54ac4 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -22,6 +22,13 @@ import traceback from typing import Any, Dict, List, Tuple, Type, Union from urllib import parse, request +try: + import shtab + + HAS_SHTAB = True +except ImportError: + HAS_SHTAB = False + from volatility3.cli import text_filter import volatility3.plugins import volatility3.symbols @@ -106,6 +113,9 @@ class CommandLine: ] ) + # Argument for doing autocompletion + print_completion_arg = "--print-completion" + # Load up system defaults delayed_logs, default_config = self.load_system_defaults("vol.json") @@ -246,10 +256,12 @@ class CommandLine: # We have to filter out help, otherwise parse_known_args will trigger the help message before having # processed the plugin choice or had the plugin subparser added. known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"] - partial_args, _ = parser.parse_known_args(known_args) - + partial_args, unknown_args = parser.parse_known_args(known_args) banner_output = sys.stdout - if renderers[partial_args.renderer].structured_output: + if ( + renderers[partial_args.renderer].structured_output + or print_completion_arg in unknown_args + ): banner_output = sys.stderr banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n") @@ -351,6 +363,10 @@ class CommandLine: # Hand the plugin requirements over to the CLI (us) and let it construct the config tree # Run the argparser + if HAS_SHTAB: + # The autocompletion line must be after the partial_arg handling, so that it doesn't trip it + # before all the plugins have been added + shtab.add_argument_to(parser, [print_completion_arg]) args = parser.parse_args() if args.plugin is None: parser.error("Please select a plugin to run") diff --git a/volatility3/cli/volargparse.py b/volatility3/cli/volargparse.py index 3048a0885..3e7eb6751 100644 --- a/volatility3/cli/volargparse.py +++ b/volatility3/cli/volargparse.py @@ -21,8 +21,6 @@ class HelpfulSubparserAction(argparse._SubParsersAction): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - # We don't want the action self-check to kick in, so we remove the choices list, the check happens in __call__ - self.choices = None def __call__( self, @@ -100,3 +98,7 @@ class HelpfulArgParser(argparse.ArgumentParser): # return the number of arguments matched return len(match.group(1)) + + def _check_value(self, action, value): + if not isinstance(action, HelpfulSubparserAction): + return super()._check_value(action, value) From e89e77637776b14c61516d2c47a7148a2f13860a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 28 Jul 2024 12:41:31 +0100 Subject: [PATCH 10/85] Try out argcomplete as well --- vol.py | 1 + volatility3/cli/__init__.py | 21 ++++++++------------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/vol.py b/vol.py index ff420cad5..c49d5985d 100755 --- a/vol.py +++ b/vol.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK # This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 883b54ac4..1209d7cdc 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -23,11 +23,11 @@ from typing import Any, Dict, List, Tuple, Type, Union from urllib import parse, request try: - import shtab + import argcomplete - HAS_SHTAB = True + HAS_ARGCOMPLETE = True except ImportError: - HAS_SHTAB = False + HAS_ARGCOMPLETE = False from volatility3.cli import text_filter import volatility3.plugins @@ -113,9 +113,6 @@ class CommandLine: ] ) - # Argument for doing autocompletion - print_completion_arg = "--print-completion" - # Load up system defaults delayed_logs, default_config = self.load_system_defaults("vol.json") @@ -256,12 +253,10 @@ class CommandLine: # We have to filter out help, otherwise parse_known_args will trigger the help message before having # processed the plugin choice or had the plugin subparser added. known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"] - partial_args, unknown_args = parser.parse_known_args(known_args) + partial_args, _ = parser.parse_known_args(known_args) + banner_output = sys.stdout - if ( - renderers[partial_args.renderer].structured_output - or print_completion_arg in unknown_args - ): + if renderers[partial_args.renderer].structured_output: banner_output = sys.stderr banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n") @@ -363,10 +358,10 @@ class CommandLine: # Hand the plugin requirements over to the CLI (us) and let it construct the config tree # Run the argparser - if HAS_SHTAB: + if HAS_ARGCOMPLETE: # The autocompletion line must be after the partial_arg handling, so that it doesn't trip it # before all the plugins have been added - shtab.add_argument_to(parser, [print_completion_arg]) + argcomplete.autocomplete(parser) args = parser.parse_args() if args.plugin is None: parser.error("Please select a plugin to run") From 0d8fb76b3a48599ac9beb8eea5d79e46fe7b877e Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 28 Jul 2024 19:57:18 +0100 Subject: [PATCH 11/85] Renderers: Allow BaseAbsentValues in value results Fixes #1216 --- volatility3/framework/renderers/format_hints.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/volatility3/framework/renderers/format_hints.py b/volatility3/framework/renderers/format_hints.py index 6120b77c9..194e38099 100644 --- a/volatility3/framework/renderers/format_hints.py +++ b/volatility3/framework/renderers/format_hints.py @@ -10,6 +10,8 @@ Text renderers should attempt to honour all hints provided in this module where """ from typing import Type, Union +from volatility3.framework import interfaces + class Bin(int): """A class to indicate that the integer value should be represented as a @@ -66,3 +68,17 @@ class MultiTypeData(bytes): and self.split_nulls == other.split_nulls and self.show_hex == other.show_hex ) + + +BinOrAbsent = lambda x: ( + Bin(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x +) +HexOrAbsent = lambda x: ( + Hex(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x +) +HexBytesOrAbsent = lambda x: ( + HexBytes(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x +) +MultiTypeDataOrAbsent = lambda x: ( + MultiTypeData(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x +) From b659a060bd6caf37eb0e51560986cc58aab067de Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 28 Jul 2024 21:57:48 +0100 Subject: [PATCH 12/85] Renderers: Ensure the version is bumped so plugins can require the format_hints properly --- volatility3/framework/constants/_version.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 21c339a6e..f219fb0af 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,11 +1,9 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 7 # Number of changes that only add to the interface -VERSION_PATCH = 2 # Number of changes that do not change the interface +VERSION_MINOR = 8 # Number of changes that only add to the interface +VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" -# TODO: At version 2.0.0, remove the symbol_shift feature - PACKAGE_VERSION = ( ".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]]) + VERSION_SUFFIX From 76414d3246717ba9dde2a3cdd94a8c1037a7374b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 29 Jul 2024 14:13:54 +1000 Subject: [PATCH 13/85] Renderers conversion fix: Create aware datetimes to represent times in UTC. Fix Python 3.12 datetime.utcfromtimestamp() deprecation. See warning note on https://docs.python.org/3/library/datetime.html#datetime.datetime.utcfromtimestamp: Because naive datetime objects are treated by many datetime methods as local times, it is preferred to use aware datetimes to represent times in UTC. As such, the recommended way to create an object representing a specific timestamp in UTC is by calling datetime.fromtimestamp(timestamp, tz=timezone.utc). Additionaly, datetime.utcfromtimestamp() is deprecated since 3.12 --- volatility3/framework/renderers/conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/renderers/conversion.py b/volatility3/framework/renderers/conversion.py index bb18fcc8a..c833cc2cf 100644 --- a/volatility3/framework/renderers/conversion.py +++ b/volatility3/framework/renderers/conversion.py @@ -19,7 +19,7 @@ def wintime_to_datetime( return renderers.NotApplicableValue() unix_time = unix_time - 11644473600 try: - return datetime.datetime.utcfromtimestamp(unix_time) + return datetime.datetime.fromtimestamp(unix_time, datetime.timezone.utc) # Windows sometimes throws OSErrors rather than ValueErrors when it can't convert a value except (ValueError, OSError): return renderers.UnparsableValue() @@ -34,7 +34,7 @@ def unixtime_to_datetime( if unixtime > 0: with contextlib.suppress(ValueError): - ret = datetime.datetime.utcfromtimestamp(unixtime) + ret = datetime.datetime.fromtimestamp(unixtime, datetime.timezone.utc) return ret From ece15c914f27e6fbb651e8b3f3ae7e525aa50138 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 29 Jul 2024 14:18:10 +1000 Subject: [PATCH 14/85] Renderers conversion exceptions fix: Since version 3.3 utcfromtimestamp() and fromtimestamp() Python datimetime module raises OverflowError instead of ValueError. As of today, Volatility3 requires Python 3.7.3 so we should only include OverflowError --- volatility3/framework/renderers/conversion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/renderers/conversion.py b/volatility3/framework/renderers/conversion.py index c833cc2cf..c94201681 100644 --- a/volatility3/framework/renderers/conversion.py +++ b/volatility3/framework/renderers/conversion.py @@ -20,8 +20,8 @@ def wintime_to_datetime( unix_time = unix_time - 11644473600 try: return datetime.datetime.fromtimestamp(unix_time, datetime.timezone.utc) - # Windows sometimes throws OSErrors rather than ValueErrors when it can't convert a value - except (ValueError, OSError): + # Windows sometimes throws OSErrors rather than OverflowError when it can't convert a value + except (OverflowError, OSError): return renderers.UnparsableValue() @@ -33,7 +33,7 @@ def unixtime_to_datetime( ) if unixtime > 0: - with contextlib.suppress(ValueError): + with contextlib.suppress(OverflowError): ret = datetime.datetime.fromtimestamp(unixtime, datetime.timezone.utc) return ret From b343734bae0d3090a5477997d27885d237096e50 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 29 Jul 2024 15:16:34 +1000 Subject: [PATCH 15/85] Renderers conversion exceptions fix: Even though the documentation states that OverflowError should be raised starting from version 3.3, it has been observed that ValueError is still being triggered. Also, in Linux, we noticed that OSError is also being raised in some cases. --- volatility3/framework/renderers/conversion.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/renderers/conversion.py b/volatility3/framework/renderers/conversion.py index c94201681..864794860 100644 --- a/volatility3/framework/renderers/conversion.py +++ b/volatility3/framework/renderers/conversion.py @@ -20,8 +20,10 @@ def wintime_to_datetime( unix_time = unix_time - 11644473600 try: return datetime.datetime.fromtimestamp(unix_time, datetime.timezone.utc) - # Windows sometimes throws OSErrors rather than OverflowError when it can't convert a value - except (OverflowError, OSError): + # Windows sometimes throws OSErrors rather than ValueError/OverflowError when it can't convert a value + # Since Python 3.3, this should raise OverflowError instead of ValueError. However, it was observed + # that even in Python 3.7.17, ValueError is still being raised. + except (ValueError, OverflowError, OSError): return renderers.UnparsableValue() @@ -33,7 +35,9 @@ def unixtime_to_datetime( ) if unixtime > 0: - with contextlib.suppress(OverflowError): + # Since Python 3.3, this should raise OverflowError instead of ValueError. However, it was observed + # that even in Python 3.7.17, ValueError is still being raised. OSError is also raised on Linux + with contextlib.suppress(ValueError, OverflowError, OSError): ret = datetime.datetime.fromtimestamp(unixtime, datetime.timezone.utc) return ret From ba7ec0059960fd6470254e8b3cd4ae15e89ea174 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 29 Jul 2024 17:55:56 -0500 Subject: [PATCH 16/85] Windows: Fixes bad structure member in callbacks This fixes a bug in the x64 callbacks symbols. The `NotificationRoutine` is currently an `unsigned int` instead of a void pointer. This prevents the correct mapping of the notification routine to the kernel module that contains it. --- volatility3/framework/symbols/windows/callbacks-x64.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/windows/callbacks-x64.json b/volatility3/framework/symbols/windows/callbacks-x64.json index 3f891b94f..705f2361d 100644 --- a/volatility3/framework/symbols/windows/callbacks-x64.json +++ b/volatility3/framework/symbols/windows/callbacks-x64.json @@ -105,8 +105,11 @@ }, "NotificationRoutine": { "type": { - "kind": "base", - "name": "unsigned int" + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } }, "offset": 24 } From d97fd777f37d58fff19df48334546c2e709f0d27 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 29 Jul 2024 18:36:43 -0500 Subject: [PATCH 17/85] Windows: Bumps netstat module version requirement This is a bump of the version number for the netstat plugin's `modules` requirement - it didn't get updated after #1173 was merged. --- volatility3/framework/plugins/windows/netstat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/netstat.py b/volatility3/framework/plugins/windows/netstat.py index 24eb02018..0908767fc 100644 --- a/volatility3/framework/plugins/windows/netstat.py +++ b/volatility3/framework/plugins/windows/netstat.py @@ -35,7 +35,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): name="netscan", component=netscan.NetScan, version=(1, 0, 0) ), requirements.VersionRequirement( - name="modules", component=modules.Modules, version=(1, 0, 0) + name="modules", component=modules.Modules, version=(2, 0, 0) ), requirements.VersionRequirement( name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) From efc48d5831d00fa7db45e853d380165731736841 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 30 Jul 2024 17:18:14 +1000 Subject: [PATCH 18/85] Make timeliner able to sort aware datetimes. Otherwise, it will raise an exception when comparing the plugin output data with this naive datetime --- volatility3/framework/plugins/timeliner.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index 26a100f53..f657a2918 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -105,7 +105,9 @@ class Timeliner(interfaces.plugins.PluginInterface): data = item[1] def sortable(timestamp): - max_date = datetime.datetime(day=1, month=12, year=datetime.MAXYEAR) + max_date = datetime.datetime( + day=1, month=12, year=datetime.MAXYEAR, tzinfo=datetime.timezone.utc + ) if isinstance(timestamp, interfaces.renderers.BaseAbsentValue): return max_date return timestamp From 56b618f5c3345984c10af7e2868254e2d2bb7ea8 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 30 Jul 2024 13:00:38 -0500 Subject: [PATCH 19/85] Windows: Updates netscan with new symbol file `netscan` was missing coverage for Windows 10 Build 20348, causing owners and create times for `_TCP_ENDPOINTS` to be missing. This adds a symbol file and the necessary version check in the netscan plugin. Testing confirms that this returns the correct creation time and owner process. --- .../framework/plugins/windows/netscan.py | 1 + .../netscan/netscan-win10-20348-x64.json | 582 ++++++++++++++++++ 2 files changed, 583 insertions(+) create mode 100644 volatility3/framework/symbols/windows/netscan/netscan-win10-20348-x64.json diff --git a/volatility3/framework/plugins/windows/netscan.py b/volatility3/framework/plugins/windows/netscan.py index 62ead3ab7..868bd8bcd 100644 --- a/volatility3/framework/plugins/windows/netscan.py +++ b/volatility3/framework/plugins/windows/netscan.py @@ -218,6 +218,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): (10, 0, 18362, 0): "netscan-win10-18362-x64", (10, 0, 18363, 0): "netscan-win10-18363-x64", (10, 0, 19041, 0): "netscan-win10-19041-x64", + (10, 0, 20348, 0): "netscan-win10-20348-x64", } # we do not need to check for tcpip's specific FileVersion in every case diff --git a/volatility3/framework/symbols/windows/netscan/netscan-win10-20348-x64.json b/volatility3/framework/symbols/windows/netscan/netscan-win10-20348-x64.json new file mode 100644 index 000000000..bd574b2b7 --- /dev/null +++ b/volatility3/framework/symbols/windows/netscan/netscan-win10-20348-x64.json @@ -0,0 +1,582 @@ +{ + "base_types": { + "unsigned long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned char": { + "kind": "char", + "size": 1, + "signed": false, + "endian": "little" + }, + "pointer": { + "kind": "int", + "size": 8, + "signed": false, + "endian": "little" + }, + "unsigned int": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "little" + }, + "unsigned be short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "big" + }, + "long long": { + "endian": "little", + "kind": "int", + "signed": true, + "size": 8 + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "symbols": {}, + "user_types": { + "_UDP_ENDPOINT": { + "fields": { + "Owner": { + "offset": 40, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_EPROCESS" + } + + } + }, + "CreateTime": { + "offset": 88, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "Next": { + "offset": 112, + "type":{ + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_UDP_ENDPOINT" + } + } + }, + "LocalAddr": { + "offset": 168, + "type":{ + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_LOCAL_ADDRESS_WIN10_UDP" + } + } + }, + "InetAF": { + "offset": 32, + "type":{ + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_INETAF" + } + + } + }, + "Port": { + "offset": 160, + "type": { + "kind": "base", + "name": "unsigned be short" + } + } + }, + "kind": "struct", + "size": 168 + }, + "_TCP_LISTENER": { + "fields": { + "Owner": { + "offset": 48, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_EPROCESS" + } + + } + }, + "CreateTime": { + "offset": 64, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "LocalAddr": { + "offset": 96, + "type":{ + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_LOCAL_ADDRESS" + } + + } + }, + "InetAF": { + "offset": 40, + "type":{ + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_INETAF" + } + + } + }, + "Next": { + "offset": 120, + "type":{ + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TCP_LISTENER" + } + } + }, + "Port": { + "offset": 114, + "type": { + "kind": "base", + "name": "unsigned be short" + } + } + }, + "kind": "struct", + "size": 128 + }, + "_TCP_ENDPOINT": { + "fields": { + "Owner": { + "offset": 752, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_EPROCESS" + } + } + }, + "CreateTime": { + "offset": 776, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "AddrInfo": { + "offset": 24, + "type":{ + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ADDRINFO" + } + } + }, + "ListEntry": { + "offset": 40, + "type": { + "kind": "union", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "InetAF": { + "offset": 16, + "type":{ + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_INETAF" + } + } + }, + "LocalPort": { + "offset": 112, + "type": { + "kind": "base", + "name": "unsigned be short" + } + }, + "RemotePort": { + "offset": 114, + "type": { + "kind": "base", + "name": "unsigned be short" + } + }, + "State": { + "offset": 108, + "type": { + "kind": "enum", + "name": "TCPStateEnum" + } + } + }, + "kind": "struct", + "size": 632 + }, + "_LOCAL_ADDRESS": { + "fields": { + "pData": { + "offset": 16, + "type": { + "kind": "pointer", + "subtype": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_IN_ADDR" + } + } + } + } + }, + "kind": "struct", + "size": 20 + }, + "_LOCAL_ADDRESS_WIN10_UDP": { + "fields": { + "pData": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_IN_ADDR" + } + } + } + }, + "kind": "struct", + "size": 4 + }, + "_ADDRINFO": { + "fields": { + "Local": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_LOCAL_ADDRESS" + } + } + }, + "Remote": { + "offset": 16, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_IN_ADDR" + } + } + } + }, + "kind": "struct", + "size": 4 + }, + "_IN_ADDR": { + "fields": { + "addr4": { + "offset": 0, + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + } + }, + "addr6": { + "offset": 0, + "type": { + "count": 16, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + } + } + }, + "kind": "struct", + "size": 6 + }, + "_INETAF": { + "fields": { + "AddressFamily": { + "offset": 24, + "type": { + "kind": "base", + "name": "unsigned short" + } + } + }, + "kind": "struct", + "size": 26 + }, + "_LARGE_INTEGER": { + "fields": { + "HighPart": { + "offset": 4, + "type": { + "kind": "base", + "name": "long" + } + }, + "LowPart": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "QuadPart": { + "offset": 0, + "type": { + "kind": "base", + "name": "long long" + } + }, + "u": { + "offset": 0, + "type": { + "kind": "struct", + "name": "__unnamed_2" + } + } + }, + "kind": "union", + "size": 8 + }, + "_INET_COMPARTMENT_SET": { + "fields": { + "InetCompartment": { + "offset": 328, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_INET_COMPARTMENT" + } + } + } + }, + "kind": "struct", + "size": 384 + }, + "_INET_COMPARTMENT": { + "fields": { + "ProtocolCompartment": { + "offset": 32, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_PROTOCOL_COMPARTMENT" + } + } + } + }, + "kind": "struct", + "size": 48 + }, + "_PROTOCOL_COMPARTMENT": { + "fields": { + "PortPool": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_INET_PORT_POOL" + } + } + } + }, + "kind": "struct", + "size": 16 + }, + "_PORT_ASSIGNMENT_ENTRY": { + "fields": { + "Entry": { + "offset": 16, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + } + }, + "kind": "struct", + "size": 32 + }, + "_PORT_ASSIGNMENT_LIST": { + "fields": { + "Assignments": { + "offset": 0, + "type": { + "count": 256, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_PORT_ASSIGNMENT_ENTRY" + } + } + } + }, + "kind": "struct", + "size": 6144 + }, + "_PORT_ASSIGNMENT": { + "fields": { + "InPaBigPoolBase": { + "offset": 24, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_PORT_ASSIGNMENT_LIST" + } + } + } + }, + "kind": "struct", + "size": 32 + }, + "_INET_PORT_POOL": { + "fields": { + "PortAssignments": { + "offset": 224, + "type": { + "count": 256, + "kind": "array", + "subtype": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_PORT_ASSIGNMENT" + } + } + } + }, + "PortBitMap": { + "offset": 208, + "type": { + "kind": "struct", + "name": "nt_symbols!_RTL_BITMAP" + } + } + }, + "kind": "struct", + "size": 11200 + }, + "_PARTITION": { + "fields": { + "Endpoints" : { + "offset": 8, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_RTL_DYNAMIC_HASH_TABLE" + } + } + }, + "UnknownHashTable" : { + "offset": 16, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_RTL_DYNAMIC_HASH_TABLE" + } + } + } + }, + "kind": "struct", + "size": 192 + }, + "_PARTITION_TABLE": { + "fields": { + "Partitions": { + "offset": 0, + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_PARTITION" + } + } + } + }, + "kind": "struct", + "size": 128 + } + }, + "enums": { + "TCPStateEnum": { + "base": "long", + "constants": { + "CLOSED": 0, + "LISTENING": 1, + "SYN_SENT": 2, + "SYN_RCVD": 3, + "ESTABLISHED": 4, + "FIN_WAIT1": 5, + "FIN_WAIT2": 6, + "CLOSE_WAIT": 7, + "CLOSING": 8, + "LAST_ACK": 9, + "TIME_WAIT": 12, + "DELETE_TCB": 13 + }, + "size": 4 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "dgmcdona-by-hand", + "datetime": "2024-07-30T13:00:00" + }, + "format": "6.0.0" + } +} From a84a3706c5acee2c93df650a167ab058fe657053 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Tue, 30 Jul 2024 14:00:41 -0500 Subject: [PATCH 20/85] Remove errant filter on ldrmodule checks --- volatility3/framework/plugins/windows/ldrmodules.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/volatility3/framework/plugins/windows/ldrmodules.py b/volatility3/framework/plugins/windows/ldrmodules.py index ffd84ea4a..a888f22e1 100644 --- a/volatility3/framework/plugins/windows/ldrmodules.py +++ b/volatility3/framework/plugins/windows/ldrmodules.py @@ -47,14 +47,6 @@ class LdrModules(interfaces.plugins.PluginInterface): self.context, self.config_path, "windows", "pe", class_types=pe.class_types ) - def filter_function(x: interfaces.objects.ObjectInterface) -> bool: - try: - return not (x.get_private_memory() == 0 and x.ControlArea) - except AttributeError: - return False - - filter_func = filter_function - for proc in procs: proc_layer_name = proc.add_process_layer() @@ -69,7 +61,7 @@ class LdrModules(interfaces.plugins.PluginInterface): # Build dictionary of mapped files, where the VAD start address is the key and value is the file name of the mapped file mapped_files = {} - for vad in vadinfo.VadInfo.list_vads(proc, filter_func=filter_func): + for vad in vadinfo.VadInfo.list_vads(proc): dos_header = self.context.object( pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", offset=vad.get_start(), From 73bc10c2834d9a8fa2ab04c098a4735542226170 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 31 Jul 2024 21:46:57 +0100 Subject: [PATCH 21/85] Wire the argcomplete into volshell too --- volatility3/cli/volshell/__init__.py | 12 ++++++++++++ volshell.py | 1 + 2 files changed, 13 insertions(+) diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index 035ed9b2e..2bf1958e2 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -21,6 +21,14 @@ from volatility3.framework import ( plugins, ) +try: + import argcomplete + + HAS_ARGCOMPLETE = True +except ImportError: + HAS_ARGCOMPLETE = False + + # Make sure we log everything rootlog = logging.getLogger() @@ -276,6 +284,10 @@ class VolShell(cli.CommandLine): # Hand the plugin requirements over to the CLI (us) and let it construct the config tree # Run the argparser + if HAS_ARGCOMPLETE: + # The autocompletion line must be after the partial_arg handling, so that it doesn't trip it + # before all the plugins have been added + argcomplete.autocomplete(parser) args = parser.parse_args() vollog.log( diff --git a/volshell.py b/volshell.py index 71d35a47c..65b11885e 100755 --- a/volshell.py +++ b/volshell.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK # This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 From d576f8cb48d064ce3dc87936df642481aa1f3186 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 31 Jul 2024 21:49:07 +0100 Subject: [PATCH 22/85] Fix CodeQL error --- volatility3/cli/volargparse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/cli/volargparse.py b/volatility3/cli/volargparse.py index 3e7eb6751..2bd53077b 100644 --- a/volatility3/cli/volargparse.py +++ b/volatility3/cli/volargparse.py @@ -99,6 +99,7 @@ class HelpfulArgParser(argparse.ArgumentParser): # return the number of arguments matched return len(match.group(1)) - def _check_value(self, action, value): + def _check_value(self, action: argparse.Action, value: Any) -> None: if not isinstance(action, HelpfulSubparserAction): return super()._check_value(action, value) + return None From fd6e4bec5cab84035491f629d234522ac6d5a8af Mon Sep 17 00:00:00 2001 From: qpalzmz112 <68213464+qpalzmz112@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:53:29 -0500 Subject: [PATCH 23/85] Updated with feedback from the PR --- .../framework/plugins/windows/psxview.py | 172 ++++++++---------- 1 file changed, 74 insertions(+), 98 deletions(-) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 1fde5b12d..dc5e77ec3 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -1,4 +1,4 @@ -import datetime, logging +import datetime, logging, string from volatility3.framework import constants, exceptions from volatility3.framework.interfaces import plugins @@ -22,7 +22,7 @@ class PsXView(plugins.PluginInterface): plugin's output in a terminal.""" # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the funcitonality - # which the original plugin to do it. + # which the original plugin used to do it. # I don't think it's worth including the sessions method either because both the original psxview plugin # and Volatility3's sessions plugin begin with the list of processes found by PsList. @@ -35,6 +35,10 @@ class PsXView(plugins.PluginInterface): _required_framework_version = (2, 0, 0) _version = (1, 0, 0) + valid_proc_name_chars = set( + string.ascii_lowercase + string.ascii_uppercase + "." + " " + ) + @classmethod def get_requirements(cls): return [ @@ -58,17 +62,6 @@ class PsXView(plugins.PluginInterface): requirements.VersionRequirement( name="handles", component=handles.Handles, version=(1, 0, 0) ), - requirements.VersionRequirement( - name="sessions", component=sessions.Sessions, version=(0, 0, 0) - ), - requirements.BooleanRequirement( - name="identify-expected", - description='In the plugin\'s output, replace false with \ - normal where false is the expected result for a Windows machine running normally. \ - Keep in mind that this plugin uses simple checks to identify "normal" behavior, \ - so you may want to double-check the legitimacy of these processes yourself.', - optional=True, - ), requirements.BooleanRequirement( name="physical-offsets", description="List processes with phyiscall offsets instead of virtual offsets.", @@ -76,54 +69,56 @@ class PsXView(plugins.PluginInterface): ), ] - def proc_name_to_string(self, proc): + def _proc_name_to_string(self, proc): return proc.ImageFileName.cast( "string", max_length=proc.ImageFileName.vol.count, errors="replace" ) - def is_ascii(self, str): - return str.split(".")[0].isalnum() + def _is_valid_proc_name(self, str): + for c in str: + if not c in self.valid_proc_name_chars: + return False + return True - def filter_garbage_procs(self, proc_list): + def _filter_garbage_procs(self, proc_list): return [ p for p in proc_list - if p.is_valid() and self.is_ascii(self.proc_name_to_string(p)) + if p.is_valid() and self._is_valid_proc_name(self._proc_name_to_string(p)) ] - def translate_offset(self, offset): - if self.config["physical-offsets"]: + def _translate_offset(self, offset): + if not self.config["physical-offsets"]: return offset kernel = self.context.modules[self.config["kernel"]] layer_name = kernel.layer_name try: - offset = list( + _, _, offset, _, _ = list( self.context.layers[layer_name].mapping(offset=offset, length=0) - )[0][2] + )[0] except: # already have physical address pass return offset - def proc_list_to_dict(self, tasks): - return {self.translate_offset(proc.vol.offset): proc for proc in tasks} + def _proc_list_to_dict(self, tasks): + tasks = self._filter_garbage_procs(tasks) + return {self._translate_offset(proc.vol.offset): proc for proc in tasks} - def check_pslist(self, tasks): - res = self.filter_garbage_procs(tasks) - return self.proc_list_to_dict(tasks) + def _check_pslist(self, tasks): + return self._proc_list_to_dict(tasks) - def check_psscan(self, layer_name, symbol_table): + def _check_psscan(self, layer_name, symbol_table): res = psscan.PsScan.scan_processes( context=self.context, layer_name=layer_name, symbol_table=symbol_table ) - res = self.filter_garbage_procs(res) - return self.proc_list_to_dict(res) + return self._proc_list_to_dict(res) - def check_thrdscan(self): + def _check_thrdscan(self): ret = [] for ethread in thrdscan.ThrdScan.scan_threads( @@ -142,13 +137,13 @@ class PsXView(plugins.PluginInterface): "Unable to find the owning process of ethread", ) - return self.proc_list_to_dict(ret) + return self._proc_list_to_dict(ret) - def check_csrss_handles(self, tasks, layer_name, symbol_table): + def _check_csrss_handles(self, tasks, layer_name, symbol_table): ret = [] for p in tasks: - name = self.proc_name_to_string(p) + name = self._proc_name_to_string(p) if name == "csrss.exe": try: if p.has_member("ObjectTable"): @@ -172,13 +167,7 @@ class PsXView(plugins.PluginInterface): constants.LOGLEVEL_VVV, "Cannot access eprocess object table" ) - ret = self.filter_garbage_procs(ret) - return self.proc_list_to_dict(ret) - - def check_session(self, pslist_procs): - procs = [p for p in pslist_procs if p.get_session_id() != None] - - return self.proc_list_to_dict(procs) + return self._proc_list_to_dict(ret) def _generator(self): kernel = self.context.modules[self.config["kernel"]] @@ -192,72 +181,60 @@ class PsXView(plugins.PluginInterface): ) ) + # get processes from each source processes = {} - processes["pslist"] = self.check_pslist(kdbg_list_processes) - processes["psscan"] = self.check_psscan(layer_name, symbol_table) - processes["thrdscan"] = self.check_thrdscan() - processes["csrss"] = self.check_csrss_handles( + processes["pslist"] = self._check_pslist(kdbg_list_processes) + processes["psscan"] = self._check_psscan(layer_name, symbol_table) + processes["thrdscan"] = self._check_thrdscan() + processes["csrss"] = self._check_csrss_handles( kdbg_list_processes, layer_name, symbol_table ) - processes["sessions"] = self.check_session(kdbg_list_processes) - seen_offsets = set() - for source in processes: - for offset in processes[source]: - if offset not in seen_offsets: - seen_offsets.add(offset) - proc = processes[source][offset] + # print results - pid = proc.UniqueProcessId - name = self.proc_name_to_string(proc) + # list of lists of offsets + todo_offsets = [list(processes[source].keys()) for source in processes] - exit_time = proc.get_exit_time() - if type(exit_time) != datetime.datetime: - exit_time = "" - else: - exit_time = str(exit_time) + # flatten to one list + todo_offsets = sum(todo_offsets, []) - in_sources = { - src: str(offset in processes[src]) for src in processes - } + # remove duplicates + todo_offsets = set(todo_offsets) - if self.config["identify-expected"]: - f = "False" - n = "Normal" + for offset in todo_offsets: + proc = None - if in_sources["pslist"] == f: - if exit_time != "": - in_sources["pslist"] = n + in_sources = {src: False for src in processes} - if in_sources["thrdscan"] == f: - if exit_time != "": - in_sources["thrdscan"] = n + for source in processes: + if offset in processes[source]: + in_sources[source] = True + if not proc: + proc = processes[source][offset] - if in_sources["csrss"] == f: - if name.lower() in ["system", "smss.exe", "csrss.exe"]: - in_sources["csrss"] = n - elif exit_time != "": - in_sources["csrss"] = n + pid = proc.UniqueProcessId + name = self._proc_name_to_string(proc) - if in_sources["sessions"] == f: - if name.lower() in ["system", "smss.exe"]: - in_sources["sessions"] = n + exit_time = proc.get_exit_time() + if type(exit_time) != datetime.datetime: + exit_time = "" + else: + exit_time = str(exit_time) - yield ( - 0, - ( - format_hints.Hex(offset), - name, - pid, - in_sources["pslist"], - in_sources["psscan"], - in_sources["thrdscan"], - in_sources["csrss"], - in_sources["sessions"], - exit_time, - ), - ) + yield ( + 0, + ( + format_hints.Hex(offset), + name, + pid, + in_sources["pslist"], + in_sources["psscan"], + in_sources["thrdscan"], + in_sources["csrss"], + exit_time, + ), + ) def run(self): offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)" @@ -268,11 +245,10 @@ class PsXView(plugins.PluginInterface): (offset_str, format_hints.Hex), ("Name", str), ("PID", int), - ("pslist", str), - ("psscan", str), - ("thrdscan", str), - ("csrss", str), - ("sessions", str), + ("pslist", bool), + ("psscan", bool), + ("thrdscan", bool), + ("csrss", bool), ("Exit Time", str), ], self._generator(), From 9e8864521c3fb06e80536a8d0f249ac19fd9a9c0 Mon Sep 17 00:00:00 2001 From: qpalzmz112 <68213464+qpalzmz112@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:02:52 -0500 Subject: [PATCH 24/85] Added debug log for failed address translation --- volatility3/framework/plugins/windows/psxview.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index dc5e77ec3..918eb44ba 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -98,9 +98,8 @@ class PsXView(plugins.PluginInterface): _, _, offset, _, _ = list( self.context.layers[layer_name].mapping(offset=offset, length=0) )[0] - except: - # already have physical address - pass + except exceptions.PagedInvalidAddressException: + vollog.debug(f"Page fault: unable to translate {offset:0x}") return offset From 90b8b5340f9bc886e9b45ecadd80b7d86b8384f3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 1 Aug 2024 11:46:10 +1000 Subject: [PATCH 25/85] Linux: Add inode, timespec, and timespec64 object extensions to support different kernel versions, ensuring we will get aware datetimes when using them. --- .../framework/symbols/linux/__init__.py | 6 + .../symbols/linux/extensions/__init__.py | 132 ++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index c4e2587f4..03353135d 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -29,12 +29,18 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class("files_struct", extensions.files_struct) self.set_type_class("kobject", extensions.kobject) self.set_type_class("cred", extensions.cred) + self.set_type_class("inode", extensions.inode) # Might not exist in the current symbols self.optional_set_type_class("module", extensions.module) self.optional_set_type_class("bpf_prog", extensions.bpf_prog) self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct) self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t) + # kernels >= 4.18 + self.optional_set_type_class("timespec64", extensions.timespec64) + # kernels < 4.18. Reuses timespec64 obj extension, since both has the same members + self.optional_set_type_class("timespec", extensions.timespec64) + # Mount self.set_type_class("vfsmount", extensions.vfsmount) # Might not exist in older kernels or the current symbols diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index e7c6b66d7..be31e298c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -4,10 +4,13 @@ import collections.abc import logging +import stat +from datetime import datetime import socket as socket_module from typing import Generator, Iterable, Iterator, Optional, Tuple, List from volatility3.framework import constants, exceptions, objects, interfaces, symbols +from volatility3.framework import renderers from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS @@ -1761,3 +1764,132 @@ class kernel_cap_t(kernel_cap_struct): ) return cap_value & self.get_kernel_cap_full() + + +class timespec64(objects.StructType): + def to_datetime(self) -> datetime: + """Returns the respective aware datetime""" + + dt = renderers.conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9) + return dt + + +class inode(objects.StructType): + def is_valid(self) -> bool: + # i_count is a 'signed' counter (atomic_t). Smear, or essentially a wrong inode + # pointer, will easily cause an integer overflow here. + return self.i_ino > 0 and self.i_count.counter >= 0 + + def is_dir(self) -> bool: + """Returns True if the inode is a directory""" + return stat.S_ISDIR(self.i_mode) != 0 + + def is_reg(self) -> bool: + """Returns True if the inode is a regular file""" + return stat.S_ISREG(self.i_mode) != 0 + + def is_link(self) -> bool: + """Returns True if the inode is a symlink""" + return stat.S_ISLNK(self.i_mode) != 0 + + def is_fifo(self) -> bool: + """Returns True if the inode is a FIFO""" + return stat.S_ISFIFO(self.i_mode) != 0 + + def is_sock(self) -> bool: + """Returns True if the inode is a socket""" + return stat.S_ISSOCK(self.i_mode) != 0 + + def is_block(self) -> bool: + """Returns True if the inode is a block device""" + return stat.S_ISBLK(self.i_mode) != 0 + + def is_char(self) -> bool: + """Returns True if the inode is a char device""" + return stat.S_ISCHR(self.i_mode) != 0 + + def is_sticky(self) -> bool: + """Returns True if the sticky bit is set""" + return (self.i_mode & stat.S_ISVTX) != 0 + + def get_inode_type(self) -> str: + """Returns inode type name + + Returns: + The inode type name + """ + if self.is_dir(): + return "DIR" + elif self.is_reg(): + return "REG" + elif self.is_link(): + return "LNK" + elif self.is_fifo(): + return "FIFO" + elif self.is_sock(): + return "SOCK" + elif self.is_char(): + return "CHR" + elif self.is_block(): + return "BLK" + else: + return renderers.UnparsableValue() + + def get_inode_number(self) -> int: + """Returns the inode number""" + return int(self.i_ino) + + def ___time_member_to_datetime(self, member) -> datetime: + if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): + # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 + # Ref Linux commit 3aa63a569c64e708df547a8913c84e64a06e7853 + return renderers.conversion.unixtime_to_datetime( + self.member(f"{member}_sec") + self.has_member(f"{member}_nsec") / 1e9 + ) + elif self.has_member(f"__{member}"): + # 6.6 <= kernels < 6.11 it's a timespec64 + # Ref Linux commit 13bc24457850583a2e7203ded05b7209ab4bc5ef / 12cd44023651666bd44baa36a5c999698890debb + return self.member(f"__{member}").to_datetime() + elif self.has_member(member): + # In kernels < 6.6 it's a timespec64 or timespec + return self.member(member).to_datetime() + else: + raise exceptions.VolatilityException( + "Unsupported kernel inode type implementation" + ) + + def get_access_time(self) -> datetime: + """Returns the inode's last access time + This is updated when inode contents are read + + Returns: + A datetime with the inode's last access time + """ + return self.___time_member_to_datetime("i_atime") + + def get_modification_time(self) -> datetime: + """Returns the inode's last modification time + This is updated when the inode contents change + + Returns: + A datetime with the inode's last data modification time + """ + + return self.___time_member_to_datetime("i_mtime") + + def get_change_time(self) -> datetime: + """Returns the inode's last change time + This is updated when the inode metadata changes + + Returns: + A datetime with the inode's last change time + """ + return self.___time_member_to_datetime("i_ctime") + + def get_file_mode(self) -> str: + """Returns the inode's file mode as string of the form '-rwxrwxrwx'. + + Returns: + The inode's file mode string + """ + return stat.filemode(self.i_mode) From 3017a7d00c3a9cfca3f94b9e7dfb8ba08d48fd0e Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 1 Aug 2024 11:46:10 +1000 Subject: [PATCH 26/85] Linux: Add inode, timespec, and timespec64 object extensions to support different kernel versions, ensuring we will get aware datetimes when using them. --- .../framework/symbols/linux/__init__.py | 6 + .../symbols/linux/extensions/__init__.py | 132 ++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index c4e2587f4..03353135d 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -29,12 +29,18 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class("files_struct", extensions.files_struct) self.set_type_class("kobject", extensions.kobject) self.set_type_class("cred", extensions.cred) + self.set_type_class("inode", extensions.inode) # Might not exist in the current symbols self.optional_set_type_class("module", extensions.module) self.optional_set_type_class("bpf_prog", extensions.bpf_prog) self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct) self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t) + # kernels >= 4.18 + self.optional_set_type_class("timespec64", extensions.timespec64) + # kernels < 4.18. Reuses timespec64 obj extension, since both has the same members + self.optional_set_type_class("timespec", extensions.timespec64) + # Mount self.set_type_class("vfsmount", extensions.vfsmount) # Might not exist in older kernels or the current symbols diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index e7c6b66d7..be31e298c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -4,10 +4,13 @@ import collections.abc import logging +import stat +from datetime import datetime import socket as socket_module from typing import Generator, Iterable, Iterator, Optional, Tuple, List from volatility3.framework import constants, exceptions, objects, interfaces, symbols +from volatility3.framework import renderers from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS @@ -1761,3 +1764,132 @@ class kernel_cap_t(kernel_cap_struct): ) return cap_value & self.get_kernel_cap_full() + + +class timespec64(objects.StructType): + def to_datetime(self) -> datetime: + """Returns the respective aware datetime""" + + dt = renderers.conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9) + return dt + + +class inode(objects.StructType): + def is_valid(self) -> bool: + # i_count is a 'signed' counter (atomic_t). Smear, or essentially a wrong inode + # pointer, will easily cause an integer overflow here. + return self.i_ino > 0 and self.i_count.counter >= 0 + + def is_dir(self) -> bool: + """Returns True if the inode is a directory""" + return stat.S_ISDIR(self.i_mode) != 0 + + def is_reg(self) -> bool: + """Returns True if the inode is a regular file""" + return stat.S_ISREG(self.i_mode) != 0 + + def is_link(self) -> bool: + """Returns True if the inode is a symlink""" + return stat.S_ISLNK(self.i_mode) != 0 + + def is_fifo(self) -> bool: + """Returns True if the inode is a FIFO""" + return stat.S_ISFIFO(self.i_mode) != 0 + + def is_sock(self) -> bool: + """Returns True if the inode is a socket""" + return stat.S_ISSOCK(self.i_mode) != 0 + + def is_block(self) -> bool: + """Returns True if the inode is a block device""" + return stat.S_ISBLK(self.i_mode) != 0 + + def is_char(self) -> bool: + """Returns True if the inode is a char device""" + return stat.S_ISCHR(self.i_mode) != 0 + + def is_sticky(self) -> bool: + """Returns True if the sticky bit is set""" + return (self.i_mode & stat.S_ISVTX) != 0 + + def get_inode_type(self) -> str: + """Returns inode type name + + Returns: + The inode type name + """ + if self.is_dir(): + return "DIR" + elif self.is_reg(): + return "REG" + elif self.is_link(): + return "LNK" + elif self.is_fifo(): + return "FIFO" + elif self.is_sock(): + return "SOCK" + elif self.is_char(): + return "CHR" + elif self.is_block(): + return "BLK" + else: + return renderers.UnparsableValue() + + def get_inode_number(self) -> int: + """Returns the inode number""" + return int(self.i_ino) + + def ___time_member_to_datetime(self, member) -> datetime: + if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): + # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 + # Ref Linux commit 3aa63a569c64e708df547a8913c84e64a06e7853 + return renderers.conversion.unixtime_to_datetime( + self.member(f"{member}_sec") + self.has_member(f"{member}_nsec") / 1e9 + ) + elif self.has_member(f"__{member}"): + # 6.6 <= kernels < 6.11 it's a timespec64 + # Ref Linux commit 13bc24457850583a2e7203ded05b7209ab4bc5ef / 12cd44023651666bd44baa36a5c999698890debb + return self.member(f"__{member}").to_datetime() + elif self.has_member(member): + # In kernels < 6.6 it's a timespec64 or timespec + return self.member(member).to_datetime() + else: + raise exceptions.VolatilityException( + "Unsupported kernel inode type implementation" + ) + + def get_access_time(self) -> datetime: + """Returns the inode's last access time + This is updated when inode contents are read + + Returns: + A datetime with the inode's last access time + """ + return self.___time_member_to_datetime("i_atime") + + def get_modification_time(self) -> datetime: + """Returns the inode's last modification time + This is updated when the inode contents change + + Returns: + A datetime with the inode's last data modification time + """ + + return self.___time_member_to_datetime("i_mtime") + + def get_change_time(self) -> datetime: + """Returns the inode's last change time + This is updated when the inode metadata changes + + Returns: + A datetime with the inode's last change time + """ + return self.___time_member_to_datetime("i_ctime") + + def get_file_mode(self) -> str: + """Returns the inode's file mode as string of the form '-rwxrwxrwx'. + + Returns: + The inode's file mode string + """ + return stat.filemode(self.i_mode) From e6308a6035156cab5abb6f0cdf537fb75e5881e8 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 1 Aug 2024 21:04:24 +0100 Subject: [PATCH 27/85] Make suggested changes by gcmoreira --- volatility3/cli/volargparse.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/volatility3/cli/volargparse.py b/volatility3/cli/volargparse.py index 2bd53077b..5ce2646ed 100644 --- a/volatility3/cli/volargparse.py +++ b/volatility3/cli/volargparse.py @@ -100,6 +100,11 @@ class HelpfulArgParser(argparse.ArgumentParser): return len(match.group(1)) def _check_value(self, action: argparse.Action, value: Any) -> None: + """This is called to ensure a value is correct/valid + This fails when we want to accept partial values for the plugin name, + so we disable the check (which will throw ArgumentErrors for failed checks) + but only for our plugin subparser, so all other arguments are checked correctly + """ if not isinstance(action, HelpfulSubparserAction): - return super()._check_value(action, value) + super()._check_value(action, value) return None From 0bda1543854d8f92e7fb2bf4c5eff3afdf117819 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 1 Aug 2024 21:08:18 +0100 Subject: [PATCH 28/85] Clarify the documentation a little --- volatility3/cli/volargparse.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/volatility3/cli/volargparse.py b/volatility3/cli/volargparse.py index 5ce2646ed..fd61ddce0 100644 --- a/volatility3/cli/volargparse.py +++ b/volatility3/cli/volargparse.py @@ -101,9 +101,16 @@ class HelpfulArgParser(argparse.ArgumentParser): def _check_value(self, action: argparse.Action, value: Any) -> None: """This is called to ensure a value is correct/valid - This fails when we want to accept partial values for the plugin name, - so we disable the check (which will throw ArgumentErrors for failed checks) - but only for our plugin subparser, so all other arguments are checked correctly + + In normal operation, it would check that a value provided is valid and return None + If it was not valid, it would throw an ArgumentError + + When people provide a partial plugin name, we want to look for a matching plugin name + which happens in the HelpfulSubparserAction's __call_method + + To get there without tripping the check_value failure, we have to prevent the exception + being thrown when the value is a HelpfulSubparserAction. This therefore affects no other + checks for normal parameters. """ if not isinstance(action, HelpfulSubparserAction): super()._check_value(action, value) From d19013c85261ea48dd774dcda66dcb8d1b36782d Mon Sep 17 00:00:00 2001 From: qpalzmz112 <68213464+qpalzmz112@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:19:24 -0500 Subject: [PATCH 29/85] fixed typo, updated plugin docstring, and updated comment --- volatility3/framework/plugins/windows/psxview.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 918eb44ba..cab27f3e3 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -17,17 +17,14 @@ vollog = logging.getLogger(__name__) class PsXView(plugins.PluginInterface): - """Lists all processes found via 6 of the methods described in \"The Art of Memory Forensics,\" which may help + """Lists all processes found via four of the methods described in \"The Art of Memory Forensics,\" which may help identify processes that are trying to hide themselves. I recommend using -r pretty if you are looking at this plugin's output in a terminal.""" # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the funcitonality # which the original plugin used to do it. - # I don't think it's worth including the sessions method either because both the original psxview plugin - # and Volatility3's sessions plugin begin with the list of processes found by PsList. - # The original psxview plugin's session code essentially just filters the pslist for processes - # whose session ID is not None. I've matched this in my code, but again, it doesn't seem worth including. + # The sessions method is omitted because it begins with the list of processes found by Pslist anyway. # Lastly, I've omitted the pspcid method because I could not for the life of me get it to work. I saved the # code I do have from it, and will happily share it if anyone else wants to add it. @@ -64,7 +61,7 @@ class PsXView(plugins.PluginInterface): ), requirements.BooleanRequirement( name="physical-offsets", - description="List processes with phyiscall offsets instead of virtual offsets.", + description="List processes with physical offsets instead of virtual offsets.", optional=True, ), ] From 98c0094da5cfb8a99c9e211004952c9104c619cd Mon Sep 17 00:00:00 2001 From: qpalzmz112 <68213464+qpalzmz112@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:51:11 -0500 Subject: [PATCH 30/85] Updated unpacked variable names --- volatility3/framework/plugins/windows/psxview.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index cab27f3e3..71919c410 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -92,7 +92,7 @@ class PsXView(plugins.PluginInterface): layer_name = kernel.layer_name try: - _, _, offset, _, _ = list( + _original_offset, _original_length, offset, _length, _layer_name = list( self.context.layers[layer_name].mapping(offset=offset, length=0) )[0] except exceptions.PagedInvalidAddressException: @@ -190,15 +190,15 @@ class PsXView(plugins.PluginInterface): # print results # list of lists of offsets - todo_offsets = [list(processes[source].keys()) for source in processes] + offsets = [list(processes[source].keys()) for source in processes] # flatten to one list - todo_offsets = sum(todo_offsets, []) + offsets = sum(offsets, []) # remove duplicates - todo_offsets = set(todo_offsets) + offsets = set(offsets) - for offset in todo_offsets: + for offset in offsets: proc = None in_sources = {src: False for src in processes} From 1dcaf9c0b15dfd00f2b35846488f19f3982ae5b2 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 13:50:51 +1000 Subject: [PATCH 31/85] PR review fixes: Rename method name from private to internal --- .../framework/symbols/linux/extensions/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index be31e298c..599fedb6f 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1839,7 +1839,7 @@ class inode(objects.StructType): """Returns the inode number""" return int(self.i_ino) - def ___time_member_to_datetime(self, member) -> datetime: + def _time_member_to_datetime(self, member) -> datetime: if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 # Ref Linux commit 3aa63a569c64e708df547a8913c84e64a06e7853 @@ -1865,7 +1865,7 @@ class inode(objects.StructType): Returns: A datetime with the inode's last access time """ - return self.___time_member_to_datetime("i_atime") + return self._time_member_to_datetime("i_atime") def get_modification_time(self) -> datetime: """Returns the inode's last modification time @@ -1875,7 +1875,7 @@ class inode(objects.StructType): A datetime with the inode's last data modification time """ - return self.___time_member_to_datetime("i_mtime") + return self._time_member_to_datetime("i_mtime") def get_change_time(self) -> datetime: """Returns the inode's last change time @@ -1884,7 +1884,7 @@ class inode(objects.StructType): Returns: A datetime with the inode's last change time """ - return self.___time_member_to_datetime("i_ctime") + return self._time_member_to_datetime("i_ctime") def get_file_mode(self) -> str: """Returns the inode's file mode as string of the form '-rwxrwxrwx'. From 79529fb8153b3a58b4a1d1c6192cb92cf107800f Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 13:50:51 +1000 Subject: [PATCH 32/85] PR review fixes: Rename method name from private to internal --- .../framework/symbols/linux/extensions/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index be31e298c..599fedb6f 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1839,7 +1839,7 @@ class inode(objects.StructType): """Returns the inode number""" return int(self.i_ino) - def ___time_member_to_datetime(self, member) -> datetime: + def _time_member_to_datetime(self, member) -> datetime: if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 # Ref Linux commit 3aa63a569c64e708df547a8913c84e64a06e7853 @@ -1865,7 +1865,7 @@ class inode(objects.StructType): Returns: A datetime with the inode's last access time """ - return self.___time_member_to_datetime("i_atime") + return self._time_member_to_datetime("i_atime") def get_modification_time(self) -> datetime: """Returns the inode's last modification time @@ -1875,7 +1875,7 @@ class inode(objects.StructType): A datetime with the inode's last data modification time """ - return self.___time_member_to_datetime("i_mtime") + return self._time_member_to_datetime("i_mtime") def get_change_time(self) -> datetime: """Returns the inode's last change time @@ -1884,7 +1884,7 @@ class inode(objects.StructType): Returns: A datetime with the inode's last change time """ - return self.___time_member_to_datetime("i_ctime") + return self._time_member_to_datetime("i_ctime") def get_file_mode(self) -> str: """Returns the inode's file mode as string of the form '-rwxrwxrwx'. From 930d29046ad8c1cd47d6167e84118d78a3e549a6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 14:15:01 +1000 Subject: [PATCH 33/85] PR review fixes: Avoid using renderers in core functions. --- .../framework/symbols/linux/extensions/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 599fedb6f..1b5e1d286 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -7,10 +7,10 @@ import logging import stat from datetime import datetime import socket as socket_module -from typing import Generator, Iterable, Iterator, Optional, Tuple, List +from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union from volatility3.framework import constants, exceptions, objects, interfaces, symbols -from volatility3.framework import renderers +from volatility3.framework.renderers import conversion from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS @@ -1770,7 +1770,7 @@ class timespec64(objects.StructType): def to_datetime(self) -> datetime: """Returns the respective aware datetime""" - dt = renderers.conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9) + dt = conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9) return dt @@ -1812,7 +1812,7 @@ class inode(objects.StructType): """Returns True if the sticky bit is set""" return (self.i_mode & stat.S_ISVTX) != 0 - def get_inode_type(self) -> str: + def get_inode_type(self) -> Union[str, None]: """Returns inode type name Returns: @@ -1833,7 +1833,7 @@ class inode(objects.StructType): elif self.is_block(): return "BLK" else: - return renderers.UnparsableValue() + return None def get_inode_number(self) -> int: """Returns the inode number""" @@ -1843,7 +1843,7 @@ class inode(objects.StructType): if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 # Ref Linux commit 3aa63a569c64e708df547a8913c84e64a06e7853 - return renderers.conversion.unixtime_to_datetime( + return conversion.unixtime_to_datetime( self.member(f"{member}_sec") + self.has_member(f"{member}_nsec") / 1e9 ) elif self.has_member(f"__{member}"): From efba3a1b7336d5b31f7ac5ee1d8e99d95bcd74f6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 14:17:57 +1000 Subject: [PATCH 34/85] PR review fixes: Convert inode's is_* functions to properties --- .../symbols/linux/extensions/__init__.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 1b5e1d286..00f6730eb 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1780,34 +1780,42 @@ class inode(objects.StructType): # pointer, will easily cause an integer overflow here. return self.i_ino > 0 and self.i_count.counter >= 0 + @property def is_dir(self) -> bool: """Returns True if the inode is a directory""" return stat.S_ISDIR(self.i_mode) != 0 + @property def is_reg(self) -> bool: """Returns True if the inode is a regular file""" return stat.S_ISREG(self.i_mode) != 0 + @property def is_link(self) -> bool: """Returns True if the inode is a symlink""" return stat.S_ISLNK(self.i_mode) != 0 + @property def is_fifo(self) -> bool: """Returns True if the inode is a FIFO""" return stat.S_ISFIFO(self.i_mode) != 0 + @property def is_sock(self) -> bool: """Returns True if the inode is a socket""" return stat.S_ISSOCK(self.i_mode) != 0 + @property def is_block(self) -> bool: """Returns True if the inode is a block device""" return stat.S_ISBLK(self.i_mode) != 0 + @property def is_char(self) -> bool: """Returns True if the inode is a char device""" return stat.S_ISCHR(self.i_mode) != 0 + @property def is_sticky(self) -> bool: """Returns True if the sticky bit is set""" return (self.i_mode & stat.S_ISVTX) != 0 @@ -1818,19 +1826,19 @@ class inode(objects.StructType): Returns: The inode type name """ - if self.is_dir(): + if self.is_dir: return "DIR" - elif self.is_reg(): + elif self.is_reg: return "REG" - elif self.is_link(): + elif self.is_link: return "LNK" - elif self.is_fifo(): + elif self.is_fifo: return "FIFO" - elif self.is_sock(): + elif self.is_sock: return "SOCK" - elif self.is_char(): + elif self.is_char: return "CHR" - elif self.is_block(): + elif self.is_block: return "BLK" else: return None From b8d68b9a5ee0c643b68eaa9f33bd051279374eb3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 14:15:01 +1000 Subject: [PATCH 35/85] PR review fixes: Avoid using renderers in core functions. --- .../framework/symbols/linux/extensions/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 599fedb6f..1b5e1d286 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -7,10 +7,10 @@ import logging import stat from datetime import datetime import socket as socket_module -from typing import Generator, Iterable, Iterator, Optional, Tuple, List +from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union from volatility3.framework import constants, exceptions, objects, interfaces, symbols -from volatility3.framework import renderers +from volatility3.framework.renderers import conversion from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS @@ -1770,7 +1770,7 @@ class timespec64(objects.StructType): def to_datetime(self) -> datetime: """Returns the respective aware datetime""" - dt = renderers.conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9) + dt = conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9) return dt @@ -1812,7 +1812,7 @@ class inode(objects.StructType): """Returns True if the sticky bit is set""" return (self.i_mode & stat.S_ISVTX) != 0 - def get_inode_type(self) -> str: + def get_inode_type(self) -> Union[str, None]: """Returns inode type name Returns: @@ -1833,7 +1833,7 @@ class inode(objects.StructType): elif self.is_block(): return "BLK" else: - return renderers.UnparsableValue() + return None def get_inode_number(self) -> int: """Returns the inode number""" @@ -1843,7 +1843,7 @@ class inode(objects.StructType): if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 # Ref Linux commit 3aa63a569c64e708df547a8913c84e64a06e7853 - return renderers.conversion.unixtime_to_datetime( + return conversion.unixtime_to_datetime( self.member(f"{member}_sec") + self.has_member(f"{member}_nsec") / 1e9 ) elif self.has_member(f"__{member}"): From 933a41fa3a60f3f02185b530a1a710b6dcae895c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 14:17:57 +1000 Subject: [PATCH 36/85] PR review fixes: Convert inode's is_* functions to properties --- .../symbols/linux/extensions/__init__.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 1b5e1d286..00f6730eb 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1780,34 +1780,42 @@ class inode(objects.StructType): # pointer, will easily cause an integer overflow here. return self.i_ino > 0 and self.i_count.counter >= 0 + @property def is_dir(self) -> bool: """Returns True if the inode is a directory""" return stat.S_ISDIR(self.i_mode) != 0 + @property def is_reg(self) -> bool: """Returns True if the inode is a regular file""" return stat.S_ISREG(self.i_mode) != 0 + @property def is_link(self) -> bool: """Returns True if the inode is a symlink""" return stat.S_ISLNK(self.i_mode) != 0 + @property def is_fifo(self) -> bool: """Returns True if the inode is a FIFO""" return stat.S_ISFIFO(self.i_mode) != 0 + @property def is_sock(self) -> bool: """Returns True if the inode is a socket""" return stat.S_ISSOCK(self.i_mode) != 0 + @property def is_block(self) -> bool: """Returns True if the inode is a block device""" return stat.S_ISBLK(self.i_mode) != 0 + @property def is_char(self) -> bool: """Returns True if the inode is a char device""" return stat.S_ISCHR(self.i_mode) != 0 + @property def is_sticky(self) -> bool: """Returns True if the sticky bit is set""" return (self.i_mode & stat.S_ISVTX) != 0 @@ -1818,19 +1826,19 @@ class inode(objects.StructType): Returns: The inode type name """ - if self.is_dir(): + if self.is_dir: return "DIR" - elif self.is_reg(): + elif self.is_reg: return "REG" - elif self.is_link(): + elif self.is_link: return "LNK" - elif self.is_fifo(): + elif self.is_fifo: return "FIFO" - elif self.is_sock(): + elif self.is_sock: return "SOCK" - elif self.is_char(): + elif self.is_char: return "CHR" - elif self.is_block(): + elif self.is_block: return "BLK" else: return None From d34b1ded3e673e2d311d14c8e104668d7ebac78c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 19:46:23 +1000 Subject: [PATCH 37/85] PR review fixes: Remove get_inode_number. It's better to use the type's original member name and handle the casting on the consumer side. --- volatility3/framework/symbols/linux/extensions/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 00f6730eb..05679523f 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1843,10 +1843,6 @@ class inode(objects.StructType): else: return None - def get_inode_number(self) -> int: - """Returns the inode number""" - return int(self.i_ino) - def _time_member_to_datetime(self, member) -> datetime: if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 From ed208347630428e21dec91f90eb431ed02595bc3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 19:46:23 +1000 Subject: [PATCH 38/85] PR review fixes: Remove get_inode_number. It's better to use the type's original member name and handle the casting on the consumer side. --- volatility3/framework/symbols/linux/extensions/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 00f6730eb..05679523f 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1843,10 +1843,6 @@ class inode(objects.StructType): else: return None - def get_inode_number(self) -> int: - """Returns the inode number""" - return int(self.i_ino) - def _time_member_to_datetime(self, member) -> datetime: if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 From 230ea09728dc9b756e27936544f03d43647cc0ba Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Fri, 2 Aug 2024 17:52:51 +0200 Subject: [PATCH 39/85] Updating code following #1230 merge --- volatility3/framework/plugins/linux/lsof.py | 26 ++++++++--------- .../framework/symbols/linux/__init__.py | 28 ++++++++----------- .../symbols/linux/extensions/__init__.py | 14 ++++++++++ 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index c1de48c1a..fa9d2bf61 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -21,7 +21,6 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists all memory maps for all processes.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) @classmethod @@ -53,7 +52,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): symbol_table: str, filter_func: Callable[[int], bool] = lambda _: False, ): - linuxutils_symbol_table = None # type: ignore + linuxutils_symbol_table = None for task in pslist.PsList.list_tasks(context, symbol_table, filter_func): if linuxutils_symbol_table is None: if constants.BANG not in task.vol.type_name: @@ -71,21 +70,17 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): fd_num, filp, full_path = fd_fields inode_metadata = linux.LinuxUtilities.get_inode_metadata(context, filp) try: - inode_num, file_size, imode, ctime, mtime, atime = next( + inode_num, itype, file_size, imode, ctime, mtime, atime = next( inode_metadata ) except Exception as e: vollog.warning( f"Can't get inode metadata for file descriptor {fd_num}: {e}" ) - # Yield NotAvailableValue for each field in case of an exception - inode_num = renderers.NotAvailableValue() - file_size = renderers.NotAvailableValue() - imode = renderers.NotAvailableValue() - ctime = renderers.NotAvailableValue() - mtime = renderers.NotAvailableValue() - atime = renderers.NotAvailableValue() - yield pid, task_comm, task, fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size + inode_num = itype = file_size = imode = ctime = mtime = atime = ( + renderers.NotAvailableValue() + ) + yield pid, task_comm, task, fd_num, filp, full_path, inode_num, itype, imode, ctime, mtime, atime, file_size def _generator(self, pids, symbol_table): filter_func = pslist.PsList.create_pid_filter(pids) @@ -100,6 +95,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): filp, full_path, inode_num, + itype, imode, ctime, mtime, @@ -112,6 +108,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): fd_num, full_path, inode_num, + itype, imode, ctime, mtime, @@ -130,6 +127,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ("FD", int), ("Path", str), ("Inode", int), + ("Type", str), ("Mode", str), ("Changed", datetime.datetime), ("Modified", datetime.datetime), @@ -144,6 +142,6 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): for row in self._generator(pids, symbol_table): _depth, row_data = row description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"' - yield description, timeliner.TimeLinerType.CHANGED, row_data[6] - yield description, timeliner.TimeLinerType.MODIFIED, row_data[7] - yield description, timeliner.TimeLinerType.ACCESSED, row_data[8] + yield description, timeliner.TimeLinerType.CHANGED, row_data[7] + yield description, timeliner.TimeLinerType.MODIFIED, row_data[8] + yield description, timeliner.TimeLinerType.ACCESSED, row_data[9] diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index a96fe9d2f..d52c43dae 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -280,23 +280,19 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): A helper function that gets the inodes metadata from a file descriptor """ dentry = filp.get_dentry() - if dentry != 0: + if dentry: inode_object = dentry.d_inode - inode_num = inode_object.i_ino - file_size = inode_object.i_size # file size in bytes - imode = stat.filemode(inode_object.i_mode) # file type & Permissions - - # Timestamps - ctime = datetime.datetime.fromtimestamp( - inode_object.i_ctime.tv_sec - ) # last change time - mtime = datetime.datetime.fromtimestamp( - inode_object.i_mtime.tv_sec - ) # last modify time - atime = datetime.datetime.fromtimestamp( - inode_object.i_atime.tv_sec - ) # last access time - yield inode_num, file_size, imode, ctime, mtime, atime + if inode_object and inode_object.is_valid(): + itype = inode_object.get_inode_type() or "?" + yield ( + inode_object.i_ino, + itype, + inode_object.i_size, + inode_object.get_file_mode(), + inode_object.get_change_time(), + inode_object.get_modification_time(), + inode_object.get_access_time(), + ) @classmethod def mask_mods_list( diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 05679523f..0ee6e7d95 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1820,6 +1820,16 @@ class inode(objects.StructType): """Returns True if the sticky bit is set""" return (self.i_mode & stat.S_ISVTX) != 0 + @property + def is_whiteout(self) -> bool: + """Returns True if the inode is a whiteout""" + return (self.i_mode & 0o140000) == 0o140000 + + @property + def is_overlay(self) -> bool: + """Returns True if the inode is an overlay""" + return (self.i_mode & 0o40000) == 0o40000 + def get_inode_type(self) -> Union[str, None]: """Returns inode type name @@ -1840,6 +1850,10 @@ class inode(objects.StructType): return "CHR" elif self.is_block: return "BLK" + elif self.is_whiteout: + return "WHT" + elif self.is_overlay: + return "OVL" else: return None From 60b1c49e49864ce7cb5ae3a3491b6a7e9e40eef3 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Fri, 2 Aug 2024 18:10:11 +0200 Subject: [PATCH 40/85] removing test code --- .../framework/symbols/linux/extensions/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0ee6e7d95..06d2e2bf4 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1820,16 +1820,6 @@ class inode(objects.StructType): """Returns True if the sticky bit is set""" return (self.i_mode & stat.S_ISVTX) != 0 - @property - def is_whiteout(self) -> bool: - """Returns True if the inode is a whiteout""" - return (self.i_mode & 0o140000) == 0o140000 - - @property - def is_overlay(self) -> bool: - """Returns True if the inode is an overlay""" - return (self.i_mode & 0o40000) == 0o40000 - def get_inode_type(self) -> Union[str, None]: """Returns inode type name From 2e9b5b62faec7d8e7fa67dd1e9243013af7cbcb4 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Fri, 2 Aug 2024 18:11:29 +0200 Subject: [PATCH 41/85] removing test code --- volatility3/framework/symbols/linux/extensions/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 06d2e2bf4..05679523f 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1840,10 +1840,6 @@ class inode(objects.StructType): return "CHR" elif self.is_block: return "BLK" - elif self.is_whiteout: - return "WHT" - elif self.is_overlay: - return "OVL" else: return None From d4ed07f95b883183fcd28871e7fc7858e649347a Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 20:51:32 +1000 Subject: [PATCH 42/85] PR review fixes: Add fixme to remember we should move wintime_to_datetime/unixtime_to_datetime out of renderers --- volatility3/framework/renderers/conversion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/renderers/conversion.py b/volatility3/framework/renderers/conversion.py index 864794860..c8ddc19fd 100644 --- a/volatility3/framework/renderers/conversion.py +++ b/volatility3/framework/renderers/conversion.py @@ -11,6 +11,7 @@ from typing import Union from volatility3.framework import interfaces, renderers +# FIXME: Move wintime_to_datetime() and unixtime_to_datetime() out of renderers, possibly framework.objects.utility def wintime_to_datetime( wintime: int, ) -> Union[interfaces.renderers.BaseAbsentValue, datetime.datetime]: From 0dfb9d8a0ff9080eef10b7505f7e1955e0f728e1 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 20:57:23 +1000 Subject: [PATCH 43/85] Linux mountinfo: Add a method to yield all filesystem superblocks --- .../framework/plugins/linux/mountinfo.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index da743bb60..319c92cca 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -37,7 +37,7 @@ class MountInfo(plugins.PluginInterface): _required_framework_version = (2, 2, 0) - _version = (1, 0, 0) + _version = (1, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -146,7 +146,7 @@ class MountInfo(plugins.PluginInterface): def _get_tasks_mountpoints( self, tasks: Iterable[interfaces.objects.ObjectInterface], - filtered_by_pids: bool, + filtered_by_pids: bool = False, ): seen_mountpoints = set() for task in tasks: @@ -184,8 +184,8 @@ class MountInfo(plugins.PluginInterface): self, tasks: Iterable[interfaces.objects.ObjectInterface], mnt_ns_ids: List[int], - mount_format: bool, - filtered_by_pids: bool, + mount_format: bool = False, + filtered_by_pids: bool = False, ) -> Iterable[Tuple[int, Tuple]]: show_filter_warning = False for task, mnt, mnt_ns_id in self._get_tasks_mountpoints( @@ -247,6 +247,31 @@ class MountInfo(plugins.PluginInterface): "Could not filter by mount namespace id. This field is not available in this kernel." ) + def get_superblocks(self): + """Yield file system superblocks based on the task's mounted filesystems. + + Yields: + super_block: Kernel's struct super_block object + """ + # No filter so that we get all the mount namespaces from all tasks + pid_filter = pslist.PsList.create_pid_filter() + tasks = pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=pid_filter + ) + + seen_sb_ptr = set() + for task, mnt, _mnt_ns_id in self._get_tasks_mountpoints(tasks): + path_root = linux.LinuxUtilities.get_path_mnt(task, mnt) + if not path_root: + continue + + sb_ptr = mnt.get_mnt_sb() + if not sb_ptr or sb_ptr in seen_sb_ptr: + continue + seen_sb_ptr.add(sb_ptr) + + yield sb_ptr.dereference(), path_root + def run(self): pids = self.config.get("pids") mount_ns_ids = self.config.get("mntns") From 231f682b2769d7a5a6de0fc96008d44c06f0825c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:01:59 +1000 Subject: [PATCH 44/85] Linux: Improve mount's object extension method docstrings --- volatility3/framework/symbols/linux/extensions/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 05679523f..de5e432d3 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -47,7 +47,7 @@ class module(generic.GenericIntelProcess): ).choices except exceptions.SymbolError: vollog.debug( - f"Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" + "Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" ) # set to empty dict to show that the enum was not found, and so shouldn't be searched for again self._mod_mem_type = {} @@ -936,7 +936,8 @@ class mount(objects.StructType): MNT_RELATIME: "relatime", } - def get_mnt_sb(self): + def get_mnt_sb(self) -> int: + """Returns a pointer to the super_block""" if self.has_member("mnt"): return self.mnt.mnt_sb elif self.has_member("mnt_sb"): @@ -1251,6 +1252,7 @@ class vfsmount(objects.StructType): return self._get_real_mnt().has_parent() def get_mnt_sb(self): + """Returns a pointer to the super_block""" return self.mnt_sb def get_flags_access(self) -> str: From a369a9e23cea620a791129d687bd2bbe9c4d4442 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:05:23 +1000 Subject: [PATCH 45/85] Linux: dentry object extension: Add a method to walk dentries subdirectories --- .../symbols/linux/extensions/__init__.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index de5e432d3..3bfbe168a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -820,6 +820,26 @@ class dentry(objects.StructType): current_dentry = current_dentry.d_parent return None + def get_subdirs(self) -> interfaces.objects.ObjectInterface: + """Walks dentry subdirs + + Yields: + A dentry object + """ + if self.has_member("d_sib") and self.has_member("d_children"): + # kernels >= 6.8 + walk_member = "d_sib" + list_head_member = self.d_children.first + elif self.has_member("d_child") and self.has_member("d_subdirs"): + # 2.5.0 <= kernels < 6.8 + walk_member = "d_child" + list_head_member = self.d_subdirs + else: + raise exceptions.VolatilityException("Unsupported dentry type") + + dentry_type_name = self.get_symbol_table_name() + constants.BANG + "dentry" + yield from list_head_member.to_list(dentry_type_name, walk_member) + class struct_file(objects.StructType): def get_dentry(self) -> interfaces.objects.ObjectInterface: From 41684478ad7c12c2d862ac8612f44315bc6f855b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:21:49 +1000 Subject: [PATCH 46/85] Linux: Add page cache support, including abstractions like RadixTree, XArray, and IDR, to support both older and latest kernel versions --- .../framework/symbols/linux/__init__.py | 346 +++++++++++++++++- .../symbols/linux/extensions/__init__.py | 244 +++++++++++- 2 files changed, 588 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 03353135d..248cb8d75 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,6 +1,8 @@ # This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import math +from abc import ABC, abstractmethod from typing import Iterator, List, Tuple, Optional, Union from volatility3 import framework @@ -30,6 +32,9 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class("kobject", extensions.kobject) self.set_type_class("cred", extensions.cred) self.set_type_class("inode", extensions.inode) + self.set_type_class("idr", extensions.IDR) + self.set_type_class("address_space", extensions.address_space) + self.set_type_class("page", extensions.page) # Might not exist in the current symbols self.optional_set_type_class("module", extensions.module) self.optional_set_type_class("bpf_prog", extensions.bpf_prog) @@ -67,7 +72,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 1, 0) + _version = (2, 2, 0) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -425,3 +430,342 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): kernel = context.modules[kernel_module_name] return kernel + + @classmethod + def choose_kernel_tree(cls, vmlinux: interfaces.context.ModuleInterface) -> "Tree": + """Returns the appropriate tree data structure instance for the current kernel implementation. + This is used by the IDR and the PageCache to choose between the XArray and RadixTree. + + Args: + vmlinux: The kernel module object + + Returns: + The appropriate Tree instance for the current kernel + """ + address_space_type = vmlinux.get_type("address_space") + address_space_has_i_pages = address_space_type.has_member("i_pages") + i_pages_type_name = ( + address_space_type.child_template("i_pages").vol.type_name + if address_space_has_i_pages + else "" + ) + i_pages_is_xarray = i_pages_type_name.endswith(constants.BANG + "xarray") + i_pages_is_radix_tree_root = i_pages_type_name.endswith( + constants.BANG + "radix_tree_root" + ) and vmlinux.get_type("radix_tree_root").has_member("xa_head") + + if i_pages_is_xarray or i_pages_is_radix_tree_root: + return XArray(vmlinux) + else: + return RadixTree(vmlinux) + + +class Tree(ABC): + """Abstraction to support both XArray and RadixTree""" + + # Dynamic values, these will be initialized later + CHUNK_SHIFT = None + CHUNK_SIZE = None + CHUNK_MASK = None + + def __init__(self, vmlinux: interfaces.context.ModuleInterface): + self.vmlinux = vmlinux + self.vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + + self.pointer_size = self.vmlinux.get_type("pointer").size + # Dynamically work out the (XA_CHUNK|RADIX_TREE_MAP)_SHIFT values based on + # the node.slots[] array size + node_type = self.vmlinux.get_type(self.node_type_name) + slots_array_size = node_type.child_template("slots").count + + # Calculate the LSB index - 1 + self.CHUNK_SHIFT = slots_array_size.bit_length() - 1 + self.CHUNK_SIZE = 1 << self.CHUNK_SHIFT + self.CHUNK_MASK = self.CHUNK_SIZE - 1 + + @property + @abstractmethod + def node_type_name(self) -> str: + """Returns the Tree implementation node type name + + Returns: + A string with the node type name + """ + raise NotImplementedError() + + @property + def tag_internal_value(self) -> int: + """Returns the internal node flag for the tree""" + raise NotImplementedError() + + @abstractmethod + def node_is_internal(self, nodep) -> bool: + """Checks if the node is internal""" + raise NotImplementedError + + @abstractmethod + def is_node_tagged(self, nodep) -> bool: + """Checks if the node pointer is tagged""" + raise NotImplementedError + + @abstractmethod + def untag_node(self, nodep) -> int: + """Untags a node pointer""" + raise NotImplementedError + + @abstractmethod + def get_tree_height(self, treep) -> int: + """Returns the tree height""" + raise NotImplementedError + + @abstractmethod + def get_node_height(self, nodep) -> int: + """Returns the node height""" + raise NotImplementedError + + @abstractmethod + def get_head_node(self, tree) -> int: + """Returns a pointer to the tree's head""" + raise NotImplementedError + + @abstractmethod + def is_valid_node(self, nodep) -> bool: + """Validates a node pointer""" + raise NotImplementedError + + def nodep_to_node(self, nodep) -> interfaces.objects.ObjectInterface: + """Instanciates a tree node from its pointer + + Args: + nodep: Pointer to the XArray/RadixTree node + + Returns: + A XArray/RadixTree node instance + """ + node = self.vmlinux.object(self.node_type_name, offset=nodep, absolute=True) + return node + + def _slot_to_nodep(self, slot) -> int: + if self.node_is_internal(slot): + nodep = slot & ~self.tag_internal_value + else: + nodep = slot + + return nodep + + def _iter_node(self, nodep, height) -> int: + node = self.nodep_to_node(nodep) + node_slots = node.slots + for off in range(self.CHUNK_SIZE): + slot = node_slots[off] + if slot == 0: + continue + + nodep = self._slot_to_nodep(slot) + + if height == 1: + if self.is_valid_node(nodep): + yield nodep + else: + for child_node in self._iter_node(nodep, height - 1): + yield child_node + + def get_page_addresses(self, root: interfaces.objects.ObjectInterface) -> int: + """Walks the tree data structure + + Args: + root: The tree root object + + Yields: + A tree node pointer + """ + height = self.get_tree_height(root.vol.offset) + + nodep = self.get_head_node(root) + if not nodep: + return + + # Keep the internal flag before untagging it + is_internal = self.node_is_internal(nodep) + if self.is_node_tagged(nodep): + nodep = self.untag_node(nodep) + + if is_internal: + height = self.get_node_height(nodep) + + if height == 0: + if self.is_valid_node(nodep): + yield nodep + else: + for child_node in self._iter_node(nodep, height): + yield child_node + + +class XArray(Tree): + XARRAY_TAG_MASK = 3 + XARRAY_TAG_INTERNAL = 2 + + def get_tree_height(self, treep) -> int: + return 0 + + @property + def node_type_name(self) -> str: + return "xa_node" + + @property + def tag_internal_value(self) -> int: + return self.XARRAY_TAG_INTERNAL + + def get_node_height(self, nodep) -> int: + node = self.nodep_to_node(nodep) + return (node.shift / self.CHUNK_SHIFT) + 1 + + def get_head_node(self, tree) -> int: + return tree.xa_head + + def node_is_internal(self, nodep) -> bool: + return (nodep & self.XARRAY_TAG_MASK) == self.XARRAY_TAG_INTERNAL + + def is_node_tagged(self, nodep) -> bool: + return (nodep & self.XARRAY_TAG_MASK) != 0 + + def untag_node(self, nodep) -> int: + return nodep & (~self.XARRAY_TAG_MASK) + + def is_valid_node(self, nodep) -> bool: + # It should have the tag mask clear + return not self.is_node_tagged(nodep) + + +class RadixTree(Tree): + RADIX_TREE_INTERNAL_NODE = 1 + RADIX_TREE_EXCEPTIONAL_ENTRY = 2 + RADIX_TREE_ENTRY_MASK = 3 + + # Dynamic values. These will be initialized later + RADIX_TREE_INDEX_BITS = None + RADIX_TREE_MAX_PATH = None + RADIX_TREE_HEIGHT_SHIFT = None + RADIX_TREE_HEIGHT_MASK = None + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + char_bits = 8 + self.RADIX_TREE_INDEX_BITS = char_bits * self.pointer_size + self.RADIX_TREE_MAX_PATH = int( + math.ceil(self.RADIX_TREE_INDEX_BITS / float(self.CHUNK_SHIFT)) + ) + self.RADIX_TREE_HEIGHT_SHIFT = self.RADIX_TREE_MAX_PATH + 1 + self.RADIX_TREE_HEIGHT_MASK = (1 << self.RADIX_TREE_HEIGHT_SHIFT) - 1 + + if not self.vmlinux.has_type("radix_tree_root"): + # In kernels 4.20, RADIX_TREE_INTERNAL_NODE flag took RADIX_TREE_EXCEPTIONAL_ENTRY's + # value. RADIX_TREE_EXCEPTIONAL_ENTRY was removed but that's managed in is_valid_node() + # Note that the Radix Tree is still in use for IDR, even after kernels 4.20 when XArray + # mostly replace it + self.RADIX_TREE_INTERNAL_NODE = 2 + + @property + def node_type_name(self) -> str: + return "radix_tree_node" + + @property + def tag_internal_value(self) -> int: + return self.RADIX_TREE_INTERNAL_NODE + + def get_tree_height(self, treep) -> int: + try: + if self.vmlinux.get_type("radix_tree_root").has_member("height"): + # kernels < 4.7.10 + radix_tree_root = self.vmlinux.object( + "radix_tree_root", offset=treep, absolute=True + ) + return radix_tree_root.height + except exceptions.SymbolError: + pass + + # kernels >= 4.7.10 + return 0 + + def _radix_tree_maxindex(self, node, height) -> int: + """Return the maximum key which can be store into a radix tree with this height.""" + + if not self.vmlinux.has_symbol("height_to_maxindex"): + # Kernels >= 4.7 + return (self.CHUNK_SIZE << node.shift) - 1 + else: + # Kernels < 4.7 + height_to_maxindex_array = self.vmlinux.object_from_symbol( + "height_to_maxindex" + ) + maxindex = height_to_maxindex_array[height] + return maxindex + + def get_node_height(self, nodep) -> int: + node = self.nodep_to_node(nodep) + if hasattr(node, "shift"): + # 4.7 <= Kernels < 4.20 + return (node.shift / self.CHUNK_SHIFT) + 1 + elif hasattr(node, "path"): + # 3.15 <= Kernels < 4.7 + return node.path & self.RADIX_TREE_HEIGHT_MASK + elif hasattr(node, "height"): + # Kernels < 3.15 + return node.height + else: + raise exceptions.VolatilityException("Cannot find radix-tree node height") + + def get_head_node(self, tree) -> int: + return tree.rnode + + def node_is_internal(self, nodep) -> bool: + return (nodep & self.RADIX_TREE_INTERNAL_NODE) != 0 + + def is_node_tagged(self, nodep) -> bool: + return self.node_is_internal(nodep) + + def untag_node(self, nodep) -> int: + return nodep & (~self.RADIX_TREE_ENTRY_MASK) + + def is_valid_node(self, nodep) -> bool: + # In kernels 4.20, exceptional nodes were removed and internal entries took their bitmask + if self.vmlinux.has_type("radix_tree_root"): + return ( + nodep & self.RADIX_TREE_ENTRY_MASK + ) != self.RADIX_TREE_EXCEPTIONAL_ENTRY + + return True + + +class PageCache(object): + """Linux Page Cache abstraction""" + + def __init__( + self, + page_cache: interfaces.objects.ObjectInterface, + vmlinux: interfaces.context.ModuleInterface, + ): + """ + Args: + page_cache: Page cache address space + vmlinux: Kernel module object + """ + self.vmlinux = vmlinux + self._page_cache = page_cache + self._tree = LinuxUtilities.choose_kernel_tree(self.vmlinux) + + def get_cached_pages(self) -> interfaces.objects.ObjectInterface: + """Returns all page cache contents + + Yields: + Page objects + """ + + for page_addr in self._tree.get_page_addresses(self._page_cache.i_pages): + if not page_addr: + continue + + page = self.vmlinux.object("page", offset=page_addr, absolute=True) + if page: + yield page diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 3bfbe168a..300ab2ed0 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -4,10 +4,11 @@ import collections.abc import logging +import functools import stat from datetime import datetime import socket as socket_module -from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union +from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union, Dict from volatility3.framework import constants, exceptions, objects, interfaces, symbols from volatility3.framework.renderers import conversion @@ -1919,3 +1920,244 @@ class inode(objects.StructType): The inode's file mode string """ return stat.filemode(self.i_mode) + + def get_pages(self) -> interfaces.objects.ObjectInterface: + """Gets the inode's cached pages + + Yields: + The inode's cached pages + """ + if not self.i_size: + return + elif not (self.i_mapping and self.i_mapping.nrpages > 0): + return + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + page_cache = linux.PageCache(self.i_mapping.dereference(), vmlinux) + yield from page_cache.get_cached_pages() + + def get_contents(self): + """Get the inode cached pages from the page cache + + Yields: + page_index (int): The page index in the Tree. File offset is page_index * PAGE_SIZE. + page_content (str): The page content + """ + for page_obj in self.get_pages(): + page_index = int(page_obj.index) + page_content = page_obj.get_content() + yield page_index, page_content + + +class address_space(objects.StructType): + @property + def i_pages(self): + """Returns the appropriate member containing the page cache tree""" + if self.has_member("i_pages"): + # Kernel >= 4.17 + return self.member("i_pages") + elif self.has_member("page_tree"): + # Kernel < 4.17 + return self.member("page_tree") + + raise exceptions.VolatilityException("Unsupported page cache tree") + + +class page(objects.StructType): + @property + @functools.cache + def pageflags_enum(self) -> Dict: + """Returns 'pageflags' enumeration key/values + + Returns: + A dictionary with the pageflags enumeration key/values + """ + # FIXME: It would be even better to use @functools.cached_property instead, + # however, this requires Python +3.8 + try: + pageflags_enum = self._context.symbol_space.get_enumeration( + self.get_symbol_table_name() + constants.BANG + "pageflags" + ).choices + except exceptions.SymbolError: + vollog.debug( + "Unable to find pageflags enum. This can happen in kernels < 2.6.26 or wrong ISF" + ) + # set to empty dict to show that the enum was not found, and so shouldn't be searched for again + pageflags_enum = {} + + return pageflags_enum + + def flags_list(self) -> List[str]: + """Returns a list of page flags + + Returns: + List of page flags + """ + flags = [] + for name, value in self.pageflags_enum.items(): + if self.flags & (1 << value) != 0: + flags.append(name) + + return flags + + def to_paddr(self) -> int: + """Converts a page's virtual address to its physical address using the current physical memory model. + + Returns: + int: page physical address + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + + vmemmap_start = None + if vmlinux.has_symbol("mem_section"): + # SPARSEMEM_VMEMMAP physical memory model: memmap is virtually contiguous + if vmlinux.has_symbol("vmemmap_base"): + # CONFIG_DYNAMIC_MEMORY_LAYOUT - KASLR kernels >= 4.9 + vmemmap_start = vmlinux.object_from_symbol("vmemmap_base") + else: + # !CONFIG_DYNAMIC_MEMORY_LAYOUT + if vmlinux_layer._maxvirtaddr < 57: + # 4-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L4 + vmemmap_base_l4 = 0xFFFFEA0000000000 + vmemmap_start = vmemmap_base_l4 + else: + # 5-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L5 + vmemmap_base_l5 = 0xFFD4000000000000 + vmemmap_start = vmemmap_base_l5 + + # FIXME: Remove this exception once 5-level paging is supported. + raise exceptions.VolatilityException( + "5-level paging is not yet supported" + ) + + elif vmlinux.has_symbol("mem_map"): + # FLATMEM physical memory model, typically 32bit + vmemmap_start = vmlinux.object_from_symbol("mem_map") + + elif vmlinux.has_symbol("node_data"): + raise exceptions.VolatilityException("NUMA systems are not yet supported") + else: + raise exceptions.VolatilityException("Unsupported Linux memory model") + + if not vmemmap_start: + raise exceptions.VolatilityException( + "Something went wrong, we shouldn't be here" + ) + + page_type_size = vmlinux.get_type("page").size + pagec = vmlinux_layer.canonicalize(self.vol.offset) + pfn = (pagec - vmemmap_start) // page_type_size + page_paddr = pfn * vmlinux_layer.page_size + + return page_paddr + + def get_content(self) -> Union[str, None]: + """Returns the page content + + Returns: + The page content + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + physical_layer = vmlinux.context.layers["memory_layer"] + page_paddr = self.to_paddr() + if not page_paddr: + return + + page_data = physical_layer.read(page_paddr, vmlinux_layer.page_size) + return page_data + + +class IDR(objects.StructType): + IDR_BITS = 8 + IDR_MASK = (1 << IDR_BITS) - 1 + INT_SIZE = 4 + MAX_IDR_SHIFT = INT_SIZE * 8 - 1 + MAX_IDR_BIT = 1 << MAX_IDR_SHIFT + + def idr_max(self, num_layers: int) -> int: + """Returns the maximum ID which can be allocated given idr::layers + + Args: + num_layers: Number of layers + + Returns: + Maximum ID for a given number of layers + """ + # Kernel < 4.17 + bits = min([self.INT_SIZE, num_layers * self.IDR_BITS, self.MAX_IDR_SHIFT]) + + return (1 << bits) - 1 + + def idr_find(self, idr_id: int) -> int: + """Finds an ID within the IDR data structure. + Based on idr_find_slowpath(), 3.9 <= Kernel < 4.11 + Args: + idr_id: The IDR element ID + + Returns: + A pointer to the given ID element + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + if not vmlinux.get_type("idr_layer").has_member("layer"): + vollog.info( + "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" + ) + return + + if idr_id < 0: + return + + cur_layer = self.top + if not cur_layer: + return + + n = (cur_layer.layer + 1) * self.IDR_BITS + + if idr_id > self.idr_max(cur_layer.layer + 1): + return + + assert n != 0 + + while n > 0 and cur_layer: + n -= self.IDR_BITS + assert n == cur_layer.layer * self.IDR_BITS + cur_layer = cur_layer.ary[(idr_id >> n) & self.IDR_MASK] + + return cur_layer.v() + + def _old_kernel_get_page_addresses(self, in_use) -> int: + # Kernels < 4.11 + total = next_id = 0 + while total < in_use: + page_addr = self.idr_find(next_id) + if page_addr: + yield page_addr + total += 1 + + next_id += 1 + + def _new_kernel_get_page_addresses(self, _in_use) -> int: + # Kernels >= 4.11 + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + tree = linux.LinuxUtilities.choose_kernel_tree(vmlinux) + for page_addr in tree.get_page_addresses(root=self.idr_rt): + yield page_addr + + def get_page_addresses(self, in_use=0) -> int: + """Walks the IDR and yield a pointer associated with each element. + + Args: + in_use (int, optional): _description_. Defaults to 0. + + Yields: + A pointer associated with each element. + """ + if self.has_member("idr_rt"): + get_page_addresses_func = self._new_kernel_get_page_addresses + else: + get_page_addresses_func = self._old_kernel_get_page_addresses + + for page_addr in get_page_addresses_func(in_use): + yield page_addr From ac27d6663a3733e002cb064afd155139103c4c43 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:26:29 +1000 Subject: [PATCH 47/85] Linux: Add two page cache plugins, linux.pagecache.Files and linux.pagecache.InodePages --- .../framework/plugins/linux/pagecache.py | 504 ++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 volatility3/framework/plugins/linux/pagecache.py diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py new file mode 100644 index 000000000..545c243e0 --- /dev/null +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -0,0 +1,504 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import math +import logging +import datetime +from dataclasses import dataclass, astuple +from typing import List + +from volatility3.framework import renderers, interfaces +from volatility3.framework.renderers import format_hints +from volatility3.framework.interfaces import plugins +from volatility3.framework.configuration import requirements +from volatility3.plugins import timeliner +from volatility3.plugins.linux import mountinfo + +vollog = logging.getLogger(__name__) + + +@dataclass +class InodeUser: + """Inode user representation, featuring augmented information and formatted fields. + This is the data the plugin will eventually display. + """ + + superblock_addr: int + mountpoint: str + device: str + inode_num: int + inode_addr: int + type: str + inode_pages: int + cached_pages: int + file_mode: str + access_time: str + modification_time: str + change_time: str + path: str + + +@dataclass +class InodeInternal: + """Inode internal representation containing only the core objects + + Fields: + superblock: 'super_block' struct + mountpoint: Superblock mountpoint path + inode: 'inode' struct + path: Dentry full path + """ + + superblock: interfaces.objects.ObjectInterface + mountpoint: str + inode: interfaces.objects.ObjectInterface + path: str + + def to_user( + self, kernel_layer: interfaces.layers.TranslationLayerInterface + ) -> InodeUser: + """Augment the inode information to be presented to the user + + Args: + kernel_layer: The kernel layer to obtain the page size + + Returns: + An InodeUser dataclass + """ + # Ensure all types are atomic immutable. Otherwise, astuple() will take a long + # time doing a deepcopy of the Volatility objects. + superblock_addr = self.superblock.vol.offset + device = f"{self.superblock.major}:{self.superblock.minor}" + inode_num = int(self.inode.i_ino) + inode_addr = self.inode.vol.offset + inode_type = renderers.UnparsableValue() + # Round up the number of pages to fit the inode's size + inode_pages = int(math.ceil(self.inode.i_size / float(kernel_layer.page_size))) + cached_pages = int(self.inode.i_mapping.nrpages) + file_mode = self.inode.get_file_mode() + access_time_dt = self.inode.get_access_time() + modification_time_str = self.inode.get_modification_time() + change_time_str = self.inode.get_change_time() + + inode_user = InodeUser( + superblock_addr=superblock_addr, + mountpoint=self.mountpoint, + device=device, + inode_num=inode_num, + inode_addr=inode_addr, + type=inode_type, + inode_pages=inode_pages, + cached_pages=cached_pages, + file_mode=file_mode, + access_time=access_time_dt, + modification_time=modification_time_str, + change_time=change_time_str, + path=self.path, + ) + return inode_user + + +class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): + """Lists files from memory""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 1, 0) + ), + requirements.ListRequirement( + name="type", + description="List of space-separated file type filters i.e. --type REG DIR", + element_type=str, + optional=True, + ), + requirements.StringRequirement( + name="find", + description="Filename (full path) to find", + optional=True, + ), + ] + + @staticmethod + def _follow_symlink(inode, symlink_path) -> str: + """Follows (fast) symlinks (kernels >= 4.2.x). + Fast symlinks are filesystem agnostic. + + Args: + inode: The inode (or pointer) to dump + symlink_path: The symlink name + + Returns: + If it can resolve the symlink, it returns a string "symlink_path -> target_path" + Otherwise, it returns the same symlink_path + """ + # i_link (fast symlinks) were introduced in 4.2 + if inode and inode.is_link and inode.has_member("i_link") and inode.i_link: + i_link_str = inode.i_link.dereference().cast( + "string", max_length=255, encoding="utf-8", errors="replace" + ) + symlink_path = f"{symlink_path} -> {i_link_str}" + + return symlink_path + + @classmethod + def _walk_dentry(cls, seen_dentries, root_dentry, parent): + + for dentry in root_dentry.get_subdirs(): + dentry_addr = dentry.vol.offset + + # corruption + if dentry_addr == root_dentry.vol.offset: + continue + + if dentry_addr in seen_dentries: + continue + + seen_dentries.add(dentry_addr) + + inode = dentry.d_inode + if not (inode and inode.is_valid()): + continue + + # This allows us to have consistent paths + if dentry.d_name.name: + name = dentry.d_name.name_as_str() + # Do NOT use os.path.join() below + new_file = parent + "/" + name + else: + continue + + yield new_file, dentry, dentry.d_parent.vol.offset + + if inode.is_dir: + for new_file, dentry, parent_address in cls._walk_dentry( + seen_dentries, dentry, new_file + ): + yield new_file, dentry, parent_address + + @classmethod + def get_inodes( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + ): + """Retrieves the inodes from the superblocks + + Args: + context: The context that the plugin will operate within + config_path: The path to configuration data within the context configuration data + + Yields: + An InodeInternal object + """ + + superblocks_iter = mountinfo.MountInfo( + context=context, + config_path=config_path, + ).get_superblocks() + + seen_inodes = set() + seen_dentries = set() + for superblock, mountpoint in superblocks_iter: + parent = "" if mountpoint == "/" else mountpoint + + # Superblock root dentry + root_dentry = superblock.s_root + if not root_dentry: + continue + + # Dentry sanity check + if not root_dentry.is_root(): + continue + + # More dentry/inode sanity checks + root_inode_ptr = root_dentry.d_inode + if not root_inode_ptr: + continue + root_inode = root_inode_ptr.dereference() + if not root_inode.is_valid(): + continue + + # Inode already processed? + if root_inode_ptr in seen_inodes: + continue + seen_inodes.add(root_inode_ptr) + + root_path = mountpoint + + inode_in = InodeInternal( + superblock=superblock, + mountpoint=mountpoint, + inode=root_inode, + path=root_path, + ) + yield inode_in + + # Children + for file_path, file_dentry, _ in cls._walk_dentry( + seen_dentries, root_dentry, parent + ): + if not file_dentry: + continue + # Dentry/inode sanity checks + file_inode_ptr = file_dentry.d_inode + if not file_inode_ptr: + continue + file_inode = file_inode_ptr.dereference() + if not file_inode.is_valid(): + continue + + # Inode already processed? + if file_inode_ptr in seen_inodes: + continue + seen_inodes.add(file_inode_ptr) + + file_path = cls._follow_symlink(file_inode_ptr, file_path) + inode_in = InodeInternal( + superblock=superblock, + mountpoint=mountpoint, + inode=file_inode, + path=file_path, + ) + yield inode_in + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + + inodes_iter = self.get_inodes( + context=self.context, config_path=self.config_path + ) + + types_filter = self.config["type"] + for inode_in in inodes_iter: + if types_filter and inode_in.inode.get_inode_type() not in types_filter: + continue + + if self.config["find"]: + if inode_in.path == self.config["find"]: + inode_out = inode_in.to_user(vmlinux_layer) + yield (0, astuple(inode_out)) + break # Only the first match + else: + inode_out = inode_in.to_user(vmlinux_layer) + yield (0, astuple(inode_out)) + + def generate_timeline(self): + """Generates tuples of (description, timestamp_type, timestamp) + + These need not be generated in any particular order, sorting + will be done later + """ + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + + inodes_iter = self.get_inodes( + context=self.context, config_path=self.config_path + ) + for inode_in in inodes_iter: + inode_out = inode_in.to_user(vmlinux_layer) + description = f"Cached Inode for {inode_out.path}" + yield description, timeliner.TimeLinerType.ACCESSED, inode_out.access_time + yield description, timeliner.TimeLinerType.MODIFIED, inode_out.modification_time + yield description, timeliner.TimeLinerType.CHANGE, inode_out.change_time + + @staticmethod + def format_fields_with_headers(headers, generator): + """Uses the headers type to cast the fields obtained from the generator""" + for level, fields in generator: + formatted_fields = [] + for header, field in zip(headers, fields): + header_type = header[1] + + if isinstance( + field, (header_type, interfaces.renderers.BaseAbsentValue) + ): + formatted_field = field + else: + formatted_field = header_type(field) + + formatted_fields.append(formatted_field) + yield level, formatted_fields + + def run(self): + headers = [ + ("SuperblockAddr", format_hints.Hex), + ("MountPoint", str), + ("Device", str), + ("InodeNum", int), + ("InodeAddr", format_hints.Hex), + ("FileType", str), + ("InodePages", int), + ("CachedPages", int), + ("FileMode", str), + ("AccessTime", datetime.datetime), + ("ModificationTime", datetime.datetime), + ("ChangeTime", datetime.datetime), + ("FilePath", str), + ] + + return renderers.TreeGrid( + headers, self.format_fields_with_headers(headers, self._generator()) + ) + + +class InodePages(plugins.PluginInterface): + """Lists and recovers cached inode pages""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="files", plugin=Files, version=(1, 0, 0) + ), + requirements.StringRequirement( + name="find", + description="Filename (full path) to find ", + optional=True, + ), + requirements.IntRequirement( + name="inode", + description="Inode address", + optional=True, + ), + requirements.StringRequirement( + name="dump", + description="Output file path", + optional=True, + ), + ] + + @staticmethod + def write_inode_content_to_file( + inode: interfaces.objects.ObjectInterface, + filename: str, + vmlinux_layer: interfaces.layers.TranslationLayerInterface, + ) -> None: + """Extracts the inode's contents from the page cache and saves them to a file + + Args: + inode: The inode to dump + filename: Filename for writing the inode content + vmlinux_layer: The kernel layer to obtain the page size + """ + if not inode.is_reg: + vollog.error("The inode is not a regular file") + return + + # By using truncate/seek, provided the filesystem supports it, a sparse file will be + # created, saving both disk space and I/O time. + # Additionally, using the page index will guarantee that each page is written at the + # appropriate file position. + try: + with open(filename, "wb") as f: + inode_size = inode.i_size + f.truncate(inode_size) + + for page_idx, page_content in inode.get_contents(): + current_fp = page_idx * vmlinux_layer.page_size + max_length = inode_size - current_fp + page_bytes = page_content[:max_length] + if current_fp + len(page_bytes) > inode_size: + vollog.error( + "Page out of file bounds: inode 0x%x, inode size %d, page index %d", + inode.vol.object, + inode_size, + page_idx, + ) + f.seek(current_fp) + f.write(page_bytes) + + except IOError as e: + vollog.error("Unable to write to file (%s): %s", filename, e) + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + + if self.config["inode"] and self.config["find"]: + vollog.error("Cannot use --inode and --find simultaneously") + return + + if self.config["find"]: + inodes_iter = Files.get_inodes( + context=self.context, config_path=self.config_path + ) + for inode_in in inodes_iter: + if inode_in.path == self.config["find"]: + inode = inode_in.inode + break # Only the first match + + elif self.config["inode"]: + inode = vmlinux.object("inode", self.config["inode"], absolute=True) + else: + vollog.error("You must use either --inode or --find") + return + + if not inode.is_reg: + vollog.error("The inode is not a regular file") + return + + inode_size = inode.i_size + if not inode.is_valid(): + vollog.error("Invalid inode at 0x%x", self.config["inode"]) + return + + for page_obj in inode.get_pages(): + page_vaddr = page_obj.vol.offset + page_paddr = page_obj.to_paddr() + page_mapping_addr = page_obj.mapping + page_index = int(page_obj.index) + page_file_offset = page_index * vmlinux_layer.page_size + dump_safe = page_file_offset < inode_size + page_flags_list = page_obj.get_flags() + page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list]) + fields = ( + page_vaddr, + page_paddr, + page_mapping_addr, + page_index, + dump_safe, + page_flags, + ) + + yield 0, fields + + if self.config["dump"]: + filename = self.config["dump"] + vollog.info("[*] Writing inode at 0x%x to '%s'", inode.vol.offset, filename) + self.write_inode_content_to_file(inode, filename, vmlinux_layer) + + def run(self): + headers = [ + ("PageVAddr", format_hints.Hex), + ("PagePAddr", format_hints.Hex), + ("MappingAddr", format_hints.Hex), + ("Index", int), + ("DumpSafe", bool), + ("Flags", str), + ] + + return renderers.TreeGrid( + headers, Files.format_fields_with_headers(headers, self._generator()) + ) From 55212008f805abdd47f2f3d7d6211c198097f8d4 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:39:51 +1000 Subject: [PATCH 48/85] Linux: Add pidhashtable plugin. This is based on the vol2 plugin, removing ancient kernel support, curating code and enhancing comments, while using the new IDR abstraction included also in this effort. --- .../framework/plugins/linux/pidhashtable.py | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 volatility3/framework/plugins/linux/pidhashtable.py diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py new file mode 100644 index 000000000..b24c73e77 --- /dev/null +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -0,0 +1,249 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import List + +from volatility3.framework import renderers, interfaces, constants +from volatility3.framework.symbols import linux +from volatility3.framework.renderers import format_hints +from volatility3.framework.interfaces import plugins +from volatility3.framework.configuration import requirements +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class PIDHashTable(plugins.PluginInterface): + """Enumerates processes through the PID hash table""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 2, 0) + ), + requirements.BooleanRequirement( + name="decorate_comm", + description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets", + optional=True, + default=False, + ), + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.vmlinux = None + self.vmlinux_layer = None + + def _is_valid_task(self, task): + return task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent) + + def _get_pidtype_pid(self): + # The pid_type enumeration is present since 2.5.37, just in case + pid_type_enum = self.vmlinux.get_enumeration("pid_type") + if not pid_type_enum: + vollog.error("Cannot find pid_type enum. Unsupported kernel") + return + + pidtype_pid = pid_type_enum.choices.get("PIDTYPE_PID") + if pidtype_pid is None: + vollog.error("Cannot find PIDTYPE_PID. Unsupported kernel") + return + + # Typically PIDTYPE_PID = 0 + return pidtype_pid + + def _get_pidhash_array(self): + pidhash_shift = self.vmlinux.object_from_symbol("pidhash_shift") + pidhash_size = 1 << pidhash_shift + + array_type_name = self.vmlinux.symbol_table_name + constants.BANG + "array" + + pidhash_ptr = self.vmlinux.object_from_symbol("pid_hash") + # pidhash is an array of hlist_heads + pidhash = self._context.object( + array_type_name, + offset=pidhash_ptr, + subtype=self.vmlinux.get_type("hlist_head"), + count=pidhash_size, + layer_name=self.vmlinux.layer_name, + ) + + return pidhash + + def _walk_upid(self, seen_upids, upid): + while upid and self.vmlinux_layer.is_valid(upid.vol.offset): + if upid.vol.offset in seen_upids: + break + seen_upids.add(upid.vol.offset) + + pid_chain = upid.pid_chain + if not (pid_chain and self.vmlinux_layer.is_valid(pid_chain.vol.offset)): + break + + upid = linux.LinuxUtilities.container_of( + pid_chain.next, "upid", "pid_chain", self.vmlinux + ) + + def _get_upids(self): + # 2.6.24 <= kernels < 4.15 + pidhash = self._get_pidhash_array() + + seen_upids = set() + for hlist in pidhash: + # each entry in the hlist is a upid which is wrapped in a pid + ent = hlist.first + + while ent and self.vmlinux_layer.is_valid(ent.vol.offset): + # upid->pid_chain exists 2.6.24 <= kernel < 4.15 + upid = linux.LinuxUtilities.container_of( + ent.vol.offset, "upid", "pid_chain", self.vmlinux + ) + + if upid.vol.offset in seen_upids: + break + + self._walk_upid(seen_upids, upid) + + ent = ent.next + + return seen_upids + + def _pid_hash_implementation(self): + # 2.6.24 <= kernels < 4.15 + task_pids_off = self.vmlinux.get_type("task_struct").relative_child_offset( + "pids" + ) + pidtype_pid = self._get_pidtype_pid() + + for upid in self._get_upids(): + pid = linux.LinuxUtilities.container_of( + upid, "pid", "numbers", self.vmlinux + ) + if not pid: + continue + + pid_tasks_0 = pid.tasks[pidtype_pid].first + if not pid_tasks_0: + continue + + task = self.vmlinux.object( + "task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True + ) + if self._is_valid_task(task): + yield task + + def _task_for_radix_pid_node(self, nodep): + # kernels >= 4.15 + pid = self.vmlinux.object("pid", offset=nodep, absolute=True) + pidtype_pid = self._get_pidtype_pid() + + pid_tasks_0 = pid.tasks[pidtype_pid].first + if not pid_tasks_0: + return + + task_struct_type = self.vmlinux.get_type("task_struct") + if task_struct_type.has_member("pids"): + member = "pids" + elif task_struct_type.has_member("pid_links"): + member = "pid_links" + else: + return None + + task_pids_off = task_struct_type.relative_child_offset(member) + task = self.vmlinux.object( + "task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True + ) + return task + + def _pid_namespace_idr(self): + # kernels >= 4.15 + ns_addr = self.vmlinux.get_symbol("init_pid_ns").address + ns = self.vmlinux.object("pid_namespace", offset=ns_addr) + + for page_addr in ns.idr.get_page_addresses(): + task = self._task_for_radix_pid_node(page_addr) + if self._is_valid_task(task): + yield task + + def _determine_pid_func(self): + pid_hash = self.vmlinux.has_symbol("pid_hash") and self.vmlinux.has_symbol( + "pidhash_shift" + ) # 2.5.55 <= kernels < 4.15 + + has_pid_numbers = self.vmlinux.has_type("pid") and self.vmlinux.get_type( + "pid" + ).has_member( + "numbers" + ) # kernels >= 2.6.24 + + has_pid_numbers = self.vmlinux.has_type("upid") and self.vmlinux.get_type( + "upid" + ).has_member( + "pid_chain" + ) # 2.6.24 <= kernels < 4.15 + + # kernels >= 4.15 + pid_idr = self.vmlinux.has_type("pid_namespace") and self.vmlinux.get_type( + "pid_namespace" + ).has_member("idr") + + if pid_idr: + # kernels >= 4.15 + return self._pid_namespace_idr + elif pid_hash and has_pid_numbers and has_pid_numbers: + # 2.6.24 <= kernels < 4.15 + return self._pid_hash_implementation + + return None + + def get_tasks(self) -> interfaces.objects.ObjectInterface: + """Enumerates processes through the PID hash table + + Yields: + task_struct objects + """ + self.vmlinux = self.context.modules[self.config["kernel"]] + self.vmlinux_layer = self.context.layers[self.vmlinux.layer_name] + pid_func = self._determine_pid_func() + if not pid_func: + vollog.error("Cannot determine which PID hash table this kernel is using") + return + + yield from sorted(pid_func(), key=lambda t: (t.tgid, t.pid)) + + def _generator( + self, decorate_comm: bool = False + ) -> interfaces.objects.ObjectInterface: + for task in self.get_tasks(): + offset, pid, tid, ppid, name = pslist.PsList.get_task_fields( + task, decorate_comm + ) + fields = format_hints.Hex(offset), pid, tid, ppid, name + yield 0, fields + + def run(self): + decorate_comm = self.config.get("decorate_comm") + + headers = [ + ("OFFSET", format_hints.Hex), + ("PID", int), + ("TID", int), + ("PPID", int), + ("COMM", str), + ] + return renderers.TreeGrid(headers, self._generator(decorate_comm=decorate_comm)) From 103537801ee0b49ca3475be11e1fe62670938e91 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:44:38 +1000 Subject: [PATCH 49/85] Linux: Add a basic eBPF program enumeration plugin to test and demonstrate using the IDR abstraction --- volatility3/framework/plugins/linux/ebpf.py | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 volatility3/framework/plugins/linux/ebpf.py diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py new file mode 100644 index 000000000..33ba71faf --- /dev/null +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -0,0 +1,78 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import binascii +import logging +from typing import List + +from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.framework.interfaces import plugins +from volatility3.framework.configuration import requirements + +vollog = logging.getLogger(__name__) + + +class EBPF(plugins.PluginInterface): + """Enumerate eBPF programs""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + ] + + def get_ebpf_programs(self, vmlinux) -> interfaces.objects.ObjectInterface: + """Enumerate eBPF programs walking its IDR. + + Args: + vmlinux: The kernel symbols object + + Yields: + eBPF program objects + """ + if not vmlinux.has_symbol("prog_idr"): + raise exceptions.VolatilityException( + "Cannot find the eBPF prog idr. Unsupported kernel" + ) + + prog_idr_addr = vmlinux.get_symbol("prog_idr").address + prog_idr = vmlinux.object("idr", offset=prog_idr_addr) + for page_addr in prog_idr.get_page_addresses(): + bpf_prog = vmlinux.object("bpf_prog", offset=page_addr, absolute=True) + yield bpf_prog + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + bpf_prog_types = vmlinux.get_enumeration("bpf_prog_type") + for prog in self.get_ebpf_programs(vmlinux): + prog_addr = prog.vol.offset + prog_type = bpf_prog_types.lookup(prog.type) + prog_tag_addr = prog.tag.vol.offset + prog_tag_size = prog.tag.count + prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size) + prog_tag = binascii.hexlify(prog_tag_bytes).decode() + prog_name = ( + utility.array_to_string(prog.aux.name) or renderers.NotAvailableValue() + ) + fields = (format_hints.Hex(prog_addr), prog_name, prog_tag, prog_type) + yield (0, fields) + + def run(self): + headers = [ + ("Address", format_hints.Hex), + ("Name", str), + ("Tag", str), + ("Type", str), + ] + return renderers.TreeGrid(headers, self._generator()) From cc04f665e35989fea4c108de2bdd1b31016145ed Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 23:47:28 +1000 Subject: [PATCH 50/85] Fix inode type --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 545c243e0..c36f8a339 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -72,7 +72,7 @@ class InodeInternal: device = f"{self.superblock.major}:{self.superblock.minor}" inode_num = int(self.inode.i_ino) inode_addr = self.inode.vol.offset - inode_type = renderers.UnparsableValue() + inode_type = self.inode.get_inode_type() or renderers.UnparsableValue() # Round up the number of pages to fit the inode's size inode_pages = int(math.ceil(self.inode.i_size / float(kernel_layer.page_size))) cached_pages = int(self.inode.i_mapping.nrpages) From 3e75c2ae9d29084485f6d2803c64d2c42e064ee0 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sun, 4 Aug 2024 14:40:45 +1000 Subject: [PATCH 51/85] Fix @functools.cache . It's available since Python 3.9 --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 300ab2ed0..2d48f677b 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1965,7 +1965,7 @@ class address_space(objects.StructType): class page(objects.StructType): @property - @functools.cache + @functools.lru_cache() def pageflags_enum(self) -> Dict: """Returns 'pageflags' enumeration key/values From 8d6fd3cd78f0fadd223ae93a70a268048b4ccfe9 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Mon, 5 Aug 2024 14:28:43 +0200 Subject: [PATCH 52/85] Moved get_inode_metadata, separated inode and FD processing, error handling precision --- volatility3/framework/plugins/linux/lsof.py | 65 ++++++++++++------- .../framework/symbols/linux/__init__.py | 26 +------- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index fa9d2bf61..167556e7d 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -6,7 +6,7 @@ found in Linux's /proc file system.""" import logging, datetime from typing import List, Callable -from volatility3.framework import renderers, interfaces, constants +from volatility3.framework import renderers, interfaces, constants, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility @@ -46,7 +46,30 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ] @classmethod - def list_fds_and_inodes( + def get_inode_metadata(cls, filp: interfaces.objects.ObjectInterface): + try: + dentry = filp.get_dentry() + if dentry: + inode_object = dentry.d_inode + if inode_object and inode_object.is_valid(): + itype = ( + inode_object.get_inode_type() or renderers.NotAvailableValue() + ) + return ( + inode_object.i_ino, + itype, + inode_object.i_size, + inode_object.get_file_mode(), + inode_object.get_change_time(), + inode_object.get_modification_time(), + inode_object.get_access_time(), + ) + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.warning(f"Can't get inode metadata: {e}") + return tuple(renderers.NotAvailableValue() for _ in range(7)) + + @classmethod + def list_fds( cls, context: interfaces.context.ContextInterface, symbol_table: str, @@ -67,26 +90,27 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ) for fd_fields in fd_generator: - fd_num, filp, full_path = fd_fields - inode_metadata = linux.LinuxUtilities.get_inode_metadata(context, filp) - try: - inode_num, itype, file_size, imode, ctime, mtime, atime = next( - inode_metadata - ) - except Exception as e: - vollog.warning( - f"Can't get inode metadata for file descriptor {fd_num}: {e}" - ) - inode_num = itype = file_size = imode = ctime = mtime = atime = ( - renderers.NotAvailableValue() - ) - yield pid, task_comm, task, fd_num, filp, full_path, inode_num, itype, imode, ctime, mtime, atime, file_size + yield pid, task_comm, task, fd_fields + + @classmethod + def list_fds_and_inodes( + cls, + context: interfaces.context.ContextInterface, + symbol_table: str, + filter_func: Callable[[int], bool] = lambda _: False, + ): + for pid, task_comm, task, (fd_num, filp, full_path) in cls.list_fds( + context, symbol_table, filter_func + ): + inode_metadata = cls.get_inode_metadata(filp) + yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata def _generator(self, pids, symbol_table): filter_func = pslist.PsList.create_pid_filter(pids) fds_generator = self.list_fds_and_inodes( self.context, symbol_table, filter_func=filter_func ) + for ( pid, task_comm, @@ -94,14 +118,9 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): fd_num, filp, full_path, - inode_num, - itype, - imode, - ctime, - mtime, - atime, - file_size, + inode_metadata, ) in fds_generator: + inode_num, itype, file_size, imode, ctime, mtime, atime = inode_metadata fields = ( pid, task_comm, diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index d52c43dae..03353135d 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,8 +1,8 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # from typing import Iterator, List, Tuple, Optional, Union -import datetime, stat + from volatility3 import framework from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.objects import utility @@ -67,7 +67,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 2, 0) + _version = (2, 1, 0) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -274,26 +274,6 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): yield fd_num, filp, full_path - @classmethod - def get_inode_metadata(cls, context: interfaces.context.ContextInterface, filp): - """ - A helper function that gets the inodes metadata from a file descriptor - """ - dentry = filp.get_dentry() - if dentry: - inode_object = dentry.d_inode - if inode_object and inode_object.is_valid(): - itype = inode_object.get_inode_type() or "?" - yield ( - inode_object.i_ino, - itype, - inode_object.i_size, - inode_object.get_file_mode(), - inode_object.get_change_time(), - inode_object.get_modification_time(), - inode_object.get_access_time(), - ) - @classmethod def mask_mods_list( cls, From c9eb81c95fa530c58e493d77cde506f001e9e4f3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 04:40:48 -0700 Subject: [PATCH 53/85] PR review fixes: Improve eBPF extension objects: bpf_prog and added bpf_prog_aux. Apply changes to the EBPF and Sockstat plugins. --- volatility3/framework/plugins/linux/ebpf.py | 15 ++---- .../framework/plugins/linux/sockstat.py | 14 +++--- .../framework/symbols/linux/__init__.py | 1 + .../symbols/linux/extensions/__init__.py | 48 +++++++++++++++++-- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py index 33ba71faf..8df506b06 100644 --- a/volatility3/framework/plugins/linux/ebpf.py +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -1,12 +1,10 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import binascii import logging from typing import List from volatility3.framework import renderers, interfaces, exceptions -from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.interfaces import plugins from volatility3.framework.configuration import requirements @@ -53,18 +51,11 @@ class EBPF(plugins.PluginInterface): def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] - vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] - bpf_prog_types = vmlinux.get_enumeration("bpf_prog_type") for prog in self.get_ebpf_programs(vmlinux): prog_addr = prog.vol.offset - prog_type = bpf_prog_types.lookup(prog.type) - prog_tag_addr = prog.tag.vol.offset - prog_tag_size = prog.tag.count - prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size) - prog_tag = binascii.hexlify(prog_tag_bytes).decode() - prog_name = ( - utility.array_to_string(prog.aux.name) or renderers.NotAvailableValue() - ) + prog_type = prog.get_type() or renderers.NotAvailableValue() + prog_tag = prog.get_tag() or renderers.NotAvailableValue() + prog_name = prog.get_name() or renderers.NotAvailableValue() fields = (format_hints.Hex(prog_addr), prog_name, prog_tag, prog_type) yield (0, fields) diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index 78217fbec..b0503b105 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -151,17 +151,15 @@ class SockHandlers(interfaces.configuration.VersionableInterface): bpfprog = sock_filter.prog - BPF_PROG_TYPE_UNSPEC = 0 # cBPF filter - try: - bpfprog_type = bpfprog.get_type() - if bpfprog_type == BPF_PROG_TYPE_UNSPEC: - return # cBPF filter - except AttributeError: + bpfprog_type = bpfprog.get_type() + if not bpfprog_type: # kernel < 3.18.140, it's a cBPF filter return None - BPF_PROG_TYPE_SOCKET_FILTER = 1 # eBPF filter - if bpfprog_type != BPF_PROG_TYPE_SOCKET_FILTER: + if bpfprog_type == "BPF_PROG_TYPE_UNSPEC": + return None # cBPF filter + + if bpfprog_type != "BPF_PROG_TYPE_SOCKET_FILTER": socket_filter["bpf_filter_type"] = f"UNK({bpfprog_type})" vollog.warning(f"Unexpected BPF type {bpfprog_type} for a socket") return None diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 248cb8d75..7a87135ff 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -38,6 +38,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): # Might not exist in the current symbols self.optional_set_type_class("module", extensions.module) self.optional_set_type_class("bpf_prog", extensions.bpf_prog) + self.optional_set_type_class("bpf_prog_aux", extensions.bpf_prog_aux) self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct) self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 2d48f677b..2b971fb79 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -5,6 +5,7 @@ import collections.abc import logging import functools +import binascii import stat from datetime import datetime import socket as socket_module @@ -1607,20 +1608,59 @@ class xdp_sock(objects.StructType): class bpf_prog(objects.StructType): - def get_type(self): + def get_type(self) -> Union[str, None]: + """Returns a string with the eBPF program type""" + # The program type was in `bpf_prog_aux::prog_type` from 3.18.140 to # 4.1.52 before it was moved to `bpf_prog::type` if self.has_member("type"): # kernel >= 4.1.52 - return self.type + return self.type.description if self.has_member("aux") and self.aux: if self.aux.has_member("prog_type"): # 3.18.140 <= kernel < 4.1.52 - return self.aux.prog_type + return self.aux.prog_type.description # kernel < 3.18.140 - raise AttributeError("Unable to find the BPF type") + return None + + def get_tag(self) -> Union[str, None]: + """Returns a string with the eBPF program tag""" + # 'tag' was added in kernels 4.10 + if not self.has_member("tag"): + return None + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + + prog_tag_addr = self.tag.vol.offset + prog_tag_size = self.tag.count + prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size) + + prog_tag = binascii.hexlify(prog_tag_bytes).decode() + return prog_tag + + def get_name(self) -> Union[str, None]: + """Returns a string with the eBPF program name""" + if not self.has_member("aux"): + # 'prog_aux' was added in kernels 3.18 + return None + + return self.aux.get_name() + + +class bpf_prog_aux(objects.StructType): + def get_name(self) -> Union[str, None]: + """Returns a string with the eBPF program name""" + if not self.has_member("name"): + # 'name' was added in kernels 4.15 + return None + + if not self.name: + return None + + return utility.array_to_string(self.name) class cred(objects.StructType): From bd37aa3930c056fc8511969592f731a29e253c84 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 04:48:49 -0700 Subject: [PATCH 54/85] PR review fixes: Fix pidhashtable plugin --- volatility3/framework/plugins/linux/pidhashtable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index b24c73e77..ef110bdf1 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -191,7 +191,7 @@ class PIDHashTable(plugins.PluginInterface): "numbers" ) # kernels >= 2.6.24 - has_pid_numbers = self.vmlinux.has_type("upid") and self.vmlinux.get_type( + has_pid_chain = self.vmlinux.has_type("upid") and self.vmlinux.get_type( "upid" ).has_member( "pid_chain" @@ -205,7 +205,7 @@ class PIDHashTable(plugins.PluginInterface): if pid_idr: # kernels >= 4.15 return self._pid_namespace_idr - elif pid_hash and has_pid_numbers and has_pid_numbers: + elif pid_hash and has_pid_numbers and has_pid_numbers and has_pid_chain: # 2.6.24 <= kernels < 4.15 return self._pid_hash_implementation From 7df0636f30d47f1d5b9373671aac410549474766 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 04:57:33 -0700 Subject: [PATCH 55/85] PR review fixes: Fix pidhashtable plugin explicit returns mixed with implicit returns --- volatility3/framework/plugins/linux/pidhashtable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index ef110bdf1..73cdff452 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -50,19 +50,19 @@ class PIDHashTable(plugins.PluginInterface): self.vmlinux_layer = None def _is_valid_task(self, task): - return task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent) + return bool(task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent)) def _get_pidtype_pid(self): # The pid_type enumeration is present since 2.5.37, just in case pid_type_enum = self.vmlinux.get_enumeration("pid_type") if not pid_type_enum: vollog.error("Cannot find pid_type enum. Unsupported kernel") - return + return None pidtype_pid = pid_type_enum.choices.get("PIDTYPE_PID") if pidtype_pid is None: vollog.error("Cannot find PIDTYPE_PID. Unsupported kernel") - return + return None # Typically PIDTYPE_PID = 0 return pidtype_pid @@ -154,7 +154,7 @@ class PIDHashTable(plugins.PluginInterface): pid_tasks_0 = pid.tasks[pidtype_pid].first if not pid_tasks_0: - return + return None task_struct_type = self.vmlinux.get_type("task_struct") if task_struct_type.has_member("pids"): From 1e3e9e2c78cbfc4e24362c323432660c98373516 Mon Sep 17 00:00:00 2001 From: Arcuri Davide Date: Tue, 6 Aug 2024 16:28:59 +0200 Subject: [PATCH 56/85] add args and kwargs to threads.py init Without args and kwargs there were an issue with timeliner plugin that tried to pass additional parameters like progress_callback raising TypeError --- volatility3/framework/plugins/windows/threads.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/threads.py b/volatility3/framework/plugins/windows/threads.py index ae70e717b..39f3b7d77 100644 --- a/volatility3/framework/plugins/windows/threads.py +++ b/volatility3/framework/plugins/windows/threads.py @@ -18,9 +18,9 @@ class Threads(thrdscan.ThrdScan): _required_framework_version = (2, 4, 0) _version = (1, 0, 0) - def __init__(self): + def __init__(self, *args, **kwargs): self.implementation = self.list_process_threads - super().__init__() + super().__init__(*args, **kwargs) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From 339a9a94f57adb2039984facf0cd22df0ba4bf90 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 23:00:09 -0700 Subject: [PATCH 57/85] PR review fixes: pidhashtable plugin add missing typing. --- volatility3/framework/plugins/linux/pidhashtable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index 73cdff452..3c429dc2f 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -49,7 +49,7 @@ class PIDHashTable(plugins.PluginInterface): self.vmlinux = None self.vmlinux_layer = None - def _is_valid_task(self, task): + def _is_valid_task(self, task) -> bool: return bool(task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent)) def _get_pidtype_pid(self): From c8cb4465da3d71a879b47d981fdd4871afd9d521 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 23:08:21 -0700 Subject: [PATCH 58/85] PR review fixes: Remove filter function, it isn't needed --- volatility3/framework/plugins/linux/mountinfo.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index 319c92cca..dfb2e2f52 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -254,10 +254,7 @@ class MountInfo(plugins.PluginInterface): super_block: Kernel's struct super_block object """ # No filter so that we get all the mount namespaces from all tasks - pid_filter = pslist.PsList.create_pid_filter() - tasks = pslist.PsList.list_tasks( - self.context, self.config["kernel"], filter_func=pid_filter - ) + tasks = pslist.PsList.list_tasks(self.context, self.config["kernel"]) seen_sb_ptr = set() for task, mnt, _mnt_ns_id in self._get_tasks_mountpoints(tasks): From f737b88d03d9be9f4f0a9b43a83e02a88a620068 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 23:12:01 -0700 Subject: [PATCH 59/85] PR review fixes: Improve _walk_dentry() and get_inodes() variable names, arguments and return values --- .../framework/plugins/linux/pagecache.py | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index c36f8a339..cf09f23d8 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -6,7 +6,7 @@ import math import logging import datetime from dataclasses import dataclass, astuple -from typing import List +from typing import List, Set from volatility3.framework import renderers, interfaces from volatility3.framework.renderers import format_hints @@ -153,7 +153,23 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): return symlink_path @classmethod - def _walk_dentry(cls, seen_dentries, root_dentry, parent): + def _walk_dentry( + cls, + seen_dentries: Set[int], + root_dentry: interfaces.objects.ObjectInterface, + parent_dir: str, + ): + """Walk dentries recursively + + Args: + seen_dentries: A set to ensure each dentry is processed only once + root_dentry: Root dentry object + parent_dir: Parent directory path + + Yields: + file_path: Filename including path + dentry: Dentry object + """ for dentry in root_dentry.get_subdirs(): dentry_addr = dentry.vol.offset @@ -173,19 +189,16 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): # This allows us to have consistent paths if dentry.d_name.name: - name = dentry.d_name.name_as_str() + basename = dentry.d_name.name_as_str() # Do NOT use os.path.join() below - new_file = parent + "/" + name + file_path = parent_dir + "/" + basename else: continue - yield new_file, dentry, dentry.d_parent.vol.offset + yield file_path, dentry if inode.is_dir: - for new_file, dentry, parent_address in cls._walk_dentry( - seen_dentries, dentry, new_file - ): - yield new_file, dentry, parent_address + yield from cls._walk_dentry(seen_dentries, dentry, parent_dir=file_path) @classmethod def get_inodes( @@ -211,13 +224,15 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): seen_inodes = set() seen_dentries = set() for superblock, mountpoint in superblocks_iter: - parent = "" if mountpoint == "/" else mountpoint + parent_dir = "" if mountpoint == "/" else mountpoint # Superblock root dentry - root_dentry = superblock.s_root - if not root_dentry: + root_dentry_ptr = superblock.s_root + if not root_dentry_ptr: continue + root_dentry = root_dentry_ptr.dereference() + # Dentry sanity check if not root_dentry.is_root(): continue @@ -246,8 +261,8 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): yield inode_in # Children - for file_path, file_dentry, _ in cls._walk_dentry( - seen_dentries, root_dentry, parent + for file_path, file_dentry in cls._walk_dentry( + seen_dentries, root_dentry, parent_dir ): if not file_dentry: continue From 805b3514c3b14c53724cd38d722540d84949d51f Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 7 Aug 2024 00:23:19 -0700 Subject: [PATCH 60/85] PR review fixes: Fix page flags list method name, this was introduced earlier in another commit of this PR. --- volatility3/framework/plugins/linux/pagecache.py | 2 +- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index cf09f23d8..fd62cc1e4 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -486,7 +486,7 @@ class InodePages(plugins.PluginInterface): page_index = int(page_obj.index) page_file_offset = page_index * vmlinux_layer.page_size dump_safe = page_file_offset < inode_size - page_flags_list = page_obj.get_flags() + page_flags_list = page_obj.get_flags_list() page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list]) fields = ( page_vaddr, diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 2b971fb79..900cc9b6c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2027,7 +2027,7 @@ class page(objects.StructType): return pageflags_enum - def flags_list(self) -> List[str]: + def get_flags_list(self) -> List[str]: """Returns a list of page flags Returns: From 46842981fb25aa6f1b242ef5990642fdeeb0cf00 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 7 Aug 2024 00:24:58 -0700 Subject: [PATCH 61/85] PR review fixes: Use contextlib.suppress() instead of an empty exception handler --- volatility3/framework/symbols/linux/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 7a87135ff..96bc56a18 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import math +import contextlib from abc import ABC, abstractmethod from typing import Iterator, List, Tuple, Optional, Union @@ -676,15 +677,13 @@ class RadixTree(Tree): return self.RADIX_TREE_INTERNAL_NODE def get_tree_height(self, treep) -> int: - try: + with contextlib.suppress(exceptions.SymbolError): if self.vmlinux.get_type("radix_tree_root").has_member("height"): # kernels < 4.7.10 radix_tree_root = self.vmlinux.object( "radix_tree_root", offset=treep, absolute=True ) return radix_tree_root.height - except exceptions.SymbolError: - pass # kernels >= 4.7.10 return 0 From de6637c9871ac16f1a5278c4831dfe60778852b9 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 00:52:34 -0700 Subject: [PATCH 62/85] PR review fixes: ebpf plugin code improvement. Use the object_from_symbol() instead --- volatility3/framework/plugins/linux/ebpf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py index 8df506b06..9d41c0ffb 100644 --- a/volatility3/framework/plugins/linux/ebpf.py +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -43,8 +43,7 @@ class EBPF(plugins.PluginInterface): "Cannot find the eBPF prog idr. Unsupported kernel" ) - prog_idr_addr = vmlinux.get_symbol("prog_idr").address - prog_idr = vmlinux.object("idr", offset=prog_idr_addr) + prog_idr = vmlinux.object_from_symbol("prog_idr") for page_addr in prog_idr.get_page_addresses(): bpf_prog = vmlinux.object("bpf_prog", offset=page_addr, absolute=True) yield bpf_prog From ee10ba8abb7b8c932d9af85a37edefc3fd03fa63 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 01:46:53 -0700 Subject: [PATCH 63/85] PR review fixes: Fix IDR explicit returns mixed with implicit returns and improve and fix code. --- .../symbols/linux/extensions/__init__.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 900cc9b6c..6eafd4173 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2134,7 +2134,7 @@ class IDR(objects.StructType): """Finds an ID within the IDR data structure. Based on idr_find_slowpath(), 3.9 <= Kernel < 4.11 Args: - idr_id: The IDR element ID + idr_id: The IDR lookup ID Returns: A pointer to the given ID element @@ -2144,28 +2144,28 @@ class IDR(objects.StructType): vollog.info( "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" ) - return + return None if idr_id < 0: - return + return None - cur_layer = self.top - if not cur_layer: - return + idr_layer = self.top + if not idr_layer: + return None - n = (cur_layer.layer + 1) * self.IDR_BITS + n = (idr_layer.layer + 1) * self.IDR_BITS - if idr_id > self.idr_max(cur_layer.layer + 1): - return + if idr_id > self.idr_max(idr_layer.layer + 1): + return None assert n != 0 - while n > 0 and cur_layer: + while n > 0 and idr_layer: n -= self.IDR_BITS - assert n == cur_layer.layer * self.IDR_BITS - cur_layer = cur_layer.ary[(idr_id >> n) & self.IDR_MASK] + assert n == idr_layer.layer * self.IDR_BITS + idr_layer = idr_layer.ary[(idr_id >> n) & self.IDR_MASK] - return cur_layer.v() + return idr_layer def _old_kernel_get_page_addresses(self, in_use) -> int: # Kernels < 4.11 From 0f3f33863370f68b7da3069db5b29282dabab932 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 01:52:57 -0700 Subject: [PATCH 64/85] PR review fixes: Since the IDR, XArray and RadixTree can store any value, it renames the function names to a more generic name --- volatility3/framework/plugins/linux/ebpf.py | 2 +- .../framework/plugins/linux/pidhashtable.py | 2 +- volatility3/framework/symbols/linux/__init__.py | 4 ++-- .../symbols/linux/extensions/__init__.py | 16 +++++++++------- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py index 9d41c0ffb..70082daf5 100644 --- a/volatility3/framework/plugins/linux/ebpf.py +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -44,7 +44,7 @@ class EBPF(plugins.PluginInterface): ) prog_idr = vmlinux.object_from_symbol("prog_idr") - for page_addr in prog_idr.get_page_addresses(): + for page_addr in prog_idr.get_entries(): bpf_prog = vmlinux.object("bpf_prog", offset=page_addr, absolute=True) yield bpf_prog diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index 3c429dc2f..c384cb5cd 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -175,7 +175,7 @@ class PIDHashTable(plugins.PluginInterface): ns_addr = self.vmlinux.get_symbol("init_pid_ns").address ns = self.vmlinux.object("pid_namespace", offset=ns_addr) - for page_addr in ns.idr.get_page_addresses(): + for page_addr in ns.idr.get_entries(): task = self._task_for_radix_pid_node(page_addr) if self._is_valid_task(task): yield task diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 96bc56a18..89f970275 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -572,7 +572,7 @@ class Tree(ABC): for child_node in self._iter_node(nodep, height - 1): yield child_node - def get_page_addresses(self, root: interfaces.objects.ObjectInterface) -> int: + def get_entries(self, root: interfaces.objects.ObjectInterface) -> int: """Walks the tree data structure Args: @@ -762,7 +762,7 @@ class PageCache(object): Page objects """ - for page_addr in self._tree.get_page_addresses(self._page_cache.i_pages): + for page_addr in self._tree.get_entries(self._page_cache.i_pages): if not page_addr: continue diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 6eafd4173..29af5c510 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2167,7 +2167,7 @@ class IDR(objects.StructType): return idr_layer - def _old_kernel_get_page_addresses(self, in_use) -> int: + def _old_kernel_get_entries(self) -> int: # Kernels < 4.11 total = next_id = 0 while total < in_use: @@ -2178,14 +2178,14 @@ class IDR(objects.StructType): next_id += 1 - def _new_kernel_get_page_addresses(self, _in_use) -> int: + def _new_kernel_get_entries(self) -> int: # Kernels >= 4.11 vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) tree = linux.LinuxUtilities.choose_kernel_tree(vmlinux) - for page_addr in tree.get_page_addresses(root=self.idr_rt): + for page_addr in tree.get_entries(root=self.idr_rt): yield page_addr - def get_page_addresses(self, in_use=0) -> int: + def get_entries(self) -> int: """Walks the IDR and yield a pointer associated with each element. Args: @@ -2195,9 +2195,11 @@ class IDR(objects.StructType): A pointer associated with each element. """ if self.has_member("idr_rt"): - get_page_addresses_func = self._new_kernel_get_page_addresses + # Kernels >= 4.11 + get_entries_func = self._new_kernel_get_entries else: - get_page_addresses_func = self._old_kernel_get_page_addresses + # Kernels < 4.11 + get_entries_func = self._old_kernel_get_entries - for page_addr in get_page_addresses_func(in_use): + for page_addr in get_entries_func(): yield page_addr From 7a8dea3356c709cda2bdd20b7cb3ff97f1f7987d Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 01:57:35 -0700 Subject: [PATCH 65/85] PR review fixes: Code scanning complains about these unused variables. Let's comment them and adapt the FIXME message --- volatility3/framework/symbols/linux/extensions/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 29af5c510..9b03235cb 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2063,10 +2063,9 @@ class page(objects.StructType): vmemmap_start = vmemmap_base_l4 else: # 5-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L5 - vmemmap_base_l5 = 0xFFD4000000000000 - vmemmap_start = vmemmap_base_l5 - - # FIXME: Remove this exception once 5-level paging is supported. + # FIXME: Once 5-level paging is supported, uncomment the following lines and remove the exception + # vmemmap_base_l5 = 0xFFD4000000000000 + # vmemmap_start = vmemmap_base_l5 raise exceptions.VolatilityException( "5-level paging is not yet supported" ) From 06508a4afba235812e7d1b7bfb79467adf2ed6fc Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 02:00:27 -0700 Subject: [PATCH 66/85] PR review fixes: Fix the IDR's old kernel get_entries --- .../framework/symbols/linux/extensions/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 9b03235cb..f7b0df6be 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2168,11 +2168,12 @@ class IDR(objects.StructType): def _old_kernel_get_entries(self) -> int: # Kernels < 4.11 + cur = self.cur total = next_id = 0 - while total < in_use: - page_addr = self.idr_find(next_id) - if page_addr: - yield page_addr + while next_id < cur: + entry = self.idr_find(next_id) + if entry: + yield entry total += 1 next_id += 1 From 2c85ea525e15c0cb745f3b806836f22ea44a4b4c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 02:03:28 -0700 Subject: [PATCH 67/85] PR review fixes: Fix page extension object get_content() explicit returns mixed with implicit returns. --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index f7b0df6be..be4df6a13 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2102,7 +2102,7 @@ class page(objects.StructType): physical_layer = vmlinux.context.layers["memory_layer"] page_paddr = self.to_paddr() if not page_paddr: - return + return None page_data = physical_layer.read(page_paddr, vmlinux_layer.page_size) return page_data From f22575669a6ccd9afaeef126e81a6adad8b880f6 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Fri, 9 Aug 2024 10:20:28 +0200 Subject: [PATCH 68/85] Modifications following the review --- volatility3/framework/plugins/linux/lsof.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 167556e7d..9a0fd7417 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -66,7 +66,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ) except (exceptions.InvalidAddressException, AttributeError) as e: vollog.warning(f"Can't get inode metadata: {e}") - return tuple(renderers.NotAvailableValue() for _ in range(7)) + return None @classmethod def list_fds( @@ -103,6 +103,10 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): context, symbol_table, filter_func ): inode_metadata = cls.get_inode_metadata(filp) + if inode_metadata is None: + inode_metadata = tuple( + interfaces.renderers.BaseAbsentValue() for _ in range(7) + ) yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata def _generator(self, pids, symbol_table): From 53f3d12341e722f1058c42436adfea600af94bab Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 9 Aug 2024 23:35:41 -0700 Subject: [PATCH 69/85] linuxutilities code improvement. Remove code duplication --- volatility3/framework/symbols/linux/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 89f970275..90f5cc8a2 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -419,9 +419,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): Returns: A kernel object (vmlinux) """ - symbol_table_arr = volobj.vol.type_name.split("!", 1) - symbol_table = symbol_table_arr[0] if len(symbol_table_arr) == 2 else None - + symbol_table = volobj.get_symbol_table_name() module_names = context.modules.get_modules_by_symbol_tables(symbol_table) module_names = list(module_names) From 17861618df3744d572955b50b2b1b1ad1d0961e5 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 10 Aug 2024 00:13:38 -0700 Subject: [PATCH 70/85] PR review fixes: Rename Tree to IDStorage. Move choose_id_storage() form LinuxUtilities to IDStorage. Use context and kernel_module_name instead of vmlinux --- .../framework/symbols/linux/__init__.py | 87 +++++++++++-------- .../symbols/linux/extensions/__init__.py | 14 +-- 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 90f5cc8a2..632ac2f6b 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -431,17 +431,51 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return kernel + +class IDStorage(ABC): + """Abstraction to support both XArray and RadixTree""" + + # Dynamic values, these will be initialized later + CHUNK_SHIFT = None + CHUNK_SIZE = None + CHUNK_MASK = None + + def __init__( + self, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + ): + self.vmlinux = context.modules[kernel_module_name] + self.vmlinux_layer = self.vmlinux.context.layers[self.vmlinux.layer_name] + + self.pointer_size = self.vmlinux.get_type("pointer").size + # Dynamically work out the (XA_CHUNK|RADIX_TREE_MAP)_SHIFT values based on + # the node.slots[] array size + node_type = self.vmlinux.get_type(self.node_type_name) + slots_array_size = node_type.child_template("slots").count + + # Calculate the LSB index - 1 + self.CHUNK_SHIFT = slots_array_size.bit_length() - 1 + self.CHUNK_SIZE = 1 << self.CHUNK_SHIFT + self.CHUNK_MASK = self.CHUNK_SIZE - 1 + @classmethod - def choose_kernel_tree(cls, vmlinux: interfaces.context.ModuleInterface) -> "Tree": - """Returns the appropriate tree data structure instance for the current kernel implementation. + def choose_id_storage( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + ) -> "IDStorage": + """Returns the appropriate ID storage data structure instance for the current kernel implementation. This is used by the IDR and the PageCache to choose between the XArray and RadixTree. Args: - vmlinux: The kernel module object + context: The context to retrieve required elements (layers, symbol tables) from + kernel_module_name: The name of the kernel module on which to operate Returns: - The appropriate Tree instance for the current kernel + The appropriate ID storage instance for the current kernel """ + vmlinux = context.modules[kernel_module_name] address_space_type = vmlinux.get_type("address_space") address_space_has_i_pages = address_space_type.has_member("i_pages") i_pages_type_name = ( @@ -455,33 +489,9 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): ) and vmlinux.get_type("radix_tree_root").has_member("xa_head") if i_pages_is_xarray or i_pages_is_radix_tree_root: - return XArray(vmlinux) + return XArray(context, kernel_module_name) else: - return RadixTree(vmlinux) - - -class Tree(ABC): - """Abstraction to support both XArray and RadixTree""" - - # Dynamic values, these will be initialized later - CHUNK_SHIFT = None - CHUNK_SIZE = None - CHUNK_MASK = None - - def __init__(self, vmlinux: interfaces.context.ModuleInterface): - self.vmlinux = vmlinux - self.vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] - - self.pointer_size = self.vmlinux.get_type("pointer").size - # Dynamically work out the (XA_CHUNK|RADIX_TREE_MAP)_SHIFT values based on - # the node.slots[] array size - node_type = self.vmlinux.get_type(self.node_type_name) - slots_array_size = node_type.child_template("slots").count - - # Calculate the LSB index - 1 - self.CHUNK_SHIFT = slots_array_size.bit_length() - 1 - self.CHUNK_SIZE = 1 << self.CHUNK_SHIFT - self.CHUNK_MASK = self.CHUNK_SIZE - 1 + return RadixTree(context, kernel_module_name) @property @abstractmethod @@ -601,7 +611,7 @@ class Tree(ABC): yield child_node -class XArray(Tree): +class XArray(IDStorage): XARRAY_TAG_MASK = 3 XARRAY_TAG_INTERNAL = 2 @@ -637,7 +647,7 @@ class XArray(Tree): return not self.is_node_tagged(nodep) -class RadixTree(Tree): +class RadixTree(IDStorage): RADIX_TREE_INTERNAL_NODE = 1 RADIX_TREE_EXCEPTIONAL_ENTRY = 2 RADIX_TREE_ENTRY_MASK = 3 @@ -741,17 +751,20 @@ class PageCache(object): def __init__( self, + context: interfaces.context.ContextInterface, + kernel_module_name: str, page_cache: interfaces.objects.ObjectInterface, - vmlinux: interfaces.context.ModuleInterface, ): """ Args: + context: interfaces.context.ContextInterface, + kernel_module_name: The name of the kernel module on which to operate page_cache: Page cache address space - vmlinux: Kernel module object """ - self.vmlinux = vmlinux + self.vmlinux = context.modules[kernel_module_name] + self._page_cache = page_cache - self._tree = LinuxUtilities.choose_kernel_tree(self.vmlinux) + self._idstorage = IDStorage.choose_id_storage(context, kernel_module_name) def get_cached_pages(self) -> interfaces.objects.ObjectInterface: """Returns all page cache contents @@ -760,7 +773,7 @@ class PageCache(object): Page objects """ - for page_addr in self._tree.get_entries(self._page_cache.i_pages): + for page_addr in self._idstorage.get_entries(self._page_cache.i_pages): if not page_addr: continue diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index be4df6a13..d00af7a3f 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1972,8 +1972,11 @@ class inode(objects.StructType): elif not (self.i_mapping and self.i_mapping.nrpages > 0): return - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - page_cache = linux.PageCache(self.i_mapping.dereference(), vmlinux) + page_cache = linux.PageCache( + context=self._context, + kernel_module_name="kernel", + page_cache=self.i_mapping.dereference(), + ) yield from page_cache.get_cached_pages() def get_contents(self): @@ -2180,9 +2183,10 @@ class IDR(objects.StructType): def _new_kernel_get_entries(self) -> int: # Kernels >= 4.11 - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - tree = linux.LinuxUtilities.choose_kernel_tree(vmlinux) - for page_addr in tree.get_entries(root=self.idr_rt): + id_storage = linux.IDStorage.choose_id_storage( + self._context, kernel_module_name="kernel" + ) + for page_addr in id_storage.get_entries(root=self.idr_rt): yield page_addr def get_entries(self) -> int: From 8f9d565f6300750ad30c0aaca0aa01cfbcb1a417 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 10 Aug 2024 00:49:18 -0700 Subject: [PATCH 71/85] PR review fixes: Fix minor typo to match verb form from other docstrings --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index fd62cc1e4..f062655a6 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -159,7 +159,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): root_dentry: interfaces.objects.ObjectInterface, parent_dir: str, ): - """Walk dentries recursively + """Walks dentries recursively Args: seen_dentries: A set to ensure each dentry is processed only once From f804b44ff6f8451e8f1091383b8d9c2300f7bfbb Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 13 Aug 2024 01:00:57 -0700 Subject: [PATCH 72/85] Fix test.yaml, it should remove *.bin and not *.lime. There is no *.lime atm. --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 55b2e4b60..668b814ce 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -46,7 +46,7 @@ jobs: - name: Clean up post-test run: | - rm -rf *.lime + rm -rf *.bin rm -rf *.img cd volatility3/symbols rm -rf linux From 7efa2210a550d55f356b6ebb35cafcdfd4a58d1f Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 13 Aug 2024 01:05:44 -0700 Subject: [PATCH 73/85] PR review fixes: Adjust LinuxUtilities version since we moved choose_id_storage() back to the IDStorage class. --- volatility3/framework/plugins/linux/pidhashtable.py | 2 +- volatility3/framework/symbols/linux/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index c384cb5cd..9a2528543 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -34,7 +34,7 @@ class PIDHashTable(plugins.PluginInterface): name="pslist", plugin=pslist.PsList, version=(2, 0, 0) ), requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 2, 0) + name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) ), requirements.BooleanRequirement( name="decorate_comm", diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 632ac2f6b..91abf7db4 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -74,7 +74,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 2, 0) + _version = (2, 1, 1) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) From 918584537fafd67b2367e283871889bf291b2951 Mon Sep 17 00:00:00 2001 From: Steven Luke <97394870+sluke-nuix@users.noreply.github.com> Date: Tue, 13 Aug 2024 08:34:35 -0400 Subject: [PATCH 74/85] Update the modules.Modules version requirement. This is a response to PR [#1173](https://github.com/volatilityfoundation/volatility3/pull/1173) --- volatility3/framework/plugins/windows/truecrypt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/truecrypt.py b/volatility3/framework/plugins/windows/truecrypt.py index 81250a749..7fd26cb4e 100644 --- a/volatility3/framework/plugins/windows/truecrypt.py +++ b/volatility3/framework/plugins/windows/truecrypt.py @@ -33,7 +33,7 @@ class Passphrase(interfaces.plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="modules", component=modules.Modules, version=(1, 1, 0) + name="modules", component=modules.Modules, version=(2, 0, 0) ), requirements.IntRequirement( name="min-length", From 8d7edfdca6fa84983b4ee734e3f45a9d07c28ff1 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 21 Aug 2024 20:36:09 +0100 Subject: [PATCH 75/85] Bump as the release branch has been cut --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index f219fb0af..4df0b9041 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change VERSION_MINOR = 8 # Number of changes that only add to the interface -VERSION_PATCH = 0 # Number of changes that do not change the interface +VERSION_PATCH = 1 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = ( From 71cdca5883b234680773e000a663750964d4860e Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Thu, 22 Aug 2024 16:45:04 +0200 Subject: [PATCH 76/85] Updating version + docstring --- volatility3/framework/plugins/linux/lsof.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 9a0fd7417..360f89749 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -18,10 +18,10 @@ vollog = logging.getLogger(__name__) class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): - """Lists all memory maps for all processes.""" + """Lists open files for each processes.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _version = (1, 2, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From feae6a9aa0f5c7869174e0b906ec83e61b3e14e3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 00:42:52 +1000 Subject: [PATCH 77/85] PR review fixes: Use plugin's open method instead of the builtin open() --- volatility3/framework/plugins/linux/pagecache.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index f062655a6..cf4151c85 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -6,7 +6,7 @@ import math import logging import datetime from dataclasses import dataclass, astuple -from typing import List, Set +from typing import List, Set, Type from volatility3.framework import renderers, interfaces from volatility3.framework.renderers import format_hints @@ -408,6 +408,7 @@ class InodePages(plugins.PluginInterface): def write_inode_content_to_file( inode: interfaces.objects.ObjectInterface, filename: str, + open_method: Type[interfaces.plugins.FileHandlerInterface], vmlinux_layer: interfaces.layers.TranslationLayerInterface, ) -> None: """Extracts the inode's contents from the page cache and saves them to a file @@ -415,6 +416,7 @@ class InodePages(plugins.PluginInterface): Args: inode: The inode to dump filename: Filename for writing the inode content + open_method: class for constructing output files vmlinux_layer: The kernel layer to obtain the page size """ if not inode.is_reg: @@ -426,7 +428,7 @@ class InodePages(plugins.PluginInterface): # Additionally, using the page index will guarantee that each page is written at the # appropriate file position. try: - with open(filename, "wb") as f: + with open_method(filename) as f: inode_size = inode.i_size f.truncate(inode_size) @@ -502,7 +504,7 @@ class InodePages(plugins.PluginInterface): if self.config["dump"]: filename = self.config["dump"] vollog.info("[*] Writing inode at 0x%x to '%s'", inode.vol.offset, filename) - self.write_inode_content_to_file(inode, filename, vmlinux_layer) + self.write_inode_content_to_file(inode, filename, self.open, vmlinux_layer) def run(self): headers = [ From 3c70c1b9f7c2251e35eea276f5d5099b68392a48 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 00:58:04 +1000 Subject: [PATCH 78/85] PR review fixes: Use context and module_name instead of vmlinux in ebpf plugin --- volatility3/framework/plugins/linux/ebpf.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py index 70082daf5..2267dd922 100644 --- a/volatility3/framework/plugins/linux/ebpf.py +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -29,15 +29,21 @@ class EBPF(plugins.PluginInterface): ), ] - def get_ebpf_programs(self, vmlinux) -> interfaces.objects.ObjectInterface: + def get_ebpf_programs( + self, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> interfaces.objects.ObjectInterface: """Enumerate eBPF programs walking its IDR. Args: - vmlinux: The kernel symbols object - + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate Yields: eBPF program objects """ + vmlinux = context.modules[vmlinux_module_name] + if not vmlinux.has_symbol("prog_idr"): raise exceptions.VolatilityException( "Cannot find the eBPF prog idr. Unsupported kernel" @@ -49,8 +55,7 @@ class EBPF(plugins.PluginInterface): yield bpf_prog def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - for prog in self.get_ebpf_programs(vmlinux): + for prog in self.get_ebpf_programs(self.context, self.config["kernel"]): prog_addr = prog.vol.offset prog_type = prog.get_type() or renderers.NotAvailableValue() prog_tag = prog.get_tag() or renderers.NotAvailableValue() From 695635199e2bd3af11bee522e054719ee1898db6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 01:14:09 +1000 Subject: [PATCH 79/85] PR review fixes: Avoid saving state in the pidhashtable plugin --- .../framework/plugins/linux/pidhashtable.py | 79 +++++++++++-------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index 9a2528543..3223aed4a 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -44,17 +44,16 @@ class PIDHashTable(plugins.PluginInterface): ), ] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.vmlinux = None - self.vmlinux_layer = None - def _is_valid_task(self, task) -> bool: - return bool(task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent)) + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + return bool(task and task.pid > 0 and vmlinux_layer.is_valid(task.parent)) def _get_pidtype_pid(self): + vmlinux = self.context.modules[self.config["kernel"]] + # The pid_type enumeration is present since 2.5.37, just in case - pid_type_enum = self.vmlinux.get_enumeration("pid_type") + pid_type_enum = vmlinux.get_enumeration("pid_type") if not pid_type_enum: vollog.error("Cannot find pid_type enum. Unsupported kernel") return None @@ -68,38 +67,46 @@ class PIDHashTable(plugins.PluginInterface): return pidtype_pid def _get_pidhash_array(self): - pidhash_shift = self.vmlinux.object_from_symbol("pidhash_shift") + vmlinux = self.context.modules[self.config["kernel"]] + + pidhash_shift = vmlinux.object_from_symbol("pidhash_shift") pidhash_size = 1 << pidhash_shift - array_type_name = self.vmlinux.symbol_table_name + constants.BANG + "array" + array_type_name = vmlinux.symbol_table_name + constants.BANG + "array" - pidhash_ptr = self.vmlinux.object_from_symbol("pid_hash") + pidhash_ptr = vmlinux.object_from_symbol("pid_hash") # pidhash is an array of hlist_heads pidhash = self._context.object( array_type_name, offset=pidhash_ptr, - subtype=self.vmlinux.get_type("hlist_head"), + subtype=vmlinux.get_type("hlist_head"), count=pidhash_size, - layer_name=self.vmlinux.layer_name, + layer_name=vmlinux.layer_name, ) return pidhash def _walk_upid(self, seen_upids, upid): - while upid and self.vmlinux_layer.is_valid(upid.vol.offset): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + + while upid and vmlinux_layer.is_valid(upid.vol.offset): if upid.vol.offset in seen_upids: break seen_upids.add(upid.vol.offset) pid_chain = upid.pid_chain - if not (pid_chain and self.vmlinux_layer.is_valid(pid_chain.vol.offset)): + if not (pid_chain and vmlinux_layer.is_valid(pid_chain.vol.offset)): break upid = linux.LinuxUtilities.container_of( - pid_chain.next, "upid", "pid_chain", self.vmlinux + pid_chain.next, "upid", "pid_chain", vmlinux ) def _get_upids(self): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + # 2.6.24 <= kernels < 4.15 pidhash = self._get_pidhash_array() @@ -108,10 +115,10 @@ class PIDHashTable(plugins.PluginInterface): # each entry in the hlist is a upid which is wrapped in a pid ent = hlist.first - while ent and self.vmlinux_layer.is_valid(ent.vol.offset): + while ent and vmlinux_layer.is_valid(ent.vol.offset): # upid->pid_chain exists 2.6.24 <= kernel < 4.15 upid = linux.LinuxUtilities.container_of( - ent.vol.offset, "upid", "pid_chain", self.vmlinux + ent.vol.offset, "upid", "pid_chain", vmlinux ) if upid.vol.offset in seen_upids: @@ -124,16 +131,14 @@ class PIDHashTable(plugins.PluginInterface): return seen_upids def _pid_hash_implementation(self): + vmlinux = self.context.modules[self.config["kernel"]] + # 2.6.24 <= kernels < 4.15 - task_pids_off = self.vmlinux.get_type("task_struct").relative_child_offset( - "pids" - ) + task_pids_off = vmlinux.get_type("task_struct").relative_child_offset("pids") pidtype_pid = self._get_pidtype_pid() for upid in self._get_upids(): - pid = linux.LinuxUtilities.container_of( - upid, "pid", "numbers", self.vmlinux - ) + pid = linux.LinuxUtilities.container_of(upid, "pid", "numbers", vmlinux) if not pid: continue @@ -141,22 +146,24 @@ class PIDHashTable(plugins.PluginInterface): if not pid_tasks_0: continue - task = self.vmlinux.object( + task = vmlinux.object( "task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True ) if self._is_valid_task(task): yield task def _task_for_radix_pid_node(self, nodep): + vmlinux = self.context.modules[self.config["kernel"]] + # kernels >= 4.15 - pid = self.vmlinux.object("pid", offset=nodep, absolute=True) + pid = vmlinux.object("pid", offset=nodep, absolute=True) pidtype_pid = self._get_pidtype_pid() pid_tasks_0 = pid.tasks[pidtype_pid].first if not pid_tasks_0: return None - task_struct_type = self.vmlinux.get_type("task_struct") + task_struct_type = vmlinux.get_type("task_struct") if task_struct_type.has_member("pids"): member = "pids" elif task_struct_type.has_member("pid_links"): @@ -165,15 +172,17 @@ class PIDHashTable(plugins.PluginInterface): return None task_pids_off = task_struct_type.relative_child_offset(member) - task = self.vmlinux.object( + task = vmlinux.object( "task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True ) return task def _pid_namespace_idr(self): + vmlinux = self.context.modules[self.config["kernel"]] + # kernels >= 4.15 - ns_addr = self.vmlinux.get_symbol("init_pid_ns").address - ns = self.vmlinux.object("pid_namespace", offset=ns_addr) + ns_addr = vmlinux.get_symbol("init_pid_ns").address + ns = vmlinux.object("pid_namespace", offset=ns_addr) for page_addr in ns.idr.get_entries(): task = self._task_for_radix_pid_node(page_addr) @@ -181,24 +190,26 @@ class PIDHashTable(plugins.PluginInterface): yield task def _determine_pid_func(self): - pid_hash = self.vmlinux.has_symbol("pid_hash") and self.vmlinux.has_symbol( + vmlinux = self.context.modules[self.config["kernel"]] + + pid_hash = vmlinux.has_symbol("pid_hash") and vmlinux.has_symbol( "pidhash_shift" ) # 2.5.55 <= kernels < 4.15 - has_pid_numbers = self.vmlinux.has_type("pid") and self.vmlinux.get_type( + has_pid_numbers = vmlinux.has_type("pid") and vmlinux.get_type( "pid" ).has_member( "numbers" ) # kernels >= 2.6.24 - has_pid_chain = self.vmlinux.has_type("upid") and self.vmlinux.get_type( + has_pid_chain = vmlinux.has_type("upid") and vmlinux.get_type( "upid" ).has_member( "pid_chain" ) # 2.6.24 <= kernels < 4.15 # kernels >= 4.15 - pid_idr = self.vmlinux.has_type("pid_namespace") and self.vmlinux.get_type( + pid_idr = vmlinux.has_type("pid_namespace") and vmlinux.get_type( "pid_namespace" ).has_member("idr") @@ -217,8 +228,6 @@ class PIDHashTable(plugins.PluginInterface): Yields: task_struct objects """ - self.vmlinux = self.context.modules[self.config["kernel"]] - self.vmlinux_layer = self.context.layers[self.vmlinux.layer_name] pid_func = self._determine_pid_func() if not pid_func: vollog.error("Cannot determine which PID hash table this kernel is using") From d964e6f61de00b5a8608d48d468d9a59b7923ec7 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 02:06:06 +1000 Subject: [PATCH 80/85] PR review fixes: Check for LinuxUtilities version everywhere we use it --- .../symbols/linux/extensions/__init__.py | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index d00af7a3f..51dc37d31 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -13,6 +13,7 @@ from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union, from volatility3.framework import constants, exceptions, objects, interfaces, symbols from volatility3.framework.renderers import conversion +from volatility3.framework.configuration import requirements from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS @@ -1608,6 +1609,19 @@ class xdp_sock(objects.StructType): class bpf_prog(objects.StructType): + def _get_vmlinux(self): + linuxutils_required_version = (2, 1, 1) + linuxutils_current_version = linux.LinuxUtilities._version + if not requirements.VersionRequirement.matches_required( + linuxutils_required_version, linuxutils_current_version + ): + raise exceptions.PluginRequirementException( + f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" + ) + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + return vmlinux + def get_type(self) -> Union[str, None]: """Returns a string with the eBPF program type""" @@ -1631,7 +1645,7 @@ class bpf_prog(objects.StructType): if not self.has_member("tag"): return None - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux = self._get_vmlinux() vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] prog_tag_addr = self.tag.vol.offset @@ -2043,13 +2057,26 @@ class page(objects.StructType): return flags + def _get_vmlinux(self): + linuxutils_required_version = (2, 1, 1) + linuxutils_current_version = linux.LinuxUtilities._version + if not requirements.VersionRequirement.matches_required( + linuxutils_required_version, linuxutils_current_version + ): + raise exceptions.PluginRequirementException( + f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" + ) + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + return vmlinux + def to_paddr(self) -> int: """Converts a page's virtual address to its physical address using the current physical memory model. Returns: int: page physical address """ - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux = self._get_vmlinux() vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] vmemmap_start = None @@ -2100,7 +2127,7 @@ class page(objects.StructType): Returns: The page content """ - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux = self._get_vmlinux() vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] physical_layer = vmlinux.context.layers["memory_layer"] page_paddr = self.to_paddr() @@ -2118,6 +2145,19 @@ class IDR(objects.StructType): MAX_IDR_SHIFT = INT_SIZE * 8 - 1 MAX_IDR_BIT = 1 << MAX_IDR_SHIFT + def _get_vmlinux(self): + linuxutils_required_version = (2, 1, 1) + linuxutils_current_version = linux.LinuxUtilities._version + if not requirements.VersionRequirement.matches_required( + linuxutils_required_version, linuxutils_current_version + ): + raise exceptions.PluginRequirementException( + f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" + ) + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + return vmlinux + def idr_max(self, num_layers: int) -> int: """Returns the maximum ID which can be allocated given idr::layers @@ -2141,7 +2181,7 @@ class IDR(objects.StructType): Returns: A pointer to the given ID element """ - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux = self._get_vmlinux() if not vmlinux.get_type("idr_layer").has_member("layer"): vollog.info( "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" From d627f243e259efce2f730860f47d9116b3c78f3d Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 10:55:01 +1000 Subject: [PATCH 81/85] PR review fixes: Add typing info to the get_inodes() class method. --- volatility3/framework/plugins/linux/pagecache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index cf4151c85..e384cbabf 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -6,7 +6,7 @@ import math import logging import datetime from dataclasses import dataclass, astuple -from typing import List, Set, Type +from typing import List, Set, Type, Iterable from volatility3.framework import renderers, interfaces from volatility3.framework.renderers import format_hints @@ -205,7 +205,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): cls, context: interfaces.context.ContextInterface, config_path: str, - ): + ) -> Iterable[InodeInternal]: """Retrieves the inodes from the superblocks Args: From 90b327e63253404a98cc5a79e3cecaa1b773048c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 11:45:29 +1000 Subject: [PATCH 82/85] PR review fixes: Make mountinfo.get_superblocks() a classmethod and adapt the code using it. --- .../framework/plugins/linux/mountinfo.py | 19 +++++++++--- .../framework/plugins/linux/pagecache.py | 31 ++++++++++++------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index dfb2e2f52..1eaec77bf 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -37,7 +37,7 @@ class MountInfo(plugins.PluginInterface): _required_framework_version = (2, 2, 0) - _version = (1, 1, 0) + _version = (1, 2, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -143,8 +143,8 @@ class MountInfo(plugins.PluginInterface): sb_opts, ) + @staticmethod def _get_tasks_mountpoints( - self, tasks: Iterable[interfaces.objects.ObjectInterface], filtered_by_pids: bool = False, ): @@ -247,17 +247,26 @@ class MountInfo(plugins.PluginInterface): "Could not filter by mount namespace id. This field is not available in this kernel." ) - def get_superblocks(self): + @classmethod + def get_superblocks( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Yield file system superblocks based on the task's mounted filesystems. + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + Yields: super_block: Kernel's struct super_block object """ # No filter so that we get all the mount namespaces from all tasks - tasks = pslist.PsList.list_tasks(self.context, self.config["kernel"]) + tasks = pslist.PsList.list_tasks(context, vmlinux_module_name) seen_sb_ptr = set() - for task, mnt, _mnt_ns_id in self._get_tasks_mountpoints(tasks): + for task, mnt, _mnt_ns_id in cls._get_tasks_mountpoints(tasks): path_root = linux.LinuxUtilities.get_path_mnt(task, mnt) if not path_root: continue diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index e384cbabf..e54891480 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -115,7 +115,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): architectures=["Intel32", "Intel64"], ), requirements.PluginRequirement( - name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 1, 0) + name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0) ), requirements.ListRequirement( name="type", @@ -204,22 +204,22 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): def get_inodes( cls, context: interfaces.context.ContextInterface, - config_path: str, + vmlinux_module_name: str, ) -> Iterable[InodeInternal]: """Retrieves the inodes from the superblocks Args: context: The context that the plugin will operate within - config_path: The path to configuration data within the context configuration data + vmlinux_module_name: The name of the kernel module on which to operate Yields: An InodeInternal object """ - superblocks_iter = mountinfo.MountInfo( + superblocks_iter = mountinfo.MountInfo.get_superblocks( context=context, - config_path=config_path, - ).get_superblocks() + vmlinux_module_name=vmlinux_module_name, + ) seen_inodes = set() seen_dentries = set() @@ -289,11 +289,13 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): yield inode_in def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] vmlinux_layer = self.context.layers[vmlinux.layer_name] inodes_iter = self.get_inodes( - context=self.context, config_path=self.config_path + context=self.context, + vmlinux_module_name=vmlinux_module_name, ) types_filter = self.config["type"] @@ -316,12 +318,15 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): These need not be generated in any particular order, sorting will be done later """ - vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] vmlinux_layer = self.context.layers[vmlinux.layer_name] inodes_iter = self.get_inodes( - context=self.context, config_path=self.config_path + context=self.context, + vmlinux_module_name=vmlinux_module_name, ) + for inode_in in inodes_iter: inode_out = inode_in.to_user(vmlinux_layer) description = f"Cached Inode for {inode_out.path}" @@ -450,7 +455,8 @@ class InodePages(plugins.PluginInterface): vollog.error("Unable to write to file (%s): %s", filename, e) def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] vmlinux_layer = self.context.layers[vmlinux.layer_name] if self.config["inode"] and self.config["find"]: @@ -459,7 +465,8 @@ class InodePages(plugins.PluginInterface): if self.config["find"]: inodes_iter = Files.get_inodes( - context=self.context, config_path=self.config_path + context=self.context, + vmlinux_module_name=vmlinux_module_name, ) for inode_in in inodes_iter: if inode_in.path == self.config["find"]: From 3bf9f8cec0e1c4c75088abe17baadd0bc6d4c3d6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 12:03:10 +1000 Subject: [PATCH 83/85] PR review fixes: Add typing info to pagecache.Files._follow_symlink() --- volatility3/framework/plugins/linux/pagecache.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index e54891480..b6c5f7cc0 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -131,7 +131,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): ] @staticmethod - def _follow_symlink(inode, symlink_path) -> str: + def _follow_symlink( + inode: interfaces.objects.ObjectInterface, + symlink_path: str, + ) -> str: """Follows (fast) symlinks (kernels >= 4.2.x). Fast symlinks are filesystem agnostic. From e97abae156721bc1c625729166c08325c164bd3a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 24 Aug 2024 15:02:31 +0100 Subject: [PATCH 84/85] Remove get_vmlinux calls Sorry, I know I asked for it, but I hadn't quite figured out what was going on. Things in the symbols/linux code are considered part of the framework, and therefore it's the framework version that should have been bumped. Since the framework comes packages with LinuxUtilities we can rely on the version numbers to be suitable. This cleans up the mess I caused, sorry for the extra work! 5:S --- volatility3/framework/constants/_version.py | 4 +- .../symbols/linux/extensions/__init__.py | 47 ++----------------- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 4df0b9041..d30446d63 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 8 # Number of changes that only add to the interface -VERSION_PATCH = 1 # Number of changes that do not change the interface +VERSION_MINOR = 9 # Number of changes that only add to the interface +VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = ( diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 51dc37d31..fdd34403a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1609,19 +1609,6 @@ class xdp_sock(objects.StructType): class bpf_prog(objects.StructType): - def _get_vmlinux(self): - linuxutils_required_version = (2, 1, 1) - linuxutils_current_version = linux.LinuxUtilities._version - if not requirements.VersionRequirement.matches_required( - linuxutils_required_version, linuxutils_current_version - ): - raise exceptions.PluginRequirementException( - f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" - ) - - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - return vmlinux - def get_type(self) -> Union[str, None]: """Returns a string with the eBPF program type""" @@ -1645,7 +1632,7 @@ class bpf_prog(objects.StructType): if not self.has_member("tag"): return None - vmlinux = self._get_vmlinux() + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] prog_tag_addr = self.tag.vol.offset @@ -2057,26 +2044,13 @@ class page(objects.StructType): return flags - def _get_vmlinux(self): - linuxutils_required_version = (2, 1, 1) - linuxutils_current_version = linux.LinuxUtilities._version - if not requirements.VersionRequirement.matches_required( - linuxutils_required_version, linuxutils_current_version - ): - raise exceptions.PluginRequirementException( - f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" - ) - - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - return vmlinux - def to_paddr(self) -> int: """Converts a page's virtual address to its physical address using the current physical memory model. Returns: int: page physical address """ - vmlinux = self._get_vmlinux() + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] vmemmap_start = None @@ -2127,7 +2101,7 @@ class page(objects.StructType): Returns: The page content """ - vmlinux = self._get_vmlinux() + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] physical_layer = vmlinux.context.layers["memory_layer"] page_paddr = self.to_paddr() @@ -2145,19 +2119,6 @@ class IDR(objects.StructType): MAX_IDR_SHIFT = INT_SIZE * 8 - 1 MAX_IDR_BIT = 1 << MAX_IDR_SHIFT - def _get_vmlinux(self): - linuxutils_required_version = (2, 1, 1) - linuxutils_current_version = linux.LinuxUtilities._version - if not requirements.VersionRequirement.matches_required( - linuxutils_required_version, linuxutils_current_version - ): - raise exceptions.PluginRequirementException( - f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" - ) - - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - return vmlinux - def idr_max(self, num_layers: int) -> int: """Returns the maximum ID which can be allocated given idr::layers @@ -2181,7 +2142,7 @@ class IDR(objects.StructType): Returns: A pointer to the given ID element """ - vmlinux = self._get_vmlinux() + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) if not vmlinux.get_type("idr_layer").has_member("layer"): vollog.info( "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" From 6a157a785f3364635de2c7a43eeab054ab7c6b3d Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 27 Aug 2024 14:45:52 +0100 Subject: [PATCH 85/85] Bump the modules version number Pull-request #1173 bumped the version of the modules plugin (even though this only needed to be a MINOR version bump, see https://github.com/volatilityfoundation/volatility3/pull/1173#discussion_r1649614761), but failed to verify that other plugins which relied on it were also updated to make use of the new plugin. This was the version system working as intended, but highlighted a review failure that the neither the author, nor the reviewers, verified that the rest of the framework (specifically other plugins which relied on modules) worked correctly with the new code (which this kind of error is designed to fix). Fixes #1244. --- volatility3/framework/plugins/windows/ssdt.py | 2 +- volatility3/framework/plugins/windows/verinfo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/ssdt.py b/volatility3/framework/plugins/windows/ssdt.py index 6a47c36e9..1fcb6cc91 100644 --- a/volatility3/framework/plugins/windows/ssdt.py +++ b/volatility3/framework/plugins/windows/ssdt.py @@ -30,7 +30,7 @@ class SSDT(plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.PluginRequirement( - name="modules", plugin=modules.Modules, version=(1, 0, 0) + name="modules", plugin=modules.Modules, version=(2, 0, 0) ), ] diff --git a/volatility3/framework/plugins/windows/verinfo.py b/volatility3/framework/plugins/windows/verinfo.py index 1c6615804..5b3c52bf6 100644 --- a/volatility3/framework/plugins/windows/verinfo.py +++ b/volatility3/framework/plugins/windows/verinfo.py @@ -46,7 +46,7 @@ class VerInfo(interfaces.plugins.PluginInterface): name="pslist", plugin=pslist.PsList, version=(2, 0, 0) ), requirements.PluginRequirement( - name="modules", plugin=modules.Modules, version=(1, 0, 0) + name="modules", plugin=modules.Modules, version=(2, 0, 0) ), requirements.VersionRequirement( name="dlllist", component=dlllist.DllList, version=(2, 0, 0)