From 6f3f645dbcfb94b797f2e244e56e43dd8e4cba0a Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 25 Jul 2024 17:59:10 +0100 Subject: [PATCH 01/29] 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 02/29] 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 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 03/29] 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 04/29] 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 05/29] 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 06/29] 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 07/29] 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 08/29] 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 09/29] 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 10/29] 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 11/29] 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 12/29] 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 13/29] 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 14/29] 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 15/29] 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 16/29] 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 17/29] 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 18/29] 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 19/29] 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 20/29] 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 3017a7d00c3a9cfca3f94b9e7dfb8ba08d48fd0e Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 1 Aug 2024 11:46:10 +1000 Subject: [PATCH 21/29] 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 22/29] 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 23/29] 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 24/29] 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 25/29] 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 79529fb8153b3a58b4a1d1c6192cb92cf107800f Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 13:50:51 +1000 Subject: [PATCH 26/29] 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 b8d68b9a5ee0c643b68eaa9f33bd051279374eb3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 14:15:01 +1000 Subject: [PATCH 27/29] 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 28/29] 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 ed208347630428e21dec91f90eb431ed02595bc3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 19:46:23 +1000 Subject: [PATCH 29/29] 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