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 01/20] 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 02/20] 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 03/20] 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 04/20] 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 73bc10c2834d9a8fa2ab04c098a4735542226170 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 31 Jul 2024 21:46:57 +0100 Subject: [PATCH 05/20] 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 06/20] 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 07/20] 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 08/20] 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 09/20] 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 10/20] 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 11/20] 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 12/20] 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 13/20] 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 14/20] 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 15/20] 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 16/20] 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 17/20] 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 1e3e9e2c78cbfc4e24362c323432660c98373516 Mon Sep 17 00:00:00 2001 From: Arcuri Davide Date: Tue, 6 Aug 2024 16:28:59 +0200 Subject: [PATCH 18/20] 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 1d43eb305d993a1306cc3971333d8a28bfee03af Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Tue, 6 Aug 2024 15:18:49 -0700 Subject: [PATCH 19/20] fix invalid json on all files --- .../symbols/windows/consoles/consoles-win10-17763-3232-x64.json | 1 - .../symbols/windows/consoles/consoles-win10-17763-x64.json | 1 - .../symbols/windows/consoles/consoles-win10-18362-x64.json | 1 - .../symbols/windows/consoles/consoles-win10-19041-x64.json | 1 - .../symbols/windows/consoles/consoles-win10-20348-1970-x64.json | 1 - .../symbols/windows/consoles/consoles-win10-20348-2461-x64.json | 1 - 6 files changed, 6 deletions(-) diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-3232-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-3232-x64.json index ba17645c4..0a8cb5782 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-3232-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-3232-x64.json @@ -434,7 +434,6 @@ "BufferRows": { "type": { "kind": "pointer", - "name": "void", "subtype": { "kind": "struct", "name": "_ROWS_ARRAY" diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json index f11fb39b1..2b98945c9 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json @@ -434,7 +434,6 @@ "BufferRows": { "type": { "kind": "pointer", - "name": "void", "subtype": { "kind": "struct", "name": "_ROWS_ARRAY" diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json index 3c1fde26e..3d76d1cb4 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json @@ -434,7 +434,6 @@ "BufferRows": { "type": { "kind": "pointer", - "name": "void", "subtype": { "kind": "struct", "name": "_ROWS_ARRAY" diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json index 39ca5f0eb..681113b0f 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json @@ -434,7 +434,6 @@ "BufferRows": { "type": { "kind": "pointer", - "name": "void", "subtype": { "kind": "struct", "name": "_ROWS_ARRAY" diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-1970-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-1970-x64.json index e0935b4be..7bc6ae9d1 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-1970-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-1970-x64.json @@ -434,7 +434,6 @@ "BufferRows": { "type": { "kind": "pointer", - "name": "void", "subtype": { "kind": "struct", "name": "_ROWS_ARRAY" diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-2461-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-2461-x64.json index a4adf8028..f612c527a 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-2461-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-2461-x64.json @@ -434,7 +434,6 @@ "BufferRows": { "type": { "kind": "pointer", - "name": "void", "subtype": { "kind": "struct", "name": "_ROWS_ARRAY" From ac08f42cfd9b9f75067c7f0bd9162d15be05c8a2 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Sun, 4 Aug 2024 12:09:59 -0700 Subject: [PATCH 20/20] #816 - fix invalid json --- .../symbols/windows/consoles/consoles-win10-20348-x64.json | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json index 5cc7ab6f5..083a966a0 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json @@ -434,7 +434,6 @@ "BufferRows": { "type": { "kind": "pointer", - "name": "void", "subtype": { "kind": "struct", "name": "_ROWS_ARRAY"